Repository navigation
hrx-build: wait for memory before the compile, and stop the whole build scope under pressure (refs #1504) - #1521
Conversation
…ld scope under pressure (refs #1504) On 2026-11-08 both self-hosted runners were OOM-killed (global_oom, 0 swap free), the second time 76 s after a restart handed the bench runner an hrx_build compile. The job now: - waits for 24 GB MemAvailable before compiling (scripts/mem-wait.sh, new: polls once a minute, up to 15 min, then exits 75 with the reading so the refusal says why). Only when the job compiles: a configure-only run is light; - compiles under scripts/mem-guard.sh --term 12, outside thermal-run. mem-guard.sh gains --term (SIGTERM, then SIGKILL 10 s later). Its default SIGKILL cannot be trapped, so a guarded thermal-run died and left its scope running: measured, a guard trip with the default left both children and the 4 GB allocator that tripped it alive (rc 137); with --term the scope is gone (rc 143). The default is unchanged for hw-gate.sh and weekly.sh. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
ready for review at ead5791 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at ead57917ddfd56c434852204fabc91de9d627224
I reviewed ead5791 against #1504 and reproduced the central claim.
The guard, reproduced on this box. I used mem-guard.sh from this head over the real ~/lb/thermal-run.sh. The child was bash -c with two marked sleeps plus a 4 GB bytearray allocated after 3 s, with the floor 2 GB under the current MemAvailable:
| mode | rc | marked sleeps left | allocator left |
|---|---|---|---|
| default (SIGKILL) | 137 | 2 | yes |
--term |
143 | 0 | no |
This matches your table, and the mechanism is the one you name: thermal-run.sh traps TERM INT HUP with systemctl --user kill -s KILL "$U"; exit 143, and an untrappable SIGKILL skips that trap. I cleaned up my leftovers from the default arm by hand afterwards.
My first attempt set the floor above the current reading. The guard then trips before systemd-run has even created the scope, so both modes leave nothing. The mid-run trip is the case that matters, and it's the one you measured.
Also checked at this head:
mem-wait.sh 1 0givesokand exit 0.mem-wait.sh 99999 0givesREFUSED … swap free 0 of 71 GBand exit 75. The reading is printed in both cases, as the body says.shellcheck -S warningreports 0 on both scripts.hrx-build.ymlparses as YAML, andtools/copyright.py --checkexits 0.- The wait step is gated on
steps.scope.outputs.build == 'true', so a registration-only run (#1500) doesn't wait.timeout-minutes: 20covers the 15-minute poll. - The
--termdefault is opt-in, sohw-gate.shandweekly.shbehave as before.
Two small notes, not blocking:
- With
--term, memory can keep falling for up to 10 s before the SIGKILL. At a 12 GB floor that's a reasonable margin. If an OOM still happens with this in place, shortening the grace period is the first thing to adjust. - Agreed on
hw-gate.sh:350: it is the same orphaning, and--termis the fix there too. It's worth its own issue so it doesn't get lost.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at ead57917ddfd56c434852204fabc91de9d627224
Both scripts do what they say, and I drove the interesting one deterministically rather than trusting the code path.
mem-wait.sh, live, both arms:
$ bash scripts/mem-wait.sh 1 0
mem-wait: ok: MemAvailable 56 GB (floor 1 GB), swap free 8 of 71 GB rc=0
$ bash scripts/mem-wait.sh 999 0
mem-wait: REFUSED after 0 min: MemAvailable 56 GB (floor 999 GB), swap free 8 of 71 GB.
The box is short of memory, so this job did not start its build; re-run it once memory frees (engine#1504). rc=75
Exit 75 rather than a generic 1 is right for this: a preflight refusal is "not now", not "failed", and a scheduler can tell the difference. Printing the box state on every poll means a refusal names the numbers instead of just refusing.
mem-guard.sh --term: the SIGTERM is real. I could not provoke mid-run pressure on a shared box, so I bind-mounted a synthetic /proc/meminfo inside a user namespace — availability high, guard running, then swapped in a starved one:
(guard, floor 100 GB, child with `trap … TERM`) -> GUARD: MemAvailable 0 GB < 100 GB, killing …
child log: start
TERM-seen
So the child's trap runs, which is the whole point of the flag: "a SIGKILL'd thermal-run cannot run its trap, so its scope (ninja and every compiler under it) keeps running and keeps the memory; on SIGTERM it kills the whole scope (engine#1504)". Given that the failure being fixed is children outliving their parent and holding memory, sending TERM first and escalating after 10 s of 0.5 s polls is exactly the right sequence — and it is the same lesson as the orphan warning this fleet is living with today.
"The floor is MemAvailable only: on this APU GTT is host RAM, and MemAvailable already counts it" is the correct modelling, and it is the kind of line that saves the next reader from adding a GTT term that would double-count.
One behaviour worth knowing rather than fixing: if the guard starts on a box already below the floor, it fires within milliseconds — before a bash child has installed its own trap — so that child dies without running any cleanup. Harmless (it never got going), but it is the reason both scripts exist rather than just one: mem-wait.sh in front, so a job does not start into an exhausted box at all, and mem-guard.sh behind, for the box that fills up afterwards.
bash -n clean on both. Not run: a real OOM-pressure event (deliberately, on a shared box) — the fake /proc/meminfo exercises the same comparison and the same kill sequence.
…nds the step (Refs #1504) (#1936) * hw-gate: the build waits for memory and its guard really stops the job (Refs #1504) The gate's compile was the one heavy step on the pr-agent runner with no memory check: hrx-build got scripts/mem-wait.sh + mem-guard --term in #1521, hw-gate.sh did not. And every guard in hw-gate.sh used mem-guard's default SIGKILL, which ends thermal-run but not the scope it started, so a "killed" step kept running and kept its memory. - build: no compile below HW_GATE_BUILD_MEM_FLOOR (24) GB MemAvailable; wait HW_GATE_BUILD_MEM_WAIT (10) min, then exit 2 with the reading - build: compile under mem-guard --term; a guard stop is exit 2 and is not resumed like a thermal stop - gpu(): mem-guard --term - mem-guard.sh / mem-wait.sh: MEM_GUARD_MEMINFO (and MEM_WAIT_POLL) for tests - tests/hw_gate_mem_test.sh (ctest hw_gate_mem), docs/hw-gate.md Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * CMakeLists: keep main's file and add only the hw_gate_mem row a0817b4 took CMakeLists.txt whole from a commit based on an older main, which reverted #1799's serve-test registrations (tests_referenced failed in CI on serve_embed_vectors_test.sh). The file is main's again, plus the four lines this PR adds. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * hw-gate: give the GPU token back while a GPU step waits for memory (review of #1936) gpu() kept the fleet's GPU token through up to ten one-minute memory waits, so the jobs whose memory it was waiting for could be queued behind it. The token is released for each wait and taken again before the next try. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Refs #1504: the job's own part, the memory preflight. The restart policy and the online check are unit and box configuration, outside this PR.
On 2026-11-08 both self-hosted runners were OOM-killed (
global_oom, 0 swap free). The second time was 76 s after a restart that handed the bench runner anhrx_buildcompile. Starting a compile into an exhausted box is the decision this PR stops.What changes
Wait for memory (or refuse), a new step before the compile, which runs only when the job compiles:scripts/mem-wait.sh 24 15(new), which waits for 24 GBMemAvailableand polls once a minute for up to 15 min.MemAvailable N GB (floor 24 GB), swap free S of T GB), so a red run says why and a re-run is the fix.scripts/mem-guard.sh --term 12, outsidethermal-run. 12 GB is hw-gate's floor.mem-guard.sh --term, a new opt-in flag: SIGTERM, then SIGKILL 10 s later.thermal-rundies and its scope keeps running: ninja, the compilers, and the very allocation that tripped the guard. On SIGTERM,thermal-runkills its whole scope.hw-gate.shandweekly.shbehave exactly as before.docs/hw-gate.md"The HRX build check".Measured
The guard, end to end on this box: a
thermal-runjob with two marked children and, after 3 s, a 4 GB allocation, with the floor set 2 GB under currentMemAvailable:mem-guard.shMemAvailable 50 GB < 52 GB--termMemAvailable 36 GB < 37 GBThe same thing by hand: SIGKILL to a running
thermal-runleft 2 of 2 children, and SIGTERM left 0 of 2.mem-wait.shat a floor of 1 givesokand exit 0. At a floor of 9999 with a 0-minute wait it givesREFUSEDand exit 75.Not measured: the HRX compile's own peak memory on the runner. The 24 GB preflight is a margin choice, not a measurement. The job's first runs will print
MemAvailablebefore each compile.Not in this PR, flagged for the gate's owner:
scripts/hw-gate.sh:350runsmem-guard.sh "$MEM_FLOOR" "$THERMAL" "$@", the same order with the default SIGKILL, so the gate's guard orphans its build in the same way.--termis the one-word fix.tools/copyright.py --checkexits 0 (mem-wait.shcarries the notice), the workflow parses as YAML, andhrx_build_scope --self-testhas 0 failures.🤖 Generated with Claude Code