Skip to content

[DRAFT] Captured spec-decode: byte-identity achievable & capture-safe; speedup fundamentally acceptance-bound - #1006

Draft
justinchuby wants to merge 13 commits into
mainfrom
squad/spec-decode-fp32-verify
Draft

justinchuby wants to merge 13 commits into
mainfrom
squad/spec-decode-fp32-verify

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 15, 2026 •

Copy link
Copy Markdown
Owner

[DRAFT] Captured spec-decode: byte-identity achievable & capture-safe; speedup is fundamentally acceptance-bound

Status: DRAFT — reports a fundamental finding, does NOT claim all gates green. Per the mission's "stop and report with evidence rather than ship a hollow fix" clause.

Summary

Byte-identity to plain M=1 greedy is achievable and CUDA-graph capture-safe. But spec-decode faster-than-greedy (G2) cannot coexist with byte-identity at the acceptance rates prompt-lookup produces on normal prompts — and this is not a GEMM/kernel problem: even the fast, non-byte-identical Marlin verify loses to greedy at these acceptance rates.

What works (committed, validated)

Single-launch batched byte-identical captured-verify GEMV kernels (General q/o/lm_head, tall-skinny down-projection, fused gate_up SwiGLU incl. the rmsnorm-prologue form qwen uses). Each M=W row's K-reduction is bit-identical to the M=1 GEMV greedy uses -> byte-identical by construction. All advertise last_call_capture_safe -> verify graph captures whole-graph (segments=1), zero capture-time alloc.

Gates

Gate Result
G1 byte-identity qwen 5/5 (W=5..9). glm 3/5 (W=5,7,8; W=6,9 diverge in the attention path, not the GEMV).
G2 speedup FAIL — fundamentally, for any kernel (see below).
G3 capture PASS — whole-graph (segments=1), captures>0, fallbacks=0.
G4 hygiene PASS — f64 oracle 25/0; fmt clean; only the pre-existing allowed platform_capacity.rs clippy casts.

Decisive evidence (qwen, whole-graph captured replay walls)

Path M=1 M=8 ratio
M=1 greedy 7.9 ms — 1.0x
Marlin M=8 (not byte-identical) — 35.9 ms 4.5x
Batched byte-identical M=8 — 65 ms 8.3x

An M=W verify forward is inherently ~4.5x (Marlin) / ~8x (byte-identical) an M=1 step (attention, 152k-vocab lm_head, MLP all scale ~M). Break-even (no thrash): spec beats greedy only when verify_ms / mean_accept < 7.9ms -> Marlin needs mean_accept >4.5, byte-identical >8.2; measured mean_accept on normal prompts is 2.4-3.3. Additionally the verify graph shares the single EP device-graph slot with M=1 decode, so miss-path decodes evict it (re-warm thrash), pushing end-to-end spec to 0.04-0.21x.

Recommendation

  1. Ship byte-identity as an opt-in determinism mode (ONNX_GENAI_SPEC_BATCHED_VERIFY), not a speed feature.
  2. Drop "spec beats greedy on prompt-lookup" — impossible for any kernel at mean_accept<4.5; re-scope G2 to high-acceptance regimes (real draft model / batch>1).
  3. Highest-leverage speed fix (independent of byte-identity): give the captured verify its own graph slot so miss-path M=1 decodes don't evict it.
  4. glm attention byte-identity (W=6,9) needs an fp32 verify-attention — tractable, not built here because G2 is blocked regardless.

Full analysis + repro: .squad/decisions/inbox/leon-spec-verify.md.

justinchuby and others added 10 commits August 15, 2026 00:51
…e decode

Extract the greedy speculative accept decision into a pure, unit-tested
`greedy_accept` with a `TieGuard`. The default (TieGuard::STRICT, eps=0) is
byte-identical to plain greedy: commits are always the strict argmax. When
ONNX_GENAI_SPEC_TIE_EPS>0 the guard detects draft tokens that are near-tie
co-winners of the argmax (logit within eps of the row max) and either counts
them (diagnostic, default) or, with ONNX_GENAI_SPEC_TIE_TOLERANT=1, accepts
them to recover acceptance on tie-prone (block-32) models. Surface
`near_tie_rejections` in SpeculativeStats and profile_native observability.

Refs #957 (Stage 4 speculative-decode e2e).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e decode

Add the CUDA-graph-captured verify path (WP4 of #957): a persistent padded
[1, width] token/position/logits binding set and a warm->arm->capture->replay
state machine (DecodeCudaState::verify_capture + decode_verify_captured),
modelled on run_one_token and the Increment-0 capture probe. Graceful eager
fallback on NotCapturable keeps correctness independent of capture.

Session surface: set_retain_graph_on_rewind / is_cuda / reset_captured_verify;
rewind(0) tears down the retained verify graph before freeing its buffers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… wire fused driver

Re-adds the CudaKvDebugStats struct header dropped when inserting
logits_bytes_to_rows, converts the read_bytes SessionError into anyhow, and
wires the prompt-lookup driver's fused captured-verify branch behind
ONNX_GENAI_SPEC_CAPTURED_VERIFY (propose-before-forward, retained graph,
row0=base / rows1..=k=verify, rewind to accepted prefix).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Completes the Stage-4 captured M=width verify path for native speculative
decode (env ONNX_GENAI_SPEC_CAPTURED_VERIFY, requires ONNX_GENAI_MARLIN_M_GT_1):

* Fixed-width fused forward: propose-before-forward, run one [1,1+draft_width]
  captured graph (row 0 = base distribution, rows 1..=k = verify rows), then
  rewind to the accepted prefix. A single constant-signature graph occupies the
  shared session slot so it CAPTURES once and REPLAYS across steps.
* Empty-draft (lookup miss) runs a cheap M=1 eager base forward
  (decode_base_eager_keep_verify_graph) instead of paying the ~2.2x width-W
  verify cost for a single token. Running an eager forward alongside an INSTALLED
  device graph corrupts memory (illegal address), so the miss path invalidates
  and the next hit re-warms — the single-graph-slot cost that caps realistic
  prompts (a dedicated second graph slot is the scoped follow-on).
* Threaded a preserve_graph flag through the eager row helpers.

Correctness (byte-identical to plain greedy) verified: glm-4-9b-int4 and
qwen2.5-14b-int4 captured-spec token streams == non-spec, CUDA smoke test green,
6 accept-guard unit tests pass. GPU7 (H200), steady tokens>=160.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…RE/AFTER

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The fused captured-verify path won 1.85-1.93x on high-acceptance prompts
but REGRESSED generic prose to ~0.60x: on realistic prompts most steps
miss the prompt-lookup, and an isolated hit re-warms the width-W verify
graph (a fresh capture, not a replay) to commit only B tokens at ~2.2x
the M=1 cost -- a net loss that never amortizes.

Two fixes, both preserving byte-identity:

1. HitDensityGate: the n-gram proposal is a cheap CPU search, so always
   propose, but only ENGAGE the width-W captured verify once recent
   would-hit density (sliding 16-step window, >=50% default) predicts the
   warmed graph will replay repeatedly. Sparse hits stay on the plain
   decode floor; clustered hits get the full 1.9x win. Env-tunable
   (ONNX_GENAI_SPEC_GATE{,_WINDOW,_MIN_HITS}); GATE=0 restores always-engage.

2. Gated-out / miss steps now run the normal M=1 CAPTURED decode
   (device-graph replay = full plain baseline speed) instead of an M=1
   EAGER forward, then reset the verify phase so the next engaged step
   re-warms cleanly. `decode` invalidates the slot on signature mismatch
   before launching any kernel, so it is safe alongside a stale width-W
   graph. This removes the residual eager-vs-replay tax on miss-heavy
   prompts (generic prose 81 -> 104 tok/s, vs 111 non-spec baseline).

Result (glm-4-9b-int4, H200 GPU7, steady): generic prose 66.8 -> 104
tok/s (no catastrophic regression); repetitive 347 tok/s (beats ORT 250);
gate costs ~2% on fully-favorable prompts. glm spec == plain greedy
byte-identical; qwen control byte-identical on non-degenerate prompts
(pre-existing Marlin-row0-vs-M1-GEMV near-tie divergence in qwen's
degenerate repetition loop is unchanged by this commit).

Removes the now-obsolete decode_base_eager_keep_verify_graph and the
always-false preserve_graph plumbing on run_cuda_eager_rows. Adds 6
HitDensityGate unit tests; CUDA smoke test disables the gate to exercise
the engage path on its short fixture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… near-tie note

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…+ M=1 base contract (#984 re-review)

Fixes the two capture-stability bugs and the greedy contract that Gaff and Chew
rejected in #984, on the opt-in captured-verify speculative path.

- BUG 1 (Gaff): the M=1 base-decode graph and the M=width captured-verify graph
  share the session's single device-graph slot. Track prev_engaged and invalidate
  the slot (both graph phases + verify phase -> NeedsWarmup) on an engage<->miss
  TRANSITION, before the incoming forward -- so an M=1 decode can never replay a
  stale width-W verify graph (glm "invalidated graph replay" / qwen
  CUDA_ERROR_ILLEGAL_ADDRESS). Consecutive same-mode steps still replay at full
  speed. New NativeDecodeSession::invalidate_graph_for_mode_switch().

- BUG 2 (Chew): the persistent GroupQueryAttention workspace is sized by query
  width and prepared only at prefill for q_seq=prompt_len. When prompt_len < W the
  width-W verify entered undersized ("workspace invariant mismatch"). Reserve the
  workspace for q_seq=W in run_verify_captured_cuda before warming/capturing.

- CONTRACT (Chew, binding): spec output must equal plain greedy (base token from
  the M=1 GEMV), not greedy-under-Marlin. Add a row-0 near-tie classifier
  (ONNX_GENAI_SPEC_ROW0_TIE_EPS, default 1.0): on a base-row near-tie, rewind the
  fused fold and recompute the base token from a fresh M=1 GEMV decode, so the
  committed base token is byte-identical to plain greedy.

Chose the coordinator-sanctioned invalidate-both fallback over a dedicated second
graph slot (documented as scoped perf-only follow-on).

Validation (GPU7 H200): f64 oracle 7/7; qwen degenerate no crash 160 steps/79
verify; spec==plain byte-identical on glm generic+repetitive and qwen degenerate;
glm favorable 326 tok/s (2.88x, past ORT); generic no re-warm crash; default-OFF
byte-identical. New CUDA regressions (engage/miss/re-engage; qwen workspace
growth) + 6 row-0 classifier unit tests. fmt+clippy clean (except 2 pre-existing
platform_capacity casts).

Feature stays opt-in (ONNX_GENAI_SPEC_CAPTURED_VERIFY=1), default-OFF.
Refs #957, #984.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep cold native generation workspace preparation prompt-shaped so speculative verify width does not leak into later M=1 fallback decode. Add qwen W=7/W=9 regression coverage for captured speculative decode byte identity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase-1 diagnosis of why captured speculative decode diverges from plain
M=1 greedy, plus a proven byte-identity reference path (opt-in, default OFF
so there is zero production change vs the aceed71 baseline).

Findings:
- The prior byte-identity reference (plain greedy with Marlin prefill ON,
  ONNX_GENAI_MARLIN_M_GT_1=1) is non-deterministic run-to-run; the
  production default (Marlin prefill OFF) is deterministic and is the
  correct reference. This is why the three prior attempts, validated
  against MARLIN=1, were unwinnable.
- The verify divergence is a K-reduction reassociation: any batched M=W
  int4 GEMM (Marlin fp16-accumulate, or the portable fp32 tiled GEMM) sums
  K in a different order than the M=1 GEMV greedy uses, flipping the argmax
  at sensitive positions. Not a margin/tie problem.
- Routing every verify row through the exact M=1 GEMV (per-row
  decomposition) is byte-identical by construction: qwen2.5-14b 5/5
  (W=5..9), glm-4-9b 4/5 (W=5..8; W=9 flips at token 157 in the q_seq=W
  block ATTENTION path -- a separate root cause needing fp32 verify
  attention).

The per-row path re-reads the int4 weights W times per verify step
(~6x slower than greedy), so it is committed OPT-IN only
(ONNX_GENAI_SPEC_PERROW_VERIFY=1, default OFF) as a validated byte-identity
reference oracle. The speed-preserving completion (batched M=W
fp32-accumulate GEMV that reads weights once + fp32 verify attention for
glm W=9) is scoped in .squad/decisions/inbox/leon-spec-verify.md.

Every per-row launch is static-grid capture-safe (verify graph still
captures/replays). Oracle 25+8 matmul_nbits tests green; fmt clean; no new
clippy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov

codecov Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.78%. Comparing base (532ef6b) to head (e8fb235).
⚠️ Report is 27 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1006      +/-   ##
==========================================
- Coverage   78.99%   78.78%   -0.22%     
==========================================
  Files         356      364       +8     
  Lines      150211   146798    -3413     
  Branches   150211   146798    -3413     
==========================================
- Hits       118661   115654    -3007     
+ Misses      26897    26528     -369     
+ Partials     4653     4616      -37     
Flag Coverage Δ
cli-ort-linux 83.52% <ø> (ø)
cli-ort-windows 83.02% <ø> (ø)
mlas 80.92% <ø> (-0.05%) ⬇️
offline 78.57% <ø> (-0.23%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 47 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

🔴 Benchmark Regression Detected

Comparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).

ℹ️ Absolute times are informational only — they vary with runner load. The % change column is the reliable signal because both sides ran under identical conditions.

Status Scenario Base PR Change
🔴 kv_cache/alloc_dealloc_pages 38.90 µs 58.52 µs +50.4%
🔴 logit_processing/seven_processor_chain_per_step 319.61 µs 471.39 µs +47.5%
⚠️ grammar_masking/llguidance_compute_mask/32 79.43 µs 99.67 µs +25.5%
⚠️ qwen3_sampling_processors/top_k_top_p_fast 699.21 µs 854.83 µs +22.3%
✅ qwen3_sampling_processors/top_k_full_sort_baseline 1.99 ms 2.29 ms +14.9%
✅ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 3.98 ms 4.19 ms +5.2%
✅ qwen3_sampling_processors/top_k_partial_selection 140.07 µs 147.10 µs +5.0%
✅ qwen3_sampling_processors/top_p_fast_after_top_k 589.10 µs 607.56 µs +3.1%
✅ sampling_latency/greedy_per_token 3.18 µs 3.28 µs +3.1%
✅ add/small_bf16_threads=1-internal/1024 12.88 µs 13.05 µs +1.3%
✅ gather/small_bf16_threads=1-internal/4096 490.4 ns 494.2 ns +0.8%
✅ qwen3_sampling_processors/top_k_top_p_full_sort_baseline 7.29 ms 7.32 ms +0.3%
✅ sampling_latency/top_p_per_token 357.22 µs 358.35 µs +0.3%
✅ sampling_latency/top_k_per_token 49.23 µs 49.27 µs +0.1%
✅ sampling_latency/min_p_per_token 198.71 µs 198.38 µs -0.2%
✅ add/small_f16_threads=1-internal/1024 13.18 µs 13.05 µs -1.0%
✅ add/large_bf16_threads=1-internal/4194304 43.97 ms 40.96 ms -6.9%
✅ gather/medium_bf16_threads=1-internal/32768 2.45 µs 2.23 µs -9.0%
✅ add/medium_f16_threads=1-internal/262144 2.63 ms 2.38 ms -9.6%
✅ matmul/medium_generic_f32_threads=1/32x512x512 2.41 ms 2.17 ms -10.1%
✅ add/medium_f32_threads=1-internal/262144 2.59 ms 2.32 ms -10.2%
✅ add/medium_bf16_threads=1-internal/262144 2.76 ms 2.47 ms -10.3%
✅ gather/medium_f16_threads=1-internal/32768 2.49 µs 2.23 µs -10.3%
✅ reduce_mean/small_f32_threads=1-internal/4096 15.58 µs 13.97 µs -10.3%
✅ tokenization/encode_tokens_per_second 402.16 µs 360.29 µs -10.4%
✅ add/large_f32_threads=1-internal/4194304 41.92 ms 37.40 ms -10.8%
✅ matmul/large_generic_f32_threads=1/32x1024x1024 10.35 ms 9.20 ms -11.1%
✅ gather/small_f16_threads=1-internal/4096 513.3 ns 455.1 ns -11.3%
✅ gather/large_bf16_threads=1-internal/131072 19.42 µs 17.08 µs -12.1%
✅ add/large_f16_threads=1-internal/4194304 44.80 ms 38.46 ms -14.2%
✅ gather/small_f32_threads=1-internal/4096 747.4 ns 639.4 ns -14.5%
✅ block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 84.05 µs 71.83 µs -14.5%
🟢 matmul/large_generic_f16_threads=1/32x1024x1024 87.22 µs 73.75 µs -15.4%
🟢 matmul/large_generic_bf16_threads=1/32x1024x1024 2.20 ms 1.83 ms -16.6%
🟢 reduce_mean/medium_f32_threads=1-internal/65536 274.96 µs 226.85 µs -17.5%
🟢 matmul/small_generic_bf16_threads=1/1x256x256 35.47 µs 29.09 µs -18.0%
🟢 gather/large_f16_threads=1-internal/131072 15.29 µs 12.53 µs -18.1%
🟢 tokenization/decode_tokens_per_second 6.76 ms 5.48 ms -18.9%
🟢 gather/medium_f32_threads=1-internal/32768 4.29 µs 3.45 µs -19.7%
🟢 matmul/medium_generic_f16_threads=1/32x512x512 35.03 µs 28.12 µs -19.7%
🟢 matmul/small_generic_f16_threads=1/1x256x256 35.05 µs 27.91 µs -20.4%
🟢 add/small_f32_threads=1-internal/1024 259.7 ns 204.7 ns -21.2%
🟢 matmul/small_generic_bf16_threads=8/1x256x256 39.27 µs 30.93 µs -21.2%
🟢 reduce_mean/large_f32_threads=1-internal/262144 1.18 ms 928.21 µs -21.6%
🟢 matmul/medium_generic_bf16_threads=1/32x512x512 624.44 µs 487.75 µs -21.9%
🟢 matmul/small_generic_f32_threads=1/1x256x256 44.43 µs 34.20 µs -23.0%
🟢 matmul/small_generic_f16_threads=8/1x256x256 36.96 µs 28.36 µs -23.3%
🟢 matmul/large_generic_f16_threads=8/32x1024x1024 108.08 µs 78.07 µs -27.8%
🟢 matmul/medium_generic_f16_threads=8/32x512x512 42.36 µs 28.22 µs -33.4%
🟢 block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 549.37 µs 349.32 µs -36.4%
🟢 matmul/small_generic_f32_threads=8/1x256x256 50.30 µs 31.98 µs -36.4%
🟢 matmul/medium_generic_bf16_threads=8/32x512x512 592.94 µs 353.02 µs -40.5%
🟢 block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 1.16 ms 677.37 µs -41.4%
🟢 gather/large_f32_threads=1-internal/131072 44.44 µs 25.66 µs -42.2%
🟢 matmul/large_generic_f32_threads=8/32x1024x1024 6.52 ms 3.72 ms -42.9%
🟢 matmul/large_generic_bf16_threads=8/32x1024x1024 2.27 ms 1.24 ms -45.3%
🟢 matmul/medium_generic_f32_threads=8/32x512x512 1.72 ms 910.13 µs -47.2%
🟢 block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 96.65 µs 47.77 µs -50.6%
🟢 block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 101.70 µs 46.74 µs -54.0%

Visual flags: ⚠️ ≥ 15% slower, 🔴 ≥ 30% slower — calibrated against measured runner noise (~27% worst-case on multi-threaded matmul)

Host info
CPU: Apple M1 (Virtual)
Cores: 3
OS: Darwin 25.5.0 arm64
Rust: rustc 1.97.1 (8bab26f4f 2026-07-14)
Load avg: { 3.21 4.15 6.10 }
What this cannot catch
  • Regressions in code paths not covered by these benchmarks (e.g., end-to-end decode with a real model)
  • Sub-threshold regressions that compound over multiple PRs
  • Performance changes that only manifest under GPU execution
  • Latency changes in the ORT integration path (these benchmarks exercise the native Rust kernels)

justinchuby and others added 3 commits August 15, 2026 10:39
…/lm_head)

Single-launch batched M=W verify for the General single-warp int4 GEMV:
hoists weight load/dequant/scale once and applies per-row hfma2, so each
row is byte-identical to M=1 greedy while weights are streamed once.
Covers q/o/lm_head; down-projection, gate_up SwiGLU and split-K (k/v)
still fall back to the per-row oracle. Opt-in via ONNX_GENAI_SPEC_BATCHED_VERIFY.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…capturability diagnosis

Adds single-launch batched byte-identical captured-verify GEMV kernels for the
tall-skinny down-projection and the fused gate_up SwiGLU (both plain and the
rmsnorm-prologue variant qwen2.5-14b uses), routing each M=W verify GEMM to the
batched sibling of the exact M=1 GEMV greedy uses. Each row's K reduction is
bit-identical to M=1 by construction, so the verify logits are byte-identical to
plain greedy; the weights are streamed once per launch and every launch is
static-grid, allocation-free and CUDA-graph capture-safe (advertises
last_call_capture_safe), so the composed verify graph captures whole-graph
(segments=1) instead of segmenting on MatMulNBits[KernelCaptureUnsupported].
Also fixes run_f16_per_row_verify to dispatch gate_up nodes through
run_f16_gate_up_swiglu (they are dispatched in run(), not run_f16()).

Validation (qwen2.5-14b-int4, deterministic Marlin-off reference):
  * G1 byte-identity: qwen 5/5 (W=5..9), glm 3/5 (W=5,7,8; W=6,9 diverge in the
    attention path, not the GEMV — a separate q_seq=W flash-attn reassociation).
  * G3 capture: verify graph is whole-graph (segments=1), captures>0, zero
    capture-time device alloc.
  * G4: f64 oracle green; fmt clean; only the pre-existing allowed
    platform_capacity.rs clippy casts.

Diagnosis instrumentation: spec_capture debug now reports captured segment
count; the leverb phase0 probe can route its M=K capture attempt through the
batched verify path (ONNX_GENAI_LEVERB_BATCHED) to measure the whole-graph
captured M=W replay wall.

DECISIVE finding (leverb phase0, qwen, whole-graph captured replay walls):
  M=1 = 7.9 ms; batched byte-identical M=8 = 65 ms (8.3x); Marlin M=8 = 36 ms
  (4.5x). An M=W verify forward is inherently ~4.5x (Marlin) / ~8x (byte-
  identical) an M=1 step on this model/HW, so captured spec-decode beats greedy
  only when mean_accept exceeds that ratio (>4.5 Marlin / >8.2 byte-identical),
  whereas prompt-lookup on normal prompts yields mean_accept 2.4-3.3. G2
  (spec faster than greedy) is therefore not achievable together with byte-
  identity at realistic acceptance; see decision drop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… G2 fundamentally acceptance-bound

Records the decisive leverb-phase0 captured replay walls (M=1=7.9ms,
byte-identical M=8=65ms/8.3x, Marlin M=8=36ms/4.5x), the break-even acceptance
math (Marlin>4.5, byte-identical>8.2 vs measured 2.4-3.3), the shared
graph-slot re-warm thrash, and the recommended contract relaxation (ship
byte-identity as an opt-in determinism mode; re-scope the speed gate to
high-acceptance regimes; give verify its own graph slot).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby justinchuby changed the title [DRAFT] Captured spec-decode byte-identity: diagnosis + opt-in reference + fast-path plan [DRAFT] Captured spec-decode: byte-identity achievable & capture-safe; speedup fundamentally acceptance-bound Aug 15, 2026
@justinchuby

Copy link
Copy Markdown
Owner Author

Triage on this draft, which has been open since 2026-08-15.

At 16 files and +4008/-165 this is the largest of my outstanding drafts, and its headline is a negative that matters: byte-identity under capture is achievable and capture-safe, but the speedup is fundamentally acceptance-bound. That conclusion is worth publishing regardless of whether the implementation lands -- it tells anyone considering speculative decoding where the ceiling actually is, which is a design fact, not an implementation detail.

I cannot validate the implementation half here. This machine no longer has a CUDA runtime (no cublasLt64_*.dll, no cudart64*.dll; a full native run reports 0 MiB GPU), so captured spec-decode cannot be exercised at all. Merging 4000 lines of unverifiable decode-path code would violate the standard I have been holding every other PR to today.

Proposed disposition, in order of preference:

  1. Split the finding from the code. Land the acceptance-bound analysis and its measurements as documentation now -- same treatment Attention floor: document GQA decode no-go probes #1011 just got -- and keep the implementation as a separate PR for a reviewer with GPU hardware.
  2. If the implementation is close to ready, say what specifically still needs a decision from me and I will unblock that part; it should not sit as a draft indefinitely because nobody knows whose move it is.

Either way this should stop being a draft. A 4000-line branch ages badly against a tree that has taken several merges to the same decode path since it was opened.

This branch has not been deployed

No deployments
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.

1 participant