Uh oh!
There was an error while loading. Please reload this page.
fix(installer): enable GPU on Windows — CPU fallback + real GPU support (#616) - #633
Conversation
…de (#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>
…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>
… 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>
…ir-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>
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(gpu): custom k3s-CUDA node image + publish workflow (#616) 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> * fix(gpu): resolve Bugbot findings on the k3s-CUDA build infra (#635) - 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> * test(gpu): seed the k3s-CUDA consumers into the check-facts sandbox (#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> * ci(gpu): run the check-facts drift gate on GPU-image changes too (#635) 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> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… 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>
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>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Second re-merge as develop kept advancing (chart → 1.9.25, READY_TIMEOUT 300→600 via #562). Conflicts: - install-k8s.ps1: kept the #616 GPU globals AND develop's ReadyTimeout=600 (600 is required — check-facts verifies it against facts.env). - manifest.sha256: regenerated against the merged tree. Verified: install-k8s.ps1 parses clean, check-facts --check passes, helm-unittest 32/32, full Pester 480 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
) 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>
Uh oh!
There was an error while loading. Please reload this page.
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>
…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>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
… (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>
Uh oh!
There was an error while loading. Please reload this page.
…ted (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>
Uh oh!
There was an error while loading. Please reload this page.
…bot) (#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>
Uh oh!
There was an error while loading. Please reload this page.
…d 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>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…t, 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>
… 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>
Uh oh!
There was an error while loading. Please reload this page.
…ot) (#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>
Uh oh!
There was an error while loading. Please reload this page.
… (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>
shujaatTracebloc
commented
Aug 10, 2026
Note for whoever promotes this (not a change request on the PR): This PR bumps the chart to 1.9.28. The latest published stable chart is 1.9.15, which predates the Nothing extra to do: promoting this through the normal release train publishes the fix. Flagging it only so the ordering is understood if someone tests a fresh Windows install against the currently-published chart and sees an ingest failure. Per-cluster workaround in the meantime, inside the node: (The confusing error that surfaced instead of a permissions message is fixed separately in tracebloc/cli#472.) |
… 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>
Uh oh!
There was an error while loading. Please reload this page.
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>…(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>
Uh oh!
There was an error while loading. Please reload this page.
aptracebloc
left a comment
There was a problem hiding this comment.
@shujaatTracebloc (cc @saadqbal, code-owner) — Exceptionally careful work. I verified the R8/drift/chart mechanics and the core logic against a fresh clone rather than trusting the description; two items I'd like resolved before I approve, neither a code-correctness blocker.
Verified empirically
- ✅ R8 manifest re-signed correctly —
shasum -c scripts/manifest.sha256→ all 18 pinned scripts OK, including the heavily-modifiedinstall-k8s.ps1. The new boot script is correctly not pinned (baked into the node image, not run by the signed bootstrap). - ✅ check-facts drift green including the 4 new
CUDA_TAGrows (ps1CUDA_BASE_TAG, Dockerfile, build.sh, workflow — all12.4.1-base-ubuntu22.04);check-facts.batspasses. - ✅ Layer-1 gate is exactly right —
$gpuVal/$runtimeClass/$gpuSelectordefault empty, set only underGPU_VENDOR=nvidia ∧ NVIDIA_DRIVER_OK ∧ K3D_GPU_FLAG≠""(the provision condition, not detection); same triple guardsSet-NodeGpuCapacity, the summary, and the doctor hint. The selector additionally rides the WSL2-only$GPU_DEVICE_SELECTOR, so Linux device-plugin nodes get an emptyGPU_VISIBLE_DEVICES— consistent with client-runtime#291. Upgrade path force-clears stale values via--set-string. - ✅ CDI boot entrypoint is genuinely fail-safe — no-op without
/dev/dxg, every step|| true, alwaysexit 0, libdxcore path-probe + linker-cache fallback, parse-or-revert on the YAML edit,cdi_ok-gated detached capacity reconciler across restarts. RuntimeClass-only (no device plugin) correctly reasoned. - ✅ Security invariants intact —
runtimeClassName: nvidiaonly selects the containerd handler; CDI injects the device +ro,nosuid,nodevmounts; no securityContext relaxation. Training-pod isolation unchanged. - ✅ Chart —
GPU_VISIBLE_DEVICESoptional string, documented, empty-by-default;helm lintclean; v1.9.28.
🟡 1. The embedded base64 has no drift guard
install-k8s.ps1 embeds the Dockerfile, RuntimeClass, and boot script as base64 (K3S_CUDA_*_B64) so the signed bootstrap can build the image locally. I decoded all three — currently byte-identical to docker/k3s-cuda/* ✓. But nothing enforces that: check-facts guards the version pins, not the full-file embedding, and Get-GpuBuildContentHash only hashes the embedded copies (local-build caching), never comparing them to the repo files. A future edit to docker/k3s-cuda/Dockerfile or the boot script that isn't re-embedded would silently diverge — workflow-published image vs installer's local build — with no CI catching it, exactly the drift class #435/#547 prevent one level up. Suggest a check-facts-style guard (or a bats test) that decodes the three blobs and diffs them against docker/k3s-cuda/*, failing CI on mismatch. In sync today, so non-blocking, but it will drift eventually.
🟡 2. Draft/validation status
The description says "kept as draft until that end-to-end validation passes" and lists "Remaining to close #616 (needs your GPU Windows box)… confirm a training pod runs on the GPU" — but the PR is not a draft. Either the live GPU-box validation was completed (please update the body), or it wasn't and this shouldn't merge yet. Mitigating: merging is bounded-safe regardless — L1 always falls back to CPU, and L2 is inert unless a GPU is present andConfirm-DockerGpu passes and the image pulls, each failure landing on CPU with a reason. So worst case on a non-GPU/failing box is "CPU as before," not breakage.
⚪ Notes
- Pester (464) not verified — no
pwshon my machine, so I couldn't run the PowerShell suite; CI must be green on it. Everything else I checked myself. - Large PR (+2004) — cohesive (image already split to #635/#639; L1/L2 cleanly separated), so not pushing to split further, but it's over the 1000-line guideline; the
install-k8s.ps1half is the bulk. Set-NodeGpuCapacityand the reconciler advertise a singlenvidia.com/gpu=1— correct for the WSL2 single-GPU laptop scope; multi-GPU isn't covered (fine to note as a known limit).
Not approving yet purely on the two 🟡 questions above — the code itself is in good shape. Happy to approve once the drift-guard is added (or explicitly deferred to a follow-up) and the validation status is confirmed. Wants @saadqbal's code-owner eyes regardless (R8 + training-pod-adjacent).
🤖 Generated with Claude Code
…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>
…es (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>
shujaatTracebloc
commented
Aug 10, 2026
Thanks — this is a genuinely useful review, and both 🟡 items are now handled. 🟡 1. Embedded base64 drift guard — partly a miss on my side, partly ergonomicsHalf-correction: a drift guard does exist and runs in CI. Your reasoning about why nothing else covers it is precisely right and worth recording: So I added the bash-side mirror you asked for: I also checked the guard actually bites rather than assuming: appending a line to 🟡 2. Validation status — completed; body updatedThe stale "kept as draft" text was mine to fix; the description now states what was actually done. Validated on real hardware today: Windows 11 + RTX 4050 Laptop, driver 532.10, Docker Desktop/WSL2 — one installer command, no registry credentials:
Four real bugs only surfaced on hardware, each now regression-tested: k3d ignores the image ENTRYPOINT (the CDI setup had to become a Still open and tracked separately: a real training pod on the GPU needs tracebloc/client-runtime#291 (jobs-manager threads On your ⚪ notes
Also worth flagging for your judgement: three findings in this PR recurred as classes (premature-success messages ×3, |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…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>
0164711There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0164711. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
…pped 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>
Uh oh!
There was an error while loading. Please reload this page.
…op computing it (backend#2220) (#766) * refactor(installer): read the training envelope from the contract, stop computing it (backend#2220) Both installers computed "how much of this machine may one training run have" from scratch, with a third copy in cli's set.go and a fourth, disagreeing policy in client-runtime's node_sizing.py. client-runtime#358 made the arithmetic single-definition; this makes the installers readers of it. Deleted from install-client-helm.sh and install-k8s.ps1: the 1-CPU/3-GiB overhead, the 1-core/2-GiB floor, and — the part nobody had chosen — the (memory, cpu) node ordering. That last one is a real divergence, not just duplication: cli's nodeLarger ranked (cpu, memory), so on a cluster of 8c/16Gi + 4c/32Gi the installer and `tracebloc resources set` anchored on DIFFERENT nodes and disagreed about the same machine. One order now, the contract's, matching cli. Installer-provisioned clusters are single-node k3d, where the orders cannot differ, so the field sees no change. Neither installer can call the Python: bash has no guaranteed JSON parser (jq is not a prerequisite — the helm-namespace parser says so in as many words) and this bootstrap is SIGNED, so it must not fetch anything unsigned at install time. So the constants are EMBEDDED and the embed is drift-guarded, the same trade the GPU node-image build inputs already make (#616/#633). Deliberately NOT a new scripts/lib/*.sh: install-k8s.ps1 is a single self-contained file and cannot source a bash lib, so a lib would have meant two mechanisms, two guard shapes, and five more sync points on the signed file surface. Three ways that can rot, one gate each: 1. the embed stops matching the vendored contract -> scripts/gen-envelope-embed.sh --check, mirrored in bats AND Pester 2. the vendored contract stops matching upstream -> envelope-contract-drift.yml, against scripts/.client-runtime-ref 3. the installers stop reproducing the contract's vectors -> the golden-vector replays, in the normal test jobs Pin, don't float (cli/.github/workflows/chart-drift.yml's rule): an unrelated client-runtime commit must not redden every open client PR; the weekly run catches a pin gone stale. The drift job mints a least-privilege App token (named repositories, contents:read) because client-runtime is private and GITHUB_TOKEN cannot read another repo. Fixes a live bug found while writing the Pester replay: [math]::Max(0, ...) binds the (Int32, Int32) overload and throws on any byte count over 2^31 ("Value was either too large or too small for an Int32"). The enclosing bare `catch {}` swallowed that into a silent fall-through to cpu=2,memory=8Gi — the first replay run had EVERY machine returning the literal. Now [long]0. Note the `catch {}` itself is a standing hazard: any exception on that path silently hands the customer the unschedulable literal with no diagnostic, and PSScriptAnalyzer has been flagging it advisory-only (PSAvoidUsingEmptyCatchBlock at :4073 and :4122). Filed separately rather than widening this PR. manifest.sha256 regenerated — both installers' digests moved. Verified: bats 164 pass (the 3 failures are pre-existing on clean develop — they need real helm, the gap client#751 just closed in CI); Pester 699/0; `make lint` clean at severity=error over 47 files; PSScriptAnalyzer 0 errors; gen-manifest/index-invariants/check-facts/leftover-guard/bats-hygiene all green. Drift detection proven by tampering a constant and repairing it. No client/** chart files touched, so no Chart.yaml bump. Refs: RFC-BACKEND-664 P0, client-runtime#358, cli#143 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(envelope-embed): a real regen must rewrite BOTH installers (Bugbot #766) The generator grepped in one place and rewrote in another, so a single string had to serve as both an ERE pattern and a Python literal. The PowerShell prefix was passed with a leading backslash so grep would read the dollar as literal; that backslash then reached re.escape(), which searched install-k8s.ps1 for a backslash-dollar assignment that does not exist there. --check was unaffected, because it never gets past the grep. So the gate looked healthy while the documented adopt path was broken: a real regen rewrote the bash installer, died on the first PowerShell constant, and left the two embeds disagreeing about the same contract -- the exact drift this whole mechanism exists to prevent. Reproduced before fixing: bash moved to 4294967296, ps1 stayed at 3221225472, exit 1. Now python3 does the whole job -- find, compare, and rewrite -- against a LITERAL key, so there is one escaping domain instead of two. Exit codes carry the outcome back to bash (0 correct/rewritten, 2 no such assignment, 3 present but wrong under --check), which also drops the leaked backslash from the drift message. Why the existing tests missed it, which is the part worth keeping: they only ever ran --check (a read) or a regen against an already-correct contract, where every constant short-circuits before the rewrite. The mutating path had no coverage at all. Two tests added that do have it, both in a scratch copy so they cannot mutate the repo: * adopt a genuinely changed contract, assert BOTH installers moved and the result is self-consistent * delete a PowerShell constant, assert the generator fails LOUDLY rather than reporting a healthy embed -- the fail-open shape gen-manifest.sh warns about in its own empty-surface guard Verified: 6/6 envelope tests, bats-hygiene 18/18, install-client-helm 162 ok with the same 3 pre-existing failures clean develop has (#64-66, they need real helm), make lint clean over 47 files, manifest unchanged (this touches only dev tooling and tests, neither of which is on the signed file surface). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(installer): skip unparseable nodes in the ps1 twin, don't rank them (Bugbot #766) Get-TrainingResources coerced an unparseable CPU or memory quantity to 0 and then ranked the node anyway. The contract's skipped_nodes says such a node is SKIPPED, and the bash twin has always done that with an explicit `|| continue` — the ps1 was the odd one out. The old memory-first order hid it: a memB of 0 could never win the anchor. Ranking cpu-first, which this PR introduced for parity with cli, exposes it. A node with a good core count and a memory unit we do not speak takes the anchor, fails the memory floor, and drops the WHOLE machine to cpu=2,memory=8Gi while a sibling node was perfectly sizeable. So the reorder was safe on k3d but not on a heterogeneous BYO cluster. Reproduced before fixing, as a test: nodes "16 64GB" + "8 32Gi" returned cpu=2,memory=8Gi where cpu=7,memory=29Gi was correct. $null now marks unparseable and the node is skipped, matching bash and the contract. Coverage added on both sides, because the single-node golden vectors could not have caught this — they only ever feed ONE node, and the bug needs an unparseable node COMPETING with a valid one: Pester: unparseable memory / unparseable cpu / all-unparseable bats: the same two cases against _machine_training_resources, pinning that the twins agree rather than assuming it bash needed no change — its test passed on the first run, which is the point of adding it. manifest.sha256 regenerated: install-k8s.ps1's digest moved. Verified: Pester 743 passed / 0 failed / 13 skipped, envelope bats 8/8, bats-hygiene 18/18, PSScriptAnalyzer 0 errors, make lint clean over 47 files, gen-envelope-embed --check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

What & why
On Windows, a machine that has a working NVIDIA GPU still ran experiments on CPU — and silently. The training pod requested
nvidia.com/gpu=1, the k3d node advertised 0 GPUs, so it satPending("Insufficient nvidia.com/gpu") until theSINGLE_NODEfallback downgraded it to CPU. GPU present, GPU wasted, no signal to the user.Root cause — an asymmetry in
install-k8s.ps1: the installer requested a GPU for every job whenever it merely detected a GPU + driver, but the GPU is only provisioned in the cluster (--gpus=all, device plugin, node verify) whenK3D_GPU_FLAGis set — and that's set only if the "install NVIDIA Container Toolkit into WSL" step fully succeeds, which has ~6 ways to silently bail (no WSL, distro needs first-run setup, apt/gpg blocked on a filtered network, timeouts).Closes#616.
Layer 1 — never request a GPU the cluster can't provide ✅ (this commit)
Safe, always-correct, CPU fallback preserved:
GPU_REQUESTS/GPU_LIMITSonK3D_GPU_FLAG— the same condition that provisions the GPU, not mere detection. Empty ⇒ no GPU request ⇒ training runs on CPU. TheSINGLE_NODEGPU→CPU downgrade stays as a belt-and-suspenders net.$GPU_SKIP_REASONat each toolkit early-return (WSL not responding / Ubuntu needs first-run setup / toolkit apt blocked or timed out / verify failed), surface it in the summary (CPU (GPU detected but not enabled: <reason>)) and warn during install.K3D_GPU_FLAG(don't suggest a test that can't run).manifest.sha256regenerated.Layer 2 — actually enable the GPU in k3d on Windows ✅ (implemented and validated on real hardware)
Makes a Windows NVIDIA GPU genuinely usable by training pods, CPU fallback preserved whenever it can't be wired up.
2a — custom k3s-CUDA node image (
docker/k3s-cuda/): the stockrancher/k3simage is Alpine with no NVIDIA runtime, so GPU pods can never schedule on it. New image rebuilds the same pinned k3s on an NVIDIA CUDA Ubuntu base, installs the NVIDIA Container Toolkit, configures containerd for thenvidiaruntime, and bakes in the device plugin +nvidiaRuntimeClass (auto-deployed on boot). Published by the manualbuild-k3s-cudaworkflow toghcr.io/tracebloc/k3s-cuda:<k3s>-cuda-<base>.2b — installer wiring (
install-k8s.ps1):Confirm-DockerGpu— the authoritative gate:docker run --rm --gpus all nvidia/cuda:<tag> nvidia-smi. Docker Desktop uses its own WSL distro, so toolkit-in-Ubuntu was never a reliable signal — actually running a GPU container is. GPU is enabled iff this passes (so we never create a--gpuscluster that would fail, and DD-GPU works even without the user's Ubuntu toolkit). Failure ⇒ CPU (Layer 1) with a clear reason.RUNTIME_CLASS_NAME=nvidiaso jobs-manager threadsruntimeClassName: nvidiainto every spawned pod.Confirm-DockerGpubehavioral + source guards; full Pester 464 pass.Validation status — done on real hardware (2026-08-10). Windows 11 + RTX 4050 Laptop, driver 532.10 (CUDA 12.1), Docker Desktop/WSL2. From one installer command, no registry credentials:
✔ GPU acceleration enabled (WSL2/CDI)→✔ GPU verified and available→ summaryMode : NVIDIA GPUcapacity/allocatable nvidia.com/gpu = 1; the CDI spec carrieslibdxcore(2 occurrences)nvidia.com/gpu: 1schedules and runs CUDA:nvidia/samples:vectoradd-cuda11.2.1→Test PASSEDGetting there required four real fixes that only surfaced on hardware, each now regression-tested: k3d ignores the image ENTRYPOINT (the CDI setup must be a
/bin/k3d-entrypoint-*.shdrop-in); PowerShell 5.1 strips JSON quotes from native args (the node-capacity patch silently becamevalue: 1as an integer and the API server rejected it — now--patch-file); the same quoting bug in the printed doctor command; and the libdxcore injection anchor missing the generator's 4-space indent.Still open, tracked separately: a real tracebloc training pod using the GPU needs tracebloc/client-runtime#291 (jobs-manager threads
NVIDIA_VISIBLE_DEVICESinto GPU pods) merged and shipped in a jobs-manager image. Everything installer- and cluster-side is complete. Note also that a fresh install currently pulls published chart 1.9.15, which predates theinit-writable-datafix (chart 1.9.25) — see the promotion note in the comments.Known limits: single GPU per node (
nvidia.com/gpu=1) — correct for the single-GPU laptop scope this targets, multi-GPU is not covered. GPU on Docker Desktop cannot use the NVML device plugin at all (nvmlInit→ERROR_NOT_SUPPORTEDthrough the paravirtualized GPU), which is why capacity is node-advertised and the GPU is injected via CDI.🤖 Generated with Claude Code
Note
High Risk
Large changes to cluster creation, node status patching, and Helm GPU env wiring on the Windows install path; failures are designed to fall back to CPU but misconfiguration could still strand GPU jobs or confuse operators.
Overview
Enables real NVIDIA GPU training on Docker Desktop/WSL2 while keeping CPU fallback whenever wiring fails, and stops requesting GPUs the cluster cannot satisfy.
The custom k3s-CUDA node image switches to CDI mode (pinned NVIDIA Container Toolkit), drops the NVML device plugin (broken on WSL2), and adds a k3d entrypoint drop-in that generates the WSL CDI spec at boot, injects
libdxcore.so, and re-patchesnvidia.com/gpucapacity after restarts. The Helm chart gains optionalenv.GPU_VISIBLE_DEVICES(→NVIDIA_VISIBLE_DEVICESin pods) for the CDI-only path; chart version 1.9.28.install-k8s.ps1is reworked around an authoritativedocker run --gpusprobe, local build or registry pull of the GPU node image (with mirror/auth),$GPU_SKIP_REASONsurfaced in the summary, WSL2/CDI capacity advertising instead of the device plugin, GPU chart values gated on$K3D_GPU_FLAG, reuse/reconcile guards so CPU-only clusters do not strand jobs Pending, and single-node topology when GPU is on.check-factsand a newgpu-embed-drift.batsguard keep CUDA base tags and embedded build blobs in sync withdocker/k3s-cuda/*.Reviewed by Cursor Bugbot for commit 544e9d3. Bugbot is set up for automated code reviews on this repo. Configure here.