ollama/discover/runner_test.go
Daniel Hiltgen 4c076813be
discover: allow Radeon 8060S iGPU by default (#16429)
Default integrated GPU filtering dropped the supported ROCm gfx1151 Radeon 8060S unless OLLAMA_IGPU_ENABLE was set, so add a ROCm gfx-target allowlist with gfx1151 as the first admitted target.  This iGPU is a known-good iGPU.

Fixes #16423
2026-06-02 11:15:01 -07:00

400 lines
11 KiB
Go

package discover
import (
"context"
"errors"
"runtime"
"testing"
"time"
"github.com/ollama/ollama/llm"
"github.com/ollama/ollama/ml"
)
func TestFilterOverlapByLibrary(t *testing.T) {
type testcase struct {
name string
inp map[string]map[string]map[string]int
exp []bool
}
for _, tc := range []testcase{
{
name: "empty",
inp: map[string]map[string]map[string]int{},
exp: []bool{}, // needs deletion
},
{
name: "single no overlap",
inp: map[string]map[string]map[string]int{
"CUDA": {
"cuda_v12": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 0,
},
},
},
exp: []bool{false},
},
{
name: "100% overlap pick 2nd",
inp: map[string]map[string]map[string]int{
"CUDA": {
"cuda_v12": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 0,
"GPU-cd6c3216-03d2-a8eb-8235-2ffbf571712e": 1,
},
"cuda_v13": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 2,
"GPU-cd6c3216-03d2-a8eb-8235-2ffbf571712e": 3,
},
},
},
exp: []bool{true, true, false, false},
},
{
name: "100% overlap pick 1st",
inp: map[string]map[string]map[string]int{
"CUDA": {
"cuda_v13": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 0,
"GPU-cd6c3216-03d2-a8eb-8235-2ffbf571712e": 1,
},
"cuda_v12": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 2,
"GPU-cd6c3216-03d2-a8eb-8235-2ffbf571712e": 3,
},
},
},
exp: []bool{false, false, true, true},
},
{
name: "partial overlap pick older",
inp: map[string]map[string]map[string]int{
"CUDA": {
"cuda_v13": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 0,
},
"cuda_v12": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 1,
"GPU-cd6c3216-03d2-a8eb-8235-2ffbf571712e": 2,
},
},
},
exp: []bool{true, false, false},
},
{
name: "no overlap",
inp: map[string]map[string]map[string]int{
"CUDA": {
"cuda_v13": {
"GPU-d7b00605-c0c8-152d-529d-e03726d5dc52": 0,
},
"cuda_v12": {
"GPU-cd6c3216-03d2-a8eb-8235-2ffbf571712e": 1,
},
},
},
exp: []bool{false, false},
},
} {
t.Run(tc.name, func(t *testing.T) {
needsDelete := make([]bool, len(tc.exp))
filterOverlapByLibrary(tc.inp, needsDelete)
for i, exp := range tc.exp {
if needsDelete[i] != exp {
t.Fatalf("expected: %v\ngot: %v", tc.exp, needsDelete)
}
}
})
}
}
func TestRecordPersistentRunnerEnv(t *testing.T) {
devices := []ml.DeviceInfo{
{DeviceID: ml.DeviceID{Library: "Metal", ID: "0"}},
{DeviceID: ml.DeviceID{Library: "CUDA", ID: "1"}},
}
recordPersistentRunnerEnv(devices, map[string]string{
"GGML_METAL_TENSOR_DISABLE": "1",
"CUDA_VISIBLE_DEVICES": "1",
})
if got := devices[0].RunnerEnvOverrides["GGML_METAL_TENSOR_DISABLE"]; got != "1" {
t.Fatalf("Metal RunnerEnvOverrides = %q, want %q", got, "1")
}
if _, ok := devices[0].RunnerEnvOverrides["CUDA_VISIBLE_DEVICES"]; ok {
t.Fatal("unexpected CUDA_VISIBLE_DEVICES in Metal RunnerEnvOverrides")
}
if devices[1].RunnerEnvOverrides != nil {
t.Fatalf("unexpected RunnerEnvOverrides recorded for non-Metal device: %#v", devices[1].RunnerEnvOverrides)
}
}
func TestFilterIntegratedGPUs(t *testing.T) {
devices := []ml.DeviceInfo{
{DeviceID: ml.DeviceID{Library: "CUDA", ID: "0"}, Description: "NVIDIA integrated", Integrated: true},
{DeviceID: ml.DeviceID{Library: "Metal", ID: "0"}, Description: "Apple GPU", Integrated: true},
{DeviceID: ml.DeviceID{Library: "Vulkan", ID: "0"}, Description: "AMD Radeon(TM) Graphics", Integrated: true},
{DeviceID: ml.DeviceID{Library: "ROCm", ID: "0"}, Description: "AMD Radeon 780M", Integrated: true, GFXTarget: "gfx1103"},
{DeviceID: ml.DeviceID{Library: "ROCm", ID: "1"}, Description: "AMD Radeon 8060S Graphics", Integrated: true, GFXTarget: "gfx1151"},
{DeviceID: ml.DeviceID{Library: "Vulkan", ID: "1"}, Description: "AMD Radeon RX 6800"},
}
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
t.Setenv("OLLAMA_IGPU_ENABLE", "false")
got := filterIntegratedGPUs(append([]ml.DeviceInfo{}, devices...))
want := []ml.DeviceID{
{Library: "CUDA", ID: "0"},
{Library: "Metal", ID: "0"},
{Library: "Vulkan", ID: "0"},
{Library: "ROCm", ID: "0"},
{Library: "ROCm", ID: "1"},
{Library: "Vulkan", ID: "1"},
}
assertDeviceIDs(t, got, want)
return
}
t.Run("auto admits only allowlisted integrated GPUs", func(t *testing.T) {
got := filterIntegratedGPUs(append([]ml.DeviceInfo{}, devices...))
want := []ml.DeviceID{
{Library: "CUDA", ID: "0"},
{Library: "ROCm", ID: "1"},
{Library: "Vulkan", ID: "1"},
}
assertDeviceIDs(t, got, want)
})
t.Run("explicit true admits all integrated GPUs", func(t *testing.T) {
t.Setenv("OLLAMA_IGPU_ENABLE", "true")
got := filterIntegratedGPUs(append([]ml.DeviceInfo{}, devices...))
want := []ml.DeviceID{
{Library: "CUDA", ID: "0"},
{Library: "Metal", ID: "0"},
{Library: "Vulkan", ID: "0"},
{Library: "ROCm", ID: "0"},
{Library: "ROCm", ID: "1"},
{Library: "Vulkan", ID: "1"},
}
assertDeviceIDs(t, got, want)
})
t.Run("explicit false drops integrated GPUs", func(t *testing.T) {
t.Setenv("OLLAMA_IGPU_ENABLE", "false")
got := filterIntegratedGPUs(append([]ml.DeviceInfo{}, devices...))
want := []ml.DeviceID{{Library: "Vulkan", ID: "1"}}
assertDeviceIDs(t, got, want)
})
}
func assertDeviceIDs(t *testing.T, got []ml.DeviceInfo, want []ml.DeviceID) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("got %d devices, want %d: %#v", len(got), len(want), got)
}
for i := range want {
if got[i].DeviceID != want[i] {
t.Fatalf("device %d = %#v, want %#v", i, got[i].DeviceID, want[i])
}
}
}
func TestRemapFilterIDForUserVisibleDevices(t *testing.T) {
tests := []struct {
name string
env map[string]string
device ml.DeviceInfo
wantID string
wantFilter string
}{
{
name: "cuda numeric parent filter",
env: map[string]string{"CUDA_VISIBLE_DEVICES": "1"},
device: ml.DeviceInfo{
DeviceID: ml.DeviceID{Library: "CUDA", ID: "0"},
FilterID: "0",
},
wantID: "0",
wantFilter: "1",
},
{
name: "cuda uuid parent filter",
env: map[string]string{"CUDA_VISIBLE_DEVICES": "GPU-f3a94ab8-b31d-61ff-9fbb-ce91ac1cdd95"},
device: ml.DeviceInfo{
DeviceID: ml.DeviceID{Library: "CUDA", ID: "0"},
FilterID: "0",
},
wantID: "0",
wantFilter: "GPU-f3a94ab8-b31d-61ff-9fbb-ce91ac1cdd95",
},
{
name: "rocm hip parent filter",
env: map[string]string{"HIP_VISIBLE_DEVICES": "2,0"},
device: ml.DeviceInfo{
DeviceID: ml.DeviceID{Library: "ROCm", ID: "1"},
FilterID: "1",
},
wantID: "1",
wantFilter: "0",
},
{
name: "vulkan parent filter",
env: map[string]string{"GGML_VK_VISIBLE_DEVICES": "1"},
device: ml.DeviceInfo{
DeviceID: ml.DeviceID{Library: "Vulkan", ID: "0"},
FilterID: "0",
},
wantID: "0",
wantFilter: "1",
},
{
name: "no parent filter keeps internal filter id",
device: ml.DeviceInfo{
DeviceID: ml.DeviceID{Library: "CUDA", ID: "0"},
FilterID: "3",
},
wantID: "0",
wantFilter: "3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for key, value := range tt.env {
t.Setenv(key, value)
}
remapFilterIDForUserVisibleDevices(&tt.device)
if tt.device.ID != tt.wantID {
t.Fatalf("ID = %q, want %q", tt.device.ID, tt.wantID)
}
if tt.device.FilterID != tt.wantFilter {
t.Fatalf("FilterID = %q, want %q", tt.device.FilterID, tt.wantFilter)
}
})
}
}
func TestNormalizeROCmDiscoveryEnv(t *testing.T) {
tests := []struct {
name string
env map[string]string
extra map[string]string
wantROCR string
wantSource string
wantOrdinal string
wantSame bool
}{
{
name: "hip becomes rocr",
env: map[string]string{"HIP_VISIBLE_DEVICES": "2"},
wantROCR: "2",
wantSource: "HIP_VISIBLE_DEVICES",
wantOrdinal: "0",
},
{
name: "gpu ordinal becomes rocr",
env: map[string]string{"GPU_DEVICE_ORDINAL": "3"},
wantROCR: "3",
wantSource: "GPU_DEVICE_ORDINAL",
wantOrdinal: "0",
},
{
name: "cuda numeric becomes rocr",
env: map[string]string{"CUDA_VISIBLE_DEVICES": "2,0"},
wantROCR: "2,0",
wantSource: "CUDA_VISIBLE_DEVICES",
wantOrdinal: "0,1",
},
{
name: "rocr wins",
env: map[string]string{"ROCR_VISIBLE_DEVICES": "1", "HIP_VISIBLE_DEVICES": "2"},
wantSame: true,
},
{
name: "cuda uuid does not become rocr",
env: map[string]string{"CUDA_VISIBLE_DEVICES": "GPU-f3a94ab8-b31d-61ff-9fbb-ce91ac1cdd95"},
wantSame: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for key, value := range tt.env {
t.Setenv(key, value)
}
got := normalizeDiscoveryEnvForGOOS("linux", []string{"/lib/ollama", "/lib/ollama/rocm_v7_2"}, tt.extra)
if tt.wantSame {
if got != nil && got["ROCR_VISIBLE_DEVICES"] != "" {
t.Fatalf("ROCR_VISIBLE_DEVICES = %q, want unset", got["ROCR_VISIBLE_DEVICES"])
}
return
}
if got["ROCR_VISIBLE_DEVICES"] != tt.wantROCR {
t.Fatalf("ROCR_VISIBLE_DEVICES = %q, want %q", got["ROCR_VISIBLE_DEVICES"], tt.wantROCR)
}
if got[tt.wantSource] != tt.wantOrdinal {
t.Fatalf("%s = %q, want %q", tt.wantSource, got[tt.wantSource], tt.wantOrdinal)
}
})
}
}
func TestBootstrapDevicesWithStatusWatchdogReturnsResult(t *testing.T) {
want := []ml.DeviceInfo{{DeviceID: ml.DeviceID{Library: "CUDA", ID: "0"}}}
devices, _, err := runBootstrapDevicesWithStatusWatchdog(
t.Context(),
[]string{"/lib/ollama", "/lib/ollama/cuda_v12"},
nil,
func(context.Context, []string, map[string]string) ([]ml.DeviceInfo, *llm.StatusWriter, error) {
return want, nil, nil
},
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(devices) != 1 || devices[0].DeviceID != want[0].DeviceID {
t.Fatalf("devices = %#v, want %#v", devices, want)
}
}
func TestBootstrapDevicesWithStatusWatchdogReturnsOnDeadline(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
defer cancel()
started := make(chan struct{})
release := make(chan struct{})
finished := make(chan struct{})
_, _, err := runBootstrapDevicesWithStatusWatchdog(
ctx,
[]string{"/lib/ollama", "/lib/ollama/rocm_v7_2"},
nil,
func(context.Context, []string, map[string]string) ([]ml.DeviceInfo, *llm.StatusWriter, error) {
close(started)
defer close(finished)
<-release
return nil, nil, nil
},
)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("err = %v, want context deadline exceeded", err)
}
close(release)
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("discovery function was not called")
}
select {
case <-finished:
case <-time.After(time.Second):
t.Fatal("discovery function did not finish after release")
}
}