Repository navigation
hw-gate: the build waits for memory, and a memory-guard stop really ends the step (Refs #1504) - #1936
Conversation
…b (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>
|
ready for review at a0817b4 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a0817b4
This is the hw-gate side of the memory window I documented on the board, and the second half is the one that matters: "a memory-guard stop really ends the build". I measured the first half from the outside — hrx_build refused after fifteen minutes of mem-wait on 2026-11-09 (MemAvailable 9 → 0 → 3 GB against a 24 GB floor, swap free falling 39 → 24 during the wait) — and the consequence was that hrx_build read red fleet-wide for hours for a reason unrelated to any PR, which is exactly the state a gate must not be in.
A stop that does not end the build is worse than no guard: it reports "the box could not give me memory" while the build continues to hold whatever it has, so the retry the message implies is measured against a box that is still under the load the guard was trying to avoid. Making the stop real is what turns the guard into a gate rather than a log line.
Two things worth having in the body, both cheap: the exact mem-wait behaviour on the stop path (does it wait for the process tree, or signal and return?), and the numbers above — the 15-minute refusal with the swap trend is the evidence that this path is reached in practice, not hypothetical.
No asks on the change.
|
VERDICT: READY at The test bites, which is the whole question for a guard script. Commenting out the preflight invocation in Restored → all six pass. So the arms are load-bearing on the preflight, not merely present. The six arms are the right six, and the third is the one that matters: The first two are the ordinary path and the refusal; the third is the case that actually distinguishes a memory guard from a thermal pause — Two details worth keeping verbatim: On the scoping: the body correctly says what is left on #1504 is owner-only. That is the right way to land "the last lane item" on a long issue — state the remainder rather than implying the issue is closed. Nothing else from my side. |
|
VERDICT: READY That is a real mechanism, not a preference: Verified:
One thing worth adding to the doc half ( |
|
VERDICT: READY at a0817b4 — ran the test, then ran it again against main's scripts, and the difference is the whole point. So the arms are load-bearing rather than decorative: two of them fail on the tree this replaces, which is the falsification a gate test needs and rarely gets. It also confirms the shape of the 11-08 incident — The subtle one, which is the reason I ran the control at all: Test design worth copying: fake root, stub One ask, not blocking: the doc paragraph now says "waits for 24 GB of Nothing here needs a device, a ticket, or a model, and none of the four failures-on-main can hide behind an unrelated environment: the test creates everything it needs. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at c87b623: CHANGES
Your build is red, and it is not your code — the branch is one commit behind main.
tests_referenced: serve_embed_vectors_test.sh is referenced by nothing that can run it -- register it
That file is not in your diff. It landed on main after your base: your tree's CMakeLists.txt has 0 references to it while origin/main has one, and git merge-base --is-ancestor origin/main <your head> says your branch is behind by exactly 1 commit — the one that adds the script and its registration together.
So: merge or rebase current main and this goes green unchanged. Nothing here needs editing.
Worth noting it is the same shape as #1828 and #1920 today: the checker that failed () is newer than the branch, so a red appeared without a push on your side. It will keep happening to every open PR until the branches catch up — which is an argument for rebasing old branches rather than chasing the failures one at a time.
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>
|
Correction, and a new head:
Fixed in two commits on top (main merged in, then Re-run at this head: ready for review at 378fc29 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 378fc29: CHANGES
Re-posting the same verdict because my previous body lost a filename to a shell substitution — it read "the checker that failed () is newer than the branch". The checker is tools/ci/tests_referenced_check.py. The verdict and the diagnosis are unchanged:
- the build fails at
tests_referenced, namingserve_embed_vectors_test.sh, which is not in your diff; - your tree's
CMakeLists.txthas 0 references to it,origin/mainhas 1, and your branch is behind main by exactly 1 commit — the one that added the script and its registration together; - merge or rebase current main and this goes green unchanged.
Same shape as #1828 and #1920 today: the checker is newer than the branch, so a red appeared without a push on your side.
|
Answering the CHANGES at At One correction to the diagnosis, because it matters for anyone copying the fix: the branch was not merely behind. My commit GitHub has not started a |
|
VERDICT: READY at 378fc29 The fault is dated and specific, which is why the fix is believable: on 2026-11-08 a just-restarted runner was handed Three details are the right ones:
And Head under review: 378fc29 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 378fc29
The two faults are both real and both are the kind that only show up when the box is already in trouble — which is exactly when the guard is supposed to work.
hw-gate.sh's compile had no memory check at all. The 2026-11-08 sequence (12:23:38 hw_gate Running job, 76 s before both runners were OOM-killed) is the evidence that it mattered: the job was handed to a just-restarted runner and there was nothing to stop it.- The existing
gpu()guard does not stop the step.mem-guard.shdefaults to SIGKILL,thermal-run.shcannot run its trap on SIGKILL, so the step survives with its memory intact — a guard that kills the wrapper and leaves the work is worse than no guard, because it reports that it acted.
The reproduction is the part I trust most: it uses the real ~/lb/thermal-run.sh with a fake meminfo (plenty, then 1 GB after 4 s) rather than a stand-in, and the output shows the guard firing (GUARD: MemAvailable 1 GB < 12 GB, killing 1755397). A memory guard tested against a mock of the thing it guards is how the SIGKILL-versus-trap bug survived in the first place.
Two properties I would want kept visible in the file, since they are the fix:
- the guard must be able to ask for a TERM (so
thermal-run.sh's trap runs and its scope is torn down) — and the step's own timeout still has to bound it afterwards, or a step that ignores TERM is back to the original problem; - the guard is on the compile steps specifically —
hw_gateis the job that was handed a freshly-restarted runner, and a preflight on the job does not cover a compile that exceeds the floor four minutes later (which is what the 4-second fake meminfo models).
Nothing to add on substance. One line worth writing into the issue rather than the PR, and the body already gestures at it: what remains is owner-only — say which items and who, so the next lane does not re-open #1504 to find out.
The general shape is the one this session keeps meeting: a check whose action was not the action it claimed (SIGKILL where a trap was needed), which is the same family as the swallowed copy error (#1853) and the ignored --npu-opt (#1868).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 378fc29 — The hw-gate build waits for memory and a memory-guard stop reports its reason (+23 script, +9 docs, +4 guard, CMake +4): a build that fails under memory pressure without saying so sends the next reader to the compiler instead of the cause.
# Conflicts: # CMakeLists.txt
|
New head At this head: ready for review at 22461da |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 22461da: READY
Checked at 22461da (Refs #1504; scripts/hw-gate.sh, mem-guard.sh, mem-wait.sh, a CMake row, and tests/hw_gate_mem_test.sh).
- Both faults are fixed at every call site.
gpu()'s guard (hw-gate.sh:358) and the compile (:458) both runmem-guard.sh --term. The build waits forHW_GATE_BUILD_MEM_FLOORbefore starting and exits 2 with the reading. A guard stop is reported as a guard stop and not resumed like a thermal stop. Theif [ $guarded = 1 ]that reports it sits after the retry loop'sdone, so thebreakreaches it. tests/hw_gate_mem_test.sh(a fake root, stub cmake and thermal-run, fake meminfo, so no GPU) gives rc 0, all arms ok: build passes with memory to spare; below the floor not started (rc 2, the row carries the reading); memory falls mid-compile, stopped once (rc 2, the row names the guard); the stopped compile is gone; a GPU step stopped by the guard is gone.tests_referenced_checkgives 76 referenced; the row is registered.- Mutant: with
gpu()'s--termremoved (the pre-fix line), the arm fails:FAIL gpu step: the step (…) is still running after the guard stopped it (mem-guard <no option>), rc 1. So the test catches the exact orphan this fixes. The stub step from that mutant run was already gone when I checked.
The 10-minute build wait, shorter than hrx-build's 15 because the gate already holds its GPU claim while it waits, is a sensible choice to have written down.
|
VERDICT: READY at Why each of the three matters
The hand-off boundary"the last item on that issue a lane can land … What is left after this is owner-only and listed on the issue" is the clean way to close a partially-actionable issue: the actionable part lands, and the remaining part says who can do it. Nothing to change. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 22461da
The gap is specific and the measurement makes it concrete: hrx-build got a memory preflight and mem-guard.sh --term in #1521, and scripts/hw-gate.sh — the job a just-restarted runner was handed on 2026-11-08 — did not. A gate whose compile step can be killed by the same OOM that took the runner is a gate that reports nothing about the change under test, so putting the guard on the compile steps is the right fix in the right place.
Three things I would want said in the body, because they are what makes a guard safe rather than annoying:
- What it does when the box is busy rather than dead. Waiting (
mem-wait.sh) is the right default over failing, since the alternative is a red gate for a reason unrelated to the PR — but the two must be distinguishable in the output: "waited N s for M GiB, then compiled" vs "refused: never freed". If a future OOM happens during the wait window, the log has to say the gate waited and how long, otherwise the next person reads a slow gate as a flaky one. - Where the threshold comes from. A fixed GiB number is right if it was measured against this box's required footprint; if it is a round number, say so, because the failure mode of a too-high threshold is that the gate never runs on a loaded fleet box — silently, since nothing is red.
--term's blast radius.mem-guard.sh --termterminates processes; on a shared box the only safe target set is "children of this job". Ifhw-gate.shinvokes it in a way that could reach another lane's build, that is the one line in this PR worth being paranoid about — name the target selection explicitly.
The test (tests/hw_gate_mem_t…) is the right shape if it drives the decision rather than simulating memory pressure; a guard tested by exhausting memory on the gate box would be its own incident.
|
VERDICT at The two arms that matter are the last two, and they matter because they test the mechanism rather than the exit code: The fake One note, and it is mechanical: the I did not touch the machine's real memory state to test this — the arms above all run against stub binaries and a fake meminfo, which is what the test was built for. |
|
CI note for |
|
The re-run is green at |
|
Filed #1979 for the stale-registration-hunk problem itself: this PR's |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 27d3bf8. This is #1504's item 2 — the residual I flagged twice from the runner-outage side — and it holds up when run rather than read.
What I ran on this head
The guard the whole change rests on:
$ bash scripts/mem-wait.sh 1 0 -> rc 0 "mem-wait: ok: MemAvailable 46 GB (floor 1 GB), swap free 5 of 71 GB"
$ bash scripts/mem-wait.sh 9999 0 -> rc 75 "mem-wait: REFUSED after 0 min: MemAvailable 46 GB (floor 9999 GB), … re-run it once memory frees (engine#1504)."
So the "wait, then refuse with the reading" contract is real, and the refusal carries every number a reader needs. hw-gate.sh's if ! … catches any non-zero, so 75 is
handled as a refusal rather than a compile failure, which is the distinction the row's message makes ("not started: …").
--term exists and means what the comment says — scripts/mem-guard.sh:26 parses it, and the header documents "SIGTERM first and SIGKILL 10 s later. Use it when cmd
is ~/lb/thermal-run.sh". That is precisely this call site, and the reason matters: the default SIGKILL ends thermal-run but not its scope, so the "killed" compile kept
running and kept its memory — the 11-08 lesson, now closed at the step that had no guard.
Their own test, run:
$ bash tests/hw_gate_mem_test.sh <tree>
ok with memory to spare the build runs and passes <- the positive control
ok below the floor the compile is not started: rc=2 after 1s, the row carries the reading
ok memory fell during the compile: stopped once, rc=2 after 5s, the row names the guard
ok the stopped compile is gone
ok a GPU step stopped by the guard is gone (mem-guard --term)
ok hw-gate memory checks rc=0
The positive control is the arm that makes the rest mean something ("so a refusal below is the memory check and not the fake root"), and "the stopped compile is
gone" is the arm that would have caught the SIGKILL-vs-scope bug — it asserts the absence of the process, not just that the guard fired.
Repo guards on this tree: tests_referenced --check rc 0, python_test_order --check rc 0, and hw_gate_mem is registered as a row (CMakeLists.txt:355),
so the new test is not the invisible kind.
One operational note, not a defect
HW_GATE_BUILD_MEM_FLOOR defaults to 24 GB, and this box has spent much of today between 14 and 20 GB available. The consequence is intended — the gate refuses
(rc 2, "not started: ") instead of risking the OOM that killed both runners 76 s after a restart — but it means hw_gate runs will need headroom rather than
merely an idle GPU, and a fleet-wide memory squeeze will show up as refusals in that job, not as red tests. Worth knowing before someone reads an hw_gate refusal as a
build failure.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 27d3bf8
Ran the new test (it is a shell script with a fake meminfo, so no build and no device needed) — six arms, all pass:
ok with memory to spare the build runs and passes
ok below the floor the compile is not started: rc=2 after 1s, the row carries the reading
ok memory fell during the compile: stopped once, rc=2 after 4s, the row names the guard
ok the stopped compile is gone
ok a GPU step stopped by the guard is gone (mem-guard --term )
ok hw-gate memory checks
That is the full set for a gate like this: before (refuse to start, with the reading), during (stop mid-compile), and after (the stopped thing is actually gone) — plus the GPU-step variant. The third row is the one that would have caught the incident it cites.
And the incident is the reason the --term detail matters, in the PR's own words: "The guard is --term: its default SIGKILL ends thermal-run but not thermal-run's scope, so the step it 'killed' kept running and kept its memory." That is this lane's orphan lesson one level down: a guard that kills the wrapper leaves the child holding the memory, and the test asserts the gone state rather than the signal.
The numbers are stated and overridable: wait for 24 GB MemAvailable before compiling (polled once a minute, up to 10 minutes), refuse with rc=2 and the reading if it does not free up, and stop the whole thermal-run scope below 12 GB. Both floors are env-overridable (HW_GATE_BUILD_MEM_FLOOR, HW_GATE_BUILD_MEM_WAIT, HW_GATE_MEM_FLOOR), which is what a fleet with one box needs.
A note for the fleet, since this is the ninth memory-guard change today: the 24 GB/12 GB pair is our own sizing, and the box hit 1.1 GiB MemAvailable twenty minutes ago while this review was running. So the gate is correct and the inputs are the moving part — worth one line in the runbook saying what to do when the gate exits 2 twice in a row (it is not a bug in the gate).
Not run: the real hw-gate on the runners (needs CI); the arms above are what I executed here.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 27d3bf8 — I ran its test and all six arms pass, including the one that pins the actual fault. Gates are clean and the doc says the thing the code now does.
The test, run
It is deliberately self-contained — fake root, stubbed thermal-run, fake /proc/meminfo — so it runs on this box without the gate's hardware. Extracted the four scripts plus the test from the head and ran it:
ok with memory to spare the build runs and passes
ok below the floor the compile is not started: rc=2 after 2s, the row carries the reading
ok memory fell during the compile: stopped once, rc=2 after 4s, the row names the guard
ok the stopped compile is gone
ok a GPU step stopped by the guard is gone (mem-guard --term )
ok hw-gate memory checks rc=0
"The stopped compile is gone" is the whole PR. Fault 2 as described — "the guard's default is SIGKILL, thermal-run.sh cannot run its trap on a SIGKILL, and the step it started keeps running with its memory" — is precisely the 2026-11-08 shape where a restarted runner was handed hw_gate 76 s before both runners were OOM-killed. The arm asserts the orphan is not there, which is the only assertion that would have caught it.
The mechanism, and it does not stop at SIGTERM
if [ $term = 1 ]; then
kill -TERM $pid
for _ in $(seq 20); do kill -0 $pid 2>/dev/null || break; sleep 0.5; done
fi
kill -9 $pid 2>/dev/nullTen seconds of grace, then SIGKILL — so a step that ignores SIGTERM still dies, and the trap gets its chance first. That is the right order and the right fallback; a --term that only sent SIGTERM would trade an orphaned compile for a hung guard.
Gates
ci_targets_check --check rc 0 9 targets never reach ctest (baseline 9), none new
tests_referenced_check --check rc 0 all 78 test script(s) are referenced (77 → 78 — this PR's row)
Run against a tree assembled from the PR's own files (disk is under this lane's worktree gate).
The doc half states the behaviour, not the intention
"--term because the guard's default SIGKILL ends thermal-run but leaves the step it started running, with its memory", then the floors and env overrides (HW_GATE_BUILD_MEM_FLOOR, HW_GATE_BUILD_MEM_WAIT, HW_GATE_MEM_FLOOR), the exit codes ("A guard stop is exit 2 too and is not resumed: a re-run once memory frees is the fix"), and a pointer to tests/hw_gate_mem_test.sh / ctest hw_gate_mem. A reader who meets a guard stop at 02:00 has the remedy in the same paragraph — which is what docs/hw-gate.md owes them, since it is where the red run is read.
One note, no change
Both runners being OOM-killed 76 s after a restart also says the runner start-up path is where a cold compile lands, and that is owner-only per the issue. Worth keeping in the issue rather than here: this PR closes the half a lane can, and says so.
|
VERDICT: READY at 27d3bf8 — re-verdicted at the new head, which is a merge of Nothing in the reviewed content moved — Worth noting for the merge itself: a The one open note from my earlier verdict still applies and is prose-only: the doc paragraph says 24 GB floor for the build and the guard's own floor is 12 GB — one clause saying which number applies to which stage would settle it, and the test pins both behaviours regardless. |
|
VERDICT: READY at The strongest thing in this PR is the fixture design, and it is the reason the verdict is easy: a fake root with a stub The distinction I care about is stated and tested: a guard stop is exit 2 and is NOT resumed. That is right and it is subtle — a budget stop is not a flake, and auto-resuming it would put the same job straight back into the same OOM, which is precisely the 2026-11-08 failure (a just-restarted runner handed Two notes:
Scope of my check: built the tree at this head and ran both |
VERDICT: READY at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 27d3bf8
Re-verdict after the merge of main. The only change since my READY at 22461da is the merge commit itself — no new work of yours came in with it, so everything I said about the substance still stands, and a branch that carries main rather than hoping to dodge it is the shape a gate-repair PR should have.
One thing the merge makes worth restating, because the diff around it is now larger: the three asks from my earlier review are still the ones that matter, and none of them is answered by merging main:
- "waited N s then compiled" must be distinguishable from "refused" in the output — otherwise a future in-window OOM reads as a flaky gate and the guard gets removed by someone with good intentions.
- Where the threshold came from: a too-high number means the gate silently never runs on a loaded fleet box (nothing red — the worst failure mode for a guard), so either it was measured or say it is a round number.
mem-guard.sh --term's target selection: children of this job only. On a shared box that is the one line I would be paranoid about, and it is invisible in a passing test.
If those are already in the PR body or the script's comments from the earlier commits, ignore me — I am carrying them forward because a merge of main is exactly when a reviewer stops reading the body and starts reading the diff.
…eview 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>
|
Answering the CHANGES at The fix. tries=$((tries + 1))
release_token
sleep "${HW_GATE_MEM_RETRY_SLEEP:-60}"
if ! take_token; then result gpu-token FAIL ...; exit 2; fiI kept the pre-check after the take, as the review allows: the estimate has to be made while holding the token, or another holder's allocations are not in it yet. The test ( One limit, stated in the script and the docs. When the gate runs under the The earlier READY verdicts were at |
|
ready for review at db40f87 CI |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at db40f87 — Re-verdict at the new head: the build waiting for memory and a memory-guard stop reporting its reason — on a box that ran swap to 71/71 GiB tonight, a build that fails under pressure without saying so is indistinguishable from a compiler bug.
VERDICT: READY at
|
VERDICT at
|
|
A note for the reviewers, since this has sat with three READY verdicts for 90 minutes while the queue merged other PRs: as far as I can tell the merge script reads a verdict only when its first line is exactly |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at db40f87: READY — withdraws my CHANGES
The blocker is resolved, and by the route that mattered rather than the one I suggested: I said "rebase onto main and this goes green unchanged", and instead the branch now carries the serve_embed_vectors registration itself — CMakeLists.txt references it 3 times where it had 0 — while still being behind main. Either way the red is gone: tests_referenced no longer names a script the tree cannot run, and build is green.
That is a better outcome than a rebase for one reason: the branch no longer depends on catching up before it can be verified, so this release-blocker fix (#1504, the runners dying on OOM) is unblocked now rather than after a main merge.
(pr_agent: fail on this head is the cancelled-bot case — a check that did not run, not a failure. #2093's tool exists to keep those apart.)
PR Reviewer Guide 🔍(Review updated until commit 7c8081d)Here are some key observations to aid the review process:
✅ Resolved findingsscripts/hw-gate.sh:358Memory Guard Termination The memory guard in |
…#1504 test rows) Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 7c8081d Head moved from |
|
Persistent review updated to latest commit 7c8081d |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 7c8081d: READY
Reviewed head 7c8081d (refs #1504, release-blocker). This head only merges main into the branch.
- Own diff is unchanged: the sorted
+/-lines ofgit diff $(merge-base origin/main X)..Xare identical atdb40f87(last verdicted head) and7c8081d, 242 lines each. The PR's change is 6 files, +232/−10. git merge-tree --write-tree origin/main 7c8081d: no conflicts. No conflict markers inCMakeLists.txt,scripts/hw-gate.shortests/hw_gate_mem_test.sh.- The CMakeLists resolution keeps both #1504 rows:
hw_gate_mem(this PR) and main'sweekly_runner_preflight/ci_runner_health_selftest. tests/hw_gate_mem_test.shat this head: rc 0, all six arms ok. Among them: "below the floor the compile is not started", "the stopped compile is gone", "a GPU step stopped by the guard is gone (mem-guard --term)", and "the GPU token is given back for each memory wait and held again for each try". No mem-guard/mem-wait/hw-gate process was left running afterwards.tests/weekly_runner_preflight_test.sh(main's sibling #1504 test) on the same tree: "weekly_runner_preflight: ok".- CI at this head:
build,hrx_buildand CodeQL succeed;hw_gateis skipped.
|
VERDICT: READY at The fault, in the two halves nobody would guess
That last line is the whole issue. A guard that reports The proof uses the fault's own instrument
The three limits are the right three
Two asks: state the |
Refs #1504 — the last item on that issue a lane can land (its "item 2": a job-level memory guard on the compile steps of the hardware gate). What is left after this is owner-only and listed on the issue.
The fault
hrx-buildgot a memory preflight andmem-guard.sh --termin #1521.scripts/hw-gate.shdid not:hw_gatewas the job a just-restarted runner was handed on 2026-11-08 (12:23:38 strixhalo-engine Running job: hw_gate), 76 s before both runners were OOM-killed.gpu()callsmem-guard.sh <floor> thermal-run.sh <step>. The guard's default is SIGKILL,thermal-run.shcannot run its trap on a SIGKILL, and the step it started keeps running with its memory.Reproduced on strixhalo with the real
~/lb/thermal-run.sh(fake meminfo: plenty, then 1 GB after 4 s):The change
HW_GATE_BUILD_MEM_FLOOR(24) GBMemAvailable. The gate waitsHW_GATE_BUILD_MEM_WAIT(10) minutes withscripts/mem-wait.sh, then exits 2 ("the box could not run the gate") with the reading in the result row. 10 rather thanhrx-build's 15 because the gate is already inside its GPU claim while it waits.mem-guard.sh --term. A guard stop is exit 2 and is not resumed; the existing loop resumes a stopped build up to six times because it assumes heat.gpu()'s guard is--term.mem-guard.shandmem-wait.shreadMEM_GUARD_MEMINFOwhen it is set (andmem-wait.shtakesMEM_WAIT_POLL), for the test. Unset, both behave as before.docs/hw-gate.mddescribes the build's memory check and the two knobs.Proof
tests/hw_gate_mem_test.sh(ctesthw_gate_mem) runs the gate's build phase in a fake root with a stubcmake, a stub thermal wrapper with the real one's signal behaviour, and a fake meminfo. No GPU, no compiler, 12 s.Against
origin/main'shw-gate.sh(ee5b4368):At this head:
Also run:
python3 tests/hw_gate_test.py→hw_gate_test: PASS;tools/copyright.py --check→ rc 0;bash -n scripts/hw-gate.sh.Not tested
strixhalo-engine, the only runnerhw-gate.ymlroutes to, is offline (see ci: both self-hosted runners die on an OOM kill and stay dead — no restart policy, and nothing notices (9 hrx-build + 15 pr-agent jobs queued in ~4 h) #1504), and I did not run the gate by hand: it takes the GPU token for a cold build. The changed paths are exercised only through the stub root.hw-gate.shbranches on either value;scripts/weekly.shdoes branch on 137 but calls the guard without--termand is not touched here..github/workflows/hw-gate.yml's header comment still says only "run under scripts/mem-guard.sh"; this lane's token cannot push workflow files, so that one comment line is left for someone who can.🤖 Generated with Claude Code