diff --git a/core/http/react-ui/src/utils/format.js b/core/http/react-ui/src/utils/format.js index 892f9a1be..b17006e08 100644 --- a/core/http/react-ui/src/utils/format.js +++ b/core/http/react-ui/src/utils/format.js @@ -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)' } diff --git a/go.mod b/go.mod index 3f023f44a..ea773aa19 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 41bdf6a83..1fa5745b0 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/xsysinfo/gpu.go b/pkg/xsysinfo/gpu.go index 98d8cd41d..fe9f07aa4 100644 --- a/pkg/xsysinfo/gpu.go +++ b/pkg/xsysinfo/gpu.go @@ -19,6 +19,7 @@ const ( VendorNVIDIA = "nvidia" VendorAMD = "amd" VendorIntel = "intel" + VendorApple = "apple" VendorVulkan = "vulkan" VendorUnknown = "unknown" ) @@ -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 @@ -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 } @@ -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 } @@ -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) @@ -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 +}