Files
ollama/discover/native_probe_test.go
Sahil KadadekarandGitHub 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

204 lines
5.8 KiB
Go

package discover
import (
"testing"
"unsafe"
"github.com/ollama/ollama/ml"
)
func TestGGMLBackendDevPropsLayout(t *testing.T) {
if unsafe.Sizeof(uintptr(0)) != 8 {
t.Skip("GGML probe layout assertions are for 64-bit builds")
}
var props ggmlBackendDevProps
if got, want := unsafe.Sizeof(props), uintptr(56); got != want {
t.Fatalf("ggmlBackendDevProps size = %d, want %d", got, want)
}
checks := []struct {
name string
got uintptr
want uintptr
}{
{"Name", unsafe.Offsetof(props.Name), 0},
{"Description", unsafe.Offsetof(props.Description), 8},
{"MemoryFree", unsafe.Offsetof(props.MemoryFree), 16},
{"MemoryTotal", unsafe.Offsetof(props.MemoryTotal), 24},
{"Type", unsafe.Offsetof(props.Type), 32},
{"DeviceID", unsafe.Offsetof(props.DeviceID), 40},
{"Caps", unsafe.Offsetof(props.Caps), 48},
}
for _, tt := range checks {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("offset = %d, want %d", tt.got, tt.want)
}
})
}
if got, want := unsafe.Sizeof(ggmlBackendDevCaps{}), uintptr(4); got != want {
t.Fatalf("ggmlBackendDevCaps size = %d, want %d", got, want)
}
}
func TestParseLlamaServerDevicesUsesNativeCUDAComputeCapability(t *testing.T) {
output := `system_info: n_threads = 4 | CUDA : ARCHS = 750,800 |
Available devices:
CUDA0: NVIDIA GeForce GTX 1060 6GB (6063 MiB, 5900 MiB free)
`
devices := parseLlamaServerDevicesWithNative(output, "", []string{"/lib/ollama", "/lib/ollama/cuda_v13"}, []nativeProbeDevice{{
Library: "CUDA",
Index: 0,
IndexMatchesBackend: true,
DeviceID: "0000:01:00.0",
ComputeMajor: 6,
ComputeMinor: 1,
CUDADriverMajor: 13,
NVIDIADriverMajor: 570,
}})
if len(devices) != 0 {
t.Fatalf("got %d devices, want unsupported CUDA device filtered", len(devices))
}
output = `system_info: n_threads = 4 | CUDA : ARCHS = 610,750,800 |
Available devices:
CUDA0: NVIDIA GeForce GTX 1060 6GB (6063 MiB, 5900 MiB free)
`
devices = parseLlamaServerDevicesWithNative(output, "", []string{"/lib/ollama", "/lib/ollama/cuda_v12"}, []nativeProbeDevice{{
Library: "CUDA",
Index: 0,
IndexMatchesBackend: true,
DeviceID: "0000:01:00.0",
ComputeMajor: 6,
ComputeMinor: 1,
CUDADriverMajor: 12,
NVIDIADriverMajor: 570,
}})
if len(devices) != 1 {
t.Fatalf("got %d devices, want 1", len(devices))
}
got := devices[0]
if got.Compute() != "6.1" {
t.Fatalf("compute = %q, want 6.1", got.Compute())
}
if got.PCIID != "0000:01:00.0" {
t.Fatalf("PCIID = %q, want 0000:01:00.0", got.PCIID)
}
if got.Driver() != "12.0" {
t.Fatalf("driver = %q, want 12.0", got.Driver())
}
if got.NVIDIADriverMajor != 570 {
t.Fatalf("NVIDIADriverMajor = %d, want 570", got.NVIDIADriverMajor)
}
}
func TestParseLlamaServerDevicesUsesNativeROCmMetadata(t *testing.T) {
output := `ggml_vulkan: 0 = AMD Radeon RX 7600 | uma: 1 | fp16: 1 |
Available devices:
ROCm0: AMD Radeon RX 7600 (8176 MiB, 7900 MiB free)
`
devices := parseLlamaServerDevicesWithNative(output, "", []string{"/lib/ollama", "/lib/ollama/rocm_v7_2"}, []nativeProbeDevice{{
Library: "ROCm",
Index: 0,
IndexMatchesBackend: true,
DeviceID: "0000:03:00.0",
GFXTarget: "gfx1102",
Integrated: false,
IntegratedKnown: true,
}})
if len(devices) != 1 {
t.Fatalf("got %d devices, want 1", len(devices))
}
got := devices[0]
if got.PCIID != "0000:03:00.0" {
t.Fatalf("PCIID = %q, want 0000:03:00.0", got.PCIID)
}
if got.GFXTarget != "gfx1102" {
t.Fatalf("GFXTarget = %q, want gfx1102", got.GFXTarget)
}
if got.Compute() != "gfx1102" {
t.Fatalf("compute = %q, want gfx1102", got.Compute())
}
if got.Integrated {
t.Fatalf("Integrated = true, want false")
}
}
func TestNVIDIADriverMajorFromDevices(t *testing.T) {
devices := []ml.DeviceInfo{
{DeviceID: ml.DeviceID{Library: "CUDA"}, NVIDIADriverMajor: 565},
}
if got := nvidiaDriverMajorFromDevices(devices); got != 565 {
t.Fatalf("driver = %d, want 565", got)
}
}
func TestMergeNativeProbeDevicesAvoidsUnreliableIndexMatch(t *testing.T) {
tests := []struct {
name string
base []nativeProbeDevice
supplement []nativeProbeDevice
wantLen int
wantPCI string
wantKnown bool
wantIGPU bool
}{
{
name: "filtered sysfs cannot overwrite a different backend device by index",
base: []nativeProbeDevice{{
Library: "ROCm",
Index: 0,
IndexMatchesBackend: true,
DeviceID: "0000:03:00.0",
}},
supplement: []nativeProbeDevice{{
Library: "ROCm",
Index: 0,
DeviceID: "0000:04:00.0",
Integrated: true,
IntegratedKnown: true,
}},
wantLen: 1,
wantPCI: "0000:03:00.0",
},
{
name: "filtered sysfs can still merge by PCI ID",
base: []nativeProbeDevice{{
Library: "ROCm",
Index: 0,
IndexMatchesBackend: true,
DeviceID: "0000:04:00.0",
}},
supplement: []nativeProbeDevice{{
Library: "ROCm",
Index: 1,
DeviceID: "0000:04:00.0",
Integrated: true,
IntegratedKnown: true,
}},
wantLen: 1,
wantPCI: "0000:04:00.0",
wantKnown: true,
wantIGPU: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := mergeNativeProbeDevices(tt.base, tt.supplement)
if len(got) != tt.wantLen {
t.Fatalf("got %d devices, want %d: %#v", len(got), tt.wantLen, got)
}
if got[0].DeviceID != tt.wantPCI {
t.Fatalf("DeviceID = %q, want %q", got[0].DeviceID, tt.wantPCI)
}
if got[0].IntegratedKnown != tt.wantKnown {
t.Fatalf("IntegratedKnown = %v, want %v", got[0].IntegratedKnown, tt.wantKnown)
}
if got[0].Integrated != tt.wantIGPU {
t.Fatalf("Integrated = %v, want %v", got[0].Integrated, tt.wantIGPU)
}
})
}
}