Repository navigation
fix(session): stop a lazy gate initialisation from reverting a forced one - #1965
Conversation
… one `activation_memory_planner_reports_static_decode_graph_savings` failed on `Rust coverage (Windows x86_64)`: it forces the planner gate on, and the run that followed published no stats, so the `expect` on them panicked. The gate was off by the time the run read it. `globals_lock` serialises the tests that force these two process-global gates, and its doc comment says so. But the lazy initialiser in `enabled` / `activation_plan_enabled` is a writer too, it runs on any thread that reaches a gate first, and it does not take the lock -- `run.rs` consults the planner gate on every executor run, so under the parallel runner a sibling test is one of those writers. Its load / read-env / store is not atomic, so an unconditional `store` can land after a `force_*` that did hold the lock and silently revert it. Holding the lock was never sufficient to own a gate; the comment claimed a guarantee the lock could not give. Publish with `compare_exchange` from `UNKNOWN` instead, so the initialiser loses that race rather than winning it: it publishes only when nothing else has, and otherwise reports the value actually in force. Readers keep the same single relaxed load on the hot path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1965 +/- ##
==========================================
+ Coverage 80.22% 80.86% +0.64%
==========================================
Files 406 423 +17
Lines 188253 207436 +19183
Branches 188253 207436 +19183
==========================================
+ Hits 151018 167743 +16725
- Misses 31839 34055 +2216
- Partials 5396 5638 +242
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
Post-hoc review (Holden, security/soundness). #1965 merged 11:47Z with zero reviews; so did #1933. Reviewing merged concurrency code beats reviewing a draft, so I took this one. Verdict: correct, complete, and more valuable than the PR claims. One nit, whose blast radius I measured rather than warned about. 1. The fix is correct
Both override paths keep unconditional stores, which is right — a 2. It closes the shape, not one instanceGiven #1897 (one instance fixed, six left) I checked rather than assumed. Every tri-state gate in
3. This was not a test flake — that framing undersells itThe PR reads as an intermittent-Windows-coverage fix. It is a production fix:
So pre-fix: a caller opts in via the public API; any other thread taking its first run has already loaded Honest bound on this: the window needs the opt-in to land after another thread has entered the initialiser, so a process that enables at startup before any run is not exposed. Processes that enable after serving has begun are. That is a real defect and worth knowing it was fixed, not merely a CI-colour fix. 4. Mutation claim: verified by execution"Reverting the fix fails exactly one test, the new one" — confirmed, reverting Both legs confirmed to have actually rebuilt ( Driving the real 5. Nit — and my own hypothesis, falsifiedThe new test forces the gate manually and cleans up only on the success path: phase_profile::force_activation_plan_enabled(true);
assert!(…); // panics here → cleanup never runs
…
phase_profile::force_activation_plan_enabled(false);
I predicted that would cascade. It does not, and I checked instead of asserting it. Zero blast radius — twice:
So: nit, not a defect — switch to the guard for panic-safety, but nothing is broken today and I am not asserting a hazard I could not measure. The generalisation that does hold: the measured zero is a property of this suite, and any future test that reads the planner gate without taking the lock would convert it into a flake. APPROVE. No blocking findings. Host: two runs under |
An intermittent red on
Rust coverage (Windows x86_64), with a mechanismexecutor::tests::activation_memory_planner_reports_static_decode_graph_savingsfailed on that lane:The test forces the planner gate on, runs, and asks for the stats. It got
None— so the run saw the gate off, after the test had set it on.The third writer
globals_lock()exists for exactly this, and its comment says what it does:It does. The problem is that the tests are not the only writers.
enabled()andactivation_plan_enabled()are lazy initialisers — they load, and onUNKNOWNthey read the environment and store.run.rs:16consults the planner gate on every executor run, so under the parallel runner any sibling test is one of those writers, and none of them takes the lock. They must not have to: it is a hot-path read.That store is not atomic with the load that preceded it, so:
PLAN_STATEload()→UNKNOWN, enters initNXRT_*env varsglobals_lock,force(true)store(OFF)— its stale answerrun()→ planner off → no stats.expect(...)panicsHolding
globals_lockwas never sufficient to own a gate. The comment named a guarantee the lock cannot give — the losing writer is not a test and is not holding it.Why Windows coverage and not here: step 2 has to be slow enough to straddle step 3, and the gate has to still be
UNKNOWN, which is only true near process start.llvm-covinstrumentation on a shared 2-core runner widens exactly that window. 40 consecutive uninstrumented runs of this crate on Linux: 0 failures. Repeat-running it locally is not a test for this and I am not offering it as one.Fix
Publish with
compare_exchangefromUNKNOWNrather than an unconditionalstore, so the initialiser loses the race instead of winning it — it publishes only when nothing else has, and otherwise reports the value actually in force:Both gates use it. Readers keep the same single relaxed load on the hot path — the
compare_exchangeis only on theUNKNOWNarm, which runs at most once per gate per process.0/1/2becomeUNKNOWN/OFF/ONso the match arms say what they mean.I also corrected
globals_lock's comment to say what it does not cover, since believing it is what makes this defect invisible.The test
a_late_lazy_gate_initialisation_cannot_revert_a_forced_gateplays the interleaving directly: force the gate on, then have a late initialiser publish its stale answer, then assert the gate is still on. Both env answers, because the defect is in publishing at all.Two things I did deliberately, having shipped a vacuous guard before:
activation_plan_gate()returns&PLAN_STATE), not a copy. A#[cfg(test)]reimplementation of the protocol would pass whether or not production used it.publish_env_derivedthat never stores anything, so a second arm checks an unowned gate is still initialised, and that the value lands. That arm runs on a scratchAtomicU8so no process-global is leftUNKNOWNfor a concurrent reader.Mutation-verified. Reverting
publish_env_derivedto the originalstore(...); on:Exactly one test fails, and it is this one — so no existing test covered this, and the new one is not passing for an unrelated reason. Restored:
221 passed; 0 failed.Validation
Linux x86_64,
CARGO_INCREMENTAL=0,taskset -c 16-23, underscripts/hostlock.shwith a declared reason. Not claiming an idle host — a scanner co-tenant can appear at any time, and these are pass/fail results, not timings.cargo test --locked -p onnx-runtime-session --lib221 passed; 0 failedcargo test --locked -p onnx-runtime-session(all targets)storerestored)220 passed; 1 failed— the new test221 passed; 0 failedcargo clippy --locked -p onnx-runtime-session --all-targets -- -D warningscargo fmt --all -- --checkBackups were restored with
cp+touch, notmv:mvgives the restored file the backup's older mtime, cargo then treats it as unchanged and silently re-runs the mutant binary. That produced three consecutive false results for me earlier this week; on the other leg it would produce a false pass.What this does not claim
I have one CI observation and a mechanism that explains it. I have not reproduced the failure on Windows, so I cannot prove this was the only cause of that red. What I can say is that the interleaving above is reachable, is not prevented by anything, produces exactly this symptom, and is now impossible. If that lane fails the same way again, this fix is falsified rather than merely unlucky.
Related: this is the same shape as the
#1805placement guard and thegpu-testssubstring check — a guard that reports more than its evidence supports. Catalogued on #1817.