Skip to content

Commit 26271a0

Browse files
committed
Install the official CUDA llama.cpp build on Windows x64 NVIDIA GPUs
The official Windows installer selects its CUDA build only when the CUDA Toolkit is installed, so NVIDIA PCs with just the driver received the Vulkan build. When every NVIDIA GPU reports compute capability 7.5 or newer, Install now stages the checksum-pinned b10826 CUDA 13.3 app and CUDA runtime archives and requires a CUDA device before promotion. Any other outcome runs the pinned vendor installer as before and records the reason in the install receipt. Signed-off-by: pgoode41 <pgoode41@gmail.com>
1 parent f305c3b commit 26271a0

13 files changed

Lines changed: 400 additions & 40 deletions

‎services/nvpair-engine-manager/MANIFEST.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ and recovery. Editing `args`/`start` directly remains trusted manifest authoring
149149
| `archive_root` | string | no | The fixed top-level directory a tar archive wraps its files in, stripped on extraction; a single safe path component. Requires `archives`. |
150150
| `upstream_first` | bool | no | Windows ARM64 `llama-app` only: try the current official upstream installer first in a bounded, separate staging attempt and fall back to the pinned `archives` when it fails verification or the device check. |
151151
| `cpu_fetch` | fetch | no | Windows ARM64 `llama-app` only: the pinned official CPU installer used when the hardware inventory confirms a non-NVIDIA device. |
152+
| `cuda_archives` | fetch[] | no | Windows x64 `llama-app` only, alongside a pinned `fetch`: checksum-pinned official CUDA app and CUDA runtime archives installed when every NVIDIA GPU reports compute capability 7.5 or newer and the staged build passes the CUDA device check. Otherwise the `fetch` installer runs. Every entry needs `url` and `sha256`. |
152153

153154
`uninstall.driver: "llama-app"` selects the driver's runtime-only removal (the
154155
`runtime` and `previous` slots), keeping the engine's separate model library
@@ -345,8 +346,8 @@ field is missing, `manifest_version` is unsupported, a platform key isn't
345346
`runtime.mode` is invalid, an action sets none or more than one of
346347
`http`/`cmd`/`remove_path`/`builtin`, an install `driver`, uninstall `driver`
347348
or action `builtin` names an unknown routine or one bundled for another
348-
engine, `archives`/`archive_root`/`upstream_first`/`cpu_fetch` appear without
349-
the `llama-app` driver or its platform policy, a `remove_path` action omits
349+
engine, `archives`/`archive_root`/`upstream_first`/`cpu_fetch`/`cuda_archives`
350+
appear without the `llama-app` driver or its platform policy, a `remove_path` action omits
350351
`root` or `path`, a `result` is set without both `array` and `field` (or a
351352
`result.match` without both `match.field` and a non-empty `match.in`), or
352353
a non-action templated string uses an unknown placeholder.

‎services/nvpair-engine-manager/README.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,20 @@ The existing version/license, candidate promotion/rollback and persistent model
430430
cache lifecycle apply. This artifact requires macOS 13.3 or newer and does not
431431
provide Radeon acceleration.
432432

433-
Ordinary Windows x64 and Linux recipes retain vendor accelerator-to-CPU selection;
433+
On Windows x64 the official installer selects its CUDA build only when the CUDA
434+
Toolkit is installed; with just the NVIDIA driver it selects Vulkan. Install
435+
therefore reads one compute capability per GPU from `nvidia-smi` first. When
436+
every NVIDIA GPU reports 7.5 (Turing) or newer, PAIR stages the checksum-pinned
437+
b10826 CUDA 13.3 app ZIP plus CUDA runtime ZIP declared in
438+
`install.cuda_archives`, which need no toolkit, and requires the same `CUDA0:`
439+
device check before promotion. No NVIDIA GPU, an older or unreadable one, an
440+
unavailable archive or a failed device check runs the pinned installer below in
441+
a separate stage instead, and its receipt records the reason as
442+
`cuda_not_used`. Parent cancellation never starts that installer. A CUDA receipt
443+
records `source: pinned-cuda-archives`, `acceleration_policy: cuda`, the archive
444+
recipe and the reported compute capabilities.
445+
446+
Linux and the Windows x64 installer path retain vendor accelerator-to-CPU selection;
434447
Apple Silicon retains its Metal installer. Selection is not automatic recovery
435448
from a GPU hang or incorrect model answer. `install_supported` and
436449
`install_reason` describe recipe availability and selection requirements, not

‎services/nvpair-engine-manager/executor.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ type Executor struct {
8686
emit func(method string, params any)
8787
client *http.Client
8888
armHardwareQuery func(context.Context, map[string]string) (string, error) // nil uses the native inventory command
89-
ollamaLoadClient *http.Client
89+
// nvidiaComputeQuery returns one compute capability per NVIDIA GPU; nil runs nvidia-smi.
90+
nvidiaComputeQuery func(context.Context, map[string]string) (string, error)
91+
ollamaLoadClient *http.Client
9092
// progress fans install/pull progress to transient subscribers (the ec
9193
// streaming handlers) in addition to the local engine:install-progress
9294
// notification path. See progress.go.

‎services/nvpair-engine-manager/llamaarchives.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import (
1717
const maxLlamaArchiveEntries = 10000
1818

1919
// Merge the pinned official app and dependency bundles into a fresh owned stage.
20-
// The actual ARM64 bundles are flat; relative directories are preserved, never flattened.
21-
func (e *Executor) stageLlamaArchives(ctx context.Context, st *engineState, candidate string) error {
20+
// The actual Windows bundles are flat; relative directories are preserved, never flattened.
21+
func (e *Executor) stageLlamaArchives(ctx context.Context, st *engineState, candidate string, archives []Fetch) error {
2222
if err := validateLlamaOwnedPaths(st.installDir); err != nil {
2323
return err
2424
}
@@ -32,7 +32,7 @@ func (e *Executor) stageLlamaArchives(ctx context.Context, st *engineState, cand
3232
return err
3333
}
3434
remaining, entries := maxDownloadBytes, 0
35-
for _, fetch := range st.plat.Install.Archives {
35+
for _, fetch := range archives {
3636
if err := ctx.Err(); err != nil {
3737
return err
3838
}

‎services/nvpair-engine-manager/llamaarchives_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func TestLlamaArchivesMergeAndRefuseCollision(t *testing.T) {
5858
index++
5959
return &http.Response{StatusCode: 200, ContentLength: int64(len(data)), Body: io.NopCloser(bytes.NewReader(data))}, nil
6060
})}
61-
err := e.stageLlamaArchives(context.Background(), st, candidate)
61+
err := e.stageLlamaArchives(context.Background(), st, candidate, st.plat.Install.Archives)
6262
if (err != nil) != collision {
6363
t.Fatalf("collision=%v: %v", collision, err)
6464
}

‎services/nvpair-engine-manager/llamaarmcpu.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func (e *Executor) prepareLlamaARMCPU(ctx context.Context, st *engineState, stag
106106
return "", nil, fmt.Errorf("official Windows ARM CPU installer: %w", err)
107107
}
108108
candidate := filepath.Join(home, "llama-app")
109-
identity, licenses, err := e.validateLlamaARMApp(ctx, st, candidate, 10826, false)
109+
identity, licenses, err := e.validateLlamaApp(ctx, st, candidate, 10826, false)
110110
if err != nil {
111111
return "", nil, err
112112
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"os"
11+
"path/filepath"
12+
"strconv"
13+
"strings"
14+
"time"
15+
)
16+
17+
// The CUDA 13 llama build carries kernels for compute capability 7.5 (Turing)
18+
// and newer. An older GPU still enumerates as a CUDA device, so the staged
19+
// build's device check alone cannot reject it.
20+
const llamaMinCUDACapability = 7.5
21+
22+
// llamaCUDACapabilities parses nvidia-smi's one-row-per-GPU compute capability output.
23+
func llamaCUDACapabilities(out string) ([]string, error) {
24+
var capabilities []string
25+
for _, line := range strings.Split(out, "\n") {
26+
line = strings.TrimSpace(line)
27+
if line == "" {
28+
continue
29+
}
30+
if _, err := strconv.ParseFloat(line, 64); err != nil {
31+
return nil, fmt.Errorf("nvidia-smi reported an unreadable compute capability %q", line)
32+
}
33+
capabilities = append(capabilities, line)
34+
}
35+
if len(capabilities) == 0 {
36+
return nil, errors.New("nvidia-smi reported no NVIDIA GPU")
37+
}
38+
return capabilities, nil
39+
}
40+
41+
// prepareLlamaWindowsCUDA stages and validates the pinned official CUDA build
42+
// when every NVIDIA GPU can run it. When CUDA does not apply, or the staged
43+
// build reports no CUDA device, it returns no candidate and the reason so the
44+
// caller runs the pinned vendor installer instead. Only cancellation and local
45+
// write failures are errors.
46+
func (e *Executor) prepareLlamaWindowsCUDA(ctx context.Context, st *engineState, stage string) (string, map[string]any, string, error) {
47+
env, err := childEnv(st)
48+
if err != nil {
49+
return "", nil, "", err
50+
}
51+
query := e.nvidiaComputeQuery
52+
if query == nil {
53+
query = func(ctx context.Context, env map[string]string) (string, error) {
54+
return e.runCommandOutput(ctx, []string{"nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader"}, env)
55+
}
56+
}
57+
queryCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
58+
raw, err := query(queryCtx, env)
59+
cancel()
60+
if ctx.Err() != nil {
61+
return "", nil, "", ctx.Err()
62+
}
63+
if err != nil {
64+
return "", nil, fmt.Sprintf("no NVIDIA GPU reported by nvidia-smi: %v", err), nil
65+
}
66+
capabilities, err := llamaCUDACapabilities(raw)
67+
if err != nil {
68+
return "", nil, err.Error(), nil
69+
}
70+
for _, capability := range capabilities {
71+
if value, _ := strconv.ParseFloat(capability, 64); value < llamaMinCUDACapability {
72+
return "", nil, fmt.Sprintf("NVIDIA compute capability %s is below the 7.5 the CUDA build requires", capability), nil
73+
}
74+
}
75+
candidate := filepath.Join(stage, "cuda")
76+
if err := e.stageLlamaArchives(ctx, st, candidate, st.plat.Install.CUDAArchives); err != nil {
77+
if ctx.Err() != nil {
78+
return "", nil, "", ctx.Err()
79+
}
80+
return "", nil, fmt.Sprintf("official CUDA archives: %v", err), nil
81+
}
82+
identity, licenses, err := e.validateLlamaCUDA(ctx, st, candidate, 10826)
83+
if err != nil {
84+
if ctx.Err() != nil {
85+
return "", nil, "", ctx.Err()
86+
}
87+
return "", nil, fmt.Sprintf("official CUDA build: %v", err), nil
88+
}
89+
identity["source"], identity["acceleration_policy"] = "pinned-cuda-archives", "cuda"
90+
identity["compute_capabilities"] = capabilities
91+
identity["archives"], identity["recipe_sha256"] = st.plat.Install.CUDAArchives, llamaArchiveRecipeHash(st.plat.Install.CUDAArchives)
92+
if err := os.WriteFile(filepath.Join(candidate, "THIRD-PARTY-LICENSES.txt"), []byte(licenses), 0600); err != nil {
93+
return "", nil, "", err
94+
}
95+
return candidate, identity, "", ctx.Err()
96+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"crypto/sha256"
10+
"encoding/hex"
11+
"encoding/json"
12+
"errors"
13+
"fmt"
14+
"io"
15+
"net/http"
16+
"os"
17+
"path/filepath"
18+
"reflect"
19+
"strings"
20+
"testing"
21+
)
22+
23+
type windowsCUDAFixture struct {
24+
*llamaUpstreamFixture
25+
vendor *Fetch
26+
failArchive bool
27+
onArchive func()
28+
}
29+
30+
// newWindowsCUDAFixture shapes the recipe like the bundled Windows x64 one: a
31+
// pinned vendor installer plus a pinned CUDA app and runtime bundle.
32+
func newWindowsCUDAFixture(t *testing.T, capabilities string, queryErr error, cuda llamaRuntimeFixture) *windowsCUDAFixture {
33+
t.Helper()
34+
upstream := newLlamaUpstreamFixture(t, llamaRuntimeFixture{Version: "version: 0.4.0-dev (build 10826, commit fixture)", Licenses: "vendor fixture license", Devices: "Available devices:\n Vulkan0: Fixture GPU"})
35+
script := []byte(`$ErrorActionPreference='Stop'
36+
if ($env:LLAMA_VERSION -ne 'b10826' -or $env:SKIP_INSTALL -ne '1' -or $env:SKIP_CUDA -or $env:SKIP_VULKAN) {throw 'vendor selection was restricted'}
37+
$stagePath=Join-Path $env:USERPROFILE 'llama-app'
38+
New-Item -ItemType Directory -Path $stagePath -Force | Out-Null
39+
Copy-Item -LiteralPath $env:FAKE_LLAMA_BIN_SOURCE -Destination (Join-Path $stagePath 'llama.exe')
40+
Copy-Item -LiteralPath $env:FAKE_LLAMA_FIXTURE_SOURCE -Destination (Join-Path $stagePath '.llama-fixture.json')
41+
`)
42+
digest := sha256.Sum256(script)
43+
f := &windowsCUDAFixture{llamaUpstreamFixture: upstream, vendor: &Fetch{URL: "https://example.invalid/pinned-vendor.ps1", SHA256: hex.EncodeToString(digest[:])}}
44+
bin, err := os.ReadFile(fakeEngineBin)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
cudaJSON, _ := json.Marshal(cuda)
49+
bundles := [][]byte{llamaFixtureZip(t, map[string][]byte{"llama.exe": bin, ".llama-fixture.json": cudaJSON}), llamaFixtureZip(t, map[string][]byte{"cudart64_13.dll": []byte("fixture CUDA runtime")})}
50+
install := f.st.plat.Install
51+
install.UpstreamFirst, install.CPUFetch, install.Archives, install.Fetch, install.CUDAArchives = false, nil, nil, f.vendor, nil
52+
for i, bundle := range bundles {
53+
h := sha256.Sum256(bundle)
54+
install.CUDAArchives = append(install.CUDAArchives, Fetch{URL: fmt.Sprintf("https://github.com/ggml-org/llama.cpp/releases/download/b10826/fixture-cuda-%d.zip", i), SHA256: hex.EncodeToString(h[:])})
55+
}
56+
f.e.nvidiaComputeQuery = func(context.Context, map[string]string) (string, error) { return capabilities, queryErr }
57+
f.e.client = &http.Client{Transport: llamaFixtureTransport(func(r *http.Request) (*http.Response, error) {
58+
url := r.URL.String()
59+
f.requests[url]++
60+
body := []byte(nil)
61+
if url == f.vendor.URL {
62+
body = script
63+
}
64+
for i, fetch := range install.CUDAArchives {
65+
if fetch.URL != url {
66+
continue
67+
}
68+
if f.onArchive != nil {
69+
f.onArchive()
70+
}
71+
if f.failArchive {
72+
return nil, errors.New("fixture CUDA archive unavailable")
73+
}
74+
body = bundles[i]
75+
}
76+
if body == nil {
77+
return nil, errors.New("unexpected fixture request")
78+
}
79+
return &http.Response{StatusCode: http.StatusOK, ContentLength: -1, Body: io.NopCloser(bytes.NewReader(body))}, nil
80+
})}
81+
return f
82+
}
83+
84+
func (f *windowsCUDAFixture) archiveRequests() int {
85+
n := 0
86+
for _, fetch := range f.st.plat.Install.CUDAArchives {
87+
n += f.requests[fetch.URL]
88+
}
89+
return n
90+
}
91+
92+
// Every NVIDIA GPU at 7.5 or newer installs the pinned CUDA build; anything that
93+
// rules CUDA out installs the vendor build and records why, without mixing the
94+
// two stages.
95+
func TestLlamaWindowsCUDASelection(t *testing.T) {
96+
noDevice := cudaLlamaFixture(10826)
97+
noDevice.Devices = "Available devices:\n CPU0: Fixture CPU"
98+
for _, tc := range []struct {
99+
name, capabilities, note string
100+
queryErr error
101+
cuda llamaRuntimeFixture
102+
failArchive bool
103+
archiveRequests int
104+
}{
105+
{name: "turing-or-newer", capabilities: "8.9\n12.0\n", cuda: cudaLlamaFixture(10826), archiveRequests: 2},
106+
{name: "no-nvidia", queryErr: errors.New("executable file not found"), cuda: cudaLlamaFixture(10826), note: "no NVIDIA GPU"},
107+
{name: "pre-turing", capabilities: "8.9\n6.1\n", cuda: cudaLlamaFixture(10826), note: "below the 7.5"},
108+
{name: "unreadable", capabilities: "[N/A]\n", cuda: cudaLlamaFixture(10826), note: "unreadable"},
109+
{name: "no-cuda-device", capabilities: "8.9\n", cuda: noDevice, note: "CUDA device", archiveRequests: 2},
110+
{name: "archive-unavailable", capabilities: "8.9\n", cuda: cudaLlamaFixture(10826), failArchive: true, note: "official CUDA archives", archiveRequests: 1},
111+
} {
112+
t.Run(tc.name, func(t *testing.T) {
113+
f := newWindowsCUDAFixture(t, tc.capabilities, tc.queryErr, tc.cuda)
114+
f.failArchive = tc.failArchive
115+
if err := f.e.installLlamaApp(context.Background(), f.st); err != nil {
116+
t.Fatal(err)
117+
}
118+
receipt := f.receipt(t)
119+
if got := f.archiveRequests(); got != tc.archiveRequests {
120+
t.Fatalf("CUDA archive requests = %d, want %d", got, tc.archiveRequests)
121+
}
122+
_, runtimeErr := os.Stat(filepath.Join(f.st.installDir, "runtime", "cudart64_13.dll"))
123+
if tc.note == "" {
124+
capabilities, _ := receipt["compute_capabilities"].([]any)
125+
if receipt["source"] != "pinned-cuda-archives" || receipt["acceleration_policy"] != "cuda" || receipt["cuda_device_verified"] != true ||
126+
len(capabilities) != 2 || receipt["recipe_sha256"] != llamaArchiveRecipeHash(f.st.plat.Install.CUDAArchives) {
127+
t.Fatalf("wrong CUDA provenance: %#v", receipt)
128+
}
129+
if f.requests[f.vendor.URL] != 0 || runtimeErr != nil {
130+
t.Fatal("CUDA install ran the vendor installer or lost the CUDA runtime")
131+
}
132+
return
133+
}
134+
note, _ := receipt["cuda_not_used"].(string)
135+
if receipt["installer_sha256"] != f.vendor.SHA256 || !strings.Contains(note, tc.note) {
136+
t.Fatalf("vendor install lacks its provenance or CUDA reason: %#v", receipt)
137+
}
138+
if f.requests[f.vendor.URL] != 1 || !os.IsNotExist(runtimeErr) {
139+
t.Fatal("vendor install skipped its installer or merged CUDA bytes")
140+
}
141+
})
142+
}
143+
}
144+
145+
// A parent cancellation during the CUDA download ends the install cancelled; it
146+
// never authorizes the vendor installer.
147+
func TestLlamaWindowsCUDACancelDoesNotFallBack(t *testing.T) {
148+
f := newWindowsCUDAFixture(t, "8.9\n", nil, cudaLlamaFixture(10826))
149+
model := f.retainModels(t)
150+
ctx, cancel := context.WithCancel(context.Background())
151+
defer cancel()
152+
f.onArchive = cancel
153+
if err := f.e.installLlamaApp(ctx, f.st); !errors.Is(err, context.Canceled) {
154+
t.Fatalf("cancellation lost: %v", err)
155+
}
156+
if f.requests[f.vendor.URL] != 0 {
157+
t.Fatal("cancelled CUDA install ran the vendor installer")
158+
}
159+
f.assertNothingPromoted(t, model)
160+
}
161+
162+
func TestLlamaWindowsCUDARecipe(t *testing.T) {
163+
mf, _ := buildRegistry("").Get("llamacpp")
164+
want := []Fetch{
165+
{URL: "https://github.com/ggml-org/llama.cpp/releases/download/b10826/llama-b10826-bin-win-cuda-13.3-x64.zip", SHA256: "34549bd7c46717333277d7bfc4c8617a12be0fad33a5253748e59e1085ffda4f"},
166+
{URL: "https://github.com/ggml-org/llama.cpp/releases/download/b10826/cudart-llama-bin-win-cuda-13.3-x64.zip", SHA256: "1462a050eb4c684921ba51dcc4cc488a036674c3e73e9945ee705b854808d03e"},
167+
}
168+
if p := mf.Platforms["windows/amd64"]; !reflect.DeepEqual(p.Install.CUDAArchives, want) || p.Install.Fetch == nil {
169+
t.Fatalf("Windows x64 lost its pinned CUDA build or vendor fallback: %+v", p.Install)
170+
}
171+
for key, p := range mf.Platforms {
172+
if key != "windows/amd64" && len(p.Install.CUDAArchives) > 0 {
173+
t.Fatalf("%s acquired the Windows x64 CUDA recipe", key)
174+
}
175+
}
176+
if _, reason := llamaInstallSupport("windows", "amd64"); !strings.Contains(reason, "CUDA") {
177+
t.Fatalf("Windows x64 support reason omits the CUDA policy: %q", reason)
178+
}
179+
for _, mutation := range []string{"other-platform", "no-fallback", "unpinned-fallback", "unpinned-archive", "upstream-first"} {
180+
t.Run(mutation, func(t *testing.T) {
181+
fresh, _ := buildRegistry("").Get("llamacpp")
182+
p, key := fresh.Platforms["windows/amd64"], "windows/amd64"
183+
switch mutation {
184+
case "other-platform":
185+
key = "linux/amd64"
186+
case "no-fallback":
187+
p.Install.Fetch = nil
188+
case "unpinned-fallback":
189+
p.Install.Fetch = &Fetch{URL: p.Install.Fetch.URL}
190+
case "unpinned-archive":
191+
p.Install.CUDAArchives[0].SHA256 = ""
192+
case "upstream-first":
193+
p.Install.UpstreamFirst = true
194+
}
195+
if err := p.validate(key); err == nil {
196+
t.Fatal("invalid Windows x64 CUDA recipe accepted")
197+
}
198+
})
199+
}
200+
}

0 commit comments

Comments
 (0)