diff --git a/pkg/xsysinfo/gpu.go b/pkg/xsysinfo/gpu.go index e5db4404e714..9e2a33c5d2f5 100644 --- a/pkg/xsysinfo/gpu.go +++ b/pkg/xsysinfo/gpu.go @@ -801,14 +801,15 @@ func GetResourceAggregateInfo() AggregateMemoryInfo { return resourceInfo.Aggregate } -// getVulkanGPUMemory queries GPUs using vulkaninfo as a fallback -// Note: Vulkan provides memory heap info but not real-time usage +// getVulkanGPUMemory queries GPUs using vulkaninfo as a fallback. +// Note: vulkaninfo JSON is a Vulkan Profiles export and does not include +// VkPhysicalDeviceMemoryProperties, so memory heaps are parsed from text output. func getVulkanGPUMemory() []GPUMemoryInfo { if _, err := exec.LookPath("vulkaninfo"); err != nil { return nil } - cmd := exec.Command("vulkaninfo", "--json") + cmd := exec.Command("vulkaninfo", "--text") var stdout, stderr bytes.Buffer cmd.Stdout = &stdout @@ -819,60 +820,172 @@ func getVulkanGPUMemory() []GPUMemoryInfo { return nil } - // Parse Vulkan JSON output - var result struct { - VkPhysicalDevices []struct { - DeviceName string `json:"deviceName"` - DeviceType string `json:"deviceType"` - VkPhysicalDeviceMemoryProperties struct { - MemoryHeaps []struct { - Flags int `json:"flags"` - Size uint64 `json:"size"` - } `json:"memoryHeaps"` - } `json:"VkPhysicalDeviceMemoryProperties"` - } `json:"VkPhysicalDevices"` - } + return parseVulkanGPUMemoryText(stdout.String()) +} - if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { - xlog.Debug("failed to parse vulkaninfo output", "error", err) - return nil - } +type vulkanGPUTextInfo struct { + index int + name string + deviceType string + totalVRAM uint64 +} +func parseVulkanGPUMemoryText(output string) []GPUMemoryInfo { var gpus []GPUMemoryInfo + var current *vulkanGPUTextInfo - for i, device := range result.VkPhysicalDevices { - // Skip non-discrete/integrated GPUs if possible - if device.DeviceType == "VK_PHYSICAL_DEVICE_TYPE_CPU" { - continue - } + inMemoryProperties := false + inMemoryHeaps := false + inHeap := false + heapSize := uint64(0) + heapDeviceLocal := false - // Sum up device-local memory heaps - var totalVRAM uint64 - for _, heap := range device.VkPhysicalDeviceMemoryProperties.MemoryHeaps { - // Flag 1 = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT - if heap.Flags&1 != 0 { - totalVRAM += heap.Size - } + flushHeap := func() { + if current != nil && inHeap && heapDeviceLocal { + current.totalVRAM += heapSize } + heapSize = 0 + heapDeviceLocal = false + inHeap = false + } - if totalVRAM == 0 { - continue + flushGPU := func() { + if current == nil || current.totalVRAM == 0 || current.deviceType == "PHYSICAL_DEVICE_TYPE_CPU" { + return } gpus = append(gpus, GPUMemoryInfo{ - Index: i, - Name: device.DeviceName, + Index: current.index, + Name: current.name, Vendor: VendorVulkan, - TotalVRAM: totalVRAM, - UsedVRAM: 0, // Vulkan doesn't provide real-time usage - FreeVRAM: totalVRAM, + TotalVRAM: current.totalVRAM, + UsedVRAM: 0, // Vulkan heap size is capacity, not real-time usage. + FreeVRAM: current.totalVRAM, UsagePercent: 0, }) } + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + if index, ok := parseVulkanGPUHeader(line); ok { + flushHeap() + flushGPU() + current = &vulkanGPUTextInfo{index: index} + inMemoryProperties = false + inMemoryHeaps = false + continue + } + + if current == nil { + continue + } + + if strings.HasPrefix(line, "deviceType") { + current.deviceType = parseVulkanValue(line) + continue + } + + if strings.HasPrefix(line, "deviceName") { + current.name = parseVulkanValue(line) + continue + } + + if line == "VkPhysicalDeviceMemoryProperties:" { + inMemoryProperties = true + inMemoryHeaps = false + flushHeap() + continue + } + + if !inMemoryProperties { + continue + } + + if strings.HasPrefix(line, "memoryHeaps:") { + inMemoryHeaps = true + continue + } + + if strings.HasPrefix(line, "memoryTypes:") { + flushHeap() + inMemoryProperties = false + inMemoryHeaps = false + continue + } + + if !inMemoryHeaps { + continue + } + + if strings.HasPrefix(line, "memoryHeaps[") { + flushHeap() + inHeap = true + continue + } + + if !inHeap { + continue + } + + if strings.HasPrefix(line, "size") { + if size, ok := parseVulkanUintValue(line); ok { + heapSize = size + } + continue + } + + if strings.Contains(line, "MEMORY_HEAP_DEVICE_LOCAL_BIT") { + heapDeviceLocal = true + } + } + + flushHeap() + flushGPU() + return gpus } +func parseVulkanGPUHeader(line string) (int, bool) { + if !strings.HasPrefix(line, "GPU") || !strings.HasSuffix(line, ":") { + return 0, false + } + + index, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(line, "GPU"), ":")) + if err != nil { + return 0, false + } + + return index, true +} + +func parseVulkanValue(line string) string { + _, value, ok := strings.Cut(line, "=") + if !ok { + return "" + } + + return strings.TrimSpace(value) +} + +func parseVulkanUintValue(line string) (uint64, bool) { + value := parseVulkanValue(line) + fields := strings.Fields(value) + if len(fields) == 0 { + return 0, false + } + + parsed, err := strconv.ParseUint(fields[0], 0, 64) + if err != nil { + return 0, false + } + + return parsed, true +} + // 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 {