Skip to content

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

Open
shujaatTracebloc wants to merge 7 commits into
developfrom
fix/616-windows-gpu-enablement
Open

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

Conversation

@shujaatTracebloc

@shujaatTracebloc shujaatTracebloc 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; needs GPU-box validation)

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.

Remaining to close #616 (needs your GPU Windows box): run build-k3s-cuda with push: true to publish the image, then re-install on the GPU machine and confirm nvidia.com/gpu becomes allocatable and a training pod runs on the GPU. Kept as draft until that end-to-end validation passes.

🤖 Generated with Claude Code


Note

Medium Risk
Changes Windows cluster creation, private image pull/auth, and training GPU/runtime Helm values—wrong gating could still strand jobs or misreport GPU mode, but CPU fallback is explicit and bounded by probes before --gpus.

Overview
Fixes #616: on Windows, GPU detection no longer implies GPU requests in Helm values or “NVIDIA GPU” in the install summary when the cluster was never wired for GPUs (which left training jobs pending on nvidia.com/gpu).

Provisioning gate: After WSL toolkit setup, GPU is turned on only if Confirm-DockerGpu (docker run --gpus all + nvidia-smi) and Confirm-GpuImagePullable (optional docker login via registry env, then pull of $K3S_CUDA_IMAGE) both succeed. Failures set $GPU_SKIP_REASON at each toolkit early-exit and in the post-probe block; install warns and summary shows CPU (GPU detected but not enabled: …).

Cluster + chart: When $K3D_GPU_FLAG is set, k3d uses the custom k3s-CUDA image (env/mirror overrides) with --gpus=all; otherwise stock rancher/k3s. GPU_LIMITS/GPU_REQUESTS and RUNTIME_CLASS_NAME=nvidia are set only when the flag is set.

Tests: New Pester coverage for summary text, Helm values, Confirm-DockerGpu, Confirm-GpuImagePullable, and source guards; manifest.sha256 updated for install-k8s.ps1.

Reviewed by Cursor Bugbot for commit 46d90a4. 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>
@shujaatTracebloc shujaatTracebloc self-assigned this Aug 6, 2026
shujaatTracebloc and 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>

@cursor cursor Bot 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 4 potential issues.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 1a49577. Configure here.

Comment thread scripts/install-k8s.ps1
if ((Confirm-DockerGpu) -and (Confirm-GpuImagePullable)) {
$K3D_GPU_FLAG = "--gpus=all"
$GPU_SKIP_REASON = ""
Ok "GPU enabled -- cluster will use the custom k3s-CUDA image with --gpus=all"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Host probe misrepresents cluster GPU readiness

High Severity

K3D_GPU_FLAG reflects host Docker capability, not actual cluster readiness. Existing clusters are reused without the CUDA image or --gpus, and failed node verification never clears the flag, yet Helm requests the GPU runtime and the summary claims acceleration. Training jobs can remain unschedulable.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: Bugbot guide — tracebloc/client

Reviewed by Cursor Bugbot for commit 1a49577. Configure here.

Comment thread scripts/install-k8s.ps1
$probeImg = "nvidia/cuda:$CUDA_BASE_TAG"
Log "Probing Docker GPU passthrough: docker run --rm --gpus all $probeImg nvidia-smi"
try {
$out = (docker run --rm --gpus all $probeImg nvidia-smi 2>&1 | Out-String)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GPU Docker calls can hang installer

Medium Severity

The new docker run, docker login, and docker pull calls are unbounded, violating the installer external-call timeout rule. A wedged Docker daemon, registry, or proxy can leave Step 2 hanging forever instead of falling back to CPU.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by learned rule: Installer scripts: kubectl/curl/helm calls must have explicit timeouts

Reviewed by Cursor Bugbot for commit 1a49577. Configure here.

Comment thread scripts/install-k8s.ps1
$probeImg = "nvidia/cuda:$CUDA_BASE_TAG"
Log "Probing Docker GPU passthrough: docker run --rm --gpus all $probeImg nvidia-smi"
try {
$out = (docker run --rm --gpus all $probeImg nvidia-smi 2>&1 | Out-String)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GPU probe bypasses configured image mirror

Medium Severity

Although the CUDA node image is re-homed through TRACEBLOC_IMAGE_REGISTRY, the authoritative capability probe always pulls nvidia/cuda directly from Docker Hub. Sites using an internal mirror because Docker Hub is blocked therefore always disable GPU support.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1a49577. Configure here.

Comment thread scripts/install-k8s.ps1
$K3D_GPU_FLAG = ""
if (-not $GPU_SKIP_REASON) {
$GPU_SKIP_REASON = "Docker Desktop can't expose the GPU to containers (enable GPU support in Docker Desktop, and update the WSL2 NVIDIA driver)"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Toolkit failure masks Docker probe failure

Low Severity

Confirm-DockerGpu does not set a failure reason, while the earlier toolkit step always seeds one. When both steps fail, the authoritative Docker failure preserves an unrelated WSL toolkit reason, directing the operator toward a fix that cannot enable Docker GPU passthrough.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1a49577. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants