gotchas.md: dispatcher deriving GPU width/pins from nvidia-smi instead of the SLURM allocation targets unallocated GPUs on narrow allocations
Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
Goal
Add a .claude/rules/gotchas.md entry (GPU/orchestration section) for a demonstrated cross-issue trap: a dispatcher that derives GPU width or device pins from nvidia-smi rather than from the SLURM allocation silently targets GPUs it was never allocated whenever the allocation is narrower than the physical node. The failure is invisible on whole-node allocations — which is why it can sit latent for many runs and then surface the first time someone runs a narrow smoke/precheck/debug job.
Observed (task #1336, 2026-08-12, SLURM job 11981 on the fellows/charmander cluster)
A deliberately narrow 1-GPU gate-chain precheck was dispatched (dispatch_issue.py launch ... --gpus 1). The per-issue dispatcher computes:
NGPU=$( (nvidia-smi --list-gpus 2>/dev/null || true) | wc -l ) # scripts/issue1336_dispatch.sh:91
and then pins devices with a literal index — CUDA_VISIBLE_DEVICES=0 on its single-GPU paths, and width=$NGPU with CUDA_VISIBLE_DEVICES=$w for w in 0..width-1 in its work-conserving worker pool.
Ground truth, probed from INSIDE the allocation (srun --jobid=11981 --overlap --ntasks=1 on the compute node — the only place these values are readable):
scontrol show job 11981 -d→AllocTRES=...,gres/gpu=1,Nodes=node-0 ... GRES=gpu:1(IDX:5)- inside the job:
CUDA_VISIBLE_DEVICES=5,SLURM_STEP_GPUS=5,SLURM_JOB_GPUS=UNSET - inside the job:
nvidia-smi --list-gpus | wc -l→ 8 - node-0 occupancy at probe time: GPUs 0-3 and 6-7 carried other jobs (5.6-51 GB, 7-51% util); GPU 4 free; GPU 5 (ours) 2,135 MiB.
So SLURM did everything correctly — it allocated one GPU and exported the right CUDA_VISIBLE_DEVICES. The dispatcher then overrode that correct value with a literal 0, re-pointing the process from our allocated physical GPU 5 to physical GPU 0, a device another job was using. NGPU=8 would additionally have fanned the worker pool across eight GPUs we did not hold (it did not fire in this run only because none of the four prechecked phases calls the pool — verified by enumerating every call site).
The two distinct mistakes, worth naming separately
nvidia-smiis not an allocation query.nvidia-smi --list-gpusis a driver-level enumeration that ignoresCUDA_VISIBLE_DEVICES, so it reports the physical device count regardless of what the scheduler granted. A GPU count derived from it is the NODE's width, never the JOB's. (Corollary, which cost this session a wrong marker claim: annvidia-smicount of 8 inside a 1-GPU job is NOT evidence that cgroup isolation is absent — the tool simply does not answer that question.)- Overriding an inherited
CUDA_VISIBLE_DEVICESwith a literal index discards the mapping. UnderCVD=5, the process'scuda:0already IS physical GPU 5. SettingCUDA_VISIBLE_DEVICES=0does not mean "our first GPU" — it re-selects physical GPU 0. The index space of the pin must be the inherited list, not the physical node.
Why it stays latent
On a whole-node allocation SLURM exports CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7, so nvidia-smi's count and the physical indices coincide with the allocated set and every literal pin resolves correctly. Task #1336 ran four prior 8-GPU attempts through this same dispatcher with no symptom. The bug appears only on a narrow allocation — precisely the shape used for cheap smoke/precheck/debug runs, i.e. exactly when someone is trying to de-risk an expensive run.
Proposed fix (prose-only, one gotchas.md entry)
State the rule and the correct pattern:
- Derive job GPU width from the allocation: parse the inherited
CUDA_VISIBLE_DEVICES(comma-separated) and take its cardinality; fall back to annvidia-smicount ONLY whenCUDA_VISIBLE_DEVICESis unset/empty.SLURM_STEP_GPUS/SLURM_JOB_GPUSandscontrol show job -d'sGRES=gpu:N(IDX:...)are corroborating sources. - Pin worker w to the w-th ELEMENT of the inherited list, never to the literal w:
This is byte-identical on a whole-node allocation (if [ -n "${CUDA_VISIBLE_DEVICES-}" ]; then IFS=',' read -ra ALLOC <<< "$CUDA_VISIBLE_DEVICES"; NGPU=${#ALLOC[@]} else NGPU=$( (nvidia-smi --list-gpus 2>/dev/null || true) | wc -l ) ALLOC=( $(seq 0 $((NGPU > 0 ? NGPU - 1 : 0))) ) fi # worker w uses ${ALLOC[w]}; single-GPU paths use ${ALLOC[0]}ALLOC[w] == w) and correct on a narrow one. - Add the probe discipline that made this diagnosable, since it is reusable and this session got it wrong twice first: a login-node shell is not the compute node. Any per-node probe —
nvidia-smi,pgrep,/proc/<pid>/environ,free, node-localdf— must run INSIDE the allocation (srun --jobid=<id> --overlap) or it describes an unrelated machine.scontrol/squeueare cluster-wide and stay valid from the login node. (In this incident anssh <cluster-alias>shell landed on node-2 while the job ran on node-0, producing a per-GPU memory table for the wrong machine and an emptypgrep.)
Acceptance criteria
.claude/rules/gotchas.mdcarries the entry in its GPU/orchestration region, naming both mistakes, the correct pattern, the whole-node-latency reason, and thesrun --overlapprobe discipline, with the#1336/ job-11981 incident cited.- If the LESSONS.md index row for
gotchas.mdenumerates trigger topics, extend it so the new topic is discoverable at plan time (the index is lint-enforced byworkflow_lint.py --check-lessons-index). uv run python scripts/workflow_lint.py(no flags) no worse than its pre-change baseline (~15 pre-existing failures onmain, unrelated — do not chase them; assert only that none newly names an edited file).
Non-goals
- No change to
scripts/issue1336_dispatch.sh(fixed in #1336's own code round v21, already dispatched) and no sweep of other issues' dispatchers in this task — the rule is the deliverable. A separate audit of existingnvidia-smi-derived widths acrossscripts/issue*_dispatch.shmay be filed later if wanted. - No new mechanical lint. "Did this dispatcher derive width from the allocation?" is a shell-semantics question a text heuristic would mostly get wrong, and a false-positive lint on every legitimate
nvidia-smicall would be worse than the prose rule. - No change to the backend router or
dispatch_issue.py— they passed--gpus 1correctly and SLURM honoured it; the defect is entirely inside per-issue dispatcher shell.
Provenance
Surfaced by the /issue 1336 autonomous session while running the plan-registered 1-GPU gate-chain precheck (SLURM 11981) that exists to de-risk a 210 GPU-h run. The precheck cleared five gates and resolved an offline-unresolvable bound; this was one of two bugs it found. Cost: no wasted compute (~0.3 GPU-h total, and the run's primary value was delivered).