ollama/discover/vulkan.go
Sahil Kadadekar fc58544422
discover: fix inverted iGPU/dGPU Vulkan classification on Windows hybrid graphics (#16669)
On Windows hybrid-graphics systems (Intel iGPU + NVIDIA dGPU), discovery
could classify the integrated GPU as discrete and the discrete GPU as
integrated, dropping the dGPU's Vulkan device and scheduling models onto
the iGPU's shared system RAM (#16667). Two index-keyed correlations
between independently-ordered device enumerations caused this:

1. The native probe's stderr was concatenated into the output passed to
   parseVulkanUMA. The probe enumerates Vulkan devices in its own order,
   so its ggml_vulkan uma lines overwrote llama-server's index-keyed UMA
   map with inverted values. Parse UMA metadata only from llama-server's
   own output.

2. applyWindowsVulkanRefinement required the raw vkEnumeratePhysicalDevices
   count to equal llama-server's Vulkan device count. The raw enumeration
   is a superset on real systems (D3D12 mapping-layer devices, Microsoft
   Basic Render Driver), so the refinement that reads the authoritative
   VkPhysicalDeviceType was always skipped. Match devices by name against
   the probed superset instead, bailing only when a device has no match or
   matches conflicting device types.

Verified on the hardware from #16667 (Intel RaptorLake-S + RTX 4080
Laptop): the raw probe returns 5 devices vs llama-server's 2; with this
change the iGPU is dropped as integrated, the dGPU's Vulkan device
dedupes against CUDA0, and the model loads on the dGPU with no
environment overrides.

Fixes #16667
2026-06-22 14:52:03 -07:00

144 lines
4.1 KiB
Go

// Vulkan discovery needs a small amount of normalization around device type.
// llama-server discovery output does not currently expose a stable structured
// backend type field, so we use explicit Vulkan UMA metadata when it is
// present and, on Windows, refine the result with a direct Vulkan API query.
// The goal is to preserve correct integrated-vs-discrete scheduling decisions
// without relying on device-name heuristics.
package discover
import (
"bufio"
"errors"
"log/slog"
"regexp"
"runtime"
"strconv"
"strings"
"github.com/ollama/ollama/ml"
)
// vulkanUMARegex matches Vulkan debug lines like:
//
// ggml_vulkan: 0 = Intel(R) Graphics (...) | uma: 1 | fp16: 1 |
var vulkanUMARegex = regexp.MustCompile(
`ggml_vulkan:\s+(\d+)\s+=.*\|\s+uma:\s+([01])\s+\|`,
)
func parseVulkanUMA(output string) map[int]bool {
integratedByIndex := make(map[int]bool)
scanner := bufio.NewScanner(strings.NewReader(output))
for scanner.Scan() {
if matches := vulkanUMARegex.FindStringSubmatch(scanner.Text()); matches != nil {
idx, _ := strconv.Atoi(matches[1])
integratedByIndex[idx] = matches[2] == "1"
}
}
return integratedByIndex
}
var errWindowsVulkanProbeUnsupported = errors.New("windows vulkan probe unsupported")
type vulkanPhysicalDevice struct {
Name string
Integrated bool
}
var probeLlamaServerVulkanDevices = func(_ []string) ([]vulkanPhysicalDevice, error) {
return nil, errWindowsVulkanProbeUnsupported
}
func refineLlamaServerDevices(devices []ml.DeviceInfo, libDirs []string) []ml.DeviceInfo {
devices = refineLinuxROCmDevices(devices)
return refineWindowsVulkanDevices(devices, libDirs)
}
func refineWindowsVulkanDevices(devices []ml.DeviceInfo, libDirs []string) []ml.DeviceInfo {
if runtime.GOOS != "windows" {
return devices
}
var vulkanIndexes []int
for i, device := range devices {
if device.Library != "Vulkan" {
continue
}
vulkanIndexes = append(vulkanIndexes, i)
}
if len(vulkanIndexes) == 0 {
return devices
}
probed, err := probeLlamaServerVulkanDevices(libDirs)
if err != nil {
if !errors.Is(err, errWindowsVulkanProbeUnsupported) {
slog.Debug("windows vulkan device refinement unavailable", "error", err)
}
return devices
}
if !applyWindowsVulkanRefinement(devices, probed) {
return devices
}
return devices
}
func applyWindowsVulkanRefinement(devices []ml.DeviceInfo, probed []vulkanPhysicalDevice) bool {
var vulkanIndexes []int
for i, device := range devices {
if device.Library == "Vulkan" {
vulkanIndexes = append(vulkanIndexes, i)
}
}
if len(probed) < len(vulkanIndexes) {
slog.Debug("windows vulkan device refinement skipped: fewer probed devices than llama-server devices",
"llama_server_count", len(vulkanIndexes), "vulkan_count", len(probed))
return false
}
// Raw Vulkan enumeration can be a superset of llama-server's device list
// (extra ICDs, D3D12 mapping-layer devices) and the two orders can differ,
// so match by name rather than requiring equal counts or matching indexes.
matches := make([]int, len(vulkanIndexes))
used := make([]bool, len(probed))
for i, deviceIndex := range vulkanIndexes {
matches[i] = -1
description := devices[deviceIndex].Description
for j, probedDevice := range probed {
if used[j] || !sameVulkanDeviceName(description, probedDevice.Name) {
continue
}
if matches[i] >= 0 {
if probed[matches[i]].Integrated != probedDevice.Integrated {
slog.Debug("windows vulkan device refinement skipped: ambiguous device name match",
"index", i, "llama_server_name", description)
return false
}
continue
}
matches[i] = j
}
if matches[i] < 0 {
slog.Debug("windows vulkan device refinement skipped: device name mismatch",
"index", i, "llama_server_name", description)
return false
}
used[matches[i]] = true
}
for i, probedIndex := range matches {
devices[vulkanIndexes[i]].Integrated = probed[probedIndex].Integrated
}
slog.Debug("windows vulkan device refinement applied", "devices", len(vulkanIndexes))
return true
}
func sameVulkanDeviceName(a, b string) bool {
return ml.SimilarDeviceDescription(a, b)
}