Skip to content

feat(bench): carry the host-lock verdict in every CPU benchmark, from one definition - #1950

Merged
justinchuby merged 2 commits into
mainfrom
seb/bench-lock-provenance
Aug 24, 2026
Merged

justinchuby merged 2 commits into
mainfrom
seb/bench-lock-provenance

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 24, 2026 •

Copy link
Copy Markdown
Owner

Closes #1948.

Every CPU benchmark now carries the advisory host lock's verdict for its own measurement window, from one definition.

scripts/hostlock.sh is how agents sharing this box declare "I am measuring, stay off". The reader for it landed in #1925/#1936, but only decode_gap_park_ab and bench_generic consulted it — so nine other benchmarks published rows that cannot be told apart from rows taken beside somebody else's cargo test. That is the same failure this campaign keeps hitting: not a wrong number, a number nobody can attribute afterwards.

The window is a type, not a convention

crates/onnx-runtime-hostmon/src/window.rs. Window::open() reads the lock, close() reads it again, and the pair reduces to a Report.

A single end-of-run read is worse than no read at all: it names a credible holder for a run that changed hands halfway, and the row looks entirely normal. So the two-ended read is not left to each caller to remember —

  • A Report cannot be obtained from one reading. Fields are private, Window is the only constructor, and that is asserted by compile_fail doctests carrying a positive control (rustdoc does not enforce error codes — measured, see the review comment) plus a mutant that makes the fields pub and must be killed.
  • One reading feeds both the field and the reason. Reading twice would allow host_lock=changed lock_reason=acc0, naming a holder for a window the field says had none.
  • One row vocabulary. Ten binaries formatting their own host_lock= line is ten chances to drift, and a field meaning one thing in one matrix and another in the next is worse than an absent field. decode_gap_park_ab's hand-rolled copy is deleted in favour of the shared helper — it was already a second vocabulary.
  • The self-owner is an argument, not an ambient variable. A test that read HOSTLOCK_OWNER would assert the shell it ran in.
  • The window opens before warmup, not before the timed region: a warmup sharing cores with a co-tenant leaves caches and frequency in a state the timed region inherits.

Wiring

open_host_lock_window() / report_host_lock() in benches/common/mod.rs, two lines per binary: activation_bench, half_decode_gemv_ab, half_prefill_route_ab, int4_acc0_attribution, int4_decode_loop_ab, int4_prefill_route_ab, int8_prefill_route_ab, matmul_nbits_prefill_ab, native_vs_mlas, and decode_gap_park_ab converted.

Unprotected windows warn loudly on stderr but never abort. Refusing to print a matrix because nobody took a lock would mostly teach people to stop taking the lock; an unlocked run on a genuinely idle box is fine. What is not fine is one that cannot be told apart from it afterwards.

Note for log scrapers: the consolidated warning says "this run was not covered end-to-end"; decode_gap_park_ab's deleted copy said "this matrix".

Tests

  • 8 integration tests drive the real hostlock.sh, including a child-process probe that exercises Window::open()/close() against a real lock directory and a real HOSTLOCK_OWNER — asserting the exact row text for mine:, foreign:, held: and changed. A child, not a process-wide env override, so nothing can leak into another test.
  • Unit tests cover both change directions, the reason-splicing guard, warning attribution and the owner-dependent split.
  • 37 mutants, 37 killed (scripts/hostlock_mutants.py, extended to a second file). Three are killed only by the child probe; one only by the added change direction. Two earlier mutants failed to compile, which is a mutant written wrong rather than a kill — fixing them is what exposed that closing was untested.
  • Green with HOSTLOCK_OWNER set and unset; cargo fmt, clippy -D warnings, and cargo doc (0 warnings) on Linux and aarch64-pc-windows-msvc; all ten bench targets compile.

Rebased onto 011fbb284, so this sits on top of #1942's configurable lock directory; its two new agreement tests and this PR's coexist.

Not verified

No benchmark binary has been run end-to-end to observe its host_lock= line print. The row text is now observed from real code reading a real lock via the child probe, so what remains unobserved is the two lines in each main. The host has been under another agent's lock or above my own contention gate throughout; I will post an observed row here before merging rather than quietly drop this section.

An independent adversarial review of the first revision found five real defects, two serious — see the review-response comment.

@codecov

codecov Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.40%. Comparing base (2f2eb6d) to head (a4dfd23).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1950      +/-   ##
==========================================
- Coverage   80.42%   80.40%   -0.02%     
==========================================
  Files         420      424       +4     
  Lines      202834   207288    +4454     
  Branches   202834   207288    +4454     
==========================================
+ Hits       163128   166680    +3552     
- Misses      34143    34973     +830     
- Partials     5563     5635      +72     
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (?)
cli-ort-windows 72.01% <ø> (-0.10%) ⬇️
mlas 85.20% <ø> (?)
offline 80.54% <100.00%> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/onnx-runtime-hostmon/src/lib.rs 85.44% <ø> (-0.71%) ⬇️
crates/onnx-runtime-hostmon/src/window.rs 100.00% <100.00%> (ø)

... and 35 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

justinchuby added a commit that referenced this pull request Aug 24, 2026
Review of #1950 found the module's central claim false: `Report`'s fields
were `pub`, so any caller could write `Report { field: Mine("me"), .. }` and
get a protected, un-warned verdict derived from no reading at all -- the
flattering answer, available by accident. The doc said that was impossible
and a test named `a_report_cannot_be_had_from_one_reading` said so too, while
the test beside it constructed one exactly that way.

- `Report`'s fields are private with accessors. The claim is now asserted by
  `compile_fail` doctests. Those are weak alone -- rustdoc does not enforce
  the error code, and a `Reportt` typo passes; measured, not assumed -- so
  they carry a positive control that must compile, and a mutation that makes
  the fields `pub` and must be killed.
- Closing takes the self-owner as an argument. Two tests read `HOSTLOCK_OWNER`
  from the ambient environment and asserted the arm that only occurs when it
  is unset, so the suite failed in the one shell this subsystem is used from,
  and the mutation harness could not reach a green baseline there.
- `warning()` names the owner the verdict was judged against rather than
  re-reading the environment, which could name one the field never saw.
- `Window::open` and `Window::close` -- the only two functions a benchmark
  calls -- were covered by nothing; every test used the injected form. A
  child-process probe drives them against the real script, the real
  `HOSTLOCK_DIR` and the real `HOSTLOCK_OWNER`, asserting `mine:`/`foreign:`/
  `held:`/`changed` row text. Three new mutants are killed by it alone.
- The changed-hands test now runs both directions; the reason-from-the-second
  -reading mutant was killed only by the Linux-gated integration test before.

37 mutants, 37 killed. Full crate suite green with `HOSTLOCK_OWNER` set and
unset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby and others added 2 commits August 24, 2026 08:37
numbers that could not say whether anyone had declared the host, which
is a worse state than none of them carrying it: a reader who sees the
field on one matrix and not on another cannot tell whether the second
was unprotected or merely older, and absence reads as "fine".

The two-ended read is now a type. `Window::open` takes the first
reading, `close` takes the second, and there is no way to obtain a
`Report` without both -- a caller wanting a one-ended verdict has to go
around the module rather than merely forget something. That matters
because the one-ended version is the flattering one: a single
end-of-run read names a credible holder for a run that changed hands,
and the row looks entirely normal.

The row text, the `changed` rule and the `UNPROTECTED` warning have one
definition rather than eleven. `decode_gap_park_ab` formatted its own,
which was fine for one caller and a drift hazard for ten; a `host_lock=`
that means one thing in one matrix and another in the next is worse than
an absent field. It is the same argument #1926 is making about
`efficiency` naming two quantities, applied to our own instruments.

The window opens before warmup, not before the timed region: a warmup
that shared cores with somebody else's run leaves caches and frequency
in a state the timed region inherits.

Verified by mutation, and the mutation run paid for itself immediately.
Six new mutants for the window brought the harness to 31, of which two
initially survived by not compiling -- and fixing them exposed that
`close` itself was untested, because every test reached the logic
through `closed_at` and nothing could make the real reader return two
different values on demand. `close_with` now takes the second read as a
parameter, for the same reason `classify_io` takes its probe as one, and
the arm that matters -- the two ends disagreeing -- is reachable. 31
mutants, 31 killed.

The integration test drives the real `hostlock.sh`: it reads a scratch
directory the script has not touched, has the script acquire it, reads
again, and asserts the window reports `changed` with no reason attached.
A `changed` row that named the late holder would invite a reader to
treat the window as covered after all. It also pins that a reason
written with a space in it comes back with an underscore, asserted
against a reason the script wrote rather than one built in-process,
because that is the path an injected reason would actually travel.

Closes #1948

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review of #1950 found the module's central claim false: `Report`'s fields
were `pub`, so any caller could write `Report { field: Mine("me"), .. }` and
get a protected, un-warned verdict derived from no reading at all -- the
flattering answer, available by accident. The doc said that was impossible
and a test named `a_report_cannot_be_had_from_one_reading` said so too, while
the test beside it constructed one exactly that way.

- `Report`'s fields are private with accessors. The claim is now asserted by
  `compile_fail` doctests. Those are weak alone -- rustdoc does not enforce
  the error code, and a `Reportt` typo passes; measured, not assumed -- so
  they carry a positive control that must compile, and a mutation that makes
  the fields `pub` and must be killed.
- Closing takes the self-owner as an argument. Two tests read `HOSTLOCK_OWNER`
  from the ambient environment and asserted the arm that only occurs when it
  is unset, so the suite failed in the one shell this subsystem is used from,
  and the mutation harness could not reach a green baseline there.
- `warning()` names the owner the verdict was judged against rather than
  re-reading the environment, which could name one the field never saw.
- `Window::open` and `Window::close` -- the only two functions a benchmark
  calls -- were covered by nothing; every test used the injected form. A
  child-process probe drives them against the real script, the real
  `HOSTLOCK_DIR` and the real `HOSTLOCK_OWNER`, asserting `mine:`/`foreign:`/
  `held:`/`changed` row text. Three new mutants are killed by it alone.
- The changed-hands test now runs both directions; the reason-from-the-second
  -reading mutant was killed only by the Linux-gated integration test before.

37 mutants, 37 killed. Full crate suite green with `HOSTLOCK_OWNER` set and
unset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby force-pushed the seb/bench-lock-provenance branch from 9c44f09 to a4dfd23 Compare August 24, 2026 08:41
@justinchuby

Copy link
Copy Markdown
Owner Author

Independent review response

Ran an adversarial review of this PR in a separate context (claude-opus-4.8, read-only, no host access). It found five real defects, two of them serious. All are fixed in a4dfd2375.

1. The PR's central claim was false — Report's fields were pub

The module doc said "there is no way to obtain a Report without both readings". They were public fields on a public struct, so any caller could write:

let fake = Report { field: LockField::Mine("i-was-never-read".into()), reason: None };
// host_lock=mine:i-was-never-read   is_protected() = true   warning() = None

The reviewer built a throwaway crate outside the tree and executed that. A verdict fabricated from zero readings reported protected and emitted no warning — the flattering answer, reachable by accident rather than by going around the module.

Worse, the test named a_report_cannot_be_had_from_one_reading asserted nothing of the kind, and the test beside it constructed a Report by hand exactly that way. The suite demonstrated the thing the doc said was impossible.

Fixed: fields are private with field() / reason() accessors. The claim is now asserted by compile_fail doctests rather than by prose.

And the doctests needed the same scrutiny. A compile_fail block passes when the snippet fails to build for any reason. I annotated them compile_fail,E0451 assuming rustdoc pins the error code, then checked: it does not. Mistyping Report as Reportt still passes.

$ # with `use onnx_runtime_hostmon::window::Reportt;`
test window.rs - window::Report (line 131) - compile fail ... ok

So the error-code pin is gone (it was decoration) and each block is now backed two ways: a positive control that must compile using the same names by the same paths, and a mutant in scripts/hostlock_mutants.py that makes the fields pub and must be killed. Falsified: with the fields public, both blocks fail.

2. Two tests asserted an ambient environment variable

window::tests::a_steady_lock_... and the integration test both went through field_from_env and hard-asserted the Held(...) arm, which only occurs when HOSTLOCK_OWNER is unset. The reviewer executed it:

$ HOSTLOCK_OWNER=sebastian cargo test -p onnx-runtime-hostmon a_steady_lock_...
  left: Foreign("roy")   right: Held("roy")   → FAILED

HOSTLOCK_OWNER is the variable this subsystem exists for, and agents on this box export it. So the suite failed in the one shell it is used from — and the mutation harness could not reach a green baseline there either, which would have made "N mutants, N killed" silently unobtainable.

Fixed: closing takes the self-owner as an argument (close_as), and no test reads the environment. The whole suite now passes with HOSTLOCK_OWNER set and unset. Related: warning() no longer re-reads the variable — it names the owner the verdict was actually judged against, since a re-read could name one the field never saw.

3. Window::open and Window::close — the only two functions a benchmark calls — were tested by nothing

Every test used the injected form. The previous "31 mutants, 31 killed" was coverage of close_as, which no benchmark calls.

Fixed by a child-process probe in agrees_with_hostlock_sh.rs: the parent points a child at a scratch lock via Command::env (so no override can leak into any other test), the child calls the real Window::open() / close(), and the parent asserts the exact row text for four cases — mine:, foreign:, held: and changed, the last with the script claiming the lock mid-window. Three mutants (window-open-does-not-read, window-close-does-not-read, window-close-forgets-the-owner) are killed only by it.

It also asserts the child ran a test at all (1 passed): a renamed filter would otherwise leave the parent asserting against output no probe produced.

4. One mutant was killed only by the Linux-gated integration test

window-reason-from-second-read survived every unit test, because the only changed-hands case ran Held → Free, where the second reading has no reason either way. The now-added Free → Held(reason) direction — the likelier shape in practice — kills it in a unit test.

5. An unmentioned behaviour change

Consolidating decode_gap_park_ab's hand-rolled copy changed the stderr wording from "this matrix was not covered end-to-end" to "this run was not covered". Deliberate (the helper is used by ten binaries, not all of which produce a matrix), but it was not in the description and anything grepping the old string stops matching. Recorded here.

Also traced and unfounded, worth recording so it is not re-litigated: bench call-site ordering (open is genuinely before all measured work in all ten, report genuinely last, no process::exit), the decode_gap_park_ab deletion changing verdict/reason/protection or stream/order, the integration tests touching the real lock directory or leaking processes on failure, close_with's FnOnce allowing a reordered read, the mutation harness leaving a mutant in the tree across two files, and Window misbehaving off Linux.

State

37 mutants, 37 killed. Rebased onto 011fbb284, which includes #1942's configurable lock directory — its two new agreement tests and mine coexist; the union is what is on the branch and the whole suite is green.

The one claim still not backed by an observation is the bench-side two-line wiring: no benchmark binary has yet been run end-to-end to see its host_lock= line print. The row text itself is now observed from real code reading a real lock (finding 3), so what is unobserved is only the two lines in each main. The host has been declared or busy continuously; I will post an observed row before merging rather than delete the caveat.

@justinchuby
justinchuby marked this pull request as ready for review August 24, 2026 08:45
@justinchuby
justinchuby merged commit a7003c9 into main Aug 24, 2026
15 of 18 checks passed
@justinchuby
justinchuby deleted the seb/bench-lock-provenance branch August 24, 2026 11:03
justinchuby added a commit that referenced this pull request Aug 25, 2026
…tion

Independent review returned REQUEST CHANGES on five findings. All five were
reproduced before being fixed; two of them are defects in the section's own
reasoning rather than typos.

Citations (both MAJOR). The hostmon guard was cited as #2026 and the
benchmark harness-bug record as #1628. Both wrong, and wrong by the same
mechanism: I used `git log -1 -- <file>`, which resolves to the file's most
recent commit, not to the commit that introduced the line. `git log -S` on
the exact text gives #1950 for the guard (a7003c9) and #1619 for the
record (fb38341); #2026 does not touch the guard at all (0 hunks). For a
section whose entire value is that the prior records were unfindable, wrong
pointers are the defect itself, so this is now a **Check:** in the text.

The `1 passed` claim was backwards (MINOR). I wrote that the substring
check false-alarms when a filter selects more than one test -- the safe
direction. Measured, it fails in the dangerous one:

  11 passing tests -> "11 passed; 0 failed" contains "1 passed" -> accepted
  2 selected, 1 failing -> "1 passed; 1 failed"  contains "1 passed" -> accepted

Both are false greens; the second accepts a run holding a real failure.
The section now states this, explains why the check is nonetheless sound in
agrees_with_hostlock_sh.rs (window_probe_child is a crate-root `#[test]`,
so exactly one test is selected by construction), and draws the conclusion
the measurements actually support: the two checks compose. The listing pins
the selection to one, which is what makes reading the run output sound
afterwards. Neither half is sufficient alone.

`--list` counts `#[ignore]`d tests (MINOR). `tests::an_ignored_test` lists
with the same `: test` suffix and resolves to n=1 while the run executes
nothing (`0 passed; 0 failed; 1 ignored`). The listing proves the name
resolves, never that the arm ran -- which is why the run-output half is not
optional. Noted in the text. My first probe of this claim returned n=0 and
appeared to refute it; the probe had used a bare name and selected nothing,
i.e. it fell into the trap being documented, so the refutation was the
apparatus failing rather than the claim.

Design doc (NIT): "a bare name matches nothing" is false for a crate-root
test, the asymmetry §9 is careful about. Qualifier restored.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bench: ten of twelve CPU benches publish numbers that cannot say whether the host was declared

1 participant