Repository navigation
bench: one arm with its memory trace (#2049 + its review answered) - #2287
Conversation
…'s review answered This lands #2049 (`tools/bench-arm.sh`, `bench-preflight.sh --watch`, ctest row bench_arm_watch) on current main, plus the three points of the CHANGES at 3e5b91a and the reason its CI failed. * An interrupted arm takes its bench with it. CMD runs in its own process group (set -m), and INT, TERM or HUP sends TERM to the whole group, then KILL after BENCH_ARM_GRACE_S (default 10 s); an EXIT trap does the same. Before, the arm had no trap and `--watch` reaches it by exec, so nothing could reap the bench: SIGTERM left it running, and SIGINT was deferred by bash and ignored. * `--watch` reaches the drop test. `bench-preflight.sh --drop-gib N --watch -- CMD` forwards it, and BENCH_DROP_GIB sets the default in both tools. Before, only --floor was forwarded, so drop was always 0 there. * The usage text says what --drop-gib cannot see: MemAvailable counts the arm's own footprint. * CI: "child status not propagated (got 1)" was the floor test, not the status. Arm 1 ran with the default 20 GiB floor, and a runner with less MemAvailable FAILs that first; it now passes --floor 0. Reproduced with BENCH_RAM_FLOOR_GIB=9999 on the old test. Test: test-bench-arm-watch.sh gains 4 arms (INT, TERM, a TERM-ignoring bench, BENCH_DROP_GIB) with a bounded wait so a deaf arm fails rather than hangs; test-bench-preflight.sh gains 4 --watch forwarding arms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 1cc4c2a: READY
Reviewed at 1cc4c2a713685419b91afba552f0aec9fb7b0e8b. It is 2 behind main and merges clean. The diff is 5 files, +257/−1: tools/bench-arm.sh (new), tools/bench-preflight.sh, the two test scripts, and the bench_arm_watch ctest row.
Checked (box-only, no GPU, model or ticket; MemAvailable 35 GiB at the time)
- The arm test.
bash tools/tests/test-bench-arm-watch.sh tools/bench-arm.shpasses all 9 checks, rc 0. That includes the three interrupt arms: SIGINT → rc 130, SIGTERM → rc 143, and a bench that ignores TERM gets KILLed after the grace. Nobench-armor worker process outlived the run (pgrepafterwards). - The preflight test.
bash tools/tests/test-bench-preflight.shreports "32 passed, 0 failed". - Repo checks.
tools/copyright.py --checkandtools/ci/tests_referenced_check.py --checkboth return rc 0.
My mutant, on the half the body's mutant does not cover. I dropped --drop-gib "$drop_gib" from bench-preflight.sh's exec bench-arm.sh line, which is #2049's original gap. The preflight test then fails 2 checks: "--watch forwards --drop-gib" and "--watch forwards BENCH_DROP_GIB" (argv --floor 20 -- true). Restored, it passes. The body's set -m mutant covers the interrupt half.
The CI cause is right. Arm 1 now passes --floor 0, so a runner with less than 20 GiB MemAvailable no longer fails the status-propagation check on the floor first.
Merging this makes #2049 redundant, as the body says; it can be closed after.
CI build at this head: completed success, "100% tests passed, 0 tests failed out of 275". bench_arm_watch is test #60 there and passed (12.0 s).
|
ready for review at 1cc4c2a CI at 1cc4c2a: every check passed. From the build job's log: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 1cc4c2a
Ran its suite: all checks passed, 9 arms -- #2049's five plus three interrupt arms and the env passthrough. The interrupt arms are the ones I could never write for the earlier head, and they are
built the hard way.
The trap the test had to solve is stated in it, and it is the reason I believe the arms:
"A non-interactive shell starts a background job with SIGINT ignored, and a signal ignored once cannot be re-caught"
So the child is spawned through python3 -c '… signal.signal(signal.SIGINT, signal.SIG_DFL); os.execvp(…)' -- a wrapper that restores the default disposition before exec, because bash -c in a
non-interactive shell would inherit SIGINT ignored and no amount of killing would reach it. A test that skipped this would pass while proving nothing, which is the defect class this session has now
found five times.
The three arms at this head:
ok SIGINT to the arm stops the bench and its worker (rc 130)
ok SIGTERM to the arm stops the bench and its worker (rc 143)
ok a bench that ignores TERM is KILLed after the grace
128+signal is asserted, not just "it died" -- rc 130 and 143 are SIGINT and SIGTERM, so an arm that exited 1 for some other reason fails. And the third arm is the one that makes the grace period
real: a child that ignores TERM is KILLed, which is the arm I have asked for in four other PRs tonight (a kill-path test must include something that refuses to die).
The implementation matches: "CMD runs in its own process group, and INT, TERM or HUP to this too…", kill -TERM -- "-$child" then kill -KILL -- "-$child" -- negative pid, so the whole group,
grandchildren included -- with set -m called out as the thing that gives the background job its own pgid. That is the tree kill, not the pid kill, and the test's grandchildren included clause
says the arm checks it.
And BENCH_DROP_GIB=1 reaches the drop test is the arm that keeps the contamination check configurable: an env override that silently did nothing would make a "clean" arm out of a
contaminated one.
bench-preflight's +19 is the same shape as #1913's /proc check, so the two now agree about what "observed the mode" means.
Not run: a real llama-bench arm (a gpu-hold ticket); the harness is what changed and its arms are fixture-driven.
…whose work is already on base) is invisible to a diffstat (#2308) Two shapes cost this fleet a round trip each tonight, and both look exactly like a PR writing its own code: * #2286's branch carried #2287's and #2252's files byte-identical, so merging it would have landed two other PRs' merge decisions inside its own; * #2020 was a md5-identical copy of #2026, and #2086 re-added files #2045 had already merged -- the work was on base, so the new PR was a fourth copy of something that landed an hour earlier. A diffstat cannot separate either from a PR that wrote the file. Blob equality can: pr_carry_check.py <repo> <ref>... [--base origin/main] CARRIED a path 2+ refs add with the same bytes and base does not have LANDED a path a ref changes to the blob base already has Read-only: no network (caller fetches into refs), no checkout, no index writes, so it is safe in a worktree someone is using. Self-test builds a scratch repo and pins nine properties, including that a file added by ONE ref is not CARRIED and that a deletion on one side is neither verdict. Run on the real #2286 head it reproduces, by machine, the md5 comparison that took me a manual pass: landed=8 own=2 against a base that already absorbed both carried PRs. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Refs #572 (its bench harness; the prefill gap itself is kernel work and stays open). Follow-up to #2049: this branch is
main+ #2049 (3e5b91a, re-applied under this lane's identity) + the answers to the CHANGES at3e5b91a(11-11 01:32Z, unanswered, no push since) and the reason its CI was red. Merging this makes #2049 redundant; it can then be closed.What #2049 adds (unchanged)
tools/bench-arm.sh [--floor GiB] [--drop-gib N] -- CMDruns one bench arm, samples MemAvailable and SwapFree every 0.25 s, printsbench-arm: mem_avail start S min M end E GiB; swap_free ...and exits with the child's status, or 1 if the arm was contaminated.bench-preflight.sh --watch -- CMDgates the box, then execs it. ctest rowbench_arm_watch.What this adds
BENCH_ARM_GRACE_S(default 10 s), and the arm exits 128+signal. An EXIT trap does the same. Before, there was no trap: SIGTERM killed the arm and left the bench running, and SIGINT was ignored altogether (bash defers it while the poll'ssleepruns and then carries on).--watchreaches the drop test.bench-preflight.sh --drop-gib N --watch -- CMDforwards it, andBENCH_DROP_GIBsets the default in both tools. Before, only--floorwas forwarded, so the drop was always 0 on that route.--watchand--drop-gib, and the limit the review named. MemAvailable counts the arm's own footprint, so N must sit above it, and an intruder smaller than that margin cannot be told from the arm.FAIL child status not propagated (got 1)came from the floor test, not the status. Arm 1 ran with the default 20 GiB floor, and a runner with less MemAvailable FAILs the floor first. Arm 1 now passes--floor 0. Reproduced on bench: one arm, with its memory trace, so a contaminated number says so (engine#572) #2049's test:BENCH_RAM_FLOOR_GIB=9999 bash tools/tests/test-bench-arm-watch.sh tools/bench-arm.sh→FAIL child status not propagated (got 1); at this head, the same command givesok a quiet arm exits with the child's status (7).Proof
Box-only tests (no GPU, model or ticket).
The same arm test against #2049's
bench-arm.sh:Mutant at this head, the trap kept and
set -mremoved (the child shares the arm's process group, so the group kill finds nothing):FAILon all three interrupt arms,3 check(s) failed.The interrupt arms wait at most 5 s for the arm to exit. An unbounded wait hung on #2049's script, which ignores SIGINT, so a deaf arm now fails the test instead of hanging it.
pgrep/pkillcome fromprocps, which.github/workflows/ci.ymlinstalls.Diff against
main:CMakeLists.txt+5 (thebench_arm_watchrow),tools/bench-arm.sh(new, with the Apache notice),tools/bench-preflight.sh,tools/tests/test-bench-arm-watch.sh(new),tools/tests/test-bench-preflight.sh.tools/copyright.py --checkrc=0.🤖 Generated with Claude Code