tools(GATE-GPU-LOCK-WRAPPER): one sanctioned way to take the GPU, and it records which lock it took (#587) - #596
Open
localai-bot wants to merge 1 commit into
Open
tools(GATE-GPU-LOCK-WRAPPER): one sanctioned way to take the GPU, and it records which lock it took (#587)#596localai-bot wants to merge 1 commit into
localai-bot wants to merge 1 commit into
Conversation
… it records which lock it took (#587) GPU serialisation on `dgx.casa` was split across two lock FILES. `$HOME/gpu.lock` is what `.agents/benchmarking.md` and `.agents/environment.md` document and what 432 references in this tree take; `~/work/marlin442` took `/tmp/gpu.lock`. Two jobs holding different files run concurrently while each believes it owns the box, and nothing reveals it: `fuser $HOME/gpu.lock` shows an empty waiter list while the other job is mid-run. On GB10 that is an OOM-reboot mechanism, because unified memory makes a `gpu_memory_utilization` reservation HOST RAM, and it voids any contention-sensitive number silently. `scripts/gpu-lock.sh` is the one sanctioned way to take it, so the path stops being something each script re-decides. `$HOME/gpu.lock` is canonical: it has the callers and the documentation, and it is per-user, where a lock under `/tmp` is world-writable -- any process on the box can hold or clobber the mutex guarding someone else's measurement -- and is cleared by the very reboot it exists to prevent, taking the evidence of who held it with it. `$GPU_LOCK` still overrides, and the wrapper stamps whichever path it RESOLVED, so a divergence shows up in the record instead of surfacing when two jobs collide. Two properties are the design. It REFUSES, never falls back. An untakeable lock aborts with exit 78 and the command does not run; a bounded wait expires with its own exit 75 rather than sitting forever behind a dead holder. A wrapper that proceeds on the assumption it holds a lock it never took is one more instrument that cannot report its own failure -- the class that produces a `Status: SUCCESS!` from a binary an ENOSPC build never recompiled, and a "policy violation" that is really a checker unable to write a temp file. It RECORDS, and the exit code comes FIRST. Disk and load say the box was unhealthy; the exit code says WHICH thing killed the run. `137` (SIGKILL -- another agent's `pkill`) versus a compiler diagnostic versus an ENOSPC line are three different diagnoses and three different repairs, and only the first says it was not your code at all. The stamp carries the resolved lock path, the requested spelling, holder pid, wait, timeout, `df -h /` and load average at acquire and again at exit, the command, and `exit-code` + `exit-reason` as the first two fields of the release block. The near-miss this is built from: a peer's same-binary A/B on this box survived #587 only because its conclusion rested on `diff -r -q` over two output directories -- byte-identity is contention-immune -- while the wall times printed beside it were not. Had the conclusion rested on the times, #587 would have voided it silently and nothing in the record would have said so. Stamping does not prevent contention. It prevents contention being undetectable afterwards. Nesting passes through on the same resolved path instead of deadlocking on a lock its own parent holds; a different path is still taken for real. RED first: 48 tests written against an absent script, all 48 erroring, then green. Eight mutations prove the checks catch their defect rather than describing it -- the refusal returning instead of exiting, dropping the `lock-path` field, dropping the `exit-code` field, swallowing the exit code, collapsing the timeout status to 0, skipping the acquisition entirely, unbounding the wait, and removing the pass-through so nesting self-deadlocks. Each is caught. THE SWEEP IS NOT CLOSED. The repo has 432 `$HOME/gpu.lock` references and zero `/tmp/gpu.lock` -- but a THIRD spelling #587 did not name: `/tmp/gpu`, no extension, taken as `exec 9>/tmp/gpu` by `scripts/dgx-online-serving.sh` and `scripts/dgx-gdn-packed-component.sh`, named by `scripts/opt-dgx-gate.sh` and `scripts/dgx-sglang-low-concurrency.sh`, and documented as this box's `${GPU_LOCK}` in `.agents/coordination.md:99`. Those callers are outside this change's authority and are owed a follow-up: until they are repointed, a run under this wrapper does NOT exclude an online-serving or gdn-packed-component run, which is #587 with different filenames. On dgx, `~/*.sh` converges on `$HOME/gpu.lock` across 14 spellings that all resolve to one file, and `~/work/marlin442`'s own scripts take NO lock at all; `/tmp/gpu.lock` did not survive the 09:00 reboot, so what held it is not recoverable from the filesystem and is not asserted here. `.agents/benchmarking.md` and `.agents/environment.md` name the wrapper as the sanctioned path, so the next agent does not read the old instruction and take a raw `flock`. Disk logged beside every result, per the box discipline: dev box 88% used / 56G free throughout; dgx 99% used / 61G free, read-only sweep only, nothing built or deleted there. Not wired into `scripts/agent-preflight.sh` SUITES or `.github/workflows/ci.yml` -- both outside this change's authority, and owed. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #587.
GPU serialisation on
dgx.casawas split across two lock files, so it didnot serialise:
$HOME/gpu.lock(what the discipline documents, 432 referencesin this tree) and
/tmp/gpu.lock(what one job took). Two jobs holdingdifferent files run concurrently while each believes it owns the GPU, and
fuser $HOME/gpu.lockshows an empty waiter list throughout. On GB10 that is anOOM-reboot mechanism; it also voids contention-sensitive numbers silently.
What lands
scripts/gpu-lock.sh— one sanctioned way to take the GPU, so the path stopsbeing something each script re-decides.
scripts/gpu-lock.sh --label 'ours-c1' --record evidence/lock.txt -- ./bench ...Canonical path:
$HOME/gpu.lock. It has the callers and the documentation,and it is per-user. A lock under
/tmpis world-writable — any process on thebox can hold or clobber the mutex guarding someone else's measurement — and
/tmpis cleared by the very reboot the lock exists to prevent, taking theevidence of who held it.
$GPU_LOCKstill overrides, and the wrapper stampswhichever path it resolved, so a divergence shows up in the record instead
of surfacing when two jobs collide.
It refuses, never falls back. Missing, unwritable, unopenable lock path, or
an unwritable
--recordfile → exit 78, command not run. Bounded waitexpiry → exit 75, its own status, so a dead holder is not something a job
sits behind forever.
It records, exit code first. Disk and load say the box was unhealthy; the
exit code says which thing killed the run —
137(SIGKILL, another agent'spkill) versus a compiler diagnostic versus an ENOSPC line are three diagnosesand three repairs, and only the first says it was not your code at all.
Stamped (stderr, and
--record FILEif given):ACQUIREmode,lock-path(resolved, absolute),requested-lock,holder-pid,waited-seconds,timeout-seconds,acquired-utc,host,label,loadavg,df-root,commandRELEASEexit-code,exit-reason,outcome,lock-path,holder-pid,waited-seconds,elapsed-seconds,released-utc,loadavg-at-exit,df-root-at-exitREFUSED/TIMEOUToutcome,reason/holder,lock-path,loadavg,df-root,the command did NOT runNo self-deadlock. A nested invocation on the same resolved path passes
through (
VLLM_CPP_GPU_LOCK_HELD); a different path is still taken for real.The near-miss worth naming: a peer's same-binary A/B on this box survived #587
only because its conclusion rested on
diff -r -qover two output directories —byte-identity is contention-immune — while the wall times printed beside it were
not. Had the conclusion rested on the times, #587 would have voided it silently
and nothing in the record would have said so. Stamping does not prevent
contention; it prevents contention being undetectable afterwards.
Evidence
RED first: 48 tests written against an absent script, 48/48 errored, then
48/48 OK (27.8 s, serial).
shellcheck -S warningclean,bash -nclean.scripts/agent-preflight.sh --staged: All gates green.Eight mutations, each caught:
return 0s instead of exitingtest_unwritable_lock_directory_refuses,test_missing_lock_parent_refuseslock-path=stamptest_acquire_block_carries_every_required_field,test_symlinked_lock_path_is_stamped_resolvedexit-code=stamptest_exit_code_is_the_first_field_of_the_release_block,test_nonzero_propagates_unchangedexit "$RC"→exit 0test_nonzero_propagates_unchanged,test_sigkill_137_propagates_unchangedEXIT_TIMEOUT=75→0test_contended_lock_times_out_with_distinct_statustrue(never takes the lock)test_lock_is_actually_taken_so_a_holder_excludes_usflock -w "$TIMEOUT"→flock(unbounded)test_wait_is_bounded_not_indefinitefalse(self-deadlock)test_nested_invocation_does_not_deadlockDisk logged beside every result per the box discipline: dev box 88% used /
56G free throughout; dgx 99% used / 61G free, read-only sweep only —
nothing built, transferred or deleted there.
Lock-spelling sweep — NOT closed
Repo: 432
$HOME/gpu.lock, 0/tmp/gpu.lock. But a third spelling#587 did not name:
/tmp/gpu, no extension —scripts/dgx-online-serving.sh:1108—exec 9>/tmp/gpuscripts/dgx-gdn-packed-component.sh:524,543—exec 9>/tmp/gpuscripts/opt-dgx-gate.sh:4,scripts/dgx-sglang-low-concurrency.sh:6— named.agents/coordination.md:99— documents this box's${GPU_LOCK}asflock /tmp/gpuThose callers are outside this change's authority. Until they are repointed, a
run under this wrapper does not exclude an online-serving or
gdn-packed-component run — #587 with different filenames.
dgx (read-only):
~/*.shconverges on$HOME/gpu.lockacross 14 spellings thatall resolve to one file;
~/work/*shows ~1.7k/tmp/gpuoccurrences, almostall inside checked-out copies of the four repo scripts above.
~/work/marlin442's own scripts (bps.sh,paired.sh,sweep_ours.sh, …)contain no lock reference at all.
/tmp/gpu.lockdid not survive the 09:00reboot, so what held it is not recoverable from the filesystem and is not
asserted here.
Owed follow-ups (outside this change's authority)
/tmp/gpucallers and.agents/coordination.md:99atscripts/gpu-lock.sh.test_gpu_lockintoscripts/agent-preflight.shSUITESand.github/workflows/ci.yml(both explicitly excluded from this change) —~30 s, no GPU, no build.
~/work/marlin442takes no lock; whoever owns it should wrap it.🤖 Generated with Claude Code