Repository navigation
hw-gate: the npu phase takes the fleet's NPU queue, not just ~/npu.lock - #822
Conversation
2026-11-02: an npu phase passed at 08:58 (65.49 tok/s, exclusive) and then FAILed at 09:48 on identical inputs - same binary, model, sealed set, kernel, driver and firmware - with two firmware TDRs (aie2_legacy_tdr_detect, Context 6 and 8) inside the measurement and oracle/prefill producing no output at all. Cause: the npu phase was the only phase that serialised on ~/npu.lock, a private flock that only gates which know about it take. The fleet serialises NPU work through npu-hold (~/NPU-QUEUE + ~/NPU-TOKEN, with npu-serve-keeper stopping the owner's serve while a ticket waits). So at 09:46-09:49 another lane held ~/NPU-TOKEN and ran a live NPU job while the gate's npu phase drove the same array: two AIE2 contexts live at once, one TDR per context, and the other lane's job was killed at its own max hold 10 s later. Fix: the npu phase now runs the checker under npu-hold (~/npu.lock kept as the inner guard), so it queues with everyone else and the keeper pauses the serve for it. Cost is a FIFO wait, which is the point. Self-deadlock avoided: a chain already wrapped in one npu-hold (a lane running "npu-hold -- gate.sh ops kat kld ... npu") must not queue behind itself - that is what wedged c18 for 51 min on the GPU queue. The claim is detected by looking for the token holder's pid among this process's ancestors, and HW_GATE_NPU_HOLD=0 opts out explicitly (mirrors HW_GATE_GPU_HOLD=0). HW_GATE_NPU_TOKEN / HW_GATE_NPU_HOLD_BIN / HW_GATE_NPU_HOLD_MINUTES exist for a box with a different queue layout and for testing. Verified with a stubbed checker and a stubbed hold, so no NPU run and no NPU token was involved: (a) outside a claim the command is wrapped in 'npu-hold -l hw-gate-npu -t 20 -w 5400 -- flock ...'; (b) with the token naming an ancestor, zero wrapper calls; (c) HW_GATE_NPU_HOLD=0, zero wrapper calls; (d) a failing checker still FAILs the row (exit 1) and prints the same row shape as the 09:48 failure (oracle=FAIL greedy64=PASS prefill=FAIL reproducible=PASS tok/s=84.67).
PR Reviewer Guide 🔍(Review updated until commit 1b97217)Here are some key observations to aid the review process:
|
|
Persistent review updated to latest commit 1b97217 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 1b97217bf (tprs-ready applied)
Reproduced every case in the body with a stub npu-hold and a stub checker — no NPU run, no NPU token taken, the
flock pointed at a /tmp lock file, --phases npu only. All five behaviours hold at this head:
| case | what the run shows |
|---|---|
| outside any claim | wrapper taken, argv exactly -l hw-gate-npu -t 20 -w 5400 -- flock -w 5400 -E 75 <lock> sh -c <checker>; row PASS … tok/s=88.10 |
token names an ancestor (wrapper whose pid went into the token, then exec bash hw-gate.sh) |
0 wrapper calls, row PASS — no self-deadlock |
HW_GATE_NPU_HOLD=0 |
0 wrapper calls, row PASS |
token held by a non-ancestor in the keeper's own format (npu-serve-keeper pid 15134 — the live serve) |
wrapper taken — this is the 09:48 shape, and it is the case that had to work |
| failing checker | row npu FAIL … oracle=FAIL greedy64=PASS prefill=FAIL reproducible=PASS tok/s=84.67, gate exit 1, i.e. the same row shape as the incident |
--help |
rc 0, 0 wrapper calls, no ~/GPU-TOKEN left behind |
Mechanism read, and it is the right one: the claim is outside the flock, so the order is npu-token → ~/npu.lock,
which is the nesting that cannot deadlock against the lanes that hold only the flock; hold_args=() expands to zero
words, so the unwrapped path is byte-for-byte the old command line; and token_held_by_ancestor parses the pid out of
both token formats (gpu-hold <label> pid <pid> <ts> and npu-serve-keeper pid <pid>) and walks /proc/<p>/stat
ppid upwards from $$. -t 20 is minutes and -w 5400 seconds in gpu-hold's own argument handling (t) MINUTES,
w) WAIT), which npu-hold forwards.
bash -n rc 0, tools/copyright.py --check rc 0, CI on this head: build, CodeQL, four Analyze, pr_agent
success (330 s), hw_gate skipped.
One nit, and it is the same class as the bug this PR fixes: if npu-hold is absent (NPU_HOLD empty — a box that
does not have it, or ~/.local/bin pruned), the phase silently falls back to ~/npu.lock alone and logs nothing. That
is exactly the configuration that produced the two TDRs, and the gate would run in it without a word. One line —
[ -n "$NPU_HOLD" ] || printf 'npu: no npu-hold on this box; taking ~/npu.lock only\n' >> "$log" before the
hold_args decision — makes the state visible instead of silent.
Not verified by me: the settle run itself (real checker, cool die, idle queue). That is correctly listed as open in the
body, and my evidence deliberately does not touch the array.
The incident this comes from
On 2026-11-02 the same npu phase passed at 08:58 (
oracle/greedy64/prefill/reproducibleall PASS, 65.49 tok/s, 7 s) and then FAILed at 09:48 on identical inputs — same build dir, samedx_decode_test(fba773e6…), same model files (model.q4nx 6abacb42…), same sealed set, same kernel, sameamdxdna.ko, samenpu_7.sbinfirmware. Two firmware TDRs fired inside the failing run:Cause
The npu phase was the only phase that serialised on
~/npu.lock— a private flock that only the gates which know about it take. The rest of the box serialises NPU work throughnpu-hold(~/NPU-QUEUE+~/NPU-TOKEN), withnpu-serve-keeperstopping the owner's serve whenever a ticket is waiting.So during that run another lane held
~/NPU-TOKEN(mtp live k4 t0 stream, 09:46:00 → killed at its 3-minute max hold 09:49:01) while the gate's npu phase drove the same array. Two AIE2 contexts were live at once; the two that TDR'd match the two sub-runs whose output files never appeared.The fix (one file,
scripts/hw-gate.sh)The npu phase now runs the checker under
npu-hold, so it queues with every other NPU user and the keeper pauses the serve for it.~/npu.lockstays as the inner guard. The cost is one FIFO wait — which is the whole point.npu-hold(a lane runningnpu-hold -- gate.sh ops kat kld … npu) must not queue behind itself: that is what wedged c18 for 51 minutes on the GPU queue. The claim is detected by finding the token holder's pid among this process's ancestors, andHW_GATE_NPU_HOLD=0opts out explicitly (mirrors the existingHW_GATE_GPU_HOLD=0).HW_GATE_NPU_TOKEN,HW_GATE_NPU_HOLD_BIN,HW_GATE_NPU_HOLD_MINUTESexist for a box with a different queue layout (and for testing).~/npu.lock, same exit codes.Verified — stubbed checker and stubbed hold, no NPU run and no NPU token involved
npu-hold -l hw-gate-npu -t 20 -w 5400 -- flock -w 5400 -E 75 <lock> sh -c <checker>; rowPASS … tok/s=88.10npu-hold(token names an ancestor pid)HW_GATE_NPU_HOLD=0oracle=FAIL greedy64=PASS prefill=FAIL reproducible=PASS tok/s=84.67— the same shape as the real 09:48 rowbash -nclean,tools/copyright.py --checkrc=0,--helpstill answers without claiming anything.Still open
A single settle run of this unit with the fix, on a cool die with the queue genuinely idle, is approved by the orchestrator and will be reported (the four sub-checks, tok/s, and the
amdxdnaerror counter before/after) separately. Until it lands, the c20 NPU row rests on the 08:58 measurement.