Skip to content

feat(gpu): custom k3s-CUDA node image + publish workflow (#616) - #635

Merged
shujaatTracebloc merged 4 commits into
developfrom
feat/616-k3s-cuda-image
Aug 6, 2026
Merged

feat(gpu): custom k3s-CUDA node image + publish workflow (#616)#635
shujaatTracebloc merged 4 commits into
developfrom
feat/616-k3s-cuda-image

Conversation

@shujaatTracebloc

@shujaatTraceblocshujaatTracebloc commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What & why

The foundational build artifact for GPU-enabled Windows edges (#616), split from the installer PR (#633) so it can land on develop and be published via CI — GitHub only dispatches a workflow_dispatch workflow from the default branch.

Inert on develop: nothing references these files until #633 wires the installer to the image, so this is a safe, behavior-free landing.

Why a custom image

The stock rancher/k3s image is Alpine with no NVIDIA container runtime, so GPU pods can never schedule on it (the node advertises 0 nvidia.com/gpu). This image rebuilds the same pinned k3s (K3S_TAG == installer K8S_VERSION) on an NVIDIA CUDA Ubuntu base, installs the NVIDIA Container Toolkit, configures containerd for the nvidia runtime, and bakes in the device plugin + nvidia RuntimeClass so the node advertises nvidia.com/gpu on first boot. Based on the official k3d CUDA recipe.

Contents

  • docker/k3s-cuda/Dockerfile — multi-stage k3s + CUDA base
  • docker/k3s-cuda/nvidia-device-plugin-daemonset.yaml — device plugin + nvidia RuntimeClass (v0.14.5)
  • docker/k3s-cuda/build.sh, README.md
  • .github/workflows/build-k3s-cuda.yaml — manual dispatch; push: true publishes to GHCR and validates the Dockerfile builds

After merge

Run build-k3s-cuda with push: true to publish ghcr.io/tracebloc/k3s-cuda:<k3s>-cuda-<base> (make the package public so it pulls with no auth). Then the installer (#633) pulls it automatically at cluster-create — the end-user runs one command, no manual build or pull, GPU or CPU auto-detected.

Part of #616.

🤖 Generated with Claude Code


Note

Low Risk
New build assets and CI drift gates only; no installer or runtime behavior changes until a follow-up PR wires the image.

Overview
Introduces docker/k3s-cuda/ — a multi-stage image that layers the same pinned k3s as CPU nodes onto an NVIDIA CUDA Ubuntu base, installs the Container Toolkit, configures containerd’s nvidia runtime, and auto-applies a baked device plugin + nvidia RuntimeClass (v0.14.5) on first boot. build.sh tags images as <K3S_TAG>-cuda-<CUDA_TAG> for deterministic pulls once the installer is wired (#633).

Adds a manual build-k3s-cuda workflow (workflow_dispatch, optional GHCR push, 60m timeout, unpinned docker/buildx only) so the large CUDA build does not run on every push.

Extends check-facts.sh (and installer-tests path filters + bats fixtures) so K3S_TAG in the Dockerfile, build.sh, and the workflow’s k3s_tag default must match facts.envK8S_VERSION, preventing a k8s bump from referencing an unpublished GPU image.

Inert until #633: nothing in this PR switches cluster create to the new image; it only lands the artifact and CI guardrails.

Reviewed by Cursor Bugbot for commit 76b2eaf. Bugbot is set up for automated code reviews on this repo. Configure here.

Foundational build artifact for GPU-enabled edges, split into its own PR so it can
land on develop and be published via CI (workflow_dispatch only dispatches from the
default branch). Inert on develop — nothing references it until the installer PR
(#633) wires it in.
The stock rancher/k3s image is Alpine with no NVIDIA runtime, so GPU pods can never
schedule on it. This image rebuilds the SAME pinned k3s on an NVIDIA CUDA Ubuntu base,
installs the NVIDIA Container Toolkit, configures containerd for the `nvidia` runtime,
and bakes in the device plugin + `nvidia` RuntimeClass so the node advertises
nvidia.com/gpu on first boot. Based on the official k3d CUDA recipe.
- docker/k3s-cuda/Dockerfile, nvidia-device-plugin-daemonset.yaml, build.sh, README.md
- .github/workflows/build-k3s-cuda.yaml (manual dispatch; push=true publishes to GHCR,
which also validates the Dockerfile builds)
Once published (public), the installer pulls it automatically at cluster-create — the
end-user runs one command; no manual build or pull.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTraceblocshujaatTracebloc self-assigned this Aug 6, 2026
shujaatTracebloc added a commit that referenced this pull request Aug 6, 2026
The Dockerfile + build workflow now live in #635 (targeting develop) so the image
can be published via CI before this installer PR merges. #633 keeps only the
installer wiring that CONSUMES the published image; nothing here changes. After
#635 merges + the image is published, this branch rebases on develop to pick the
build infra back up transitively.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 6, 2026 13:00
Comment thread.github/workflows/build-k3s-cuda.yaml Outdated
Comment threaddocker/k3s-cuda/Dockerfile
Comment threaddocker/k3s-cuda/Dockerfile
Comment thread.github/workflows/build-k3s-cuda.yaml
- workflow: pass GITHUB_TOKEN + actor via `env:` and reference quoted shell vars
instead of interpolating context straight into the run line of a packages:write
job (GHA injection hardening).
- workflow: add `timeout-minutes: 60` so a stalled nvcr.io pull / wedged push
fails fast instead of spinning to the 6h default (parity with sibling workflows).
- Dockerfile: the NVIDIA keyring + apt-list curls now carry the TLS floor +
bounded timeouts inline (`--tlsv1.2 --connect-timeout 30 --max-time 60`), the
way scripts/install.sh spells them out (house rule; a Dockerfile can't source
curl_secure()).
- drift guard: the k3s pin was hardcoded in the Dockerfile ARG, build.sh, and the
workflow input default with a comment CLAIMING the workflow enforced it —
nothing did. Wire all three into check-facts.sh so they're verified == facts.env
K8S_VERSION; a bump now fails CI (and `--write` restamps them) instead of
silently deriving a GPU image tag that was never published (#547 class).
check-facts.sh --check passes; shellcheck clean; workflow YAML valid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c666c37. Configure here.

Comment threadscripts/check-facts.sh
Comment threadscripts/check-facts.sh
shujaatTraceblocand others added 2 commits August 6, 2026 15:23
…635)
Adding the GPU image's k3s pin as check-facts consumers made check-facts.bats
fail — the throwaway repo the tests build only seeds scripts/, so the new
Dockerfile / build.sh / workflow consumers reported "file not found" and the
--check/--write tests went red. Seed those three files into setup() (matching the
spec) so the drift-guard tests exercise them too. Also proves the workflow
extractor picks the k3s_tag default (v… tag) and leaves cuda_tag alone.
Local: check-facts.bats 14/14, bats-hygiene 18/18.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The check-facts --check gate lives in installer-tests.yaml, whose triggers were
path-filtered to scripts/**. A change touching only docker/k3s-cuda/* or the build
workflow never ran the gate, so a K3S_TAG edit in the GPU image could drift from
facts.env and merge green — defeating the drift rows just added (Bugbot). Add
docker/k3s-cuda/** and the build workflow to both the push and pull_request path
filters so the gate runs on those changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc
shujaatTracebloc merged commit 1932eec into developAug 6, 2026
35 of 38 checks passed
@shujaatTracebloc
shujaatTracebloc deleted the feat/616-k3s-cuda-image branch August 6, 2026 14:04
shujaatTracebloc added a commit that referenced this pull request Aug 6, 2026
Resolve conflicts after today's develop advances (#612 chart bumps → 1.9.21,
#635 build-infra + check-facts k3s-cuda consumers + installer-tests paths):
- scripts/manifest.sha256: the only content conflict — regenerated against the
merged tree (authoritative for the final install-k8s.ps1).
- docker/k3s-cuda/* and build-k3s-cuda.yaml: took develop's versions (they live
on develop now via #635; #633 had removed its copies to avoid an add/add).
Verified on the merged tree: install-k8s.ps1 parses clean (GPU funcs intact),
check-facts --check passes, helm-unittest 32/32, full Pester 480 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
shujaatTracebloc added a commit that referenced this pull request Aug 10, 2026
…rt (#616) (#633)
* fix(installer): Layer 1 — never request a GPU the cluster can't provide (#616)
On Windows a machine WITH a working NVIDIA GPU still ran CPU-only: the installer
requested nvidia.com/gpu for every job whenever it merely DETECTED a GPU + driver,
but the GPU is only provisioned in the cluster when K3D_GPU_FLAG is set (the WSL
NVIDIA-toolkit step fully succeeded). When that step didn't complete, jobs got a
GPU request the node couldn't satisfy -> Pending "Insufficient nvidia.com/gpu"
until the SINGLE_NODE fallback downgraded them to CPU. GPU wasted, silently.
Layer 1 (safe, always-correct; CPU fallback preserved):
- Gate GPU_REQUESTS/GPU_LIMITS on K3D_GPU_FLAG (the SAME condition that provisions
the GPU), not on mere detection. Empty gpuVal => no GPU request => training runs
on CPU. The SINGLE_NODE downgrade stays as a belt-and-suspenders net.
- Make the skip LOUD: capture a specific $GPU_SKIP_REASON at each toolkit
early-return (WSL not responding / Ubuntu needs first-run setup / toolkit apt
blocked or timed out / verify failed), surface it in the install summary
("CPU (GPU detected but not enabled: <reason>)") and warn during install.
- Gate the doctor's GPU-test hint on K3D_GPU_FLAG too (don't suggest a test that
can't run).
Layer 2 (actually enabling the GPU in k3d-on-Windows) follows in this PR.
Tests: 4 new Pester tests (values carry no GPU request when not enabled / carry it
when enabled; summary shows CPU+reason vs NVIDIA GPU). Full suite 456 pass.
manifest.sha256 regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): Layer 2a — custom k3s-CUDA node image for GPU edges (#616)
The stock rancher/k3s image is Alpine-based with no NVIDIA container runtime, so
GPU pods can never schedule on it. Add a drop-in replacement k3s node image that
rebuilds the SAME pinned k3s (K3S_TAG == installer K8S_VERSION) on an NVIDIA CUDA
Ubuntu base, installs the NVIDIA Container Toolkit, configures containerd for the
`nvidia` runtime, and bakes in the device plugin + `nvidia` RuntimeClass so the
node advertises nvidia.com/gpu on first boot. Based on the official k3d CUDA recipe.
- docker/k3s-cuda/Dockerfile (multi-stage k3s + CUDA base)
- docker/k3s-cuda/nvidia-device-plugin-daemonset.yaml (device plugin + RuntimeClass, pinned v0.14.5)
- docker/k3s-cuda/build.sh (build/push, tag encodes k3s + CUDA versions)
- docker/k3s-cuda/README.md
- .github/workflows/build-k3s-cuda.yaml (manual dispatch; push=true publishes to GHCR)
Image tag encodes both the k3s pin and CUDA base so a new k8s pin can't silently
reuse a stale GPU image. The installer wiring to actually USE this image (cluster
--image swap, GPU capability probe, RUNTIME_CLASS_NAME=nvidia) lands next in this
PR; CPU fallback (Layer 1) always remains.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): Layer 2b — wire the installer to the custom k3s-CUDA image (#616)
Make a Windows NVIDIA GPU genuinely usable by training pods, with CPU fallback
whenever it can't be wired up.
- Authoritative GPU gate: add Confirm-DockerGpu, which runs
`docker run --rm --gpus all nvidia/cuda:<tag> nvidia-smi`. Docker Desktop uses
its OWN WSL distro, so the toolkit-in-Ubuntu step was never a reliable signal;
actually running a GPU container is. Enable GPU iff the probe passes -> we never
create a `--gpus` cluster that would fail, and DD-GPU works even without the
user's Ubuntu toolkit. Probe failure => K3D_GPU_FLAG cleared + a clear reason =>
CPU (Layer 1).
- Cluster create: use the custom k3s-CUDA image ($K3S_CUDA_IMAGE, env-overridable
via TRACEBLOC_K3S_CUDA_IMAGE, default ghcr.io/tracebloc/k3s-cuda:<K8S_VERSION>-cuda-<base>)
when GPU is enabled; stock rancher/k3s otherwise. Same pinned k3s either way.
- Set RUNTIME_CLASS_NAME=nvidia in values when GPU is enabled, so jobs-manager
threads runtimeClassName: nvidia into every spawned pod (the RuntimeClass is
baked into the CUDA image). Empty otherwise.
Tests: Confirm-DockerGpu behavioral tests (pass/fail/no-GPU) + source guards for
the image swap, probe gating, and RuntimeClass; existing GPU values tests extended
for RUNTIME_CLASS_NAME. Full Pester suite 464 pass. manifest.sha256 regenerated.
NOTE: end-to-end GPU execution needs validation on a real GPU Windows box (build +
publish the image via the build-k3s-cuda workflow, then re-install). CPU path is
unaffected and fully covered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): make the GPU node image mirror-aware (one-command, air-gap) (#616)
The one installer command must do everything with no external steps — the user
never builds or pulls the GPU image by hand; the installer pulls it automatically
at cluster-create. Re-home the default k3s-CUDA image onto TRACEBLOC_IMAGE_REGISTRY
when a private mirror is configured (#585), so the single command also works on a
restricted/air-gapped network, same as every other image. Explicit
TRACEBLOC_K3S_CUDA_IMAGE still overrides. Source guard updated. Pester 464 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(installer): move k3s-CUDA image build infra to its own PR (#635)
The Dockerfile + build workflow now live in #635 (targeting develop) so the image
can be published via CI before this installer PR merges. #633 keeps only the
installer wiring that CONSUMES the published image; nothing here changes. After
#635 merges + the image is published, this branch rebases on develop to pick the
build infra back up transitively.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): pull the private GPU node image with creds — keep it private (#616)
We keep ghcr.io/tracebloc/k3s-cuda PRIVATE rather than public. So the installer must
authenticate to pull it — the end user still runs ONE command; the creds come from env,
not a separate docker login.
- Confirm-GpuImagePullable: derives the registry host from $K3S_CUDA_IMAGE, `docker login`s
with TRACEBLOC_REGISTRY_USERNAME/PASSWORD (the same vars the mirror uses, #585) when set,
then `docker pull`s the image — which both VERIFIES access and PRE-LOADS it so k3d
cluster-create reuses the local copy (no second pull).
- The GPU gate now requires BOTH the passthrough probe AND a pullable image:
`if ((Confirm-DockerGpu) -and (Confirm-GpuImagePullable))`. Either failure => clean CPU
fallback with a specific reason (private-registry hint names the exact env vars to set;
cred-set-but-failed says to check read access) instead of a cluster-create that dies
pulling an unauthorized image.
Tests: 4 Confirm-GpuImagePullable cases (login+pull ok / cred fail / no-cred hint / gate
source-guard); updated the stale probe-gate guard. Full Pester 468 pass. manifest regen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): bound the GPU docker calls with a timeout (Bugbot) (#616)
The new `docker run` (GPU probe), `docker login`, and `docker pull` calls were
unbounded — a wedged Docker daemon, registry, or proxy could hang Step 2 forever
instead of falling back to CPU (installer external-call timeout rule).
Route all three through a new Invoke-DockerCli helper that runs docker in a bounded
background job (Wait-JobWithProgress deadline); on timeout it kills the job and
returns Code=124 so callers fall back to CPU cleanly. Timeouts: probe 180s, login
60s, pull 900s (the GB CUDA image on a slow/proxied network — generous but finite).
Login stdin (the token) is passed in-memory via the arg hashtable, never to disk,
argv, or logs.
Tests: rewired Confirm-DockerGpu / Confirm-GpuImagePullable tests to mock
Invoke-DockerCli (regular function — mockable, unlike a job'd docker), added
timeout-path (Code 124 -> CPU) cases + a source guard that every GPU docker call is
bounded. Full Pester 482 pass. manifest regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): drift-guard the CUDA tag via facts.env (Bugbot) (#616)
CUDA_BASE_TAG (the CUDA base that derives the pulled GPU image tag) was hardcoded in
four places — install-k8s.ps1, docker/k3s-cuda/Dockerfile ARG, build.sh, and the
build-k3s-cuda workflow default — outside facts.env / check-facts, so a CUDA bump in
one spot could leave the installer pulling a GPU image tag that was never built
(#547-class drift, same as the k3s pin).
Add CUDA_TAG=12.4.1-base-ubuntu22.04 to facts.env and wire all four consumers into
check-facts.sh (verified == facts.env; --write restamps them). The workflow extractor
distinguishes the cuda_tag default (…-ubuntu…) from the k3s_tag default (vX…k3s).
check-facts.bats fixture seeds the two new consumers.
check-facts.sh --check passes (15 consumers), check-facts.bats 14/14, shellcheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): kill docker on timeout — no orphaned native process (Bugbot) (#616)
Invoke-DockerCli used Start-Job; Stop-Job stops the PS job but can orphan the native
docker.exe it spawned, so a timed-out run/login/pull could keep going after the
installer fell back to CPU. Run docker as a direct child Process instead and Kill()
it on timeout, so the deadline actually terminates docker. 5.1-safe (.NET Framework
Process API); args have no spaces (flags + image tags) so a plain join is safe; login
stdin is written in-memory, never to disk/argv/logs.
Callers are unchanged (still mock Invoke-DockerCli); updated the source guard to
assert WaitForExit(timeout) + Kill(). Full Pester 482 pass. manifest regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): accurate GPU-skip reasons + drift-check the GPU image (Bugbot) (#633)
Two Bugbot findings:
- GPU_SKIP_REASON now reflects the ACTUAL failure. A probe/pull timeout (Code 124)
reports "timed out", not a GPU-unavailable or credentials error; a real GPU
passthrough failure reports the Docker Desktop hint, not stale Ubuntu guidance.
- Test-K3sVersionDrift now recognizes the GPU node image (…/k3s-cuda:<k3s>-cuda-<base>),
extracting the k3s pin from before "-cuda-", so a GPU cluster no longer silently
escapes the version-drift check after a pin bump (it only parsed rancher/k3s: before).
Tests: reason assertions on the timeout/non-zero paths + a drift source guard for the
GPU image. Full Pester 483 pass. manifest regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): single-node GPU cluster so one card isn't double-counted (Bugbot) (#616)
k3d's --gpus=all exposes the SAME host GPU to every node container, and the
baked-in device-plugin DaemonSet registers it once per node. A default
server+agent cluster therefore advertises nvidia.com/gpu=1 on BOTH nodes --
2 allocatable for 1 physical card -- and Kubernetes can schedule two jobs onto
the same device. Extra k3d nodes live on the same Docker host and all see the
same card, so multi-node can never add real GPUs; it only double-counts.
When the GPU is enabled, collapse to a single node (agents=0) so the card is
advertised exactly once. An explicit user AGENTS is overridden loudly, not
silently. CPU installs are unchanged (topology only shifts inside the
K3D_GPU_FLAG="--gpus=all" branch). +1 source-guard test; full Pester 484 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): reused CPU cluster can't silently get GPU values (Bugbot) (#616)
The GPU gate enables --gpus=all + GPU chart values in Step 2, before
New-K3dCluster runs. On a re-install that REUSES an existing cluster (rather
than recreating it), a cluster first built in CPU mode keeps its stock
rancher/k3s node -- no NVIDIA runtime, advertises 0 GPUs, no `nvidia`
RuntimeClass -- and k3s's node image is fixed at create time. Writing GPU
values against it would strand every experiment Pending: exactly the #616
failure this PR removes.
Reconcile in the reuse path: Confirm-ReusedClusterGpuCapable inspects the
reused server node's image (bounded docker inspect, mirrors
Test-K3sVersionDrift) and, when GPU was requested but the node isn't the
custom k3s-CUDA image, disables GPU for this run (CPU fallback stays safe)
and tells the user to recreate the cluster to enable GPU. The healthy
fast-path already exits before the GPU gate, so it can't wrongly enable GPU.
Pure Test-NodeImageGpuCapable classifier is unit-tested (stock/CUDA/mirror/
empty); the bounded inspect + clear-flag + call-site are source-guarded.
Full Pester 492 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): build the GPU node image locally so GPU installs need no login (#616)
Restores the one-command promise for GPU. The GPU node image was published
private, so enabling GPU forced the user to supply a registry token -- exactly
the friction #612 (CPU) never had, because every image it pulls is public.
Default GPU path is now a LOCAL build: on an NVIDIA machine the installer runs
`docker build` from PUBLIC bases only (rancher/k3s + NVIDIA's public nvcr.io CUDA
base + the public NVIDIA container toolkit), tags it locally, and creates the
cluster with that image -- no registry login, no private package, one command.
Build-GpuNodeImage is idempotent (reuses an already-built image), bounded with a
visible progress bar (Wait-ProcessWithDeadline), sanity-checks that the built
image runs k3s, and on any failure falls back to CPU with a clear reason. The
Dockerfile + device-plugin manifest are embedded (self-contained verified
artifact) and a Pester drift guard keeps them byte-identical to docker/k3s-cuda/*.
An explicit prebuilt image / mirror (TRACEBLOC_K3S_CUDA_IMAGE or
TRACEBLOC_IMAGE_REGISTRY, i.e. air-gap tenants) still PULLS instead of building.
CPU is untouched: the whole block is behind the NVIDIA gate + the docker-run
probe, so a CPU machine builds nothing and gets stock rancher/k3s exactly as
before. Windows-installer only; staging/prod (Linux + chart) are not affected.
Full Pester 499 pass; check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): 5 GPU edge cases from Bugbot (latest, reuse, preflight, fallback) (#616)
Round-3 Bugbot findings on the GPU installer, all CPU-safe:
- Healthy reinstalls skip the GPU fix (Med): the completed+healthy fast path
exited before any GPU reconciliation, so a cluster whose values request GPU on
a CPU-only node kept stranding jobs while looking healthy. Add
Test-HealthyClusterGpuConsistent to the fast path -- warns + recreate remedy.
- K8S_VERSION=latest bypasses the CUDA image (Med): with latest, cluster-create
adds no --image (stock node) but GPU stayed enabled -> stranded jobs. Refuse
GPU for latest/empty K8S_VERSION at the gate (CPU fallback + reason).
- Custom GPU image overrides fail reuse (Med): the reuse check only matched
`k3s-cuda:`, downgrading a renamed/digest mirror image. Test-NodeImageGpuCapable
now also accepts an exact match against the configured $K3S_CUDA_IMAGE.
- Optional GPU build could abort the install (Med): Build-GpuNodeImage created
the build-context dir OUTSIDE its try, so a temp/AV/disk error hit the fatal
trap. Moved dir creation inside try + added a catch -> CPU fallback.
- nvcr.io missing from preflight (Low): Find-Gpu now runs before Test-Preflight,
which probes nvcr.io + nvidia.github.io when an NVIDIA GPU is present and we'll
build. SOFT (warn, not hard-fail) since GPU degrades to CPU.
+10 tests; full Pester 509 pass; check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): base64-embed the GPU build inputs so host lints don't trip (CI green)
The raw here-string embed of docker/k3s-cuda/* broke two host-side guards that scan
install-k8s.ps1:
- check-style.sh flagged `apt-get install ... curl` (curl as a PACKAGE name) as a
bare-curl call.
- install.Tests.ps1's "no non-ASCII in string literals" test flagged the em-dashes
in the embedded Dockerfile/manifest comments (an existing test, not to be rewritten).
Embed the two files as base64 instead (pure ASCII, no `curl` token), decode them to the
build context at build time. Same self-contained + drift-guarded property (the Pester
drift test now decodes and compares to docker/k3s-cuda/*), with nothing for the
container-vs-host lints to catch. No behaviour change to the build itself.
Ran the FULL scripts/tests dir this time (both install.Tests.ps1 and install-k8s.Tests.ps1):
541 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): mirror-aware GPU probe + preflight the configured GPU registry (Bugbot) (#616)
Two air-gap/mirror findings:
- Mirror GPU path still hit Docker Hub (Med): the GPU passthrough probe pulled
nvidia/cuda from Docker Hub regardless of the mirror. Add $CUDA_PROBE_IMAGE,
re-homed onto TRACEBLOC_IMAGE_REGISTRY when set, and use it in Confirm-DockerGpu
-- so a mirrored/air-gapped GPU install doesn't fall back to CPU on a blocked
Docker Hub. (The device-plugin image baked into a PREBUILT node image is the
mirror operator's build-time concern; the installer only pulls that image.)
- Custom GPU registry bypassed preflight (Low): the new nvcr.io probes were added
only on the build path, but the pull path contacts the configured registry. Add
an else-branch that probes the $K3S_CUDA_IMAGE registry host (when it's a real
host, not a bare Docker Hub repo) so an unreachable custom/mirror registry is
surfaced at preflight, not at pull time. All GPU probes stay SOFT (warn -> CPU
fallback), never hard-failing a CPU-capable install.
Full scripts/tests: 543 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): regenerate embedded GPU build inputs from the merged Dockerfile (#616)
Merged develop, which now carries #639's k3s-CUDA Dockerfile fix. PR CI builds the
branch MERGED with develop, so the drift test compared the embedded base64 (generated
from #633's older Dockerfile) against develop's newer one and failed. Regenerated the
base64 embed from the current docker/k3s-cuda/Dockerfile + device-plugin manifest so
the embed matches the merge tree.
Full scripts/tests: 543 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): GPU registry login before the probe + correct Docker Hub host (Bugbot) (#616)
Two mirror/auth findings from the mirror-aware probe change:
- Private mirror probe ran before login (Med): with an authenticated
TRACEBLOC_IMAGE_REGISTRY, Confirm-DockerGpu pulled the mirror-hosted CUDA probe
image BEFORE Confirm-GpuImagePullable did `docker login`, so a credentialed
install was rejected and fell back to CPU. Extracted Connect-GpuRegistry and
call it in the gate BEFORE the probe (and reuse it inside Confirm-GpuImagePullable).
- Docker Hub overrides logged into the wrong host (Low): for a bare `owner/image`
ref, the login host was `owner` instead of Docker Hub. Added the pure
Get-RegistryHost: a segment is a registry only if it has a '.'/':' or is
'localhost'; otherwise login targets docker.io.
Full scripts/tests: 550 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): reconcile GPU on adopted-reuse + re-verify a reused GPU image (Bugbot) (#616)
- Adopted releases retained stale GPU requests (Med): the adopted-reuse path uses
helm --reuse-values, so an older release's GPU_REQUESTS/GPU_LIMITS survived even
after cluster reconciliation cleared K3D_GPU_FLAG -> stranded jobs. Moved the GPU
value decision before the adopted/fresh split and pass the three GPU env keys as
--set-string on the adopted upgrade, so both paths reconcile GPU to this run.
- Failed GPU images were reused unchecked (Med): Build-GpuNodeImage treated any
existing tag as ready. A build that completed but failed the k3s sanity check left
a broken image behind that the next run would reuse. Extracted Test-GpuImageRunsK3s
and run it on the reuse path too; a broken existing image is rebuilt, not reused.
Full scripts/tests: 554 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): node is authoritative for GPU + login to all GPU registries (Bugbot) (#616)
- Mirrored installs could keep GPU on with a dead device plugin (Med): the
device-plugin image is baked as nvcr.io/... and on a blocked mirror/air-gap
network the DaemonSet never becomes ready, so the node advertises 0 GPUs while
GPU requests stayed active -> stranded jobs. Confirm-GpuNode now makes the NODE
authoritative: after the allocatable-GPU wait, a 0 count clears K3D_GPU_FLAG
(before Install-ClientHelm writes values) -> CPU fallback with a clear reason.
- Combined registry overrides logged into the wrong host (Med): with
TRACEBLOC_K3S_CUDA_IMAGE and TRACEBLOC_IMAGE_REGISTRY on different hosts,
Connect-GpuRegistry logged into the node-image host while the probe pulled from
the mirror host -> probe rejected, GPU needlessly disabled. It now logs into every
DISTINCT host across the node image and the probe image.
(The earlier "Static analysis" red was a transient PSGallery download corruption
installing PSScriptAnalyzer -- infra flake, cleared by re-running CI; no code cause.)
Full scripts/tests: 557 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): sanity-check pulled GPU images + full pull-path preflight (Bugbot) (#616)
- Pulled node images bypassed the sanity check (Med): Confirm-GpuImagePullable
accepted any successful pull, so a mis-tagged/broken mirror image enabled GPU and
then aborted k3d cluster-create. It now runs Test-GpuImageRunsK3s on the pulled
image (symmetric with the local build) -> CPU fallback on a broken image.
- GPU preflight missed required hosts (Med): the pull-path preflight probed only
$K3S_CUDA_IMAGE's host. nvcr.io (the baked device-plugin image, pulled at runtime
on BOTH paths) is now probed whenever GPU is enabled, and the pull path probes
every DISTINCT host across the node image AND the probe image (they can differ when
both overrides are set). All SOFT (warn -> CPU fallback), never a hard fail.
Full scripts/tests: 559 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): a null docker-build exit code is not a failure (Bugbot) (#616)
With redirected stdout/stderr, $proc.ExitCode can remain $null after the process
exits (even though Wait-ProcessWithDeadline calls WaitForExit()), so `$proc.ExitCode
-ne 0` misclassified a SUCCESSFUL GPU build as failed and silently dropped GPU.
Mirror the #611 k3d-create idiom: fail only on a CONFIRMED non-zero exit; a null code
defers to the k3s sanity check (Test-GpuImageRunsK3s), which is the authoritative
"did the build produce a working image" success marker. A null exit + working image
now enables GPU; a null exit + broken image still falls back to CPU.
Full scripts/tests: 561 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): GPU node-verification + fallback correctness (Bugbot) (#616)
Four findings surfaced on the develop-merge review:
- GPU verification always read zero (High): Confirm-GpuNode parsed the whole
allocatable map (`jsonpath='{...allocatable}'` renders Go's map[...] with no JSON
quotes), so the "nvidia.com/gpu" regex never matched -> every healthy GPU read as
0. Combined with the new 0-count fallback that reverted ALL GPU installs to CPU.
Fixed: target the scalar field directly (jsonpath '...allocatable.nvidia\.com/gpu').
- Plugin failure left GPU requests enabled (High): when Install-GpuDevicePlugin
returned false, K3D_GPU_FLAG stayed set and Confirm-GpuNode was skipped, so the
chart still requested GPUs -> stranded jobs. Now a real plugin failure (flag still
set) clears the flag -> CPU fallback.
- K8S_VERSION=latest retained the flag (Med): the latest branch recorded a reason
but didn't clear K3D_GPU_FLAG that Install-NvidiaContainerToolkit may have set ->
a stock 'latest' cluster could get --gpus=all without the CUDA image. Now cleared.
- Fast-path read stale GPU values (Low): Test-HealthyClusterGpuConsistent read local
values.yaml, which is only clientId-healed on the adopted path. Now reads the LIVE
Helm release values (helm get values) instead.
Full scripts/tests: 564 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): disable NVIDIA_REQUIRE_CUDA so an older-but-valid driver enables GPU (#616)
Real-box test (RTX 4050, driver 532.10 = CUDA 12.1) showed GPU falling back to CPU.
Root cause: the CUDA base image bakes NVIDIA_REQUIRE_CUDA=cuda>=12.4, and the NVIDIA
container runtime REFUSES to start the container when the driver is older ("unsatisfied
condition: cuda>=12.4"). So a perfectly good GPU on a slightly older driver read as
"Docker can't expose the GPU" and dropped to CPU.
The probe and the k3s node run nvidia-smi / k3s, NOT CUDA workloads -- the real
CUDA/driver compatibility for training is enforced per-pod by each training image -- so
this requirement gate is meaningless for us. Disable it:
- Confirm-DockerGpu probe: docker run ... -e NVIDIA_DISABLE_REQUIRE=1
- node image: ENV NVIDIA_DISABLE_REQUIRE=1 (so the k3s node boots under --gpus on an
older driver); base64 embed regenerated.
Now GPU enables on any driver that can actually pass the GPU through, regardless of the
base image's CUDA version. Full scripts/tests: 566 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): key GPU-image reuse on build-input content hash, not just the tag (Bugbot) (#616)
Adding NVIDIA_DISABLE_REQUIRE didn't change the image tag, and Build-GpuNodeImage
reused any cached image that merely printed a k3s version -- so a stale pre-fix image
would be reused and still fail cluster-create on an older driver.
Stamp a short content hash of the build inputs (embedded Dockerfile + device-plugin
manifest) as a `tracebloc.k3s-cuda-content` label at build time, and on reuse require
that label to match the CURRENT hash (plus the k3s sanity check). Any change to the
build inputs now busts the cache -> a stale image is rebuilt, not reused. Generalizes
beyond this one fix to any future Dockerfile change.
Full scripts/tests: 568 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): fast-path retries GPU + image sanity check exercises --gpus (Bugbot) (#616)
- Healthy fast path prevented GPU retries (Med): a completed CPU-fallback install
exited "nothing to do" before Find-Gpu/probe, so re-running after fixing
Docker/driver/registry could never enable GPU -- contradicting the fallback
guidance. Find-Gpu now runs before the fast path, and when an NVIDIA GPU is present
but the running cluster is CPU-only (Test-RunningClusterGpuCapable) the fast path
falls through to the full walk to retry GPU instead of exiting.
- Image sanity check skipped GPU startup (Med): Test-GpuImageRunsK3s ran without
--gpus, so a stale mirror image lacking NVIDIA_DISABLE_REQUIRE passed the check and
then failed k3d cluster-create on an older driver (aborting instead of CPU fallback).
It now runs WITH --gpus (no bypass), exercising the exact requirement gate
cluster-create hits -> a stale image drops to CPU cleanly.
Full scripts/tests: 571 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): fast-path GPU-live check uses allocatable GPU + bound nvidia-smi (Bugbot) (#616)
- Fast path mistook a CUDA image for a working GPU (Med): Test-RunningClusterGpuCapable
keyed on the node IMAGE name, but a CUDA node with a failed device plugin has 0 GPUs
-- so a CPU-fallback re-run would wrongly take the fast path and never retry. It now
checks the node's LIVE allocatable nvidia.com/gpu (the same authoritative signal
Confirm-GpuNode uses), bounded with --request-timeout.
- Unbounded nvidia-smi in Find-Gpu (Med): Find-Gpu now runs before the fast path, and
its driver/VRAM nvidia-smi probes had no deadline, so a wedged driver could hang every
re-run. Extracted Invoke-BoundedProcess (generic bounded+killable child; Invoke-DockerCli
is now a thin wrapper over it) and Confirm-NvidiaDriver uses it for nvidia-smi (15s).
Full scripts/tests: 572 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): fast path reconciles GPU both directions -- enable AND CPU-drift (Bugbot) (#616)
The prior fix only handled "GPU present but node CPU-only -> retry". The reverse also
leaked: when the node now advertises a GPU (e.g. a delayed device-plugin recovery) but
the live release still requests CPU, the fast path exited "nothing to do" and training
silently stayed on CPU.
The fast path now shortcuts ONLY when GPU is FULLY consistent -- the node advertises a
GPU AND the live release requests one (Test-RunningClusterGpuCapable +
Test-LiveReleaseRequestsGpu). Any other combo on a GPU machine falls through to the full
walk, which reconciles GPU_REQUESTS in either direction. Extracted the live-release GPU
check into Test-LiveReleaseRequestsGpu (shared with Test-HealthyClusterGpuConsistent).
Full scripts/tests: 572 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(gpu): real GPU on Windows/WSL2 via CDI (validated on hardware) (#616)
Validated end-to-end on a live RTX 4050 laptop (driver 532.10, Docker Desktop/WSL2):
a pod that REQUESTS nvidia.com/gpu now schedules AND runs CUDA compute
(vectorAdd "Test PASSED"). Previously GPU always fell back to CPU on Windows.
Why the old approach couldn't work, and what replaces it:
* The NVIDIA k8s DEVICE PLUGIN cannot work on Docker Desktop/WSL2: the GPU is
paravirtualized (/dev/dxg + a WSL driver store), so nvmlInit() returns
ERROR_NOT_SUPPORTED and the plugin registers 0 GPUs. Because it OWNS the
nvidia.com/gpu extended resource it also pins the node at 0, stranding jobs.
-> the node image no longer ships it (RuntimeClass only), and the installer
advertises nvidia.com/gpu itself via a node-status patch (Set-NodeGpuCapacity).
* CUDA itself works fine through CDI. The node image now runs
nvidia-container-runtime in CDI mode and generates the WSL CDI spec AT BOOT
(tracebloc-cdi-boot.sh) -- it must be at boot because the driver-store path
contains a per-machine hash.
* Critical gap found: `nvidia-ctk cdi generate --mode=wsl` OMITS libdxcore.so
(it lives in the standard lib path, not the driver store). Without it libcuda
loads but can't reach /dev/dxg, surfacing as a misleading "CUDA driver version
is insufficient for CUDA runtime version". The boot script injects it.
CPU and Linux are unaffected: the boot script is a strict no-op without /dev/dxg,
the installer only takes the CDI path when /dev/dxg exists in the node (otherwise
the standard device-plugin path is unchanged), and every step failure-falls back
to CPU. Follow-up (separate repo/PR): jobs-manager must set
NVIDIA_VISIBLE_DEVICES=nvidia.com/gpu=all on GPU training pods.
+8 tests; full scripts/tests 580 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): node self-heals GPU capacity + verify the CDI spec (Bugbot) (#616)
- GPU capacity lost after node restart (HIGH): nvidia.com/gpu was advertised by a
one-shot install-time status patch, but a manually patched extended resource is
NOT durable -- the kubelet re-reports node status on start, so a Docker Desktop or
Windows restart dropped it while the chart still requested a GPU, stranding every
job Pending until the installer was re-run. There's no device plugin to own the
resource on WSL2, so the NODE now re-asserts it: the boot script (the entrypoint,
so it runs on every start) backgrounds a reconciler that waits for the local API
and re-patches whenever the capacity is missing/0. A reboot self-heals with no
user action. Interval override: TRACEBLOC_GPU_RECONCILE_SECS.
- CDI success wasn't verified (MED): the WSL2 path printed "GPU acceleration
enabled" after only the capacity patch. Since the boot script guards every step
with `|| true`, a failed `nvidia-ctk cdi generate` would leave us advertising a
GPU pods can't use -- jobs schedule then fail CUDA with no cluster-level signal.
Now the installer verifies /etc/cdi/nvidia.yaml exists and is non-empty in the
node before claiming success, else CPU fallback with a clear reason.
Both stay CPU-safe: the reconciler is backgrounded + fully guarded (can never delay
or block k3s) and only runs when /dev/dxg exists; the spec check failure-falls back
to CPU. +2 tests; full scripts/tests 582 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(gpu): chart wiring for the CDI device selector + path-correct GPU guidance (#616)
Completes the GPU-on-Windows chain: the installer now tells the chart to thread the
CDI device selector into GPU training pods, which jobs-manager consumes as
NVIDIA_VISIBLE_DEVICES (tracebloc/client-runtime#291). Without this the node
advertises a GPU and pods schedule, but CUDA fails inside them.
Chart: documented env.GPU_VISIBLE_DEVICES in values.yaml + values.schema.json. No
template change needed -- jobs-manager-deployment's generic env pass-through already
forwards it to both containers (verified with `helm template`: present when set,
absent when unset/empty). Chart 1.9.27 -> 1.9.28 (version + appVersion in lockstep).
Installer: $GPU_DEVICE_SELECTOR is set ONLY on the WSL2/CDI path and written as
env.GPU_VISIBLE_DEVICES (also forced via --set-string on the adopted-reuse reconcile,
so a stale value can't survive). Empty on a normal device-plugin (Linux) node, where
the plugin owns NVIDIA_VISIBLE_DEVICES and sets concrete GPU UUIDs -- forcing a CDI
selector there would break device resolution.
Also fixes two misleading-guidance findings (Bugbot):
- The 0-GPU skip reason always blamed the NVIDIA device plugin and told operators to
mirror nvcr.io/nvidia/k8s-device-plugin. On the WSL2/CDI path there IS no device
plugin, so that was a dead end. Now branches per path.
- The doctor's GPU smoke-test command omitted runtimeClassName: nvidia (pods only get
the GPU under it), so running it on a WORKING cluster looked like failure. It now
carries the RuntimeClass, and on WSL2/CDI suggests a CUDA workload instead of
nvidia-smi -- verified on real hardware, nvidia-smi fails in a pod on WSL2 (NVML
unsupported) even when CUDA compute works.
+4 tests; full scripts/tests 586 pass / 0 fail; check-style, check-facts, helm lint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): remove a leftover device plugin, correct the CDI cause, collapse SERVERS (Bugbot) (#616)
- CDI path left the device plugin fighting (HIGH): a device plugin OWNS the
nvidia.com/gpu extended resource and on WSL2 re-reports 0 on every sync, so a
DaemonSet left behind by an older install (or by a run where the /dev/dxg probe
transiently missed and we took the plugin path) would overwrite the capacity patch
and keep GPU disabled across ALL re-runs. The CDI path now deletes it first
(--ignore-not-found, so the normal case is a clean no-op) before advertising.
- Wrong CPU-fallback reason on CDI (MED): when Set-NodeGpuCapacity failed, no
GPU_SKIP_REASON was set, so the caller's generic fallback filled in a device-plugin
failure -- the wrong cause on a path that never uses the plugin. Set it here.
- GPU mode ignored SERVERS (MED): AGENTS was collapsed to 0 but SERVERS was left
alone, and EVERY server node runs the boot reconciler and advertises
nvidia.com/gpu=1 for the SAME physical card -- so SERVERS>1 offered N GPUs for one
device. GPU mode now forces servers=1 too, overriding an explicit value loudly.
+3 tests; full scripts/tests 589 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): ship CDI setup as a k3d entrypoint DROP-IN -- the image ENTRYPOINT never ran (#616)
Root cause of "GPU couldn't be wired into the cluster (CDI spec missing)" on a real
Windows box. k3d does NOT use the node image's ENTRYPOINT: it replaces it with its own
/bin/k3d-entrypoint.sh, which runs every /bin/k3d-entrypoint-*.sh drop-in and then execs
k3s. Confirmed on the live node:
Entrypoint = ["/bin/k3d-entrypoint.sh"]
/bin/k3d-entrypoint-{cgroupv2,dns,mounts}.sh <- k3d's own drop-ins
/usr/local/bin/tracebloc-cdi-boot.sh <- ours, never invoked
/etc/cdi <- missing (spec never generated)
So the CDI spec was never created and the installer correctly fell back to CPU (the
verification added in c37e133 is what surfaced this instead of silently "succeeding").
Fix: install the script as /bin/k3d-entrypoint-tracebloc-cdi.sh -- k3d's documented
extension point -- and restore the stock ENTRYPOINT ["/bin/k3s"]. As a drop-in it must
RETURN rather than exec k3s (k3d does that), and must always exit 0: k3d runs drop-ins
with `|| exit 1`, so a non-zero exit would abort the whole node, and GPU is optional.
The capacity reconciler is now fully detached (</dev/null, output to /dev/null) since
the drop-in exits immediately after forking it.
Also corrected the Dockerfile header, which still claimed the image bakes in the device
plugin (it ships only the RuntimeClass now).
+2 regression tests (drop-in path + stock ENTRYPOINT; returns and never execs k3s);
full scripts/tests 591 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): pass the node capacity patch via --patch-file (PS 5.1 ate the JSON quotes) (#616)
Root cause of "Couldn't advertise GPU capacity on the node" on a real Windows box.
Windows PowerShell 5.1 does not preserve embedded double quotes when it builds a native
command line, so
kubectl patch node N --type=json -p '[{"op":"add","path":...,"value":"1"}]'
reached kubectl as [{op:add,...}] -> invalid JSON -> the patch failed on EVERY run. The
same command works by hand only because each quote is escaped as \" there. (This is why
the CDI spec now generates fine but capacity never appeared.)
Fix: write the patch to a temp file and use --patch-file, which sidesteps the shell
entirely. UTF8 without BOM (a BOM breaks kubectl's JSON parse). Also retried 6x/5s: the
node object can still be settling right after cluster-create, and a transient 404 there
would drop an otherwise-working GPU to CPU for the whole run. Temp file always cleaned up.
+1 regression test (asserts --patch-file, no inline -p, BOM-less encoding, retry loop);
full scripts/tests 592 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): print the doctor's GPU test with escaped quotes so it can actually be pasted (#616)
Same PowerShell quote-stripping class as the capacity patch, now in user-facing guidance:
the doctor printed `--overrides='{"spec":...}'`, which dies with "error: Invalid JSON
Patch" when pasted into Windows PowerShell (confirmed on a live box). A suggested command
that can't be run is worse than none, so the JSON is now emitted with every quote escaped
as \" -- the form proven to work by hand.
Verified the rendered string is paste-able:
--overrides='{\"spec\":{\"runtimeClassName\":\"nvidia\", ... }}'
full scripts/tests 592 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): libdxcore injection missed the generator's indentation (real CUDA failure) (#616)
Root cause of "CUDA driver version is insufficient" inside GPU pods on a real box.
Confirmed live: the CDI spec existed and parsed (`nvidia-ctk cdi list` -> 1 device), but
`grep libdxcore /etc/cdi/nvidia.yaml` found NOTHING -- the injection never happened. The
awk anchor was `^ mounts:$` (exactly two spaces), while nvidia-ctk marshals YAML with
yaml.v3 at FOUR spaces, so it never matched. Without libdxcore, libcuda loads but can't
reach /dev/dxg -> that misleading driver error, on a node that advertises a healthy GPU.
Fixes:
- Indent-agnostic anchor (`^[[:space:]]*mounts:[[:space:]]*$`) and the inserted item now
MIRRORS the generator's own first-item indentation instead of hardcoding it. Hardcoding
was doubly wrong: YAML forbids mixing indents in one list, so a fixed 4-space item next
to 2-space items would have made the whole spec unparseable (CDI then injects nothing --
same symptom, different cause). Verified both 2- and 4-space specs parse with libdxcore
present, and that the old awk produced invalid YAML on a 2-space spec.
- The edit is only adopted if `nvidia-ctk cdi list` still parses the result; otherwise the
original spec is restored (GPU without libdxcore beats a silently disabled GPU).
- The installer now verifies libdxcore is IN the spec (not merely that a spec exists) and
falls back to CPU with a specific reason if not -- so this class of silent miss surfaces
at install time instead of as a confusing CUDA error later.
+2 regression tests; full scripts/tests 594 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): never advertise a GPU that CDI can't back, + honest GPU messaging (Bugbot) (#616)
- Reconciler advertised GPU without CDI (HIGH): the boot reconciler re-asserted
nvidia.com/gpu whenever /dev/dxg existed, even if `cdi generate` failed or left an
incomplete spec. The installer already refuses in that case, but a Docker Desktop or
Windows restart re-ran the reconciler and put capacity back onto a node where injection
is broken -- pods schedule, then fail CUDA with no cluster-level signal. The reconciler
now applies the SAME standard (cdi_ok: spec non-empty AND contains libdxcore AND
`nvidia-ctk cdi list` parses) and simply doesn't start otherwise.
- libdxcore path was fragile (MED): hardcoded /usr/lib/x86_64-linux-gnu/libdxcore.so, but
it lives elsewhere on other Docker Desktop / WSL2 versions (/usr/lib/wsl/lib, the WSL
driver store). A miss silently skipped the injection while the spec still passed the
non-empty check -> GPU advertised, CUDA fails with the misleading driver error. Now
probes the known locations, falls back to the linker cache, and mounts it at the path
where it was actually found.
- Green success before verification (MED): `Ok "GPU acceleration enabled (WSL2/CDI)"`
printed at the capacity patch, before Confirm-GpuNode confirms allocatable GPU -- so a
failed verification showed a green enabled line followed by a CPU fallback. Now an
Info "verifying..." line; Confirm-GpuNode's "GPU verified and available" is the only
success claim.
- Misleading preflight warning + stale comments (LOW): the soft GPU preflight always said
the node image "can't be built", including on the pull/mirror path where nothing is
built locally -- now path-aware. Also corrected three comments that still claimed the
device plugin is baked into the node image (this PR ships only the RuntimeClass).
+4 tests; full scripts/tests 598 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): the in-WSL toolkit step no longer claims GPU is ready (Bugbot) (#616)
Install-NvidiaContainerToolkit printed a green "GPU acceleration ready", set
K3D_GPU_FLAG, and CLEARED GPU_SKIP_REASON after merely verifying the toolkit inside the
WSL distro. But Confirm-DockerGpu is the authoritative gate and runs later, so:
- operators could see a green ready line and then a CPU fallback, and
- clearing the skip reason dropped the real cause when the later gate failed.
It now reports only what it established ("NVIDIA Container Toolkit present in <distro>",
Info not Ok) and logs that GPU is still gated on the Docker GPU probe. It no longer
touches K3D_GPU_FLAG or GPU_SKIP_REASON.
Nice invariant this establishes: K3D_GPU_FLAG is now assigned "--gpus=all" in exactly
ONE place -- the authoritative gate -- which a new test pins, along with the toolkit step
no longer setting the flag or clearing the reason.
+1 test; full scripts/tests 599 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(gpu): machine-check the smoke-test JSON instead of eyeballing it (Bugbot) (#616)
Bugbot reported an "extra closing brace" in the WSL2/CDI --overrides payload. Verified
FALSE POSITIVE two ways: a JSON parser accepts it with the correct structure, and this is
the exact escaped form that ran on a live box (kubectl created the pod; it failed later at
CUDA, not at parsing). The `}}}` after "1" is correct -- it closes limits, then resources,
then the container object.
No behaviour change, but the concern deserves to be checkable rather than argued: extracted
the command into a PURE Get-GpuSmokeTestCommand (selector in, string out), so tests can
render both variants, strip the paste-escaping and ConvertFrom-Json them. A genuine brace
slip now fails CI instead of shipping a command that errors on a healthy cluster. Also
removes the duplication between the two branches and gives the diagnostics bundle one
source of truth.
+2 tests (JSON validity for the WSL2/CDI and device-plugin variants), and the previous
source-scraping assertions now go through the builder. Full scripts/tests 601 pass / 0
fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): close version-drift gaps that would cost GPU on a different Windows machine (#616)
Audit prompted by "what breaks on someone else's laptop". The reassuring part first:
a version mismatch CANNOT break the install. The k3d drop-in runs under `|| exit 1`, so a
failure there would abort the node and fail cluster-create -- I verified under real `dash`
(the node image's /bin/sh, no bashisms) that it exits 0 with the toolkit absent, with every
nvidia-ctk subcommand failing, and with a garbage spec. It degrades to CPU instead.
Two gaps found that would silently LOSE GPU on another machine:
1. The NVIDIA Container Toolkit was UNPINNED (`apt-get install nvidia-container-toolkit`),
so two machines built weeks apart could get different builds. The whole WSL2 path leans on
version-sensitive surfaces -- `cdi generate --mode=wsl`, `config --set
nvidia-container-runtime.mode=cdi`, and the exact YAML the generator emits (our libdxcore
injection parses it) -- so a future release changing the spec shape would break GPU on new
installs while existing ones kept working. Pinned to the build validated on hardware
(1.19.1-1) via ARG NCT_VERSION, and it FALLS BACK to latest if that version has aged out of
the apt repo, so a stale pin degrades to "unpinned" rather than failing the build (which
would cost GPU entirely). Also logs `nvidia-ctk --version` so the image records what it got.
2. The cdi_ok gate required `nvidia-ctk cdi list` to EXIST. That subcommand is
version-dependent, so on a toolkit build without it we would have refused to advertise a
perfectly working GPU -- a false negative on someone else's machine. The gate now uses
format-stable structural facts (spec non-empty, declares nvidia.com/gpu, exposes /dev/dxg,
carries the libdxcore mount) and uses `cdi list` only as an EXTRA veto when available.
Verified both directions with a dash harness: a good spec + a toolkit lacking `cdi list` now
advertises the GPU; a spec missing libdxcore still does not.
+2 tests; full scripts/tests 603 pass / 0 fail; dash -n, check-style, check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(gpu): every GPU-enable failure now names an actionable cause (#616)
The residual risks that differ machine to machine all degraded to CPU correctly, but most
collapsed into "the GPU node image build failed (docker build exit 1)" -- true, and useless
to the person holding the laptop. Now each one names what happened and the single thing to
do about it:
- older Docker Desktop / no BuildKit labs frontend -> "update Docker Desktop, or point
TRACEBLOC_K3S_CUDA_IMAGE at a prebuilt image"
- full disk -> "free up space and re-run"
- retired CUDA base tag -> names the tag + TRACEBLOC_CUDA_BASE_TAG
- TLS-inspecting proxy (x509) -> TRACEBLOC_CA_BUNDLE
- blocked/offline registry -> TRACEBLOC_IMAGE_REGISTRY (the air-gap path)
- registry rate limit -> retry or use the mirror
- anything else -> exit code + a pointer to the build output in the log
Classification lives in a PURE Get-GpuBuildFailureReason so every branch is unit-tested.
Also:
- The Docker GPU probe failure now quotes the DETECTED driver version and a concrete
minimum ("update to 525 or newer"). Our install gate accepts 460+, but WSL2 CUDA needs
much newer, so "update your driver" left people unable to tell if theirs qualified.
- GPU-detected-but-not-enabled gets its OWN summary block instead of being crammed into the
Mode line: what happened, why, "fix that then re-run", and the log path. Previously the
reason was a long parenthetical that was easy to miss.
+12 tests; full scripts/tests 615 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): no green line may claim GPU is enabled before verification (Bugbot) (#616)
Third report of this defect, so I swept for the whole family instead of patching one line.
Fixed the reported one: the top-level gate printed Ok "GPU enabled -- cluster will use the
custom k3s-CUDA image with --gpus=all" at Step 2, before cluster-create, the node's CDI
wiring, and Confirm-GpuNode -- any of which can still clear K3D_GPU_FLAG. Now Info, worded
as intent ("GPU support prepared … verified once the node is up").
The sweep found TWO MORE instances on the device-plugin path (pre-existing, not introduced
here, but the same defect and the same misleading sequence): Ok "GPU acceleration enabled."
both when the DaemonSet already existed and after a successful rollout -- printed before
Confirm-GpuNode, which can find 0 allocatable GPUs and fall back to CPU. Both are now Info
lines that say verification is pending. Message-only; no behaviour change on that path.
Established the invariant instead of relying on review: Ok is reserved for facts already
established, Info for intent. Exactly two Ok-level GPU lines remain, and both state something
verified -- the image was built AND passed its k3s sanity check, and the node WAS observed
advertising a GPU. A test pins the count at 2, asserts neither says "enabled", and asserts
the four pre-verification lines are Info.
+3 tests; full scripts/tests 618 pass / 0 fail; check-style + check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): the CDI revert must not require `cdi list` either (Bugbot) (#616)
My own inconsistency, caught correctly. I made the cdi_ok gate tolerate a missing
`nvidia-ctk cdi list` (it's version-dependent), but left the REVERT one block earlier
calling it unconditionally. On a toolkit without that subcommand the sequence was:
libdxcore injected -> `cdi list` "fails" (absent) -> edit reverted -> spec now lacks
libdxcore -> installer reports "the spec is missing libdxcore" -> CPU
i.e. a perfectly good injection thrown away, and a reason that is factually wrong (the
library WAS found and mounted) and unactionable.
The revert now reverts only when the parser is AVAILABLE and actively REJECTS the result --
the same rule as cdi_ok. Verified with a dash harness: with `cdi list` absent, libdxcore
survives in the spec, the spec is still valid YAML, and the reconciler advertises the GPU.
+1 test asserting every `cdi list` use is availability-gated: two probes, exactly two
executable bare uses (comments excluded), and both guarded shapes pinned.
Full scripts/tests 619 pass / 0 fail; dash -n, check-style, check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(gpu): shell-runnable drift guard for the embedded node-image files (review #633)
Review asked for a check-facts/bats-style guard on the base64 embeds, having verified they
are byte-identical today but noting nothing enforces it: check-facts guards the version
PINS, not whole-file embedding, and Get-GpuBuildContentHash only hashes the EMBEDDED copies
for local-build caching -- it never compares them to the repo files. A future edit to
docker/k3s-cuda/* that isn't re-embedded would silently diverge (published image vs the
installer's local build) with nothing failing.
A Pester suite already asserts exactly this and runs in CI on windows-latest AND
ubuntu-latest, so drift was in fact caught -- the reviewer simply had no pwsh to see it.
That is a fair ergonomics gap, so this adds the bash-side mirror: decode each of the three
blobs and diff against docker/k3s-cuda/*, plus non-empty checks (a truncated re-embed) and
the drop-in shape invariant (no `exec /bin/k3s`, always `exit 0` -- the mistake that shipped
once). CI already runs `bats scripts/tests/*.bats`, so it is picked up automatically.
Verified the guard actually bites: appending a line to nvidia-runtimeclass.yaml without
re-embedding turns test 2 red, and restoring it returns 5/5 green. On failure it prints the
diff and the exact remedy (re-embed + regenerate the manifest). bats-hygiene passes (every
assertion carries `|| return 1`, per #527).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): quote whitespace args + fail fast when GPU hosts are known-blocked (Bugbot) (#616)
Two of four findings were real; the other two are refuted with evidence in the threads.
1. (Med) Arguments with whitespace could split. Invoke-BoundedProcess joins args into ONE
command line, so an unquoted value containing a space -- a registry username, or a temp
path under a profile like "C:\Users\First Last\..." -- silently became two arguments and
corrupted the command. Now quotes any arg containing whitespace, leaves already-quoted
values alone (so call sites that quote themselves aren't double-quoted), and preserves an
empty string as a present-but-empty argument. Fixes the class, not just docker login.
2. (Med) A re-run on a restricted network looked hung. Preflight already soft-probes the GPU
download hosts, but the gate then ran the probe (180s) and the build/pull (up to 15-20
min) anyway, so the "re-run to retry GPU" advice we print led to minutes of timeouts
before the CPU fallback. Preflight now records the unreachable host and the gate
short-circuits on it with an actionable reason (set TRACEBLOC_IMAGE_REGISTRY / a prebuilt
image). Defaults empty, so a reachable machine is unaffected.
+5 tests (incl. behavioural joiner cases: space-containing value, already-quoted path, empty
string). Full scripts/tests 625 pass / 0 fail; embed-drift bats 5/5; check-style +
check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(gpu): only path-required hosts may skip GPU -- don't break air-gapped mirrors (Bugbot) (#616)
A regression I introduced one commit earlier. The fast-fail short-circuit treated EVERY soft
GPU host probe as blocking, but on a mirror / prebuilt-image install nvcr.io is unreachable
BY DESIGN -- the node image and the CDI probe image are both re-homed to the mirror. So the
"don't waste minutes timing out" optimisation disabled GPU for exactly the air-gapped case
the mirror exists to serve, and the remedy told the operator to configure the mirror they had
already configured.
Probes now carry gpuBlocking, set only for the hosts the CURRENT path actually needs:
- build path: nvcr.io + nvidia.github.io -> blocking
- mirror/prebuilt path: the configured GPU registry host -> blocking; nvcr.io still probed
(a warning is useful) but NON-blocking
Only a blocking probe arms GPU_HOSTS_UNREACHABLE.
The skip remedy is now path-aware too: on the mirror path it says to check that the configured
GPU registry is reachable and holds the k3s-CUDA image, rather than suggesting a mirror.
+2 tests pinning both directions. Full scripts/tests 627 pass / 0 fail; check-style +
check-facts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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

@shujaatTracebloc@aptracebloc@LukasWodka