Skip to content
Open
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
191 changes: 152 additions & 39 deletions pkg/xsysinfo/gpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Loading