Repository navigation
refactor(engine): unify native + ORT session-state policy behind SessionStore - #1255
Conversation
…ionStore Extract the duplicated session lookup, rewind bound-checks + error text, checkpoint arithmetic, and token truncation policy shared by the native and ORT decode backends into a single `session_state` module, mirroring the `KvPrefixStore` precedent. Backend-specific mechanism (ORT draft/target validation, scheduler completion, paged-KV; native persistent decoder rewind) stays in per-backend adapters implementing the shared `SessionStore` trait. Adds a two-part DRY guard: a compile-time `CheckedPosition` newtype and a test-time tripwire that fails if the rewind bound-check string is open-coded outside the shared policy module. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Reviewed and merged. This closes the last structural piece of the owner's standing directive that native and ORT decode must share one set of state-management logic rather than being treated separately. Why this was worth doing. Before this, The asymmetries kept are the right ones. I specifically did not want a forced symmetry here. ORT must validate and rewind draft/speculative state and paged-KV page ownership; native rewinds a persistent in-process decoder. Keeping Falsification — the evidence I actually cared about. Inverting the single shared bound check turns both backends red (3 ORT I also falsified the DRY guard itself, because a guard that cannot fail is worse than no guard — we have shipped one of those before. Appending a function to The Verification: |
CI's format lane had fallen behind — `cargo fmt --all -- --check` was failing on `main` at `7d669c76`. The drift came from several changes landing in the same window: the session-state unification (#1255), the server stack (#1197/#1249/#1250/#1254), and the native CUDA vision kernels (#1251). **No logic changes.** This is `cargo fmt --all` and nothing else — 7 source files, plus two `.squad/decisions/inbox` notes that were already sitting uncommitted in the tree. Verified locally: `cargo fmt --all -- --check` exits clean on this branch. Co-authored-by: justinchuby <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 #1255 +/- ##
==========================================
+ Coverage 79.85% 80.52% +0.67%
==========================================
Files 358 360 +2
Lines 155466 158790 +3324
Branches 155466 158790 +3324
==========================================
+ Hits 124144 127872 +3728
+ Misses 26720 26291 -429
- Partials 4602 4627 +25
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
🔴 Benchmark Regression DetectedComparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).
Visual flags: Host infoWhat this cannot catch
|
Replacement for #1190, rebased onto current `main` (the original could not be squash-merged because #1210 is stacked on its head branch, which triggers GitHub's stacked-PR 403). Content is identical to #1190; only the base is refreshed. ## What this adds A 20-file explanatory wiki under `wiki/` (crate architecture, execution backends, EP contracts, memory model, metadata-driven runtime, request lifecycle, testing/verification, performance playbook, repository map). It repeatedly labels itself non-authoritative and points at `docs/` and source for verification. ## Accuracy review (the bar for docs in this repo) I spot-checked the load-bearing claims against `main` as it stands today (after the memory capability-split revert #1247, session-state unification #1255, `reasoning_content` #1257, CUDA mask-reservation fix #1264): - **Crate architecture** — the crate roster and layering match the actual workspace `Cargo.toml` members and `crates/` tree. The map is explicitly non-exhaustive and says so. - **Memory model (the highest-risk area)** — `wiki/memory/Memory Management for Beginners.md` is marked `status: proposed` and frames the capability split as a *recommendation*, while describing the **current combined `DeviceAllocator`** as the present reality. I verified against `crates/onnx-runtime-memory-governor/src/allocator.rs`: the trait indeed still carries `allocate`, `commit_allocation_range`/`decommit_allocation_range`, `allocation_committed_bytes`, `mapped_bytes`, `create_shared_prefix`, and the successful-no-op default it warns about. Because the doc calls the split a proposal and the combined trait the current state, it **survives** the #1192→#1247 revert rather than being invalidated by it. The proposed `onnx-runtime-memory-api` crate correctly does not exist. `with_memory(...)` — cited as the mechanism that already satisfies issue #513's core goal — exists (`onnx-genai-ort`, `ep-cpu`, `ep-cuda`). - **Execution backends** — the ORT-vs-native-nxrt description and EP contract are accurate and high-level; nothing contradicts the #1255 session-state unification. - **Repository Map** — every code path it points at exists (`decode_loop.rs`, `sampling.rs`, `speculative/`, all crate dirs). - **Links** — all 58 repo-relative links resolve. (The one apparent miss, `docs/path.md`, is inside a fenced note-*template* code block, not a real link.) - **Benchmark-number rule (`docs/benchmarks/2026-08-15-...` §32.2)** — no perf figure is copied without metadata; the only "speedup" mentions are the docs *teaching* that a bare speedup is insufficient, and the GB values are illustrative diagram quantities, not measurements. ## Verification - Nothing under a test, build script, or CI job references the new `wiki/` files (checked with `git grep`), so this cannot break the build or suite. - No in-tree markdown lint / mdbook config exists, so there is no docs build to run here — stated rather than invented. No inaccuracies found inside the diff; no corrections were needed. Closes #1190. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c80f8522-983c-47f7-8241-2155a823aabe
What & why
The engine kept two parallel session implementations selected by
if self.decode_backend == EngineDecodeBackend::Native, one overself.native_sessionsand one overself.sessions. The policy around asession — the "session {id} not found" lookup, the rewind bound-checks and their
exact error strings, the checkpoint arithmetic, and
truncate/clear— wasbackend-independent but duplicated verbatim across the two arms (the
checked_subbound check and its message inrewind_session_by, theposition > currentcheck inrewind_session_to, etc.). This is the same classof drift risk that #1170 closed for the KV length layer with
KvPrefixStore.This PR extracts that policy into one place and routes both backends through it,
following the
KvPrefixStoreprecedent rather than inventing a new pattern.The seam
New module
crates/onnx-genai-engine/src/engine/session_state.rs:SessionStoretrait — mirrorsKvPrefixStore. Backends implementlogical_len(viaSessionLen),validate_rewind,rewind,reset,close, with identical method names/signatures on both sides, exactly asthe
NativeKvStore/OrtKvStoreadapters already do.require_session,checkpoint,token_count,rewind_by,rewind_to,reset,closeown everything that isbackend-independent: the lookup + not-found text, both rewind bound-check
strings,
SessionPosition::new(len)checkpoint arithmetic, and the dispatchinto the single backend truncation method.
runtime.rs:OrtSessions/OrtSessionsRef(always compiled)and
NativeSessions/NativeSessionsRef(#[cfg(feature = "native-backend")])implement
SessionStore. The six public methods (checkpoint_session,rewind_session_by,rewind_session_to,reset_session,close_session,session_token_count) now select an adapter and callsession_state::*. Therequire_ort_backend(...)guard stays in the ORT arm.Error text is now authored once. Rendered strings are byte-identical to the
originals (only internal variable names differ, e.g.
{id}vs{session_id}),so behaviour is unchanged. No test asserted these strings before; the ones that
assert backend rewind errors (
failed_rewind_of_*) are untouched.Asymmetries I deliberately kept (and why)
Forcing these into false symmetry would be worse than the small duplication that
remains, so — mirroring how
KvPrefixStore'sresetis legitimately differentper backend — they stay backend-specific behind the trait:
validate_rewind: ORT validates the target and draft/speculative stateand paged-KV page ownership (
validate_target_state_rewind_to_len) so arejected rewind leaves the session untouched; the three
failed_rewind_of_*tests pin this. Native's
validate_rewindis a deliberateOk(())— it alwaysadmits and clamps against its in-process decoder length. These are genuinely
different objects: ORT owns paged KV + a draft runner; native owns one
persistent in-process decoder.
rewind/reset/closemechanism: ORT doesvalidate →
scheduler.complete→ remove →rewind_target_state_to_len→reinsert; native truncates its token vec, rewinds/resets the single persistent
decoder (gated on
native_active_session) and touches LRUlast_access.create_sessionis left backend-specific and is not routed through theseam: it is object construction, not policy, and the two session value types
(
EngineSessionvsNativeSessionState) are genuinely different. Unifying itwould be a forced abstraction with no shared policy to host.
truncatefor ORT is intentionally not lifted into the sharedpolicy. It lives inside
rewind_target_state_to_len(kv_bridge.rs), which isshared with the speculative-decode hot path (
speculative/mod.rs). Liftingit would fracture that path for no gain. The shared policy therefore owns the
logical-length bound check (truly common) and delegates the truncation.
DRY guard (mandatory, per #1170)
Two-part, mirroring
ReusedPrefix+KV_REWIND_CALLERS:CheckedPositionnewtype with a private field. It is onlyever constructed by
session_state::rewind_toafter the bound check, and itis the required argument to
SessionStore::rewind/validate_rewind. A futurebackend therefore cannot rewind KV without going through the one bound check
— there is no other way to obtain the token.
the_rewind_bound_check_lives_only_in_the_shared_policyscans the crate sources for the
"cannot rewind session"fragment and failsif it appears anywhere except
engine/session_state.rs. A third backend thatopen-codes a copy turns the suite red. (
fork_sessionuses the distinct"cannot fork session", so there is no collision.)Performance
These are per-turn / per-request session ops, not the per-token decode hot
path (
generate_incremental_with_callback). The trait dispatch happens once perrewind/checkpoint/reset, off the hot path, so the dynamic call cannot show up in
decode throughput. No hot-path code changed; no per-token dispatch was
introduced. (Restructured, not measured — because there is nothing on the hot
path to measure.)
Falsification (the important part)
I broke the shared policy deliberately — inverted the one bound check in
session_state::rewind_to(requested > current→requested < current) — andconfirmed both backends go red from that single line:
failed_rewind_of_windowed_evicted_position…,failed_rewind_of_ort_owned_kv…,failed_rewind_of_runner_backed_state…— all3 FAILED (they now hit the bound check before their backend validation).
--features native-backend):native_session_rewind_by_truncates_logical_lengthFAILED withcannot rewind session 1 to token 3; current length is 5.Reverting the one line restored both to green. A break in the shared path turns
both backends red, which is the evidence that the extraction actually unified
them rather than leaving two look-alikes. I added
native_session_rewind_by_truncates_logical_lengthbecause there was previouslyno integration test exercising a valid explicit rewind on the native
backend (the existing native "rewind" test drives prefix-reuse, a different
path) — this closes that gap and gives the native half of the falsification.
Verification
cargo test -p onnx-genai-engine --lib→ 414 passed, 0 failed, 1 ignored.cargo test -p onnx-genai-engine --features native-backend→ all targets passexcept the pre-existing
pipeline_load_rejects_kv_page_pool_before_fixed_state_when_host_budget_cannot_fit_either(a host-RAM KV-budget test — confirmed failing identically on a stashed
baseline; it does not touch session code).
cargo clippy -p onnx-genai-engine --all-targets -- -D warningsand the samewith
--features native-backend→ clean.cargo test -p onnx-genai-engine --features native-backend,cuda --test native_engine -- --test-threads=1ran onthe GPU (not ignored): 16 passed, 1 failed, 0 ignored. Passing includes the
new
native_session_rewind_by_truncates_logical_length,native_sub4_cuda_fallback_generates_coherent_decode, andqwen15b_native_decode_locks_accurate_near_tie_token(real CUDA decode). The 1failure,
engine_native_cuda_matches_cpu_tokens("full CUDA mask reservationsize overflow" during native decoder load), is a pre-existing CUDA
mask-workspace sizing issue on this 8 GB card — confirmed failing identically
on a stashed baseline, unrelated to session state.
Behaviour note
close_session/reset_sessionnow run the existence check (viarequire_session) beforescheduler.complete. Previously ORTclosecalledscheduler.completebefore its remove-based existence check. This is notobservable through any error (
scheduler.completeon a missing id is benign) andresetalready checked existence first.Do not merge — please review.