mirror of
https://github.com/ollama/ollama.git
synced 2026-07-20 00:50:07 -05:00
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
204 lines
5.8 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|