Repository navigation
fix(native-cuda): re-zero recurrent/conv state on session reuse (LinearAttention corruption) - #554
Conversation
Native CUDA decode corrupted generation #2+ within a reused NativeDecodeSession on hybrid LinearAttention models (dense conv_state / recurrent_state). First generate() was correct; subsequent generations on the same session returned non-deterministic degenerate output. Scope (Step-0 blast-radius test): LinearAttention-only. A plain GQA transformer (KV-only) is clean under `profile_native --runs 2`; only models carrying fixed-size recurrent/conv state corrupt. So the general KV/position/decode-step reset path is fine. Root cause: DecodeCudaState zero-initializes the fixed recurrent/conv-state device bindings once in new(), but reset() -> rewind(0) only re-zeroed the attention-mask binding. Growable KV is masked and length-tracked (stale slots inert); fixed recurrent state is an unmasked rolling cache, so gen#2 inherited gen#1's terminal state -> garbage. Reproduces with capture ON and with ONNX_GENAI_CUDA_GRAPH=0, so it is a state-reset bug, not a capture bug. Fix (general, no model-specific casing): track the fixed-state bindings as `fixed_state_binding_range` and re-zero them in rewind() at target_len == 0 (the reset boundary), matching the constructor's zero-init and the state_pairs' declared `init: zeros`. Pure-KV decoders have an empty range and are unaffected. Regression test (non-vacuous, cuda-gated behind ONNX_GENAI_RUN_CUDA_SMOKE): a synthetic recurrent decoder whose logits are a direct function of the incoming conv_state, decoded twice across a reset; asserts gen#1 == gen#2. Without the fix it fails gen#1 [0,60,144] != gen#2 [252,312,396]; with the fix it passes. Verified end-to-end: 27B int4 LinearAttention now deterministic (capture ON and OFF), GQA unchanged. Refs #384. 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 #554 +/- ##
==========================================
+ Coverage 80.59% 81.16% +0.57%
==========================================
Files 315 315
Lines 123446 123446
Branches 123446 123446
==========================================
+ Hits 99489 100199 +710
+ Misses 19907 19191 -716
- Partials 4050 4056 +6
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
|
| Status | Scenario | Base | PR | Change |
|---|---|---|---|---|
sampling_latency/top_p_per_token |
905.80 µs | 1.08 ms | +19.1% | |
grammar_masking/llguidance_compute_mask/32 |
68.61 µs | 79.54 µs | +15.9% | |
| ✅ | logit_processing/seven_processor_chain_per_step |
1.10 ms | 1.25 ms | +13.8% |
| ✅ | sampling_latency/top_k_per_token |
438.38 µs | 494.72 µs | +12.9% |
| ✅ | gather/large_f32_threads=1-internal/131072 |
23.20 µs | 25.99 µs | +12.0% |
| ✅ | sampling_latency/min_p_per_token |
320.19 µs | 356.79 µs | +11.4% |
| ✅ | kv_cache/alloc_dealloc_pages |
35.07 µs | 38.94 µs | +11.0% |
| ✅ | matmul/medium_generic_f16_threads=1/32x512x512 |
27.83 µs | 29.09 µs | +4.5% |
| ✅ | matmul/large_generic_bf16_threads=8/32x1024x1024 |
1.24 ms | 1.28 ms | +3.2% |
| ✅ | add/large_f32_threads=1-internal/4194304 |
37.60 ms | 38.44 ms | +2.2% |
| ✅ | matmul/large_generic_f32_threads=1/32x1024x1024 |
8.56 ms | 8.65 ms | +1.1% |
| ✅ | gather/medium_f32_threads=1-internal/32768 |
3.40 µs | 3.43 µs | +1.0% |
| ✅ | matmul/medium_generic_f16_threads=8/32x512x512 |
27.71 µs | 27.89 µs | +0.6% |
| ✅ | matmul/small_generic_f32_threads=1/1x256x256 |
33.72 µs | 33.93 µs | +0.6% |
| ✅ | matmul/medium_generic_f32_threads=1/32x512x512 |
2.14 ms | 2.16 ms | +0.6% |
| ✅ | matmul/large_generic_bf16_threads=1/32x1024x1024 |
1.81 ms | 1.82 ms | +0.6% |
| ✅ | matmul/medium_generic_bf16_threads=1/32x512x512 |
485.07 µs | 487.54 µs | +0.5% |
| ✅ | matmul/small_generic_f32_threads=8/1x256x256 |
32.06 µs | 32.17 µs | +0.3% |
| ✅ | add/medium_f16_threads=1-internal/262144 |
2.41 ms | 2.42 ms | +0.2% |
| ✅ | matmul/medium_generic_f32_threads=8/32x512x512 |
885.93 µs | 887.57 µs | +0.2% |
| ✅ | gather/small_f32_threads=1-internal/4096 |
591.3 ns | 591.9 ns | +0.1% |
| ✅ | matmul/large_generic_f16_threads=1/32x1024x1024 |
72.86 µs | 72.90 µs | +0.1% |
| ✅ | matmul/medium_generic_bf16_threads=8/32x512x512 |
358.09 µs | 358.20 µs | +0.0% |
| ✅ | matmul/small_generic_bf16_threads=8/1x256x256 |
29.72 µs | 29.71 µs | -0.0% |
| ✅ | add/medium_f32_threads=1-internal/262144 |
2.35 ms | 2.34 ms | -0.1% |
| ✅ | matmul/large_generic_f16_threads=8/32x1024x1024 |
77.93 µs | 77.87 µs | -0.1% |
| ✅ | matmul/small_generic_f16_threads=1/1x256x256 |
27.71 µs | 27.69 µs | -0.1% |
| ✅ | tokenization/decode_tokens_per_second |
5.73 ms | 5.73 ms | -0.1% |
| ✅ | add/small_f16_threads=1-internal/1024 |
12.08 µs | 12.05 µs | -0.2% |
| ✅ | add/medium_bf16_threads=1-internal/262144 |
2.43 ms | 2.42 ms | -0.3% |
| ✅ | add/small_bf16_threads=1-internal/1024 |
11.89 µs | 11.86 µs | -0.3% |
| ✅ | gather/small_f16_threads=1-internal/4096 |
436.8 ns | 435.2 ns | -0.4% |
| ✅ | reduce_mean/small_f32_threads=1-internal/4096 |
14.11 µs | 14.06 µs | -0.4% |
| ✅ | sampling_latency/greedy_per_token |
3.03 µs | 3.01 µs | -0.5% |
| ✅ | add/large_bf16_threads=1-internal/4194304 |
39.01 ms | 38.80 ms | -0.5% |
| ✅ | matmul/small_generic_bf16_threads=1/1x256x256 |
29.14 µs | 28.91 µs | -0.8% |
| ✅ | gather/medium_f16_threads=1-internal/32768 |
2.16 µs | 2.14 µs | -0.9% |
| ✅ | matmul/large_generic_f32_threads=8/32x1024x1024 |
3.60 ms | 3.56 ms | -1.2% |
| ✅ | gather/small_bf16_threads=1-internal/4096 |
439.3 ns | 433.7 ns | -1.3% |
| ✅ | add/small_f32_threads=1-internal/1024 |
183.8 ns | 180.7 ns | -1.7% |
| ✅ | reduce_mean/large_f32_threads=1-internal/262144 |
924.98 µs | 908.36 µs | -1.8% |
| ✅ | gather/large_bf16_threads=1-internal/131072 |
9.55 µs | 9.37 µs | -1.8% |
| ✅ | matmul/small_generic_f16_threads=8/1x256x256 |
28.94 µs | 28.38 µs | -1.9% |
| ✅ | tokenization/encode_tokens_per_second |
355.72 µs | 348.63 µs | -2.0% |
| ✅ | add/large_f16_threads=1-internal/4194304 |
39.14 ms | 38.34 ms | -2.0% |
| ✅ | reduce_mean/medium_f32_threads=1-internal/65536 |
236.17 µs | 227.80 µs | -3.5% |
| ✅ | gather/medium_bf16_threads=1-internal/32768 |
2.26 µs | 2.13 µs | -5.4% |
| ✅ | gather/large_f16_threads=1-internal/131072 |
10.48 µs | 9.34 µs | -10.9% |
Visual flags:
Host info
CPU: Apple M1 (Virtual)
Cores: 3
OS: Darwin 25.4.0 arm64
Rust: rustc 1.97.1 (8bab26f4f 2026-07-14)
Load avg: { 5.56 4.10 5.05 }
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)
|
VERDICT: APPROVE Independent review by Harry (I did NOT author this; Mary did). Every claim in the PR description was re-verified from scratch in a scratch worktree (wt-harry-554) on an H200, device 0. All of Mary's claims hold. Code review (fix commit aab80e4 — only 3 files: cuda.rs, tests.rs, decision note)General, no special-casing (VERIFIED). The fix adds fixed_state_binding_range: Range to DecodeCudaState. In new(), bindings are laid out [0]=mask, then KV (kv_start..kv_end), then fixed recurrent/conv state (kv_end..fixed_state_end) — the sort_by_key(contains(fixed_state_inputs)) puts fixed-state pairs last, so the range covers exactly the conv_state/recurrent_state bindings and nothing else (not the mask, not KV). No model names, no architecture switches (cuda.rs:1116, 1412). For pure-KV decoders fixed_state_inputs is empty so kv_end == fixed_state_end -> empty range -> the re-zero loop is a no-op. Common path provably unchanged. Right time (VERIFIED). rewind() re-zeros the range only at target_len == 0 (cuda.rs:1496-1514). reset() -> rewind(0) is called at the start of every generate_with_callback() (mod.rs:356). Mid-generation never calls rewind(0); non-zero rewind (incremental/speculative) intentionally leaves recurrent state intact, mirroring the CPU path (backend.rs:86-105: past.clear() at 0, recurrent left intact otherwise). The re-zero uses the same native_cuda_memset_zero + checked_shape_bytes(physical_shape, dtype) as the constructor's zero-init (cuda.rs:1106-1113) — symmetric. First-generation short-circuit (backend.rs:58, current_len==0) is safe because the constructor already zero-inits. Run evidence (all commands via profile_native, which reuses ONE session across runs and bail!s on any cross-run mismatch — profile_native.rs:744-757)27B int4 hybrid LinearAttention (overlay /home/justinchu/mary-model-overlays/qwen36-ortref, conv_state+recurrent_state state_pairs, 64 layers), prompt "The capital of France is", --tokens 16 --warmups 0 --runs 2:
So it reproduces both capture ON and OFF -> state-reset bug, not a capture bug. Confirmed. Regression test native_cuda_reused_session_rezeros_recurrent_state (ONNX_GENAI_RUN_CUDA_SMOKE=1, feature cuda):
GQA qwen2.5-0.5b int4 (KV-only common path), --runs 2: DETERMINISTIC -> " Paris. It is the largest city in France and the second largest in the European". No regression on the common path. Gates
ConclusionRoot cause, scope (LinearAttention-only / empty range for pure-KV), fix generality, reset-boundary timing, CPU parity, the non-vacuous regression test, and end-to-end 27B determinism (capture ON and OFF) are all independently confirmed. Approving for merge. — Harry (independent reviewer) |
…dation (#555) Consolidates 6 inbox decision notes into decisions.md (20458->20332 B, under the 20480 gate) and archives two historical wave records. Updates agent histories. Wave summary (all merged): - #544 — async fence-ordered CUDA weight page-in (#87 increment-1) + deterministic anti-regression test - #552 — profile_native capture-counter observability for genai_config decoders - #554 — native-CUDA session-reuse recurrent-state reset fix (closes #553); 27B LinearAttention gen#2+ corruption - 27B native offload A/B proof: 2.9x VRAM reduction, byte-exact output State-only change (decisions/histories/archive). Logs are gitignored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ture scoping (#556) State consolidation only (no code). Merges 3 decision notes into decisions.md (20332→20465 B, under gate). **Key durable record:** the ⚑ PENDING-JUSTIN 27B Scan→CUDA-capture decision — the ~15-30× decode lever is NOT a contained increment (prefill/decode share one plan/session; static single-trip inline corrupts prefill). Correct fix = runtime shape-conditional dual-path capture infra, which reshapes the delicate #443/#543 control-flow capture core. Awaiting @justinchuby go-ahead before touching that core. Also records: 27B decode profile (Scan=56.5%, ~35× off HBM roofline, structural not kernel), and the #554 session-reuse fix. Round-8 wave record archived; histories compacted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…capture slice 1a) (#564) ## 27B decode: flag-gated single-trip Scan inline dual-path (slice 1a of Scan→CUDA-capture) First slice of the Justin-approved 27B Scan→CUDA-capture workstream (root-cause: eager `Scan`/LinearAttention recurrence = 56.5% of 27B decode, structurally un-capturable; ~15-30× lever). **Slice 1a is correctness-only — NO capture changes yet.** ### What A runtime-conditional dual-path in `exec_scan`: when `ONNX_GENAI_SCAN_INLINE_SINGLE_TRIP` is ON **and** the runtime trip_count==1, the Scan body runs once straight-line via a shared `run_scan_body_step` helper; otherwise the unchanged loop. Selection is **runtime-keyed** (not a graph rewrite) because prefill (trip_count>1) and decode (trip_count==1) share one executor/plan — a static seq=1 inline would corrupt prefill. - **Flag default OFF** ⇒ zero behavior change (loop for all trip counts). - Loop and inline share the same body-step + finishing code → **byte-exact with a one-iteration loop by construction**. DRY, no model/op special-casing. - **No capture-core changes** (provider.rs/capture.rs untouched); Scan still declines capture in both paths. That's slice 1b. ### Evidence - **27B (qwen3.6-27b int4, greedy 48 tok, CUDA):** token ids **identical** flag OFF vs ON across prefill + 48 decode steps. - **Non-vacuous tests** (CPU always-on + CUDA-gated): assert byte-equality vs loop AND runtime-keyed engagement (counter==1 only at trip_count==1, ==0 on prefill). Mutation-checked (independent reviewer ran 2 CPU + 1 CUDA mutations, all FAIL as required). - Regressions green: #554 session-reuse, #544 prefetch-WAR, cuda_control_flow_safety, CPU executor/control_flow/session suites. fmt+clippy clean. ### Review Independent review by Melina (author Mary locked out on rejection) — **APPROVE** with full non-vacuity evidence. ### 1b handoff Let the single-trip inlined body enter CUDA-graph capture (blast radius `provider.rs:458` + `executor/capture.rs`); assert captures/replays counters rise and 27B tokens stay byte-identical to this 1a reference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#565) ## Wire pure-native multi-component pipeline decode (GAP-3 Inc-A) First slice of the Justin-approved native multi-component pipeline workstream (GAP-3). Previously `pipeline/mod.rs build()` rejected pure-native pipeline selection with *"native pipeline decode is not yet implemented"* — blocking native decode for any multi-component (split embed→decoder) model. ### What Removed the construction-time bail. The pure-native path (`PipelineBackend::Native`) now falls through to the **already-working, backend-agnostic** flat-autoregressive decode loop (`run_autoregressive` → `PipelineDecodeLoopBackend`), building every component as a native `ComponentSession` + the decoder as `NativePipelineDecoder` — the same thing the hybrid env-flag path builds, but driven by backend selection instead of env injection. - **DRY:** new `native_component_selection()` converges both native sources (Native-backend and Ort+env-flag hybrid) on ONE decision point + the SAME builders. No forked construction; hybrid (#543/#541) byte-unchanged. - **Scope:** NON-PAGED only — `use_native_decoder ⇒ paged_enabled=false`, so S2 `mirror_last_present_kv` (`decoder_component.rs:256`) is never reached (that's Inc-C). No decode-loop / `native_decode/*` / capture-core changes — zero overlap with the concurrent Scan work. - Non-flat-AR native plans get a precise `native_pipeline_plan_unsupported` error (no silent mis-route). Auto→Native is normalized so Auto-resolved-native also drives native selection. ### Evidence - **Token-exact 3-way** (CPU, `tiny-gemma4-vlm`): ORT oracle == hybrid-env == pure-native, all `[0,5,6,7]`. - **CUDA differential** (`tiny-gqa-embeds-cuda`): pure-native == hybrid, both `[0,5,6,7]`. - **Non-vacuous:** reviewer re-inserted the bail → test FAILS at the pure-native case. - Regressions green: #384 native parity, #543/#541 hybrid, #554 session-reuse, 343 lib tests. fmt+clippy clean. ### Review Independent review by Harry (author Cohaagen locked out on rejection) — **APPROVE** with mutation + regression evidence. ### Handoff - Inc-B: native `prompt_only` prologue (e.g. vision_encoder). - Inc-C: native present-KV mirroring / paging at `decoder_component.rs:256` (unblocks Qwen3.6-35B-A3B MoE). - Follow-up: native-only loader (pure-native still loads ORT `PipelineModels`; ORT-unloadable block-quant models need it). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…GAP-3 Inc-C) (#566) ## GAP-3 Inc-C — native present-KV mirroring → paged native pipeline decode Closes the S2 bail in `NativePipelineDecoder::mirror_last_present_kv` so the pure-native pipeline decoder (Inc-A #565) can run **paged** (cross-request KV prefix reuse), not only the non-paged flat-AR path. Host-KV path; device-resident present-KV (35B-A3B GPU) is explicitly deferred to Inc-D. ### What's wired - `native_decode/mod.rs`: `supports_host_kv_mirror` gate, `host_present_kv` (read), `seed_growable_kv` (seed). - `pipeline/decoder_component.rs`: real `mirror_last_present_kv` — byte-identical geometry to ORT's `mirror_present_kv_to_pages` (shared `extract_present_token`/`append_token_kv`); `supports_paged_kv` + `load_paged_prefix` seams. - `pipeline/flat_autoregressive.rs`: DRY-factored `claim_paged_prefix` shared by ORT + native admit paths (only the KV sink differs). - `pipeline/mod.rs`: read-only `#[cfg(feature="native-backend")]` test accessor `materialize_published_prefix_kv` (non-mutating prefix-cache read-back). - `tests/native_pipeline_backend_selection_parity.rs`: `native_paged_prefix_reuse_matches_fresh_and_ort` — warm==cold==ORT tokens **plus** direct native-vs-ORT paged-KV **byte-equality** assertion. ### Correctness - Token-exact 3-way: pure-native warm == native cold == ORT oracle; `reused=4`. - **Geometry non-vacuity (independently re-run by reviewer):** no-op mirror, key/value swap, zeroed mirror, and forced-reused=0 mutations ALL fail — the byte-equality assert catches geometry corruption that argmax alone missed. - No production regressions: Inc-A #565, #541/#543 hybrid, #554 session-reuse green. - Scope: zero edits to the parked capture core (`plan_capture_region`, `executor/capture.rs`, `CudaGraphLifecycle`). ### Deferred to Inc-D (35B-A3B GPU end-to-end) Device-resident present-KV read-out, in-place-GQA CPU KV, f16/non-rank-4 round-trip, MoE routed-expert specifics if needed. These decoders correctly gate `supports_paged_kv=false` → Inc-A non-paged fallback (no silent-wrong paged run). ### Reviews Independent opus review with strict author-lockout: Mary REJECTED the original (vacuous geometry test) → revised the test as authorized non-author → Harry independent re-review **APPROVE** (all mutations re-run, byte-equality confirmed, production byte-identical, deferral gate verified). Note: run the parity test binary with `--test-threads=1` (two tests share a process-global decoder-device env var; parallel races flake — pre-existing on HEAD). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…DA decode (GAP-3 Inc-D) (#567) ## GAP-3 Inc-D — device-resident present-KV read-out → paged native CUDA decode Lifts the Inc-C (`#566`) `supports_paged_kv=false` gate for **device-resident f32 rank-4 CUDA GQA present-KV**, so a native CUDA pipeline decoder now runs **paged** (cross-request KV reuse) instead of the Inc-A non-paged fallback. Closes the present-KV threading gap for Qwen3.6-35B-A3B GPU decode. ### How (pure post-step plumbing — no kernel/capture-core edits) - `native_decode/cuda.rs`: `read_present_kv` reads the KV binding **after** the decode step's existing stream sync (`read_bytes`→`copy_to_host`→`dtoh`, which synchronizes) using the **physical/capacity** shape `[1,H,max_len,Dh]` so strides address the padded buffer; `seed_prefix` is the device counterpart of Inc-C's host seed; `device_present_kv_view` isolates the physical-shape handling. - `native_decode/mod.rs`: `present_kv`/`seed_kv`/`supports_device_kv_mirror` unify host-growable (Inc-C) and device-CUDA (Inc-D) onto the **same** `extract_present_token`/`append_token_kv` geometry + host f32 paged store (DRY, byte-comparable with ORT). - `pipeline/decoder_component.rs`: `supports_paged_kv = host OR device`; `mirror_last_present_kv &self→&mut self` (rippled to trait + ORT impl); `load_paged_prefix → seed_kv`. ### Correctness - `native_paged_prefix_reuse_matches_ort_on_cuda_device`: paged-native-CUDA == non-paged-native-CUDA == ORT oracle == closed-form tokens; mirrored pages **byte-equal** CUDA-vs-ORT; `reused=4`. - **H=2 unit test** for the physical-vs-logical stride bug (all existing fixtures are H=1, where the head-stride error is invisible). Mutating to the logical stride fails it. - Non-vacuity (independently re-run by reviewer): gate-revert, logical-stride, and forced-reused=0 mutations ALL fail. - Honest gating: f16 / non-rank-4 / CPU-in-place-GQA / sink-discontinuous stay `supports_paged_kv=false` → Inc-A non-paged fallback (no silent-wrong paged run). - No regressions: Inc-C #566, Inc-A #565, #541/#543 hybrid, #554 reuse (14/14), native_decode lib (54); scope clean (no standard_attention/GQA kernel, capture core, or `plan_capture_region` edits). ### Remaining (follow-up Inc-D.1) Real 35B-A3B export in **f16** device KV → f16 read-out + lossless paged round-trip; and CPU-in-place-GQA f32 (needs its own H≥2 ORT-oracle fixture — not free). Both correctly gated to non-paged today. ### Reviews Independent opus review, author-lockout enforced: Mary (native-decode specialist) **APPROVE** — 8/8 items verified with reproduced evidence on GPU 0, full mutation battery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…code (GAP-3 Inc-D.1) (#568) ## GAP-3 Inc-D.1 — f16 device-resident present-KV read-out → paged native CUDA decode Relaxes the Inc-D (`#567`) device paged gate `cuda && f32 && rank==4` → `cuda && (f32||f16) && rank==4`, so **f16 device-resident rank-4 CUDA GQA present-KV** runs **paged** instead of falling to the Inc-A non-paged path. This is the unlock for real fp16 models (e.g. gemma4-e2b, whose decoder KV is confirmed FLOAT16) to decode natively paged. ### How (dtype handling in native read/seed only) - `native_decode/tensor.rs`: `kv_dtype_to_f32` widens f16→f32 via `half` `to_f32_vec` (identical to ORT `to_vec_f32_lossy`); `f32_slice_to_dtype_bytes` narrows f32→f16 via `half::f16::from_f32` (identical to ORT `from_f32_slice_as`) — NOT the logits bit-twiddle. Narrower shared with the embedding-input path (DRY). - `native_decode/cuda.rs`: gate `kv_bindings_paged_rank4`; dtype-branch the read-out + seed. Host paged store stays **f32 for f16 models** (identical to ORT) → the Inc-C/D byte-equality oracle is preserved unchanged. - bf16, CPU-in-place-GQA, non-rank-4, sink-discontinuous stay gated → non-paged fallback (bf16 additionally bails defensively inside the convert). ### Correctness - New `native_paged_prefix_reuse_matches_ort_on_cuda_device_f16`: paged-native-f16 == non-paged-native-cold == ORT-cold tokens; device-mirrored pages **byte-equal** CUDA-vs-ORT (f32 store both sides); `reused=4`. - Convert unit tests: native widen == `half` reference; **f16→f32→f16 bit-exact across all 65536 non-NaN f16 patterns**; f32 identity. - New fixture `tiny-gemma4-vlm-cuda-f16` (Concat-KV so an ORT oracle exists; `value=key*2` bit-exact — the `+0.5` variant was rejected for hitting an f16 round-to-even midpoint). - Non-vacuity (independently re-run by reviewer): gate→f32-only, raw-u16-as-f32 wrong-convert, and mirror-disabled mutations ALL fail. - No regressions: Inc-D #567 f32 path still green, Inc-C #566, Inc-A #565, #541/#543 hybrid, #554 reuse (14/14), 354 lib tests. Scope clean (no attention/GQA kernel, capture core, provider, `CudaGraphLifecycle`, or ORT-bridge edits). ### Remaining (follow-up Inc-D.2) qwen3-30b-a3b is `torch_dtype=bfloat16`; if its ONNX export keeps bf16 KV it stays gated → Inc-D.2 flips the bf16 arm (helpers already have it) after confirming ORT widens bf16→f32 in its paged store. MoE FFN produces no KV (orthogonal); present-KV dtype is the only decode-path gate. ### Reviews Independent opus review with strict author-lockout: Harry **APPROVE** — convert matches ORT exactly, round-trip bit-exact (all 65536 patterns), bf16 excluded, full mutation battery fires, scope clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Native CUDA decode corrupted generation #2+ within a reused
NativeDecodeSessionon hybrid LinearAttention models (denseconv_state/recurrent_state). The firstgenerate()was correct; subsequent generations on the same session returned non-deterministic degenerate garbage. Discovered during the 27B weight-offload A/B (see #384).Repro (before):
profile_native --runs 2on the 27B →native greedy decode was not deterministic: first=[11751,...] rerun=[279,6511,...].Scope (Step-0 blast-radius test — decided early)
LinearAttention-only, NOT a general session-reuse bug.
qwen2.5-0.5b-instruct-cuda,--runs 2: clean / deterministic.So the general KV / position / decode-step reset path is fine for all multi-turn/server/REPL usage. Only models carrying fixed-size recurrent/conv state were affected.
Root cause
DecodeCudaStatezero-initializes the fixed recurrent/conv-state device bindings once, innew().reset()→rewind(0)(start of everygenerate()) only re-zeroed the attention-mask binding and resetlogical_len. Growable KV is masked + length-tracked (stale slots inert); fixed recurrent state is an unmasked rolling cache, so gen#2 inherited gen#1's terminal recurrent state → garbage. Reproduces with CUDA-graph capture ON and withONNX_GENAI_CUDA_GRAPH=0(eager) → a state-reset bug, not a capture bug.Fix (general — no model-specific special-casing)
fixed_state_binding_rangeonDecodeCudaState(empty for pure-KV models).rewind(), whentarget_len == 0, re-zero those bindings (same zero-init the constructor applies;state_pairsdeclareinit: zeros). Only at the reset boundary — speculative recurrent rewind to a non-zero length is intentionally unsupported, mirroring the CPU path.Pure-KV decoders have an empty range and are entirely unaffected (GQA path verified unchanged). CPU native was checked and is clean (its
rewind(0)clears recurrent state via thepastmap), so the regression test is CUDA-gated.Regression test (non-vacuous)
native_cuda_reused_session_rezeros_recurrent_state(#[cfg(feature="cuda")], gated byONNX_GENAI_RUN_CUDA_SMOKE=1): a synthetic recurrent decoder whose logits are a direct function of the incomingconv_state, decoded twice across areset(), asserting gen#1 == gen#2 (and that the per-step logits grow, proving state feeds logits).Non-vacuity proof (re-zero disabled):
gen#1 [0,60,144] != gen#2 [252,312,396]→ FAIL. With the fix → PASS.The existing
profile_native --runs 2determinism check is the end-to-end CI guard on real models.End-to-end verification (greedy,
"The capital of France is",--runs 2)" Paris.\n\n<think>\n\n</think>\n\nThat is correct. Paris is the capital and"ONNX_GENAI_CUDA_GRAPH=0)Guardrails
weight_paging.rs/provider.rs(harry-6, feat(cuda-offload): async fence-ordered weight page-in overlap (#87 first increment) #544 /squad/87-async-pagein).origin/main(fa1afed).cargo fmt --allapplied.Refs #384. Possibly related to the repeated/degenerate-sentence report on multi-turn native decode.
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com