Repository navigation
Fuse SSM/linear-attention reduction: drop fp32 cuDNN ReduceSum round-trip on native decode - #1486
Conversation
…und-trip Route f16 ReduceSum/ReduceMean to the single-kernel NVRTC block reduction (fp16 IO, f32 register accumulation) instead of cuDNN. cuDNN rejects a half reduceTensorCompType and forces CUDNN_DATA_FLOAT, implementing an f16 reduce as an fp16->fp32 op_tensor cast + fp32 ReduceSum + fp32->fp16 cast: a three-kernel round-trip through a full-size fp32 temporary. A captured-graph node trace of qwen3.5-0.8b-hybrid decode (Deckard) localized that fragmentation at ~20.7% of decode GPU-kernel time on the SSM/linear-attention path (cudnn ReduceSum 8.1% + fp16<->fp32 op_tensor casts 2.7%, firing 18x/step). The NVRTC block reduction already does the exact ONNX accumulate-in-f32-then-cast rule in one capture-safe kernel with no fp32 temporary and half the memory traffic; it already serves bf16 and every extended f16 reduction, so this is a general routing change, not a per-model special case. f32 stays on cuDNN (native f32 comp type, byte-identical). Native qwen3.5-0.8b-hybrid fp16io decode (H200, --test-threads=1, matched A/B): before 4.664 ms/tok (214.41 tok/s) -> after 4.318 ms/tok (231.61 tok/s) reclaim 0.346 ms/tok (+17.2 tok/s). generated_token_ids byte-identical. cuda_graph captures=4 fallbacks=0 (capture preserved). Parity: reduce_comptype_fp16_gpu, reduce_capture_gpu, nvrtc_reduce_capture_gpu, linear_attention_gpu all pass (tolerances unchanged). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…kernel
The SSM/linear-attention normalize-scale is exported as the primitive chain
`Add(sumsq, eps) -> Sqrt -> Reciprocal -> Mul`, so `Sqrt` and `Reciprocal` run
as two standalone per-layer kernels. A captured-graph node trace of
qwen3.5-0.8b-hybrid decode measured `sqrt_f16` (1.2%) + `reciprocal_f16` (1.2%)
= ~2.4% of decode GPU-kernel time, firing 2x per SSM layer (36/step).
Add `CudaRsqrtFusion`: for every `Sqrt` whose single output is consumed only by
one `Reciprocal` (and isn't a graph output), rewire the `Reciprocal` to read the
pre-`Sqrt` value, tag it `_cuda_rsqrt`, and delete the `Sqrt`. The node stays a
standard-domain `Reciprocal` (shape inference / registry dispatch unchanged); the
CUDA unary-math factory reads the marker and launches a fused `rsqrt_{dtype}`
kernel. The kernel reproduces the exact two-kernel rounding (Sqrt rounds
`sqrtf(x)` to the storage dtype, then Reciprocal reads it back), so output is
bit-identical to the unfused pair. General over f32/f16/bf16 and any shape;
single-consumer/no-escape guard preserves semantics.
Native qwen3.5-0.8b-hybrid fp16io decode (H200, --test-threads=1), incremental
on top of the SSM-reduce fusion:
before 4.318 ms/tok (231.61 tok/s) -> after 4.282 ms/tok (233.56 tok/s).
generated_token_ids byte-identical. cuda_graph captures=4 fallbacks=0.
nsys: sqrt_f16 + reciprocal_f16 both gone, replaced by one rsqrt_f16
(2.4% -> 1.3% of GPU-kernel time).
Tests: optimizer unit tests (fuse + single-consumer guard) and a GPU
byte-identity parity test (fused rsqrt == Reciprocal(Sqrt(x)) for f16/bf16).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-up (2nd commit): fuse SSM
|
| ms/tok | tok/s | captures | fallbacks | |
|---|---|---|---|---|
| after reduce-fusion | 4.318 | 231.61 | 4 | 0 |
| + rsqrt fusion | 4.282 | 233.56 | 4 | 0 |
generated_token_idsbyte-identical to baseline (greedy).- nsys:
sqrt_f16andreciprocal_f16both gone, replaced by onersqrt_f16(2.4% → 1.3% of GPU-kernel time — the pair collapsed). - Tests: optimizer unit tests (fuse + single-consumer guard) + GPU byte-identity parity test (
fused_rsqrt_is_byte_identical_to_sqrt_then_reciprocal, f16/bf16).
Combined result of this PR (both fusions)
Native qwen3.5-0.8b-hybrid fp16io decode: 4.664 → 4.282 ms/tok (214.4 → 233.6 tok/s, +8.9%), byte-identical greedy tokens, capture preserved.
Note: pre-existing unrelated failure binary_fixed_decode_shape_captures_replays_and_gates_other_paths (an Add binary-kernel capture-compat assertion) fails identically on the parent commit — not introduced here.
…verdict (#1489) Coordinator campaign-brain update (doc-only, .squad/identity/now.md). - Bank the honest fp16 fair-test reversal: qwen3.5 fp32 'moats' were export artifacts; fp16io same-graph is ~2.1x ORT-ahead (magnitude flagged unverified pending a real ORT captured harness). - Record PR #1486 native decode fusions (+8.9% qwen3.5-0.8b-hybrid, byte-identical, capture preserved). - Record #1474 memory-stack verification verdict (6 deterministic async-deferred-release GPU test failures; no decode regression). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ceSum off cuDNN to NVRTC block reduction (#1495) ## Summary Fuses the highest-ROI sub-lever of the native gated-delta **LinearAttention decode** path identified by Deckard's fair-gap kernel attribution: the **fp32 cuDNN `ReduceSum`**. The gated-delta linear-attention decode runs a per-layer q/k L2-norm SumSq as an f32 `ReduceSum` over `d_k` (16 outputs × 128 elements, **36×/decode step**). `ReduceKernel` previously sent *all* f32 sum/mean through `cudnnReduceTensor`, whose generic-reduce per-launch overhead dominates at that tiny decode shape — making it the **single largest decode op** (~11–13% of forward op time). This routes f32 sum/mean to the existing typed **NVRTC block reduction** (one capture-safe kernel, f32 register accumulation) whenever the reduction is well-parallelised — `out_count >= multiprocessor_count` **or** `reduce_count <= REDUCE_BLOCK (256)` — and keeps cuDNN only for the low-parallelism *"few outputs, huge group"* regime (e.g. global reduce-all) where a single serialising block would lose to cuDNN's multi-block reduce. It's the **fp32 analogue of the f16/bf16 routing from #1486**. ## Generality (RULES.md) The routing decision is a **general parallelism property** (SM count + per-output group size) — no head-dim, layer-count or qwen3.5 shape is hardcoded. Works for any gated-delta linear-attention config and any reduce shape/axis/rank. ## Results — qwen3.5-0.8b-hybrid **fp16io** export (H200, GPU 0, ORT 1.28 CUDA build) | | Baseline (cuDNN f32 reduce) | This PR | Δ | |---|---|---|---| | Decode throughput | **200.95 tok/s** | **209.71 tok/s** | **+4.36%** | | Decode latency | 4.976 ms/tok | 4.768 ms/tok | −0.208 ms | - Decode `generated_token_ids`: **byte-identical** before/after. - Golden lock `qwen35_0_8b_text_decode_lock`: **PASS** (byte-identical greedy stream). - Matches Deckard's ReduceSum-only prediction (~252 µs recovered ≈ +4.4%). `ONNX_GENAI_PROFILE_OPS` steady-state: `ReduceSum` share dropped from ~11–13% → ~7–10% of forward op dispatch. ## Validation - `qwen35_0_8b_text_decode_lock` (`--ignored`): **PASS** — byte-identical greedy tokens. - `reduce_capture_gpu`, `nvrtc_reduce_capture_gpu`, `reduce_comptype_fp16_gpu` (`--features cuda,gpu-tests`): **all pass**. - Added two **low-parallelism f32 capture tests** so the retained cuDNN path keeps coverage after the well-parallelised shapes moved to NVRTC. - Capture-safe: reuses the block-reduction path that already gates its trailing `synchronize()` on `!capturing` and marks the call capture-safe (folds into one graph segment). ## Honest ceiling — deferred sub-levers Deckard's #2 (fold the transpose/split/concat layout shuffle into kernel addressing) and #3 (fuse the recip/sqrt/sigmoid/softplus/mul gating chain into the `LinearAttention` epilogue) require a **graph-level fusion pass** rewriting the surrounding ONNX nodes into the kernel — a large change to graph optimisation + the kernel signature, with real byte-identity/generality risk. Out of clean surgical scope here. The ReduceSum lever alone delivered its full predicted ROI; Transpose (~2–3%), Sigmoid (~3%), Split (~2.4%) remain as the next graph-fusion targets. Working as Gaff (CUDA-kernel / SSM-fusion specialist).⚠️ CI intentionally not awaited (async). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… decay Softplus) into the decode kernel (#3) (#1496) ## Summary **Sub-lever #3** of the native-vs-ORT gated-delta `LinearAttention` decode gap (qwen3.5-0.8b hybrid, H200). Follows #1495 (fp32 ReduceSum → NVRTC block reduction, +4.36%) and #1486. The exported per-layer decode graph feeds the `com.microsoft::LinearAttention` kernel through standalone elementwise gate chains that ORT runs fused but native ran **decomposed**: - **beta = `Sigmoid(x)`** — delta-rule mixing gate - **decay = `exp(neg_exp_A · Softplus(a + dt_bias))`** — per-head decay gate (exported chain `Add(a, dt_bias) → Softplus → Mul(neg_exp_A, ·) → [Cast]`) New structural graph-fusion pass **`CudaLinearAttentionGatingFusion`** folds both chains into the kernel: rewires the beta slot onto the pre-`Sigmoid` value, the decay slot onto the pre-`Add` activation `a`, appends `dt_bias`/`neg_exp_A` as trailing kernel inputs, and marks two private attrs the CUDA factory consumes. Drops ~5 tiny elementwise nodes per layer (18 layers) from the captured decode graph. ## Byte-identity (non-negotiable) The kernel reproduces each folded op's device function bit-for-bit (`la_sigmoid`/`la_softplus` mirror `op_sigmoid`/`op_softplus`) **and** rounds every intermediate through the storage dtype (`round_store<T>`) exactly where the standalone `Add`/`Softplus`/`Mul`/`Sigmoid` kernels round — a no-op for the fp32 text export, an f16 round at each op boundary for the fp16-I/O hybrid. The fold is refused unless every folded operand shares the kernel's I/O dtype, so a dtype-changing `Cast` is never silently skipped. ## Generality (rules.md) Match is purely structural — op type, single-consumer / no-escape topology, which `Mul`/`Add` operand is a graph initializer, optional identity `Cast` tail. **No head-dim, layer-count, or qwen3.5 shape is baked in.** Fires for any head count / layer count; per-head and per-key-dim decay layouts both handled with element-count guards. Any gate whose chain escapes to another consumer is left exactly as exported. ## Capture-safety No host syncs, no per-step alloc, static launch config; the kernel just does more arithmetic per thread. Private attrs are compile-time constants (NVRTC key bumped `linear_attention_v1` → `v2`). ## Opt-out `ONNX_GENAI_CUDA_DISABLE_LINATTN_GATING_FUSION=1` restores the exact exported (unfused) chains. ## Verification - **Golden lock** (qwen3.5-0.8b text, fp32, native CUDA whole-graph capture): **PASS** — byte-identical, all 16 tokens. - **Pass unit tests** (3 new): full fold (both gates, ±trailing cast), beta-escape guard, env opt-out — all pass. - **Decode tok/s** (qwen3.5-0.8b fp16-I/O, H200, two-length method, best of 3 reps): | | rep1 | rep2 | rep3 | avg | |---|---|---|---|---| | fused (enabled) | 237.4 | 238.3 | 236.6 | **~237.4** | | unfused (disabled) | 232.7 | 229.3 | 226.3 | ~229.4 | **+3.4%** steady-state decode throughput. Honest, comparable to #1495's +4.36%. ## Sub-lever #2 (layout / L2-norm addressing) — DEFERRED, honest verdict **Not landed.** Folding the q/k L2-norm + transpose/split/concat layout shuffle into kernel addressing would require (1) reproducing ORT `ReduceSum`'s exact summation order (the one matched in #1495) *inside* the per-thread LA loop, redundantly across d_v threads — high byte-divergence risk against the golden lock; and (2) strided/permuted addressing in a kernel that currently declares `supports_strided_input = false` — a large tensor-plumbing change with a much bigger blast radius than #3. Neither can be guaranteed byte-identical + capture-safe within clean scope, so per honest-ceiling it is deferred rather than forced. A future pass can tackle it in isolation with its own dedicated byte lock. ## Files - `crates/onnx-runtime-ep-cuda/src/kernels/linear_attention.rs` - `crates/onnx-runtime-ep-cuda/src/optimizer.rs` _CI not awaited._ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#1519) Consolidation-only state PR (no code). Persists the campaign brain + decisions log to main after PR #1516 (int8 GEMV split-K, the 6th clean token-identical win) merged. ## What's in here - **`.squad/decisions.md`** — merges Deckard's corrected captured-replay attribution + Gaff's #1516 GEMV drop (22,963 → 25,954 bytes, under gate). - **`.squad/identity/now.md`** — new top verdict block: 6-win arc (#1486/#1495/#1496/#1501/#1503/#1516), fair gap 1.24× → **~1.057× ORT-ahead**, data-shuffle retired as a dead lever, honest ceiling ~260–270 tok/s reached → kernel-chasing stopped. - **`.squad/agents/{gaff,deckard}/history.md`** — appended. - **Removes** `.squad/decisions/inbox/gaff-gemv-bias-fusion.md` — it rode onto main via #1516's squash (tracked-inbox anti-pattern); already consolidated into decisions.md. ## Campaign standing Native qwen3.5-0.8b fp16io decode: **~263.8 tok/s** vs ORT eager ~279–290 = **~1.057× ORT-ahead** (from 1.24× at session start). Residual is a batch=1 launch/latency floor, not a buildable kernel lever. No manufactured levers remain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔴 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
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1486 +/- ##
===========================================
- Coverage 82.10% 80.12% -1.99%
===========================================
Files 12 376 +364
Lines 5471 164127 +158656
Branches 5471 164127 +158656
===========================================
+ Hits 4492 131506 +127014
- Misses 780 27794 +27014
- Partials 199 4827 +4628
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Summary
Route f16
ReduceSum/ReduceMeanto the existing single-kernel NVRTC block reduction (fp16 IO, f32 register accumulation) instead of cuDNN. This eliminates the self-inflicted fp32 round-trip on the hybrid decode hot path.The problem (profiling finding)
A faithful captured-graph node trace of qwen3.5-0.8b-hybrid (fp16io, keep_io_types=False) decode — agent Deckard,
.squad/decisions/inbox/deckard-fair-hybrid-gap.md— localized native's #1 fixable inefficiency: the SSM / linear-attention path does its reduction as a cuDNN fp32ReduceSumbracketed by fp16↔fp32op_tensorcast wrappers, ~20.7% of decode GPU-kernel time (LinearAttention 9.9% + cuDNN ReduceSum 8.1% + op_tensor casts 2.7%), firing 18×/step.cudnnReduceTensorrejects a halfreduceTensorCompTypeand forcesCUDNN_DATA_FLOAT, so an f16 reduce becomes a three-kernel round-trip (fp16→fp32 cast, fp32 ReduceSum, fp32→fp16 cast) through a full-size fp32 temporary.The fix
The typed NVRTC block reduction already implements the exact ONNX accumulate-in-f32-then-cast rule in a single capture-safe kernel — no fp32 temporary, half the memory traffic. It already serves bf16 and every extended f16 reduction, so this is a general routing change (all f16 sum/mean, any shape/axis/dim), not a qwen-specific special case. f32 stays on cuDNN (native f32 comp type, byte-identical).
One-line behavioral change: the cuDNN reduce gate goes from
Float32 | Float16→Float32.Before / after — native decode (H200,
--test-threads=1, matched A/B on the same build)Reclaim: 0.346 ms/tok (+17.2 tok/s, ~7.4%) — within Deckard's predicted 0.3–0.4 ms.
Parity
generated_token_idsbyte-identical before vs after (greedy decode).reduce_comptype_fp16_gpu,reduce_capture_gpu,nvrtc_reduce_capture_gpu,linear_attention_gpu.Capture preserved
cuda_graph: enabled=true captures=4 replays=504 fallbacks=0— no eager fallback; the fused f16 reduce folds into a single captured segment (verified byreduce_capture_gpu's single-segment assertion).Generality
The NVRTC block reduction is shape/axis/dim/head agnostic (base/delta offset walk, one block per output element). No dims hardcoded; no model-shape assumptions. bf16 already used this path; f32 remains on cuDNN.
Refs profiling finding:
.squad/decisions/inbox/deckard-fair-hybrid-gap.md.