[CUDA] PagedAttention: quantized KV cache, XQA decode, MLA, QK-Norm and head sink - #29912
Merged
Merged
Conversation
Tianlei Wu (tianleiwu)
marked this pull request as draft
July 28, 2026 17:37
v_head_size can only differ from head_size in LATENT mode, where value, value_cache and v_scale are all absent. Using effective_v_head_size for those tensors implied a width that can never occur. Reserve effective_v_head_size for output 0 and the V view of key_cache, and state the rule explicitly in the schema and MLA sections.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends the CUDA contrib com.microsoft::PagedAttention operator to cover additional modern serving features (absorbed MLA / latent KV layout, quantized KV cache, QK RMSNorm, head sinks, explicit slot mapping, and replay-stable host metadata for CUDA Graph friendliness) and adds a paged-KV XQA decode path for quantized caches.
Changes:
- Expand the
PagedAttentionop schema + type/shape inference to support quantized caches (T_CACHE+ scales), optional/conditional cache/value IO forkv_cache_layout=LATENT, and new optional inputs (slot mapping, head sink, QK-norm, metadata). - Update the CUDA kernel implementation to handle quantized caches, new prologues/epilogues, backend selection (Flash/MEA/paged-decode/XQA/latent), and add paged-XQA translation units/loaders.
- Update documentation and Python symbolic shape inference for the new operator behavior.
Reviewed changes
Copilot reviewed 27 out of 29 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/python/tools/symbolic_shape_infer.py | Adds special-case output/cache inference for packed QKV and LATENT layout. |
| onnxruntime/core/graph/contrib_ops/bert_defs.cc | Updates PagedAttention schema + shape inference for new inputs/attrs, quantized cache types, and LATENT output rules. |
| onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc | Registers additional typed CUDA kernels for PagedAttention (activation + cache dtype pairs). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_loader.h | Declares paged-KV XQA decode launcher and shared-memory query helper. |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_loader.cu | Implements dispatcher for paged-XQA decode by head size / cache quant type / bf16. |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_loader_impl.cuh | Shared TU body for generating paged-XQA kernels across group sizes and dtypes. |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_impl_gen.cuh | Instantiation template mirroring XQA contiguous kernels but binding paged-KV entry points. |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_fp16_int8_64.cu | Instantiates fp16+int8 paged-XQA kernel (head=64). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_fp16_int8_128.cu | Instantiates fp16+int8 paged-XQA kernel (head=128). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_fp16_fp8_64.cu | Instantiates fp16+fp8 paged-XQA kernel (head=64, gated). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_fp16_fp8_128.cu | Instantiates fp16+fp8 paged-XQA kernel (head=128, gated). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_bf16_int8_64.cu | Instantiates bf16+int8 paged-XQA kernel (head=64). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_bf16_int8_128.cu | Instantiates bf16+int8 paged-XQA kernel (head=128). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_bf16_fp8_64.cu | Instantiates bf16+fp8 paged-XQA kernel (head=64, gated). |
| onnxruntime/contrib_ops/cuda/bert/xqa/xqa_paged_bf16_fp8_128.cu | Instantiates bf16+fp8 paged-XQA kernel (head=128, gated). |
| onnxruntime/contrib_ops/cuda/bert/xqa/utils.cuh | Adds int8→half conversion fastpath; adjusts constants for paged kernels. |
| onnxruntime/contrib_ops/cuda/bert/xqa/mhaUtils.cuh | Extends paged KV cache metadata (extraSeqLen) to unify seq-len handling. |
| onnxruntime/contrib_ops/cuda/bert/xqa/mha_impl.cuh | Fixes constexpr-divisor hazards; plumbs extraSeqLen into paged KV cache list. |
| onnxruntime/contrib_ops/cuda/bert/paged_attention.h | Extends kernel class state for new attrs/options + XQA shared-mem caching. |
| onnxruntime/contrib_ops/cuda/bert/paged_attention.cc | Implements new backend selection, metadata bounds, quantized cache wiring, and XQA/latent paths. |
| onnxruntime/contrib_ops/cuda/bert/paged_attention_impl.h | Adds helpers for paged decode and latent shared-memory sizing; templates on cache dtype. |
| onnxruntime/contrib_ops/cuda/bert/paged_attention_impl.cu | Adds quantized cache read/write, fused QK-norm+RoPE prologue, paged decode, latent backend, XQA paged decode, and updated gathers. |
| onnxruntime/contrib_ops/cuda/bert/paged_attention_helper.h | Validates new inputs/attrs (LATENT, slot mapping, head sink, QK-norm, cache quantization/data types, metadata). |
| onnxruntime/contrib_ops/cuda/bert/attention_data.h | Extends PagedAttentionData for cache dtype, scales, decode workspaces, and XQA scratch. |
| onnxruntime/contrib_ops/cpu/bert/attention_parameters.h | Extends PagedAttentionParameters with sink/QK-norm/quant/LATENT/offset fields. |
| onnxruntime/contrib_ops/cpu/bert/attention_common.h | Adds KV cache logical dtype enum + parsing helpers. |
| docs/ContribOperators.md | Updates PagedAttention docs to reflect new schema surface area. |
This was referenced Aug 17, 2026
This was referenced Aug 19, 2026
Hariharan Seshadri (hariharans29)
added a commit
that referenced
this pull request
Aug 21, 2026
## [WebGPU] PagedAttention: direct paged decode, fused paged prefill, Unpack/Repack skip (Phase 2 partial) This PR is the Phase 2 follow-up to #31611. It replaces the "always gather + always Unpack/Repack" v1 fallback with two paged-aware FlashAttention programs that read the paged KV cache directly, and a fast path that lets FA consume the packed varlen Q buffer without materializing padded BSNH scratch. Net effect: **~2× faster decode, ~1.15× faster uniform prefill, ~1.3× faster varlen prefill** on the shape matrix below, with no regressions. The Phase 1 gather-then-flash fallback shipped in #31611 remains intact and still runs on adapters / configs where the paged-aware shaders can't safely dispatch (see `Correctness invariants` below). ### What's shipped 1. **Direct paged split-reduce decode.** `FlashAttentionPagedDecodeQKV` + `FlashAttentionPagedDecodeVxReduce` index `key_cache` / `value_cache` directly through `block_table`. Selected when `max_seqlen_q < 32` — mirrors the dense-FA split-reduce threshold. Eliminates the dense K/V scratch and its gather bandwidth for every decode step. 2. **Fused paged prefill.** `FlashAttentionPagedPrefillProgram` is a straight port of the dense-FA prefill shader's shared-memory path with page-table-aware K/V tile loads (`bert/flash_attention_paged_prefill.wgsl.template`). Supports fp16, BSNH Q, packed varlen Q (`q_varlen` template variant), and variable-Q-length causal masking via `seqlen_k` + `seqlens_q`. No attention_bias / head_sink / TurboQuant. 3. **Unpack/Repack skip fast paths.** When direct paged attention runs, we can hand FA a rank-4 view over the raw packed Q buffer instead of allocating padded BSNH scratch: - **Uniform mode** (`B * max_seqlen_q == token_count`): view is `[B, max_seqlen_q, N, H]`. Covers decode, `B==1` prefill, and equal-length batched prefill (the common continuous-batching case). - **Varlen mode**: view is `[token_count, 1, N, H]` plus `cumulative_seqlens_q`; only the fused paged-prefill shader can index it (`q_varlen`). Skipping Unpack+Repack removes 2 dispatches (~300–500 µs of CPU dispatch cost per Run on D3D12) plus a `B * max_seqlen_q * hidden * 2 B` scratch allocation (tens of MB at long prefill). ### Dispatch-count reduction | Route (no rotary, non-packed) | #31611 (merged) | This PR (shm-path adapters) | |---|---|---| | **Decode** (`max_seqlen_q < 32`) | Scatter + Gather + UnpackQ + DecodeQKV + DecodeVxReduce + Repack = **6** | Scatter + PagedDecodeQKV + PagedDecodeVxReduce = **3** | | **Prefill** (`max_seqlen_q ≥ 32`) | Scatter + Gather + UnpackQ + FlashAttention + Repack = **5** | Scatter + FlashAttentionPagedPrefill = **2** | Decode's FA is 2 kernels (split-K: QKV + VxReduce); prefill's FA is 1 kernel (`FlashAttentionProgram`). On configs where `ShouldRunFusedPagedPrefill` rejects (fp32, `head_size > 256`, `block_size < max_k_step`), the prefill row falls back to the 5-dispatch #31611 cascade; decode's direct paged split-reduce path has no such gate. Neither route is adapter-gated — the paged shaders use no subgroup intrinsics and run on every WebGPU adapter that meets the fp16 / shm-budget / alignment predicates. ### Correctness invariants **Prefill selection** consults one shared predicate: ```cpp bool ShouldRunFusedPagedPrefill(context, is_fp16, max_seqlen_q, head_size, block_size); ``` It rejects (→ gather-then-flash fallback) when any of: - `!is_fp16` — only fp16 variant is compiled today. - `max_seqlen_q < 32` — decode uses the split-reduce programs instead. - `head_size` exceeds the workgroup shared-memory budget (fp16: `head_size > 256`). - `block_size < max_k_step` — the fused shader assumes one K/V tile lives in one paged block (one `block_table` lookup per tile). `paged_attention_helper` only enforces `block_size >= 16` power-of-two; e.g. `block_size=16` with fp16 `head_size<=128` (`max_k_step=32`) would splice into a physically-adjacent block that isn't the next entry in the table. The fused paged-prefill shader uses only workgroup shared memory (no subgroup intrinsics), so there is no adapter-class gate — subgroup adapters (Qualcomm / AMD / Intel with subgroups) take the paged shm kernel directly instead of falling back to gather + dense-FA-subgroup. Because the same predicate gates the "skip `RunGatherKV`", "skip `q_padded` scratch", and "select fused shader" decisions, the three cannot drift. **Decode selection** (`max_seqlen_q < 32`) is a pure shape check — no adapter, dtype, or block-size gate. The direct paged split-reduce kernels (`FlashAttentionPagedDecodeQKV` + `FlashAttentionPagedDecodeVxReduce`) are the sole decode path when the kernel dispatches at all (fp16 is enforced at kernel registration, so no fp32 fallback is possible). Unlike fused prefill, the decode kernels do one `block_table` lookup per K/V slot rather than per tile, so they have no `block_size` alignment requirement. **WGSL correctness gotcha handled** in the fused prefill shader. `cumulative_seqlens_q` is `array<i32>` but row indices are `u32`. Both `loadq` and `writeo` explicitly cast (`u32(cumulative_seqlens_q[b]) + q_idx`); without the cast, tint surfaces the type-resolution failure as an opaque `absl::…raw_hash_map<>::at` at runtime. ### Performance Machine: dev-box discrete WebGPU adapter (D3D12), 24-core host. Google Benchmark harness at `onnxruntime/test/onnx/microbenchmark/paged_attention.cc`, `--benchmark_min_time=0.3s`, wall-clock timing via `UseManualTime()`. Earlier revisions of this PR included an `ORT_WEBGPU_PAGED_ATTENTION_USE_FUSED` env-var kill switch used for A/B measurement against the #31611 cascade. That toggle has been removed (the direct/fused paths are selected internally by shape and config; the numbers below are the reason). The A/B was performed by temporarily broadening the toggle locally to also force `use_direct_paged_decode=false` and `skip_unpack_repack=false`, so fused=0 exercised the exact gather-then-flash cascade shipped in #31611. All numbers below are with that broadened toggle; the broadening was reverted before final push. Column meanings: **nH** = num query heads, **nKV** = num KV heads, **H** = head dim. Shape families: - MHA_H64 (nH=16, nKV=16, H=64), MHA_H128 (nH=16, nKV=16, H=128) - GQA_Qwen (nH=14, nKV=2, H=128), GQA_Llama (nH=32, nKV=4, H=128) #### Decode (16 shapes) | Shape (B/nH/nKV/H/past) | this PR (µs) | #31611 (µs) | Speedup | |---|---:|---:|---:| | 1/16/16/128/2048 | 669 | 3612 | **5.40×** | | 2/16/16/128/512 | 627 | 3248 | **5.18×** | | 1/16/16/64/2048 | 861 | 3835 | **4.45×** | | 2/16/16/64/2048 | 907 | 3617 | **3.99×** | | 2/16/16/64/512 | 603 | 1369 | 2.27× | | 2/16/16/128/2048 | 2129 | 4816 | 2.26× | | 1/16/16/128/512 | 607 | 1150 | 1.89× | | 2/32/4/128/2048 | 1590 | 3010 | 1.89× | | 1/14/2/128/2048 | 979 | 1601 | 1.64× | | 2/32/4/128/512 | 656 | 990 | 1.51× | | 1/16/16/64/512 | 576 | 843 | 1.46× | | 2/14/2/128/512 | 694 | 984 | 1.42× | | 1/14/2/128/512 | 600 | 757 | 1.26× | | 2/14/2/128/2048 | 970 | 1194 | 1.23× | | 1/32/4/128/512 | 643 | 751 | 1.17× | | 1/32/4/128/2048 | 1434 | 1437 | 1.00× | Range **1.00×–5.40×**, geomean ~2.0×. Biggest wins on long-past MHA (H=128, past=2048) where gather bandwidth dominated. The one 1.00× row is a small-K/V-cache GQA case where gather cost was already low. #### Uniform prefill (24 shapes) Range **1.01×–1.25×**, geomean ~1.13×. Highlights (all wins): | Shape (B/nH/nKV/H/T) | this PR (µs) | #31611 (µs) | Speedup | |---|---:|---:|---:| | 1/32/4/128/128 | 1225 | 1530 | **1.25×** | | 2/14/2/128/128 | 1111 | 1385 | **1.25×** | | 2/16/16/64/128 | 766 | 948 | 1.24× | | 2/32/4/128/512 | 9723 | 12054 | 1.24× | | 1/16/16/128/128 | 856 | 1051 | 1.23× | | 2/16/16/128/1024| 17296 | 21029 | 1.22× | | 1/14/2/128/128 | 718 | 877 | 1.22× | | 2/32/4/128/128 | 1640 | 1966 | 1.20× | | 1/16/16/64/128 | 750 | 891 | 1.19× | | 2/16/16/128/128 | 1294 | 1491 | 1.15× | *(14 more rows 1.01×–1.15×; full log in tree.)* Short-T shapes gain most from Unpack/Repack skip; long-T shapes are dominated by FA compute time. #### Varlen prefill (12 shapes, halving q_lens = `{max_T, max_T/2, max_T/4, …}`) Range **1.14×–1.73×**, geomean ~1.29×. | Shape (B/nH/nKV/H/maxT) | q_lens | this PR (µs) | #31611 (µs) | Speedup | |---|---|---:|---:|---:| | 4/16/16/128/512 | `{512,256,128,64}` | 5987 | 10375 | **1.73×** | | 4/14/2/128/512 | `{512,256,128,64}` | 5312 | 7815 | **1.47×** | | 4/32/4/128/512 | `{512,256,128,64}` | 11165 | 15427 | **1.38×** | | 4/16/16/128/1024 | `{1024,512,256,128}` | 20127 | 27661 | **1.37×** | | 2/32/4/128/512 | `{512,256}` | 8345 | 10732 | 1.29× | | 2/16/16/128/1024 | `{1024,512}` | 15121 | 19084 | 1.26× | | 4/14/2/128/1024 | `{1024,512,256,128}` | 17685 | 21562 | 1.22× | | 4/32/4/128/1024 | `{1024,512,256,128}` | 39571 | 47468 | 1.20× | | 2/14/2/128/1024 | `{1024,512}` | 13141 | 15430 | 1.17× | | 2/16/16/128/512 | `{512,256}` | 4528 | 5235 | 1.16× | | 2/32/4/128/1024 | `{1024,512}` | 28871 | 33383 | 1.16× | | 2/14/2/128/512 | `{512,256}` | 4067 | 4645 | 1.14× | Wins grow with batch size — bigger B means more of the padded-BSNH round-trip gets eliminated (B=4/maxT=512 packs only 46.9% of `B·maxT` tokens; the padded scratch #31611 allocates is >2× bigger than the actual data). ### Tests - `onnxruntime/test/contrib_ops/paged_attention_op_test.cc` `PagedAttention.EndToEnd_*` — 12/12 non-CUDA tests pass. Covers MHA, GQA, single/multi-batch, variable past lengths, empty tokens, packed QKV, rotary, mixed prefill+decode, cache aliasing via IO-binding. New: `EndToEnd_Prefill_MultiBatch_Varlen_Fused` (B=2, token_count=48 with q_lens (32,16), head_size=128, MHA) — regression test for the fused varlen prefill path. - Micro-benchmark harness `onnxruntime/test/onnx/microbenchmark/paged_attention.cc` — 52 registered shapes (16 decode + 24 uniform prefill + 12 varlen prefill). ### Related - Phase 1 (v1 fallback): #31611 (merged) - Schema extensions: #29912 (merged) - Design doc: `docs/design/webgpu_paged_attention.md` (updated in this PR) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tianlei Wu <tlwu@microsoft.com>
This was referenced Aug 21, 2026
Closed
This was referenced Sep 1, 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.
Description
PagedAttentionis ORT's continuous-batching attention operator, but onmainit only supportsFP16/BF16 caches with RoPE and softcap, has no paged decode kernel, and forces a device→host
synchronization on every node on every step (which makes it uncapturable by CUDA graphs). This PR
brings it to feature parity with
GroupQueryAttentionfor the popular LLM families and adds thepaging and latent-cache primitives that serving frameworks need, additively — every model valid
under the shipped
com.microsoft::PagedAttentionopset-1 schema keeps working unchanged.The design rationale, the compatibility invariant, and the alternatives that were considered and
rejected are written up in the new design document
docs/contrib_ops/cuda/paged_attention.md; the sectionnumbers referenced below point into it.
Summary of Changes
Schema (
bert_defs.cc,docs/ContribOperators.md)All additions are trailing optional inputs, new attributes whose defaults reproduce current
behavior, or widened type constraints (§4).
slot_mappinghead_sinkq_norm_weight/k_norm_weightk_scale/v_scaleattention_metadata[max_query_len, max_kv_len], which removes the per-node per-step D→H sync (§4.7)qk_norm_epsilon1e-6k_quant_type/v_quant_typeNONENONE|PER_TENSOR|PER_CHANNELk_cache_dtype/v_cache_dtype""kv_cache_layoutSEPARATESEPARATE|LATENT(absorbed MLA: one cache, novalue/value_cache)v_head_size0LATENTonly (DeepSeek-V3 uses 576/512)rotary_offset0[rotary_offset, rotary_offset + rotary_dim)so MLA can rotate only the positional suffixkey_cache/value_cachemove fromTto a newT_CACHEconstraint (float16,bfloat16,int8,float8e4m3fn), andvalue_cache/value_cache_outbecome optional so aLATENTnode canomit them. Shape inference now takes the cache element type from inputs 3/4 rather than from
query,which was wrong for a quantized cache.
CUDA kernels (
paged_attention_impl.cu,paged_attention.cc/.h,paged_attention_helper.h)LaunchPagedDecodeAttention) — split-KV, block-table-aware decode withnative head-sink, softcap, sliding-window and on-the-fly cache dequantization.
onnxruntime/contrib_ops/cuda/bert/xqa/) — TensorRT-LLM's XQA kernelsextended to the paged block layout: 8 new translation units
xqa_paged_{fp16,bf16}_{int8,fp8}_{64,128}.cuplus a shared paged loader. Selected forquantized-cache decode.
ReshapeAndCachequantizes on write; all read paths dequantize withk_scale/v_scaleunderPER_TENSORorPER_CHANNELgranularity.ApplyHeadSink— exact post-hoc LSE rescale (1/(1+exp(s_h − lse))) applied after thequantized/unquantized branch, so no backend can silently drop the sink (§6).
QkNormRotaryTNH— fuses QK-RMSNorm, RoPE (withrotary_offset) and the packed-QKV unpackinto one pass.
PagedLatentAttentionKernel/LatentAttention) — single latent cache, V readas the leading
v_head_sizechannels of the same row that supplies K (§12).shapes and the
block_table.shape[1] * block_sizecapacity bound; per-step quantities are read ondevice. The unconditional
cudaStreamSynchronizeis gone from the capturable path (§4.7).int8→fp16conversion fast path (xqa/utils.cuh,cvtS8x4ToF16x4) — replaces a scalarI2Floop with aprmt+sub.f16x2sequence (5 full-rate instructions per 4 elements, bitidentical). Shared with the non-paged GQA loader.
<T, T_CACHE>-typed; FP8 combinations are behindUSE_FP8_KV_CACHE && !DISABLE_FLOAT8_TYPES.GQA bug fix (
flash_api.{h,cc},group_query_attention_impl.cu)mha_fwdhadconstexpr void* head_sink = nullptr;hardcoded inside it, andFlashAttentionAndQuantizeKV— the only GQA prompt path taken when the KV cache is quantized —called it. So for gpt-oss with an INT8/FP8 KV cache, the attention sinks were silently dropped for
the entire prompt on every layer while decode stayed correct.
mha_fwdnow takeshead_sinkandGQA forwards it.
Op-level prefill error drops 0.074829 → 0.000122. On gpt-oss-20b (int4 body, INT8 per-channel
KV), MMLU-Pro-800 goes 0.6175 (494/800) → 0.7200 (576/800). Existing CI missed this because
atol["int8_fp16"] = 1e-1intest_gqa.pyis ~800× wider than the post-fix error.Tooling and docs
symbolic_shape_infer.py: correct output width for packed-QKV andLATENTnodes, and cacheoutputs typed from the cache inputs.
docs/contrib_ops/cuda/paged_attention.mddesign document; regeneratedContribOperators.mdand
OperatorKernels.md.Testing
test_paged_attention_cuda.pygrows from a smoke test to ~2k lines / 198 cases, with new suites forfeatures (
slot_mapping, head sink, QK-Norm), quantized cache (int8/fp8 × per-tensor/per-channel),the paged decode kernel, the XQA decode path,
attention_metadata, and MLA — each against a PyTorchreference.
The GQA suite is included because the int8 conversion fast path is shared with the non-paged loader.
Backward compatibility. A node with none of the new inputs/attributes takes exactly the code path
it does today:
T_CACHE == T,value_cachepresent,kv_cache_layout == SEPARATE, all quantizationNONE. The compatibility invariant is stated normatively in §4.2.Experimental Results
Measured on gpt-oss-20b, H200. E2E numbers are driven through onnxruntime-genai; the CUDA-graph and
engine-side plumbing they depend on is not part of this PR — they are included to show what the
operator-side changes enable, not as a claim about this diff alone.
Paged decode kernel (isolated,
nh=64 / kvh=8 / hs=64 / block=256)XQA on/off at
b=8, ctx=4096, per decode call:PER_TENSORPER_CHANNELPER_TENSORPER_CHANNELBefore XQA the quantized paths were ~2.5× slower than fp16 — the generic kernel was the bottleneck,
not the KV bytes.
The
cvtS8x4ToF16x4conversion path then closes the residual int8-vs-fp8 gap (nsys median, SASS goesfrom 3928 to 3592 instructions with 192 → 0
I2F):The int8/fp8 gap goes from up to +59% down to ≤ 7.6% (int8 is faster at the largest config), so the
two cache formats can now be chosen on accuracy grounds.
End-to-end decode throughput (mxfp4 body, INT8 KV, prompt 128 / new 256)
attention_metadata(no sync)Including XQA, batch-1 int8 decode goes 199.2 → 305.7 tok/s (+53.5%). After this work attention
is no longer the bottleneck at b=1 — the MoE GEMMs and 49
MatMulNBitsnodes dominate the step, withXQA at 24 × 8.6 µs.
PagedAttention vs GroupQueryAttention, matched models
Two models built from the identical recipe (int4 body, INT8 per-channel KV, identical
num_heads/kv_num_heads/scale/window/rotary, byte-identical weight file), differing only in theattention operator. Greedy generation on this stack is bit-reproducible (0/198 discordance across
replicates), so there is no sampling noise to subtract.
The two benchmarks disagree in direction and both deltas are 3 questions: equivalent within noise.
(An apparent +8 pp advantage for paged in earlier runs turned out to be the GQA sink bug fixed above,
seen from the other side.)
Peak device memory at matched KV capacity agrees to within 24 MiB (0.16%) from 16k to 128k
max_length— paged costs nothing extra, and its advantage is structural (a shared pool sized toaggregate demand rather than
batch × max_length).The b=32 gap was profiled with
nsys --cuda-graph-trace=node: the captured graph body is at paritywith GQA's eager model pass (6.220 ms vs ~6.2 ms) and the entire regression is a 2.384 ms
search/sampling tail, which the onnxruntime-genai
Engineruns once per request rather than onceper batch. It is not attributable to this operator, and a partial engine-side fix already recovers
b=32 to 4517 tok/s.
Follow-ups (not in this PR)
attention_biasandoutput_qk(§10, §11) — schema slots reserved, kernels deferred.int4/float4e2m1) packed caches — attribute vocabulary reserved and rejected atvalidation until a backend exists (§21.4).
.Alias(3, 1).Alias(4, 2)on the kernel def, so a non-aliasing allocation plan fails at partitiontime instead of run time (§4.4).
atol["int8_fp16"]intest_gqa.pynow that the sink bug is fixed.