Skip to content

fix(installer): enable GPU on Windows — CPU fallback + real GPU support (#616) - #633

Merged
shujaatTracebloc merged 50 commits into
developfrom
fix/616-windows-gpu-enablement
Aug 10, 2026
Merged

fix(installer): enable GPU on Windows — CPU fallback + real GPU support (#616)#633
shujaatTracebloc merged 50 commits into
developfrom
fix/616-windows-gpu-enablement

Conversation

@shujaatTracebloc

@shujaatTraceblocshujaatTracebloc commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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 sat Pending ("Insufficient nvidia.com/gpu") until the SINGLE_NODE fallback 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) when K3D_GPU_FLAG is 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:

  • Gate GPU_REQUESTS/GPU_LIMITS on K3D_GPU_FLAG — the same condition that provisions the GPU, not mere detection. Empty ⇒ no GPU request ⇒ training runs on CPU. The SINGLE_NODE GPU→CPU downgrade stays as a belt-and-suspenders net.
  • Loud skip: 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 summary (CPU (GPU detected but not enabled: <reason>)) and warn during install.
  • Gate the doctor's GPU-test hint on K3D_GPU_FLAG (don't suggest a test that can't run).
  • Tests: 4 new Pester tests; full suite 456 pass. manifest.sha256 regenerated.

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 stock rancher/k3s image 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 the nvidia runtime, and bakes in the device plugin + nvidia RuntimeClass (auto-deployed on boot). Published by the manual build-k3s-cuda workflow to ghcr.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 --gpus cluster that would fail, and DD-GPU works even without the user's Ubuntu toolkit). Failure ⇒ CPU (Layer 1) with a clear reason.
  • Cluster create uses the custom CUDA image when GPU is enabled (stock k3s otherwise; same pinned k3s).
  • Sets RUNTIME_CLASS_NAME=nvidia so jobs-manager threads runtimeClassName: nvidia into every spawned pod.
  • Tests: Confirm-DockerGpu behavioral + 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 → summary Mode : NVIDIA GPU
  • node reports capacity/allocatable nvidia.com/gpu = 1; the CDI spec carries libdxcore (2 occurrences)
  • a pod requestingnvidia.com/gpu: 1 schedules and runs CUDA: nvidia/samples:vectoradd-cuda11.2.1Test PASSED

Getting 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-*.sh drop-in); PowerShell 5.1 strips JSON quotes from native args (the node-capacity patch silently became value: 1 as 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_DEVICES into 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 the init-writable-data fix (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 (nvmlInitERROR_NOT_SUPPORTED through 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-patches nvidia.com/gpu capacity after restarts. The Helm chart gains optional env.GPU_VISIBLE_DEVICES (→ NVIDIA_VISIBLE_DEVICES in pods) for the CDI-only path; chart version 1.9.28.

install-k8s.ps1 is reworked around an authoritative docker run --gpus probe, local build or registry pull of the GPU node image (with mirror/auth), $GPU_SKIP_REASON surfaced 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-facts and a new gpu-embed-drift.bats guard keep CUDA base tags and embedded build blobs in sync with docker/k3s-cuda/*.

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

…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>
@shujaatTraceblocshujaatTracebloc self-assigned this Aug 6, 2026
shujaatTraceblocand others added 3 commits August 6, 2026 14:39
…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>
shujaatTracebloc added a commit that referenced this pull request Aug 6, 2026
* 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>
@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 6, 2026 15:07
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>
Comment threadscripts/install-k8s.ps1 Outdated
Comment threadscripts/install-k8s.ps1 Outdated
Comment threadscripts/install-k8s.ps1 Outdated
Comment threadscripts/install-k8s.ps1
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>
Comment threadscripts/install-k8s.ps1
Comment threadscripts/install-k8s.ps1
)
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>
Comment threadscripts/install-k8s.ps1 Outdated
shujaatTraceblocand others added 2 commits August 7, 2026 09:51
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>
Comment threadscripts/install-k8s.ps1
Comment threadscripts/install-k8s.ps1
… (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>
Comment threadscripts/install-k8s.ps1
…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>
Comment threadscripts/install-k8s.ps1
…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>
Comment threadscripts/install-k8s.ps1 Outdated
…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>
Comment threadscripts/install-k8s.ps1
Comment threadscripts/install-k8s.ps1 Outdated
Comment threadscripts/install-k8s.ps1 Outdated
Comment threadscripts/install-k8s.ps1 Outdated
shujaatTraceblocand others added 2 commits August 7, 2026 11:39
…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>
Comment threadscripts/install-k8s.ps1 Outdated
…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>
Comment threadscripts/install-k8s.ps1 Outdated
… (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

Copy link
Copy Markdown
ContributorAuthor

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 init-writable-data init container from #612 (chart 1.9.25). Since the installers pull the released chart, a fresh install on a hostPath cluster can hit unwritable ingest/training volumes until a stable chart ≥ 1.9.25 is out — data ingestion fails at the copy step.

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:

mkdir -p /tracebloc/<env>/data /tracebloc/<env>/logs && chmod -R 2777 /tracebloc/<env>/data /tracebloc/<env>/logs

(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>
saadqbal
saadqbal previously approved these changes Aug 10, 2026
Comment threadscripts/install-k8s.ps1 Outdated
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>
Comment threaddocker/k3s-cuda/k3d-entrypoint-tracebloc-cdi.sh

@aptraceblocaptracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@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 correctlyshasum -c scripts/manifest.sha256 → all 18 pinned scripts OK, including the heavily-modified install-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_TAG rows (ps1 CUDA_BASE_TAG, Dockerfile, build.sh, workflow — all 12.4.1-base-ubuntu22.04); check-facts.bats passes.
  • ✅ Layer-1 gate is exactly right$gpuVal/$runtimeClass/$gpuSelector default empty, set only under GPU_VENDOR=nvidia ∧ NVIDIA_DRIVER_OK ∧ K3D_GPU_FLAG≠"" (the provision condition, not detection); same triple guards Set-NodeGpuCapacity, the summary, and the doctor hint. The selector additionally rides the WSL2-only $GPU_DEVICE_SELECTOR, so Linux device-plugin nodes get an empty GPU_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, always exit 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 intactruntimeClassName: nvidia only selects the containerd handler; CDI injects the device + ro,nosuid,nodev mounts; no securityContext relaxation. Training-pod isolation unchanged.
  • ✅ ChartGPU_VISIBLE_DEVICES optional string, documented, empty-by-default; helm lint clean; 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 pwsh on 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.ps1 half is the bulk.
  • Set-NodeGpuCapacity and the reconciler advertise a single nvidia.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

shujaatTraceblocand others added 2 commits August 10, 2026 13:16
…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

Copy link
Copy Markdown
ContributorAuthor

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 ergonomics

Half-correction: a drift guard does exist and runs in CI. install-k8s.Tests.ps1 has a Describe "Embedded GPU build inputs stay in sync with docker/k3s-cuda (#616 drift guard)" with three tests that decode each blob and compare it to the repo file — and the Pester job runs on ubuntu-latest as well as windows-latest, so an un-re-embedded edit would already have gone red. You couldn't see it because you had no pwsh, which is exactly the problem worth fixing.

Your reasoning about why nothing else covers it is precisely right and worth recording: check-facts guards the version pins, not whole-file embedding, and Get-GpuBuildContentHash only hashes the embedded copies for local-build cache-busting — it never compares them to docker/k3s-cuda/*.

So I added the bash-side mirror you asked for: scripts/tests/gpu-embed-drift.bats (0be16ab) — decodes all three blobs and diffs them against docker/k3s-cuda/*, plus a non-empty check (catches a truncated re-embed) and the drop-in shape invariant (no exec /bin/k3s, always exit 0 — the mistake that actually shipped once). CI already runs bats scripts/tests/*.bats, so it's picked up automatically, and you can now verify it locally with no pwsh.

I also checked the guard actually bites rather than assuming: appending a line to nvidia-runtimeclass.yaml without re-embedding turns test 2 red (with a diff and the exact remedy printed); restoring it returns 5/5 green. bats-hygiene passes — every assertion carries || return 1 per #527.

🟡 2. Validation status — completed; body updated

The 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:

  • ✔ GPU acceleration enabled (WSL2/CDI)✔ GPU verified and availableMode : NVIDIA GPU
  • node reports nvidia.com/gpu = 1; the CDI spec carries libdxcore
  • a pod requestingnvidia.com/gpu: 1 schedules and runs CUDA (vectoraddTest PASSED)

Four real bugs only surfaced on hardware, each now regression-tested: k3d ignores the image ENTRYPOINT (the CDI setup had to become a /bin/k3d-entrypoint-*.sh drop-in); PowerShell 5.1 strips JSON quotes from native arguments, so the capacity patch silently became value: 1 as 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 assuming 2-space indent where nvidia-ctk emits 4.

Still open and tracked separately: a real training pod on the GPU needs tracebloc/client-runtime#291 (jobs-manager threads NVIDIA_VISIBLE_DEVICES) merged and shipped in a jobs-manager image. Installer/cluster side is complete.

On your ⚪ notes

  • Pester is green in CI on both runners — currently 619 passing.
  • Size: agreed, and I won't argue it's small. The install-k8s.ps1 half is the bulk, and roughly a third of the growth since your review is Bugbot-driven hardening rather than new capability.
  • Multi-GPU: correct, and now stated as a known limit in the description. nvidia.com/gpu=1 is right for the single-GPU laptop scope; multi-GPU would need per-device CDI entries and a real device plugin, which WSL2 can't run.

Also worth flagging for your judgement: three findings in this PR recurred as classes (premature-success messages ×3, cdi list version-gating ×2) because my first fixes were too local. Each is now pinned by an invariant test rather than a patch. Per the org standard on recurring findings, I think two rules belong in .cursor/BUGBOT.md — happy to add them here or as a follow-up, your call.

saadqbal
saadqbal previously approved these changes Aug 10, 2026
aptracebloc
aptracebloc previously approved these changes Aug 10, 2026
Comment threadscripts/install-k8s.ps1
Comment threadscripts/install-k8s.ps1
Comment threadscripts/install-k8s.ps1
Comment threadscripts/install-k8s.ps1
…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>

@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 1 potential issue.

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 0164711. Configure here.

Comment threadscripts/install-k8s.ps1 Outdated
…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>
@shujaatTracebloc
shujaatTracebloc merged commit 5ebac77 into developAug 10, 2026
46 checks passed
@shujaatTracebloc
shujaatTracebloc deleted the fix/616-windows-gpu-enablement branch August 10, 2026 13:06
shujaatTracebloc added a commit that referenced this pull request Aug 20, 2026
…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>
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.

Windows: GPU never used even when present — installer requests nvidia.com/gpu it can't provision (jobs stall Pending, fall back to CPU)

4 participants

@shujaatTracebloc@saadqbal@aptracebloc@LukasWodka