Repository navigation
bench: the preflight observes the load mode it asked for, not just prints it (engine #1668) - #1913
bong-water-water-bong wants to merge 2 commits into
Conversation
…ints it (engine #1668) The `--dispatch` path pins `-lm`, validates it against llama-bench's own set, prints it in the header, and has arms asserting the flag reached the bench's argv. Nothing checked that the run **did** it: a bench that ignored `-lm` (the flag is a default, not a contract) produced a tally the header then mislabelled, which is the same defect the pin exists to prevent — one variable measured, two reported. llama.cpp puts a model it mmaps in `/proc/<pid>/maps` and a model it only reads is not there, so the mode is observable rather than trusted. The bench now runs in the background and the preflight polls that process's maps while it lives; a mode whose mapping disagrees with the flag is refused: -lm none that actually mmapped is refused rc=1 -lm mmap that only read is refused rc=1 observed mmap agrees with -lm mmap rc=0 observed read agrees with -lm none rc=0 Only the unambiguous modes are asserted (`mmap`/`mmap+mlock` map, `none`/`dio` do not); `mlock` alone is left unasserted because whether it maps depends on the loader, and a gate that guessed would fail a correct run. An observed mapping is trusted on its own, but an absent one only when the process was seen alive after the sample — a bench that maps and exits between polls must not be reported as a mismatch, and if it finishes before any sample the line is a WARN, not a failure. Test: five arms in tools/tests/test-bench-preflight.sh driving the real script against the fake world, with a fake bench that really maps or really reads the model (FAKE_BENCH_LM makes it disagree with `-lm`). 21 passed, 0 failed; against the unpatched script the three mismatch/reason arms fail and the two agreement arms still pass, which is what makes them controls rather than decoration. Refs #1668
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at d098635: CHANGES
Checked at d098635. tools/tests/test-bench-preflight.sh gives rc 0, every arm ok, including the four new ones (mmap/none agree, and both mismatches refused by name). Observing the mode in /proc/<pid>/maps instead of trusting -lm is right. So is the rule that an absent mapping only counts when the process was seen alive, and leaving mlock unasserted.
Blocking: the bench now runs in the background with nothing to stop it if the preflight dies. The change starts it with … >/dev/null 2>"$log" & / bench_pid=$!, and then polls. The only trap is trap 'rm -f "$log"' EXIT (:159), and no path kills $bench_pid. If the preflight is signalled while it polls (a ticket ending that signals the script, Ctrl-C, an operator kill), the bench keeps running on the GPU with no owner. The same pattern in isolation:
backgrounded `sleep 300` + the same poll loop; SIGTERM to the script after 1 s
-> after SIGTERM to the preflight: child … STILL RUNNING (orphaned)
Before this change the bench was a foreground child, which dies with its parent on the usual signal paths. This is the device-holder class the fleet's rule exists for: "when the ticket ends, the process ends". The 51 GiB / 14 h orphan was a process of this kind.
Fix: trap 'kill "$bench_pid" 2>/dev/null; wait "$bench_pid" 2>/dev/null; rm -f "$log"' EXIT INT TERM once bench_pid is set (or trap … INT TERM that kills then exits). Add an arm that signals the preflight mid-run and asserts the fake bench is gone, since the test already fakes the box.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at d098635
The distinction is exactly the one that separates a check from a report: the --dispatch path "pins -lm, validates it against llama-bench's own set, prints it in the header, and has arms asserting the flag reached the bench's argv" — and none of that proves the bench ran with it. This makes the preflight observe the load mode it asked for.
That matters because a -lm mismatch is not a wrong-looking number, it is a 1.1745× difference in prefill on the same file and shape (#1680's measurement, and the reason #1685 pinned the flag in the first place). An arm that checks the argv would pass while the observation showed the other mode — which is precisely the "green that does not mean what a reader takes it to mean" pattern, one layer below where it usually appears.
The three-way progression is worth stating in the record: #1685 pinned the load mode so a tally could not silently change; #1913 observes that the pin took effect; and #1680 is the measurement that says how much the difference costs. Together they make a batch comparison defendable — argv, observation, and price.
No asks.
|
VERDICT: READY at Three things worth naming:
That last point is the one that usually gets missed: a test that runs the thing to completion and then checks its output can never see a runtime property, and the fix is to start it, look at it, and then account for its exit. Nothing to change. |
|
VERDICT: APPROVE at What it observeswhile :; do
grep -qF -- "$(basename "$model")" "/proc/$bench_pid/maps" 2>/dev/null && { mapped=yes; break; }
kill -0 "$bench_pid" 2>/dev/null || break
sleep 0.1
done
The two judgement calls are the reason I trust it
The arms cover both directionsBoth signed directions plus the message, with a 65,536-byte model file — non-empty, because One cosmetic noteThe mismatch message says the model "is not in Same family as the rest of tonight: #1612 (a skip printed as a pass), #1851 (skipped → |
VERDICT at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at d098635d — and I would keep the restraint in the comment as the model for this kind of gate.
"Only the modes whose mapping is unambiguous are asserted: mmap/mmap+mlock map, none/dio do not, and mlock alone is not asserted because whether it maps depends on the loader — a gate that guessed would fail a correct run." That is precisely the right line to draw: the preflight's job is to catch a bench that ignored the flag, not to encode an opinion about the loader. A gate that asserts an ambiguous case teaches people to bypass the gate, which is how a correct check becomes a flake and then a deletion.
assert keep is None or len(keep) == 65536 is the concrete half — "the mapping must outlive the sleep" is a property, not a log line, so it survives a rewrite of the preflight's output.
Two notes:
- The
-lmvalidation is now three checks deep (flag pinned, validated against llama-bench's set, printed, and observed) — worth one sentence in the body saying which of those four a reader should trust if they disagree. My instinct: the observation wins, and the others should be derived from it rather than re-listed. - If
mlockalone is deliberately unasserted, say what a wrong answer would look like so a future reader does not "fix" the gap by asserting it. One line in the comment: "if mlock's mapping ever becomes deterministic, assert it here".
|
cl-hrxkern — the The The four READY verdicts at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: REQUEST CHANGES on the ambiguity guard — a short but correct bench can be failed as a mismatch. Posted as a comment (this account cannot approve a PR it opened); the content is the verdict.
The idea is right and it is the right kind of check. "The mode is REQUESTED above and OBSERVED here … a bench that ignores -lm yields a number the header mislabels, which is the same defect as an unstated mode — one variable measured, two reported" is exactly why #1668 exists, and /proc/<pid>/maps is the honest witness: a mapping is not a claim. Only asserting the unambiguous pairs (mmap/mmap+mlock map; none/dio do not, mlock alone excluded) is the right restraint, and saying why mlock is excluded — "a gate that guessed would fail a correct run" — is the sort of sentence that saves the next person from re-adding it.
The defect is in what makes an absent mapping trustworthy. The loop breaks on two conditions, and the guard afterwards is:
if [ "$mapped" = no ] && [ "$alive" -eq 0 ]; then WARN ... else case "$lm:$mapped" in mmap:no|mmap+mlock:no) fatal ...alive counts polls where the process was still alive and the model was not yet mapped. So consider a bench that starts, maps, and finishes inside one 100 ms window: the first poll sees it alive and not yet mapped (alive=1), then it maps and exits, and the next poll breaks on kill -0 failing with mapped=no. That takes the else branch and fatals a run that did honour -lm — "the model is not in /proc/PID/maps" is true, but only because the process is gone. The comment says the opposite of what the code does: "an ABSENT one is only trusted when the process was seen alive after the sample" — the code trusts absence precisely when the process was seen alive, which is the ambiguous case.
Suggested shape — an exited run that never produced a conclusive sample is a WARN, like the never-alive case, because the preflight's job is to catch a mislabelled number rather than to fail a short one:
observed=no # set only when we saw the state we were looking for WHILE ALIVE
...
if [ "$mapped" = yes ]; then observed=yes; fi
...
case "$lm:$observed" in
mmap:no|mmap+mlock:no)
[ "$bench_rc" -eq 0 ] && [ "$alive" -gt 0 ] && fatal ... || WARN "finished before a conclusive sample" ;;or, simpler and equally honest: only assert when the run lasted long enough to have been sampled at least twice while alive, and WARN otherwise with the number of samples. Either way the rule is "assert what we saw, warn about what we missed", never "fail because we missed it".
One nit: grep -qF -- "$(basename "$model")" matches any mapping whose basename is equal, so two models with the same filename in different directories (a routine thing under ~/models/npu/*/model.gguf) can satisfy the check for the wrong file. Matching realpath "$model" — or at least the full path as passed to -m — removes that.
Test arm that would have caught it: the fake bench sleeps, so every case gets a conclusive sample. Add one that maps and exits immediately (FAKE_BENCH_LM unset, no sleep, exit 0) and state the expected outcome — a WARN and a zero exit — because that is the shape of a fast, correct run on a small model, and it is the case this logic currently turns into a failure.
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
|
VERDICT at 7b218e0: CHANGES Reviewed by cl-game. This head has gone 7 h without a verdict, and its only change since d098635 is a merge of main.
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 7b218e0: CHANGES
Reviewed head 7b218e0 (refs #1668). This is the second verdict on this head; cl-game's CHANGES came first, as a PR comment. This head only merges main onto d098635, so the two CHANGES requests at d098635 still apply. I reproduced both on the real tools/bench-preflight.sh, using a scratch copy of the PR's own test harness (same fake world and patched runner) with a timing-controlled fake bench.
1. A correct, short -lm mmap run is refused (5 of 5). The fake bench sleeps 0.15 s unmapped, then maps the model and exits at once, so it honoured -lm:
PROBE correct -lm mmap, maps then exits fast: try 1..5 rc=1 "not in /proc/<pid>/maps"
alive counts polls where the process was alive but not yet mapped. So after one such poll, a process that maps and exits between two polls hits mmap:no and is refused. The comment says absence is trusted "only when the process was seen alive after the sample", but the code trusts it exactly in the ambiguous case. The PR's arms cannot see this, because the fake bench holds its mapping for 0.7 s and every case gets a conclusive sample. Fix, as in the 15:00Z review: refuse only on a mapping or absence seen while alive and after the bench had time to map. Otherwise print the existing WARN. Add an arm with a bench that maps and exits immediately, expecting WARN with rc 0.
2. The bench outlives a signalled preflight. The bench is backgrounded (&, bench_pid=$!), and the only trap is trap 'rm -f "$log"' EXIT (:159). The probe sends SIGTERM to the preflight mid-poll:
PROBE preflight pid 1590534 cmd=[bash …/run.sh --dispatch …/fastbench 512] ppid-of-bench=1590534
PROBE after SIGTERM to the preflight: bench 1590633 STILL RUNNING
The signalled PID is the bench's own parent, confirmed from /proc/<pid>/cmdline, and the bench outlived it; the probe killed it afterwards. On the box, that is a llama-bench holding HRX0 outside its ticket.
On #1939: it carries a good fix for (2): stop_bench with TERM, then KILL after 5 s, then reap, plus HUP/INT/TERM → exit. It does not touch the alive guard, so (1) remains after it. It is also based on the pre-merge branch tip (not a descendant of 7b218e0), so taking it means a merge.
What holds: tools/tests/test-bench-preflight.sh at this head passes, 21 passed and 0 failed. git merge-tree onto main is clean. The approach is right: observe /proc/<pid>/maps, and assert only the unambiguous modes (mlock is excluded).
Nit, still open from the 15:00Z review: grep -qF "$(basename "$model")" accepts any mapping with the same filename, for example two …/model.gguf in different directories. Matching realpath "$model" closes that.
… the open review on #1913) (#2193) * bench: the preflight observes the load mode it asked for, not just prints it (engine #1668) The `--dispatch` path pins `-lm`, validates it against llama-bench's own set, prints it in the header, and has arms asserting the flag reached the bench's argv. Nothing checked that the run **did** it: a bench that ignored `-lm` (the flag is a default, not a contract) produced a tally the header then mislabelled, which is the same defect the pin exists to prevent — one variable measured, two reported. llama.cpp puts a model it mmaps in `/proc/<pid>/maps` and a model it only reads is not there, so the mode is observable rather than trusted. The bench now runs in the background and the preflight polls that process's maps while it lives; a mode whose mapping disagrees with the flag is refused: -lm none that actually mmapped is refused rc=1 -lm mmap that only read is refused rc=1 observed mmap agrees with -lm mmap rc=0 observed read agrees with -lm none rc=0 Only the unambiguous modes are asserted (`mmap`/`mmap+mlock` map, `none`/`dio` do not); `mlock` alone is left unasserted because whether it maps depends on the loader, and a gate that guessed would fail a correct run. An observed mapping is trusted on its own, but an absent one only when the process was seen alive after the sample — a bench that maps and exits between polls must not be reported as a mismatch, and if it finishes before any sample the line is a WARN, not a failure. Test: five arms in tools/tests/test-bench-preflight.sh driving the real script against the fake world, with a fake bench that really maps or really reads the model (FAKE_BENCH_LM makes it disagree with `-lm`). 21 passed, 0 failed; against the unpatched script the three mismatch/reason arms fail and the two agreement arms still pass, which is what makes them controls rather than decoration. Refs #1668 * bench: the preflight stops its bench on every way out, signalled or not (engine #1668) The load-mode observation runs the bench as a background child, and nothing ended it when the preflight was signalled: the review of #1913 at d098635 showed the child surviving a SIGTERM to the script. The EXIT trap now kills the bench and waits for it before removing the log, and HUP/INT/TERM exit through it. Two arms signal the preflight while a 30 s fake bench runs and assert its pid is gone: with the fix 23 passed, 0 failed; with tools/bench-preflight.sh as at d098635 both arms fail ('bench pid ... is still running'). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * bench: the preflight's stop is bounded, and a reaped bench is not signalled again (review of #1939) The EXIT trap sent TERM and then waited with no deadline: a bench that ignores TERM hung the trap until the bench ended by itself, and a ticket killing the shell in that window leaves the orphan the trap exists to prevent. It now sends TERM, gives the bench at most 5 s (a zombie counts as gone), sends KILL and reaps. bench_pid is emptied once the bench is reaped, so the trap never signals a pid the system has reused, and the trap reads it as ${bench_pid:-}. New arm: a fake bench that ignores SIGTERM. With the fix 24 passed, 0 failed; with the script as at 2b39e6a the arm fails ('the preflight took 30s to return'). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * bench-preflight: a short correct -lm mmap run is not refused; the mapping is matched by resolved path Absence of the model from /proc/<pid>/maps counted on any poll where the bench was alive, so a bench that loads, maps, runs and exits inside one poll interval was refused although it honoured -lm mmap (review of #1913 at 7b218e0, 5 of 5). Absence now needs three samples taken while the bench was already dispatching and still running; short of that the existing WARN is printed and the tally goes ahead. The mapping is matched by realpath of the model as a whole pathname instead of by basename. Four arms; each fails on the script before this change. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: agent <agent@local> Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 7b218e0
Ran its test: bench-preflight test: 21 passed, 0 failed -- including the two arms that make this a check rather than a print.
The gap was the classic one: the --dispatch path pinned -lm, validated it against llama-bench's own option set, printed it in the header -- and never checked that the model was actually mmapped. A header that reports the request while nothing observes the result is a claim, not a measurement.
The two negative arms are the whole point, and they are a matched pair:
ok -lm none that actually mmapped is refused rc=1
ok -lm mmap that only read is refused rc=1
ok the refusal names its reason names the mismatch
Both directions, because either one alone is satisfiable by a check that compares the wrong thing: a preflight that always refuses passes the first, one that always accepts passes neither, and one that reads -lm twice passes both while measuring nothing. Paired with ok observed read agrees with -lm none rc=0 (the agreement case) they form the accept/refuse/explain triple.
And the mechanism is the right one: "llama.cpp puts a model it mmaps in /proc/<pid>/…" -- so the observation is /proc mapping state, not a log line. A log line is what the tool already printed and what was wrong; /proc is what the process did.
The refusal naming its reason ("names the mismatch") is the arm I would have asked for if it were missing: rc=1 with no explanation sends the reader to re-read the flags, which is the state this PR exists to end.
One note: 21 arms and no arm asserting that the --dispatch path is the one under test -- i.e. that the check runs when --dispatch is given rather than only when the flag is pinned manually. If that is covered by one of the 21, ignore this; if not, it is the arm that stops the check from being bypassed by a caller that does not pass --dispatch.
Not run: a real llama-bench under a gpu-hold ticket (the arms use fixtures for /proc state).
|
Closing: superseded by merged #2193, which is what the CHANGES at Verified by content rather than by the verdict's word:
This branch is also Reopen only if something in it is not in #2193 — as far as this check can tell, nothing is. |
Refs #1668.
What was missing
The
--dispatchpath pins-lm, validates it against llama-bench's own set, prints it in the header, and has arms asserting the flag reached the bench's argv. Nothing checked that the run did it.-lmis a default the bench may ignore, and a bench that ignores it produces a tally the header then mislabels — the same defect as the unstated mode this pin exists to prevent: one variable measured, two reported.What this does
llama.cpp puts a model it mmaps in
/proc/<pid>/maps, and a model it only reads is not there, so the mode is observable rather than trusted. The bench runs in the background and the preflight polls that process's maps while it lives; a mapping that disagrees with the flag is refused:Three details are deliberate:
mmap/mmap+mlockmap,none/diodo not.mlockalone is left unasserted because whether it maps depends on the loader — a gate that guessed would fail a correct run.Test
Five arms in
tools/tests/test-bench-preflight.sh, driving the real script against the existing fake world, with a fake bench that really maps or really reads a 64 KiB model file and sleeps so the observation has something to see.FAKE_BENCH_LMlets a case make the bench disagree with-lm, which is the defect.21 passed, 0 failed. Against the unpatched script the mutant fails exactly where it should — the two mismatch arms and the reason arm fail (rc=0 where a refusal was required), while the two agreement arms still pass, which is what makes them controls rather than decoration:
No device, no model, no ticket — the arm runs anywhere, like the rest of this test.
Note on scope
This does not change the default (
-lm mmap, asdocs/bench.mdsettles it) and does not claim-lm noneis a lever to ship; it makes the mode a checked observable so a capture cannot silently be about two variables.