Skip to content

Add managed llama.cpp engine with Windows CUDA support - #116

Closed
nv-pgoode wants to merge 23 commits into
NVIDIA:developfrom
nv-pgoode:codex/llamacpp-core-minimal
Closed

nv-pgoode wants to merge 23 commits into
NVIDIA:developfrom
nv-pgoode:codex/llamacpp-core-minimal

Conversation

@nv-pgoode

@nv-pgoode nv-pgoode commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Changelog title

Managed llama.cpp engine

Changelog body

  • Install and manage llama.cpp from PAIR, with models retained across runtime uninstall.
  • Route llama.cpp requests through the unified proxy to any downloaded model; the engine loads it on demand.
  • Prefer the official CUDA build on compatible NVIDIA systems (Windows x64, Windows ARM64 and Linux), with a recorded fallback when CUDA cannot be used.
  • Record the accelerator every llama.cpp installation actually has (CUDA, ROCm, Metal, Vulkan, CPU) and show it on the engine row.
  • Bound llama.cpp downloads by lack of progress instead of a fixed 30-minute budget; force-stop an engine that ignores the graceful stop.
  • Refuse new inference requests while every eligible node reports itself busy instead of queuing them into a saturated engine.
  • Report a streamed response the client received in full as completed even when the client disconnects right after the final chunk.

Bumps

  • services: minor
  • nvpair-cluster-manager: none
  • nvpair-engine-manager: minor
  • nvpair-errors: none
  • nvpair-job-scheduler: minor
  • nvpair-manual-nodes: minor
  • nvpair-node-info: minor
  • nvpair-node-scanner: minor
  • nvpair-node-settings: none
  • nvpair-proxy: minor
  • nvpair-tui: minor
  • nvpair-ui-broker: minor
  • nvpair-workload-manager: none

Summary

Add managed llama.cpp using the existing Engine Manager, unified proxy and
engine-settings UI. No new service process, no parallel settings implementation,
and no llama.cpp-specific routing rule.

  • Install the official llama app into owned runtime storage; start/stop it;
    download/import/load/unload/delete models; cancel model downloads.
  • On Windows x64, stage the pinned official CUDA app and runtime archives when
    every detected NVIDIA GPU has compute capability 7.5 or newer, and require a
    working CUDA device probe before promotion. Otherwise retain vendor selection
    and record why CUDA was not used; no local CUDA Toolkit installation is required.
  • On Linux, apply the same gate: NVIDIA GPUs at compute capability 7.5 or newer
    run the pinned installer restricted to its CUDA payload (SKIP_VULKAN=1 SKIP_ROCM=1), a CUDA0: device is required before promotion and the transfer
    is retried once; otherwise the unrestricted installer runs and the receipt
    records cuda_not_used. Non-NVIDIA hosts are unaffected. Windows ARM64
    installs the pinned CUDA archives directly; the upstream-latest attempt is
    opt-in (install.upstream_first).
  • Record the verified accelerator: every install receipt (pair-install.json)
    carries source, acceleration_policy and devices from llama cli --list-devices; engine:status exposes them as acceleration/devices; the
    desktop engine row and the TUI show the backend (CUDA, ROCM, METAL, VULKAN, CPU).
  • Bound install and model-pull transfers by lack of progress (10 minutes idle,
    6 hours total) rather than a 30-minute wall clock, so a slow but live link
    finishes and a stalled one fails with that reason. Progress is anything
    observable: the staging tree or model cache growing, downloader stderr, and
    on Windows the child's own I/O counters (PowerShell buffers each download in
    memory). llama.cpp downloads land inside the install stage, never on the
    system temp drive. Fifteen seconds after an ignored graceful stop, the whole
    process group is force-killed. The runtime promotion retries a rename that
    Windows denies while Defender still holds the freshly written binaries.
  • Proxy follow-ups found by live validation: a node answering 429/503 enters a
    one-second busy hold and a new request whose every eligible owner is held is
    refused with a local 503 and Retry-After instead of being queued into the
    saturated engine; a streamed response the client received in full is
    reported completed even if the client closes right after the final chunk.
  • Keep model files when uninstalling the runtime. Treat external runtimes as
    read-only rather than taking ownership of another installation.
  • Use the existing port/settings flow: proxy 8080, engine 8081 by default.
  • Load models on demand: the managed router runs serve --models-autoload as a
    fixed argument, so a request for a downloaded model loads it first (the first
    response waits for the load; the router keeps up to four models resident and
    evicts idle ones). Routing eligibility follows the advertised catalog, exactly
    as it does for Ollama and LM Studio; the loaded-model watcher remains for the
    UI's residency indicator and Load/Eject controls.
  • Preserve client-disconnect and shutdown cleanup, retry/CORS behavior and
    exactly-once workload terminal delivery, with correct workload attribution.
  • Add desktop and TUI engine/model controls, including producer-shaped tests
    for live proxy events and observed model residency.

Excluded: a per-engine switch to turn on-demand loading off (with
loaded-only routing), engine updating, GPU/OS telemetry or chart redesign,
expanded workload browsing, explicit inference-workload cancellation
controls/API, and a per-node pin for llama.cpp requests. Those are independent
follow-ups, not prerequisites for this integration.

Non-goals and known limits

  • Existing installed runtimes keep their current backend. To move an existing
    Vulkan installation to the CUDA selection path, uninstall and reinstall the
    managed runtime; model files are retained.
  • The vendor bucket has no aarch64/linux/cuda/110 payload (Jetson Thor); the
    gate records the missing build as cuda_not_used and the runtime lands on
    Vulkan. Receipts written before this change carry no acceleration_policy
    and the status shows nothing for them until reinstall.
  • Engine Manager's manifest-only onboarding requirement is not met for three
    llama.cpp capabilities (staged official-app installer, on-disk model cache
    operations, router identity probe); they are engine-specific Go accepted only
    for the bundled manifest and are recorded in spec.md as a current gap.
  • The broker's managed-facade coordinator for llama.cpp is a copy of LM
    Studio's with two policy differences; sharing it is left for the next
    managed engine.
  • A cold load longer than the proxy's two-minute first-response budget fails
    that request while the router finishes loading; retry or load explicitly.
  • Under concurrent load, the vendor's Vulkan build on an Intel Arc A770 stopped
    answering once; the proxy's response-header timeout and failover covered it.
    That is a vendor-build behaviour, recorded here so it is not mistaken for a
    routing fault.

Review First

Start with services/nvpair-engine-manager/manifests/llamacpp.json (fixed
serve --models-autoload, declared --host/--port controls), then
services/nvpair-proxy/proxy.go subscribedToNode (one eligibility rule for
every facade), services/nvpair-manual-nodes/manager.go probeLlamaCpp, and
the settings regressions in settings_llama_test.go and
services/nvpair-ui-broker/enginesettings_test.go. For the accelerator work:
llamaacceleration.go (classifier), llamalinuxcuda.go (Linux gate),
stall.go (progress-bounded contexts) and TestLlamaLinuxCUDAGate. For the
proxy follow-ups: the busy hold in proxy.go with busy_test.go, and the
completed-versus-cancelled classification around emitTerminal.

Validation

  • All 13 GitHub checks pass on 81624e8 and again on a7391b0: desktop
    checks, SPDX, release intent, build-script checks on three operating
    systems, the full Linux Go component, cross-process and tooling suites, and
    six platform installer builds.
  • Local Windows checks on 26271a0: all 15 Go modules build and vet. Full Engine Manager,
    broker, TUI, manual-node, cluster-manager, scheduler, workload-manager, errors,
    node-settings, shared and EAP suites pass; focused scanner/node-info and the
    cross-process external-listener refusal check pass. Network-test limits below.
  • Desktop on 26271a0: 260 tests pass, 2 existing skips; typecheck, lint,
    dead-code, service contracts, build-script and SPDX checks pass. Go binaries
    and Electron production bundles build.
  • Windows x64 CUDA on 26271a0: fresh managed CUDA installation passed.
    Four consecutive full GPU journeys passed after one earlier streaming-reload
    timeout. The passes cover cold/warm and streamed inference, unload/reload,
    unknown-model rejection, restart persistence, workload attribution and cleanup.
    The initial timeout remains recorded; its cause is not established.
  • Windows CPU Desktop/TUI on predecessor f305c3b: actual Load/Unload,
    Start/Stop, proxy inference, completed-workload display and clean exit passed.
  • Linux x64 NVIDIA on 26271a0: fresh managed install, model pull and first
    proxied inference passed, followed by warm inference, a completed long stream,
    streamed reload after unload, unknown-model rejection, restart persistence and
    five locally attributed completed workloads. Owned compute ran on the selected
    GPU; memory returned to baseline, both brokers exited normally, test ports and
    owned processes closed, and temporary state was removed. Unrelated GPU process
    identities were preserved. The earlier interrupted harness run is retained.
  • Fleet validation of 81624e8 binaries on 13 lab hosts (2026-09-25), one full
    install/pull/serve/unload/restart/cleanup journey each: Windows ARM64 RTX
    Spark N1X (pinned CUDA), Snapdragon (CPU), Windows x64 RTX 3500 Ada, RTX 5090
    and RTX 5070 (CUDA), RTX 2070 on driver 566 (CUDA probe failed, recorded
    Vulkan fallback), Intel Arc A770+B580, Arc 130V and AMD 840M (Vulkan), Intel
    Mac (CPU), three GB10 DGX Sparks (CUDA), Jetson Thor (Vulkan). It also showed
    that Linux fell to Vulkan silently under download contention and that receipts
    could not tell CUDA from Vulkan; both are addressed by the follow-up commits.
  • Follow-up commits (fefad2c through the head of this branch): Engine Manager,
    proxy and broker suites green, cross-vet for linux/arm64, linux/amd64,
    darwin/arm64, darwin/amd64 and windows/arm64, desktop 266 tests plus
    typecheck/lint/dead-code/service-contract checks, TUI tests, SPDX. Live
    re-validation with those binaries: Intel Arc Windows x64 and Arc 130V (Vulkan
    receipt and status), Apple M1 and M4 Max (Metal receipt after the classifier
    fix), Snapdragon Windows ARM64 (CPU build through the new ARM64 dispatch), RTX
    5090 and RTX 5070 over Wi-Fi (pinned CUDA archives finished after 35- and
    73-minute downloads that the previous 30-minute budget had cut off), a
    two-GPU RTX PRO 6000 workstation (both CUDA devices recorded and used), GB10
    DGX Sparks over slow links (restricted CUDA install with one observed retry
    after the vendor script landed on its CPU build). A 30 KB/s laptop exposed two
    false-stall cases fixed by a7391b0.
  • Desktop application on CUDA (recorded, 2026-09-27): the desktop app built from
    this branch installed llama.cpp with the pinned CUDA archives on a two-GPU RTX
    PRO 6000 workstation through its own Install control, showed the CUDA pill with
    both devices, pulled a model, and paired seven lab nodes into a cluster from the
    GUI (RTX 5090, RTX 5070, RTX 3500 Ada, RTX 2070 on the Vulkan fallback, Arc
    A770/B580, Arc 130V, Snapdragon). Bursts of 24, 24, 24 and 48 concurrent
    768-token streaming requests: 144 of 144 completed at the client, every node
    served, four to six nodes running at once with their performance panels open,
    each completed job attributed to the node that ran it. The run surfaced the
    busy-hold, completed-versus-cancelled and rename-retry follow-ups above.
  • GitHub Actions built unsigned installers for Windows x64/arm64, Linux
    x64/arm64 and macOS x64/arm64 on 81624e8; these are build results, not native
    hardware qualification for every platform.
  • The separately committed desktop startup repair prevents late initial
    snapshots from replacing newer running/stopped events; regression checks pass.
  • Test-only follow-up 81624e8 corrects an assertion that assumed Windows
    environment-name case folding on Unix. Canonical cache overrides remain
    rejected on every platform; lowercase aliases are additionally rejected on
    Windows.

Remaining Qualification

  • The full proxy, scanner, node-info and cross-process suites passed on the
    isolated Linux CI runner. The local Windows gate used the focused network
    selection described above; this does not claim full Windows network-suite coverage.
  • The Windows ARM64 direct-pinned CUDA path of fefad2c is covered by unit
    tests; the ARM64 RTX Spark lab host passed on 81624e8 through the fallback
    path and was reserved during the follow-up runs.
  • Intel Mac re-validation of the follow-up commits was blocked by the lab
    host's endpoint agent, which kills the unsigned test binary during download
    regardless of commit; the Intel Mac passed on 81624e8 earlier the same day.
  • The two-GPU Linux server (RTX 4090 x2) was offline throughout; multi-GPU CUDA
    was qualified on the two-GPU Windows workstation instead.
  • The first Windows CUDA streaming-reload timeout was not reproduced in four
    later full runs or a targeted reload probe, but is not explained or erased.

Terve adapted the earlier integration onto the unified proxy. The earlier
llama.cpp work includes the proxy adaptation from #41; #18 and #36 provided
additional design/review context. This does not represent those PRs as merged
unchanged or claim authorship of their underlying shared PAIR infrastructure.

Checklist

  • Contribution rules read; every commit has a matching DCO sign-off.
  • Required tests and documentation updated; no new dependency added.
  • Changed source/filenames/messages checked for private data and internal links.
  • Release intent declared; bot-owned versions and changelog unchanged.
  • Windows retained-CPU desktop/TUI functional journeys completed.
  • Windows CUDA desktop functional journey completed (recorded, eight-node cluster).
  • Remaining platform/GPU qualification above completed.

pgoode41 and others added 23 commits September 22, 2026 03:44
Register llama.cpp in the shared engine table and give it a facade on
the unified proxy at 8080, with the PAIR-managed engine on 8081. Engine
Manager installs the official llama app into an owned directory from a
checksum-pinned installer or archive recipe (Windows ARM64 tries the
current upstream build first and falls back to pinned CUDA archives;
confirmed non-NVIDIA ARM and Intel Macs use the pinned CPU recipes),
starts and stops it, keeps a private model cache, and exposes download,
import, load, unload, delete and download cancellation. Uninstall
removes only the runtime slots and keeps models.

Routing eligibility for llama.cpp is loaded models only, because the
engine runs with --no-models-autoload. The proxy keeps per-facade
cancellation isolated and delivers exactly one terminal workload event
even when a committed upstream stream is truncated; the classification
of that case follows the foundation's existing mid-stream test. The
broker advertises the engine, probes manual peers for it, and applies
the same engine-settings port path it uses for Ollama and LM Studio,
made profile-generic so a third engine does not fall through two-engine
branches. llama.cpp declares reviewed launch controls (--port, --host)
with serve --no-models-autoload fixed; its owned cache environment is
injected on every launch rather than edited, and the settings preview
refuses attempts to set it.

This is the focused integration replayed onto the public develop
foundation from the internal review branch. Runtime updating is not
offered for llama.cpp (the desktop bridge refuses update rather than
substituting uninstall and reinstall), and the GPU/OS telemetry work is
kept as a separate patch candidate.

Co-authored-by: Terve <ntervalon@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
validateLlamaPath on Unix resolved the whole path with EvalSymlinks and
returned nil as soon as the leaf did not exist, so a symlinked ancestor
of a not-yet-created runtime slot, model file or archive entry was never
inspected, and a dangling link at the leaf passed. The Windows guard
already walks every component up to the volume root and refuses any
reparse point regardless of whether the leaf exists.

Make the Unix guard do the same with Lstat per component: skip missing
components, refuse a symlink anywhere in the chain, return other errors.
Existing real paths keep the strictness they had.

The fixture covers an ordinary tree, a plain missing leaf, a models slot
redirected outside the managed root, missing children beneath it, and a
dangling leaf, and checks the outside marker is untouched. It carries the
!windows build tag; on this Windows host it was compile-checked with
GOOS=linux and GOOS=darwin vet and test builds, not executed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Register the engine type, display name and links; map the llamacpp-proxy
relay source; carry the engine manager's install-support, install-reason
and managed facts through the bridge so an external llama.cpp runtime is
shown read-only and the install decision follows the manager rather than
the displayed OS; keep cached models distinct from loaded ones; and route
model import, load, delete and download cancellation through the owning
engine manager, locally and on peers. The settings editor develop
introduced is reused for llama.cpp and hidden for an external runtime
PAIR does not own.

The bridge refuses the update command for llama.cpp instead of letting
it fall into the generic uninstall-then-install pair; remote update is
refused for every engine as before. Workload identity carries the
engine and proxy run so two engines' equal request ids stay distinct.
The service contract inventory is regenerated for the new methods.

Co-authored-by: Terve <ntervalon@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Describe the managed llama.cpp engine in the product docs: install,
lifecycle, model actions, loaded-only routing, launch settings, data
retention on uninstall, and the terminal interface keys. The inference
dispatcher recognises the engine, the app-data wipe scripts preserve an
unmigrated llama model library instead of deleting it, and shell scripts
are checked out with LF endings so Windows-built packages ship
executable Unix scripts.

Co-authored-by: Terve <ntervalon@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Defer optional desktop/TUI inference-cancel controls and expanded workload browsing while retaining full workload identity and engine/model controls. Restore the public-base live workload table.

Consume llama proxy relay events, nested Engine Manager residency events, and LM Studio model keys. Cover the real consumer boundaries with socket-free regressions for all three engines.

Signed-off-by: pgoode41 <pgoode41@gmail.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
The desktop's service-binary cache keys on a content hash of the Go sources
and module files, but nvpair-engine-manager compiles manifests/*.json into
the binary (`//go:embed manifests/*.json`). A manifest-only change, such as a
pin bump or a launch default, therefore produced a different binary while
the cache still reported cli-bin as current, so a developer or packaging run
could ship stale engine behaviour without noticing.

Hash the embedded manifests with the sources. Verified by building twice
(rebuild, then skip), appending one byte to llamacpp.json (rebuild), and
restoring it (rebuild back to the earlier fingerprint).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
(cherry picked from commit 2dae370af14c1662d0f05c6f3ba63b4e030396bd)
Each fix was verified against source and carries a regression test.

- The paired-node settings relay dropped every llama.cpp snapshot, so a
  peer's llama editor never received live updates; relay every engine in the
  shared table.
- The llama.cpp advertiser polled without the node configuration lock its
  siblings hold, so a tick could re-register lc between a settings apply's
  withdraw and restart.
- The copied engine:set-port reservation guard was unreachable because the
  settings path intercepts the method before the relay; removed with its
  parser.
- The registry pinned one vendor build's literals (archive root name, archive
  count, platform keys); validate the recipe shape and leave the exact pins to
  the tests so a pin bump is a manifest change.
- Install and uninstall discarded the Detect result after promoting or
  removing the runtime; both now fail like the generic paths.
- The fake engine gained the vendor's `download` shape so pull_model, preset
  rejection and cancel_pull are exercised; public Uninstall for llama.cpp is
  covered for runtime-only removal, model retention, saved Off and refusal
  while another process serves the port.
- The broker engine-table tests pin llama.cpp's ownership, probe path,
  occupied-facade outcome and the third restore gate.
- The node list offered Start/Stop for an observe-only external runtime and
  Install where the node reported no recipe; one shared predicate now gates
  the engine row, model manager and node list, and an unreachable Cancel
  button is gone.
- Documentation drift: manual-probe port, engine default port, removed
  workload cancellation, executable count, facade relay list, profile and
  advertiser comments. spec.md and MANIFEST.md now name the bundled driver,
  builtins, identity probe, archive recipe fields, {model_dir} and dotted
  result fields as the fail-closed exception to manifest-only onboarding, and
  EngineStatus's install and managed fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
(cherry picked from commit 9845b82963d827e3abddb3668edf673ab5c770ce)
Managed llama.cpp launched with --no-models-autoload, so a downloaded model
was not served until someone loaded it explicitly, and the stack grew a
second routing rule to match: the proxy consulted a loaded-only inventory for
this one engine, the residency watcher published "unknown" whenever llama
missed an observation, the manual-node probe filtered the router's list down
to loaded ids, and the shared node record carried a loaded-set accessor used
by the proxy alone.

Run the vendor default instead: `serve --models-autoload` is now a fixed
argument (stated explicitly so a LLAMA_ARG_MODELS_AUTOLOAD in the user's
environment cannot flip it). A cached model loads on the first request that
names it, which is how Ollama and LM Studio already behave through PAIR, so
the per-engine eligibility switch, the loaded-set accessor and both filters
go away and every facade routes on the advertised catalog. The loaded-model
watcher and `loadedByEngine` stay for the UI's residency dot and its
Load/Eject controls.

Documentation follows: the explicit-load guidance becomes the on-demand
behaviour (the first response waits for the load; the router keeps up to four
models resident and evicts idle ones), the stale claim that a llama request
can be cancelled from the workload card is dropped because that cancellation
was deferred in dd958ba, and the managed engine port is 8081, not 8082.

A persisted settings switch for turning on-demand loading off is deliberately
not part of this change; it follows separately.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Two sentences still described llama.cpp routing on the loaded set after
931647a made every facade route on the advertised catalog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Ignore per-engine hydration results when a newer state notification arrived while the request was in flight. This prevents an already-running engine from displaying Start and then stopping when toggled.

Cover both running and stopped updates for llama.cpp, Ollama, and LM Studio, plus a later uncontested refresh. Native UI reproduction retained privately; fixed-build UI retest remains pending.

Signed-off-by: pgoode41 <pgoode41@gmail.com>
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>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Two hunks conflicted with develop and are resolved here:

- services/nvpair-engine-manager/lifecycle.go: the HTTP probe now defers
  httpcon.DrainAndClose (develop's connection-reuse fix from NVIDIA#37) after the
  llama.cpp identity check has read the body, instead of closing it unread.
- services/nvpair-proxy/engines.go: develop's per-engine base route sets
  (NVIDIA#27) gain llamaCppBaseRoutes, and the llama.cpp facade profile is built
  with slices.Concat like Ollama and LM Studio, so it also classifies the
  Anthropic /v1/messages route. The llama.cpp router at b10826 serves
  /v1/messages itself, so the facade routes it like the other engines.

The route-role test gains llama.cpp cases for the shared inference routes
and for the router's own /models passthrough.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Every managed llama.cpp install now records what it actually produced:
`pair-install.json` carries `acceleration_policy` and the `llama cli
--list-devices` rows as `devices`, and `engine:status` exposes them as
`acceleration` and `devices` for a managed runtime, so a Vulkan or CPU
landing is visible in the receipt, the desktop engine row and the TUI
instead of silent. On the lab fleet the same vendor installer put one GB10
Spark on CUDA and two identical ones on Vulkan, and Jetson Thor on Vulkan,
with nothing in the receipt to tell them apart.

Linux gets the fail-closed CUDA selection Windows x64 already had. When
every NVIDIA GPU reports compute capability 7.5 or newer, Install runs the
pinned vendor installer restricted to its CUDA payload (SKIP_VULKAN,
SKIP_ROCM), requires a CUDA device before promotion and retries the
transfer once; only when CUDA cannot be had does the unrestricted installer
run, and the receipt records the reason as `cuda_not_used`. Non-NVIDIA
hosts never enter the gate, so Vulkan and CPU keep working as before.

Transfers are bounded by lack of progress rather than a fixed 30-minute
budget, which failed real installs and pulls on slow lab links (a 680 MB
CUDA runtime at 0.3 MB/s needs 38 minutes). The vendor installer's staging
tree and the vendor downloader's stderr are the progress signals; a stall
of ten minutes, or six hours in total, fails with that reason.

Windows ARM64 with NVIDIA hardware installs the pinned CUDA archives
directly. Without a CUDA Toolkit the upstream-latest installer could only
produce a CPU build, fail the CUDA check and fall back to those same
archives after a wasted attempt; that path stays available behind
`install.upstream_first`.

A stop that the engine, or a model child it spawned, ignores for fifteen
seconds is finished with a force-kill of the whole process group. Two
llama processes outlived a stop on an Intel Mac during validation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
…d paths

Live validation of the previous commit on an Apple M1 recorded
acceleration_policy "cpu": ggml names its Metal device "MTL0" and its HIP
device "ROCm0", so the alphabetic-prefix classifier never matched them. The
classifier now maps ggml device prefixes to policy names (cuda, rocm, metal,
vulkan, sycl, opencl, cann, musa) and the desktop type comment follows.

Findings from the independent review of that commit:

- The gated install paths returned ctx.Err() after a cancellation, which
  turned a stall ("no download progress for 10m0s") into "context canceled".
  They return context.Cause(ctx) now; a user Stop still reads as canceled.
- pull_model kept only the first 16 KiB of the vendor's stderr and read it by
  lines, but the vendor redraws its progress in place. Bytes read are now the
  progress signal and the diagnostic keeps head and tail, so a late "No space
  left on device" still classifies.
- Device enumeration on the non-CUDA validation path ran inside the shared
  30-second budget and could fail an install when merely slow; it now runs on
  its own budget after validation and is recorded, not gated.
- The stage-tree watcher is joined before promotion so no "installing"
  heartbeat can follow the terminal progress event.
- A successful retry on the Linux gate records why the first attempt was
  rejected (retried_after); on a DGX Spark the first restricted attempt landed
  on the vendor's CPU build because install.sh has no CPU skip, and only the
  device check caught it. README wording says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
Live runs of the progress-bounded transfers on a 30 KB/s laptop link found
two cases where bytes were flowing but nothing we watched moved, so the
watchdog cancelled a live transfer as a stall after ten minutes:

- The vendor's `llama download` writes nothing to stderr while it transfers
  (its progress bar goes to stdout only on a TTY), so a model pull longer than
  the idle budget always failed. The model cache directory now counts as
  progress while it grows, alongside stderr bytes.
- On Windows the vendor installer downloads through PowerShell, which buffers
  each payload in memory and writes the file at the end, so the staging tree
  stayed flat for the whole transfer. The child's own I/O counters
  (GetProcessIoCounters) now count as progress; Unix tools stream to disk, so
  the tree watcher already covers them there.

Both signals also emit the heartbeat a waiting UI needs. A downloader that
fails without any stderr (seen once on an RTX 5070 host) now has its stdout
tail classified instead, and a bounded excerpt is logged locally; the
user-facing message stays classified. Vendor-installer and pinned-archive
receipts record `source` like the CUDA receipts do.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
The engine row and node list read "External llama.cpp runtime" while
PAIR itself was installing llama.cpp. isExternalRuntime treated every
status other than not-installed with managed !== true as an observed
external runtime. The engine manager always reports managed as a
boolean, so an absent flag is a status PAIR synthesized before any fact
arrived (a placeholder, a discovery-only peer, a pending op on a node
without facts), and the installing status carries the pre-install
managed: false fact. Neither is a detected runtime.

Only a settled running or stopped runtime the manager reports as
unmanaged is external now. The predicate test covers every process
status for undefined, true and false; the state test reproduces the
live install, a local installing status with managed false and a peer
pending op with no facts, and asserts neither renders as external.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
The engine manager wrote every download to the system temp directory. On
2026-09-26 a Windows workstation with 287 MB free on its system drive
failed the pinned 516 MiB CUDA archive set there ("write %TEMP% zip: There
is not enough space on the disk") and fell back to the vendor installer's
Vulkan build. The receipt recorded cuda_not_used correctly, but the
outcome depended on a drive the install never touches while the app-data
drive holding the install stage had room.

The llama.cpp paths (the pinned CUDA and Windows ARM64 archives, the
vendor installer script on every platform, the Linux CUDA-restricted
installer and the opt-in upstream-latest installer) now fetch into a
`.downloads` directory inside their install stage under the managed
install directory, so a transfer only ever depends on the drive that
holds the install. The stage's existing lifecycle applies: it is removed
after a successful promotion and kept for diagnosis after a failure.
Generic engine downloads keep using the system temp directory;
downloadInto takes the target directory and download passes none.

watchTreeGrowth skips that directory. The stage watcher exists for the
vendor installer's silent curl; PAIR's own transfers already report
"downloading N%" and keep the stall context alive, and an "installing"
heartbeat every fifteen seconds would otherwise displace the percent line
for most of a slow transfer. The stall-bounded and 30-minute budgets are
unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
The 2026-09-26 desktop log showed engine:models-changed frames tagged
"llamacpp" that carried Ollama model names, and an Ollama bucket holding
"local_llamacpp" and an LM Studio embedding key. Neither is cross-wiring:
the push is {trigger engine, full multi-engine snapshot} by design, the
broker relays engine:* verbatim, and the desktop reads only the per-engine
loadedByEngine bucket. The odd names came from the engine itself: the
process answering as Ollama on 11434 was a HiveMind gateway whose
/api/tags federates LM Studio models and lists local_llamacpp, and whose
/api/ps repeats rows.

Add regression coverage so a future change cannot turn that appearance
into a real defect:

- engine-manager: sweep two fake engines named ollama and lmstudio that
  share one model name while ollama also reports a llama.cpp-looking name.
  Only the engine whose loaded set moved is the trigger, each ByEngine and
  LoadedByEngine bucket repeats exactly its own engine's inventory, the flat
  union carries the shared name once, and the marshalled push keeps the
  keys the desktop and TUI parse without leaking names across buckets.
- desktop: replay the log payload through the supervisor's broker dispatch
  and assert the Ollama, LM Studio and llama.cpp rows of the self node are
  built from their own buckets, that duplicated residency rows collapse,
  and that a name two engines serve is marked loaded only under the engine
  that reported it resident.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
A local client sent about 100 inference requests a second for
qwen3.8:27b, a model that only this node's own Ollama advertised.
Ollama's queue filled and it answered 503 within milliseconds, but the
proxy admitted every request as a queued workload and, per the spec
§5.1 retry policy, re-dispatched it into the same full queue five times
over fifteen seconds. The desktop log shows 236k queued-state events,
27.6k jobs failed with "upstream returned HTTP 503", and the Jobs view
held a wall of queued cards for minutes. The no-owner path was not
involved: a model nobody advertises is already refused with a local 502
before it becomes a workload, which the extended test now pins down.

A node that answers 429 or 503 now enters a one-second busy hold,
renewed on every such answer and cleared the moment it commits a
response. A new inference request whose every eligible owner is held is
refused at admission with a local 503 and Retry-After, contacts no
engine, and emits no workload event. Admitted requests keep their full
retry budget, so a single owner that is loading a model is still
retried; one free owner disables the gate; control routes are never
gated. The transition into a hold is logged once, the per-request
refusals at debug, so shedding a flood does not itself flood the log.

Tests cover the refusal, the hold lapsing, clear-on-commit, a free owner
among busy ones, control routes, and the no-owner 502 emitting no
workload. README and spec describe the hold alongside the existing
failover and no-owner rules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
…iles

On a Windows lab node with Defender real-time protection, two consecutive
llama.cpp installs failed at the very end: the staged runtime had been
downloaded, validated and receipted, and promoteLlamaRuntime's rename of the
candidate onto `runtime` was denied ("Access is denied"). The third install
succeeded. Reproduced on that node: a directory holding freshly written
executables that were just executed for the validation probes cannot be
renamed for about a second, then can. Nothing else held the files.

The renames in the promotion (current -> previous, candidate -> current, and
the rollback) now retry while the error is one Windows reports for a file
briefly held by another process (access denied, sharing violation, lock
violation), every 250 ms for up to ten seconds. Any other error, and Unix,
return at once. Tests inject a held rename through a package hook and cover
the recovery, a permanent error returned immediately, and the bounded window.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
…early

A client that closes its socket the instant it has read the terminal frame
(`data: [DONE]`, `event: message_stop`, or an NDJSON `"done":true` frame)
cancels the request context before the ReverseProxy copy has consumed the
upstream EOF, which the llama.cpp router sends a few milliseconds later. That
cancel also propagates to the upstream request, so the copy ends with a
context error rather than EOF. Both reporters keyed purely on the cancelled
context: the disconnect watcher emitted "cancelled" at once and the deferred
finalize classified the same condition as cancelled. In a recorded eight-node
run, 21 of 120 fully delivered 200 streams were labelled cancelled; a client
that read to EOF got 24 of 24 completed.

The committed response capture now records when the body is complete: the
route's terminal frame has been written (line-anchored for SSE, so model text
quoting the marker does not count), Content-Length bytes have been written, or
the upstream body reached EOF and its bytes are out. The disconnect watcher
does not emit when the body is complete, and finalize treats a cancelled
context as "cancelled" only when it is not. A close before the last frame, a
failed write to a dead client, and a truncated upstream are classified as
before.

Tests run a real server and raw TCP clients for the OpenAI, Anthropic and
NDJSON dialects in both the late-close and mid-stream-close cases, and the
completion record's line anchoring, content length and EOF paths. spec.md
section 5.4 gains the outcome row and the reasoning.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: pgoode41 <pgoode41@gmail.com>
@Noah-Tervalon-Nvidia

Noah-Tervalon-Nvidia commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

POC, so we're going to close the PR. Going to be bringing in llama.cpp with another targeted commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants