[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 23, 2026
This was referenced Sep 1, 2026
This was referenced Sep 9, 2026
Open
This was referenced Sep 16, 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.