Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/http/react-ui/src/utils/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ export function vendorColor(vendor) {
if (v.includes('nvidia')) return '#76b900'
if (v.includes('amd')) return '#ed1c24'
if (v.includes('intel')) return '#0071c5'
if (v.includes('apple')) return '#a2aaad'
return 'var(--color-accent)'
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ require (
github.com/mudler/cogito v0.9.5-0.20260315222927-63abdec7189b
github.com/mudler/edgevpn v0.31.1
github.com/mudler/go-processmanager v0.1.0
github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2
github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8
github.com/mudler/xlog v0.0.6
github.com/nats-io/nats.go v1.50.0
github.com/onsi/ginkgo/v2 v2.28.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,8 @@ github.com/mudler/localrecall v0.5.9-0.20260321005011-810084e9369b h1:XeAnOEOOSK
github.com/mudler/localrecall v0.5.9-0.20260321005011-810084e9369b/go.mod h1:xuPtgL9zUyiQLmspYzO3kaboYrGbWmwi8BQPt1aCAcs=
github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2 h1:+WHsL/j6EWOMUiMVIOJNKOwSKiQt/qDPc9fePCf87fA=
github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2/go.mod h1:EA8Ashhd56o32qN7ouPKFSRUs/Z+LrRCF4v6R2Oarm8=
github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8 h1:Ry8RiWy8fZ6Ff4E7dPmjRsBrnHOnPeOOj2LhCgyjQu0=
github.com/mudler/memory v0.0.0-20260406210934-424c1ecf2cf8/go.mod h1:EA8Ashhd56o32qN7ouPKFSRUs/Z+LrRCF4v6R2Oarm8=
github.com/mudler/skillserver v0.0.6 h1:ixz6wUekLdTmbnpAavCkTydDF6UdXAG3ncYufSPK9G0=
github.com/mudler/skillserver v0.0.6/go.mod h1:z3yFhcL9bSykmmh6xgGu0hyoItd4CnxgtWMEWw8uFJU=
github.com/mudler/water v0.0.0-20250808092830-dd90dcf09025 h1:WFLP5FHInarYGXi6B/Ze204x7Xy6q/I4nCZnWEyPHK0=
Expand Down
147 changes: 134 additions & 13 deletions pkg/xsysinfo/gpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
VendorNVIDIA = "nvidia"
VendorAMD = "amd"
VendorIntel = "intel"
VendorApple = "apple"
VendorVulkan = "vulkan"
VendorUnknown = "unknown"
)
Expand All @@ -29,7 +30,8 @@ const (
var UnifiedMemoryDevices = []string{
"NVIDIA GB10",
"GB10",
// Add more unified memory devices here as needed
"NVIDIA Thor",
"Thor",
}

// GPUMemoryInfo contains real-time GPU memory usage information
Expand Down Expand Up @@ -196,6 +198,12 @@ func DetectGPUVendor() (string, error) {
return VendorVulkan, nil
}

// Check for Apple Silicon (macOS)
if appleGPUs := getAppleGPUMemory(); len(appleGPUs) > 0 {
xlog.Debug("GPU vendor detected via system_profiler", "vendor", VendorApple)
return VendorApple, nil
}

// No vendor detected
return "", nil
}
Expand Down Expand Up @@ -258,6 +266,12 @@ func GetGPUMemoryUsage() []GPUMemoryInfo {
gpus = append(gpus, vulkanGPUs...)
}

// Try Apple Silicon (macOS only)
if len(gpus) == 0 {
appleGPUs := getAppleGPUMemory()
gpus = append(gpus, appleGPUs...)
}

return gpus
}

Expand Down Expand Up @@ -351,18 +365,44 @@ func getNVIDIAGPUMemory() []GPUMemoryInfo {
usagePercent = float64(usedBytes) / float64(totalBytes) * 100
}
} else if isNA {
// Unknown device with N/A values - skip memory info
xlog.Debug("nvidia-smi returned N/A for unknown device", "device", name)
gpus = append(gpus, GPUMemoryInfo{
Index: idx,
Name: name,
Vendor: VendorNVIDIA,
TotalVRAM: 0,
UsedVRAM: 0,
FreeVRAM: 0,
UsagePercent: 0,
})
continue
// Check if this is a Tegra/Jetson device — if so, it uses unified memory
if isTegraDevice() {
xlog.Debug("nvidia-smi returned N/A on Tegra device, using system RAM", "device", name)
sysInfo, err := GetSystemRAMInfo()
if err != nil {
xlog.Debug("failed to get system RAM for Tegra device", "error", err, "device", name)
gpus = append(gpus, GPUMemoryInfo{
Index: idx,
Name: name,
Vendor: VendorNVIDIA,
TotalVRAM: 0,
UsedVRAM: 0,
FreeVRAM: 0,
UsagePercent: 0,
})
continue
}

totalBytes = sysInfo.Total
usedBytes = sysInfo.Used
freeBytes = sysInfo.Free
if totalBytes > 0 {
usagePercent = float64(usedBytes) / float64(totalBytes) * 100
}
} else {
// Truly unknown device with N/A values - skip memory info
xlog.Debug("nvidia-smi returned N/A for unknown device", "device", name)
gpus = append(gpus, GPUMemoryInfo{
Index: idx,
Name: name,
Vendor: VendorNVIDIA,
TotalVRAM: 0,
UsedVRAM: 0,
FreeVRAM: 0,
UsagePercent: 0,
})
continue
}
} else {
// Normal GPU with dedicated VRAM
totalMB, _ := strconv.ParseFloat(totalStr, 64)
Expand Down Expand Up @@ -790,3 +830,84 @@ func getVulkanGPUMemory() []GPUMemoryInfo {

return gpus
}

// getAppleGPUMemory detects Apple Silicon GPUs using system_profiler (macOS only).
// Apple Silicon uses unified memory, so GPU memory is reported as system RAM.
func getAppleGPUMemory() []GPUMemoryInfo {
if _, err := exec.LookPath("system_profiler"); err != nil {
return nil
}

cmd := exec.Command("system_profiler", "SPDisplaysDataType", "-json")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
xlog.Debug("system_profiler failed", "error", err, "stderr", stderr.String())
return nil
}

var result struct {
SPDisplaysDataType []struct {
Name string `json:"_name"`
Model string `json:"sppci_model"`
Cores string `json:"sppci_cores"`
DeviceType string `json:"sppci_device_type"`
Vendor string `json:"spdisplays_vendor"`
} `json:"SPDisplaysDataType"`
}

if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
xlog.Debug("failed to parse system_profiler output", "error", err)
return nil
}

var gpus []GPUMemoryInfo
for i, display := range result.SPDisplaysDataType {
if display.DeviceType != "spdisplays_gpu" {
continue
}
if !strings.Contains(strings.ToLower(display.Vendor), "apple") {
continue
}

name := display.Model
if name == "" {
name = display.Name
}
if name == "" {
name = "Apple GPU"
}

// Apple Silicon uses unified memory — report system RAM
ramInfo, err := GetSystemRAMInfo()
if err != nil {
xlog.Debug("Apple GPU detected but failed to get system RAM", "error", err)
gpus = append(gpus, GPUMemoryInfo{
Index: i,
Name: name,
Vendor: VendorApple,
})
continue
}

usagePercent := 0.0
if ramInfo.Total > 0 {
usagePercent = float64(ramInfo.Used) / float64(ramInfo.Total) * 100
}

xlog.Debug("Apple Silicon GPU detected (unified memory)", "device", name, "total_ram", ramInfo.Total)
gpus = append(gpus, GPUMemoryInfo{
Index: i,
Name: name,
Vendor: VendorApple,
TotalVRAM: ramInfo.Total,
UsedVRAM: ramInfo.Used,
FreeVRAM: ramInfo.Free,
UsagePercent: usagePercent,
})
}

return gpus
}
Loading