Repository navigation
MTP verify-dedicated interior arena: both CUDA-graph slots replay (Primary + Verify) - #1658
Merged
Merged
Conversation
…erify Give the fixed M=k+1 MTP verify forward its own executor sibling (structural clone of the main graph, shares only weights+EP) so its JIT-sized interior device-buffer arena is independent of the M=1 base decode on the main executor. This removes the shared-arena clobber (interior Slice [1,1] vs [1,2]) that made the verify capture decline, and lets the Verify-slot graph coexist with the Primary M=1 decode graph and replay every step. - Executor::build_verify_sibling (build.rs): structural clone, graph_slot=Verify. - Session: verify_exec field + enable/run/try_capture/replay/reset_verify_sibling methods; pin_fixed_capacity_kv_capture_symbols also pins the sibling. - cuda.rs: configure_verify_capture builds the sibling (drops the now-unneeded main-exec workspace pin + slot flips); run_verify_graph_phase routes the verify forward through the sibling. Engine lib suite --features native-backend: 579 passed / 0 failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…boundary The verify-dedicated sibling executor's captured M=k+1 verify graph bakes its StepScoped workspace device pointer. Without pinning, `release_step_workspace` returned that scratch to the shared EP arena between replays; the interleaved M=1 decode on the main executor then reserved the same freed slot, so the next verify replay read reallocated memory -- a nondeterministic CUDA illegal access (700) at higher token counts / multi-generation runs. The sibling only ever runs the fixed M=K shape, so its workspace is reserved once at that peak and never grows: pin it permanently (`build_verify_sibling`) to keep the baked scratch pointer stable across every replay. Also reset the verify sibling's captured graph only at the generation boundary (`rewind_inner` target_len==0, where recurrent/conv states are re-zeroed), not in the generic per-step `invalidate_graph` (whose Blocker-B teardowns must not stale the verify, since fixed-capacity KV never moves). GPU-proven on qwen38-27b-int4-mtp (H200, k=1, 83.3% accept): cuda_graph_verify: captures=4 replays=184 fallbacks=0 invalidations=0 Previously-crashing configs (warmups>=2, tokens>=112) now run clean, no NaN, coherent output. Primary slot still replays=0 (Blocker B) -- separate fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…oss spec rewind With the verify running on its own sibling, the M=1 Primary decode graph was still invalidated every spec step (`cuda_graph: replays=0 invalidations=195`) by the commit path. Two causes, both fixed: 1. `commit_recurrent_state_to_accepted` re-advanced the accepted tokens with a single M=num_accepted batched forward, resizing the shared Primary interior arena to [1,num_accepted] and invalidating the captured M=1 graph. The recurrent/conv advance is inherently sequential, so re-advance ONE token at a time (M=1): state-equivalent (unit tests stay byte-identical) but keeps the Primary executor pinned at the [1,1] decode shape so its graph replays. 2. The contents-only KV roll-back (`rewind_inner` at non-zero target_len) inside the commit invalidated the Primary graph. That rewind leaves every binding's physical_shape/device_ptr fixed, so the M=1 replay signature stays valid — the exact case the `retain_decode_graph_across_spec` seam covers. Its old caveat (eager verify tears the graph down; unsafe until the verify workspace is pinned) is resolved now the verify is a separate workspace-pinned sibling, so enable the seam when verify-capture arms. Scoped to recurrent MTP; greedy has no verify_width, never hits a non-zero rewind, stays byte-identical. GPU-proven on qwen38-27b-int4-mtp (H200, k=1, 83.3% accept) — BOTH slots replay: cuda_graph: captures=4 replays=504 fallbacks=0 invalidations=3 cuda_graph_verify: captures=4 replays=184 fallbacks=0 invalidations=0 Primary invalidations 195->3 (one per generation reset); throughput 15.6->20.7 tok/s; token-identical, no NaN. (Primary replays=504 matches the greedy baseline.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…replay 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 #1658 +/- ##
===========================================
+ Coverage 81.02% 82.65% +1.62%
===========================================
Files 384 12 -372
Lines 180358 5477 -174881
Branches 180358 5477 -174881
===========================================
- Hits 146142 4527 -141615
+ Misses 29271 757 -28514
+ Partials 4945 193 -4752
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
justinchuby
added a commit
that referenced
this pull request
Aug 21, 2026
InferenceSession gained a verify_exec field in #1658 but the two executor/tests.rs InferenceSession initializers were not updated, leaving `cargo test -p onnx-runtime-session --features cuda` red on main. Add verify_exec: None to both. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
added a commit
that referenced
this pull request
Aug 21, 2026
…vice recurrent snapshot — 20.68→34.34 tok/s, token-identical (#1663) ## Summary Two GPU-validated, **token-identical** speedups on the real Qwen3.8-27B int4 hybrid (GDN+GQA) MTP self-speculative decode (k=1, 83.3% accept, 2.67 tok/step). Branch off origin/main \`66e19c2b5\`. Median-of-5 on an idle H200: **20.68 → 34.34 tok/s (+66%)**. ### 1. approach-B — eliminate the redundant re-advance On **full accept** (~67% of steps), the eager M=K verify forward already advanced KV + GDN/conv recurrent state to exactly the committed length, so the snapshot→restore→re-advance commit is pure redundancy and is skipped. Partial accept still rebuilds by re-running the accepted prefix (GDN exposes no intermediate per-position states). **20.68 → 26.73 tok/s (+29%)**; Primary graph replays 504 → 248. ### 2. device-to-device recurrent snapshot The per-step recurrent snapshot copied all fixed-state (GDN SSM + conv1d) bindings device→HOST every spec step (~10ms PCIe round-trip), unused on full-accept steps. Replaced with a **stream-ordered device→device** snapshot into a per-binding device scratch arena: - \`ExecutionProvider::copy_device_to_device\` (default errors; CUDA overrides with \`cuMemcpyDtoDAsync\` on the EP stream → ordered ahead of the verify forward, no host sync). - \`DeviceIoBinding::snapshot_device_into\` / \`restore_device_from\` + \`allocator()\`; \`DeviceBuffer\` re-exported from onnx-runtime-session. - \`DecodeCudaState.fixed_state_snapshot_scratch\` lazily allocated, sized from \`fixed_state_binding_range\` (no hardcoded dims). \`RecurrentStateSnapshot\` carries a \`device_scratch\` flag; CPU host-past path unchanged. **26.73 → 34.34 tok/s.** Also fixes a pre-existing red on main: \`InferenceSession\` gained \`verify_exec\` in #1658 but two \`executor/tests.rs\` initializers were never updated, breaking \`cargo test -p onnx-runtime-session --features cuda\`. ## Per-step target-forward budget (48 verify steps) | | base M=1 | re-advance M=1 | verify M=2 | Primary replays | |---|---|---|---|---| | before approach-B | 48 | 80 | 48 | 504 | | after approach-B | 48 | 16 | 48 | 248 | D2D snapshot does not change the forward count; it removes the per-step PCIe D2H state stage. ## Validation (all green) - **Both slots replay, fallbacks=0:** \`cuda_graph: replays=248 fallbacks=0 invalidations=3\`; \`cuda_graph_verify: replays=184 fallbacks=0 invalidations=0\`. - **Token-identical:** \`generated_token_ids\` md5 \`be7ed565\` == baseline across median-of-5, no NaN. - **Greedy inert:** engine lib \`--features native-backend\` **579 passed / 0 failed** (snapshot machinery only runs under spec-decode with recurrent state). - ep-cuda \`graph::tests\` **8/8**; session \`--features cuda\` green. - **Median-of-5 A/B** (H200, \`CUDA_VISIBLE_DEVICES=5\`, batch=1, tokens=128, warmups=3, int4 block-32, ORT 1.28 cuda13, \`--release --features bench-native,native-cuda,cuda-13000\`, \`profile_native --steady\`): **34.34 tok/s** (34.29–34.36). ## Honest remaining gap Still below the ~56 tok/s greedy baseline. The structural lever left is **fusing out the separate M=1 base decode** (per-step cost → a single M=2 verify forward). Blocked by a circular dependency (MTP head needs the hidden after the last committed token, only produced by the base decode); solvable via EAGLE-style frontier-hidden reseeding while staying token-identical, but the speedup hinges on preserving acceptance rate through the changed seeding — deferred as the next, riskier turn. approach-B + D2D are landed and durable regardless. Validated by unit tests + GPU token-identity; end-to-end campaign number reported honestly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Gives the fixed M=k+1 speculative-verify forward its OWN executor sibling with a private interior device-buffer arena, unblocking verify CUDA-graph capture+replay, then closes Blocker B so the M=1 Primary decode graph also replays. Both
cuda_graphslots now replay on one executor — the campaign's core proof — GPU-validated on the real qwen38-27b-int4 hybrid (GDN+GQA) MTP artifact.Branches off
39885ffc5(per-slot executor host capture state, #1654).What changed
build_verify_sibling,Session::verify_exec+ verify-sibling run/capture/replay/reset methods): a structural clone of the main graph on theVerifyslot, sharing only immutable weights/EP + the persistent external KV/recurrent-state bindings. Its interior[1,M]scratch is independent of the M=1 decode's[1,1]scratch, so the interleaved base decode can no longer resize/clobber the M=2 verify graph (the oldSlice [1,2] vs [1,1]decline → eager fallback).retain_decode_graph_across_specwhen verify-capture arms so the contents-only KV roll-back no longer invalidates the Primary graph.No hardcoded dims (M derived from
num_speculative_tokens+1). Greedy path inert (noverify_width, never hits a non-zero rewind → byte-identical).Validation (H200, k=1, 83.3% accept, median of 5, ORT 1.28 cuda13, int4 block-32)
Both slots replay, fallbacks=0, invalidations→~0 (every one of 5 runs):
native-backend), ep-cudagraph::tests8/8, recurrent-commit +native_verify_logits_require_restored_recurrent_statebyte-identity oracles pass.Honest perf finding (not a speedup)
Graphing the verify was necessary but not sufficient. Approach-A (snapshot→restore→re-advance) redundantly re-runs the accepted tokens on commit, and MTP runs verify(M=2) + per-token re-advance forwards per step for ~2.67 tokens/step — strictly more device work per accepted token than greedy's single graphed M=1 forward on this launch-bound decode. So MTP (20.63 tok/s) still trails the campaign-established greedy baseline (~56 tok/s; corroborated here by Primary
replays=504exactly matching the greedy run). The greedy baseline artifact is out-of-sandbox this session.Next lever: move to approach-B — thread
num_acceptedthrough the verify to SELECT the committed recurrent state from per-position post-states instead of re-scanning, removing the redundant re-advance forwards. The two-slot graphed executor here is the prerequisite.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com