[CUDA] Add cuDNN paged SDPA decode tier for PagedAttention - #32493
Conversation
Adds a cuDNN paged SDPA tier to the CUDA PagedAttention dispatch cascade,
between XQA and FlashAttention 2. Decode-only (max_query_len_bound == 1,
token_count == batch_size), unquantized KV cache, standard causal SDPA
(no softcap / sliding window / head sink / bias).
Gate mirrors GroupQueryAttention's cuDNN tier:
- UseCudnnFlashAttention() (ORT_ENABLE_CUDNN_FLASH_ATTENTION=1 or the
sdpa_kernel bit) enables it explicitly.
- AllowCudnnFlashAttentionAuto() enables it automatically on sm >= 90.
- ORT_ENABLE_CUDNN_FLASH_ATTENTION=0 is the shared kill switch across
every cuDNN attention path in the CUDA EP.
No new env var; cuDNN attention configuration stays uniform across
Attention / MultiHeadAttention / GroupQueryAttention / PagedAttention.
Motivation
----------
FlashAttention 2's paged split-KV (Flash Decoding) leaves substantial
throughput on the table for GQA at long context. cuDNN 9.5+ ships a paged
SDPA path that is dramatically faster in that regime.
Benchmarks (RTX 5060 Ti, CUDA 12.8, cuDNN 9.12.0.46, sm_120, 15-rep median)
--------------------------------------------------------------------------
onnxruntime_benchmark BM_PagedAttentionDecode_Cuda/*, cuDNN paged vs. FA2
paged (ORT_ENABLE_CUDNN_FLASH_ATTENTION=0):
GQA (num_heads > kv_num_heads, group_size > 1), H=128:
Shape past=512 past=2048 past=8192 past=16384
B=1 nH:14/kv:2 0.76x 1.34x 3.42x 5.77x
B=1 nH:32/kv:4 0.83x 1.30x 2.95x 4.80x
B=2 nH:14/kv:2 0.76x 1.24x 2.96x 4.77x
B=2 nH:32/kv:4 1.03x 2.04x 4.89x 4.16x
MHA (num_heads == kv_num_heads):
Shape past=512 past=2048 past=8192 past=16384
B=1 nH:16/kv:16 H:64 0.71x 0.97x 1.83x 1.28x
B=1 nH:16/kv:16 H:128 0.77x 1.28x 1.24x 1.33x
B=2 nH:16/kv:16 H:64 0.73x 0.91x 0.93x 0.95x
B=2 nH:16/kv:16 H:128 0.78x 1.12x 0.93x 0.96x
GQA at past >= 2048 is the intended target (1.3-5.8x win). MHA long
context is roughly parity. Short-context decode (past=512) shows a
bounded 0.71-0.83x regression (20-25 us absolute) that only hits very
early in a generation before context grows past 2048. Users that need
the short-context FA2 path can opt out with
ORT_ENABLE_CUDNN_FLASH_ATTENTION=0.
Changes
-------
Kernel + API surface:
- cudnn_fmha/cudnn_flash_attention.{h,cc}: new is_supported_paged(...) and
run_paged(...) entry points, wrapping cuDNN's frontend SDPA_attributes
with a page-table graph.
- attention_data.h: three new fields on PagedAttentionData for the cuDNN
handle, per-batch KV-length scratch, and allocator.
- paged_attention.{h,cc}: constructor reads the new gates
(enable_cudnn_paged_, auto_enable_cudnn_paged_); ComputeInternal adds
the cuDNN paged tier with metadata-gated eligibility so no D->H
readback is triggered.
- paged_attention_impl.{h,cu}: RunPagedAttentionCudnn(...) builds the
per-batch KV-length scratch and calls cudnn_sdpa::run_paged.
Build:
- cmake/onnxruntime_providers_cuda.cmake: suppress nvcc warning #20303-D
(CUTLASS subbyte_reference.h uses __nv_atomic_load_n with a memory-order
arg, which nvcc flags as sm_70+ on the sm_60 arch list; -Werror
all-warnings otherwise promotes it to a hard error).
- cmake/onnxruntime_unittests.cmake: include the microbenchmark in the
CUDA build (was WebGPU-only).
Tests + benchmarks:
- test/contrib_ops/paged_attention_op_test.cc: new cuDNN paged tests and
eligibility-gate assertions.
- 4 pre-existing tests (Cuda_AttentionMetadataShape2Compatibility...,
Cuda_FlashSplitKvLongContext, Cuda_FlashSplitKvCudaGraphReplay,
Cuda_FlashSplitKvSkipsShortReplayRange) now set
ORT_ENABLE_CUDNN_FLASH_ATTENTION=0. These tests force non-cuDNN
backends by disabling FA/MEA/decoder; after the auto-on-sm>=90 gate
they would otherwise dispatch to cuDNN instead of the intended tier.
- test/onnx/microbenchmark/paged_attention.cc: parallel _Cuda benchmark
family alongside the existing WebGPU family.
Test plan
---------
- onnxruntime_provider_test.exe --gtest_filter=PagedAttention.* on
Windows / CUDA 12.8 / cuDNN 9.12.0.46 / RTX 5060 Ti (sm_120): 17 pass,
16 skipped (WebGPU + EndToEnd requiring absent runtime), 0 fail.
- 15-rep A/B benchmark sweep as tabulated above.
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Reviewed the cuDNN paged SDPA tier. The design is sound: the stride trick that presents ORT's [num_blocks, block_size, num_heads_kv, head_size] cache as cuDNN's [num_blocks, num_heads_kv, block_size, head_size] container is correct and zero-copy, dropping the causal mask for s_q == 1 matches the reasoning already documented in run(), the gate is genuinely metadata-only (no new D->H readback), and the mutual-exclusion bookkeeping across use_paged_decode / use_flash_attention / use_memory_efficient_attention / the XQA-restore block / the "no backend" error is complete. Reusing UseCudnnFlashAttention() / AllowCudnnFlashAttentionAuto() instead of inventing a new env var is the right call, and the max_seq_len_kv == max_num_blocks_per_seq * block_size alignment note plus its regression test capture a hard-won debugging result.
Two things I would like resolved before merge, plus a set of suggestions. Inline comments carry the details.
Blocking-ish
Cuda_CudnnPagedBuildsWhenPageTableOverAllocatedhard-asserts the cuDNN backend with only aGetCudaArchitecture() < 800guard. On a CUDA build without cuDNN flash attention, on cuDNN < 9.5, or on an sm_8x device where the paged planner declines, this is a red test rather than a skip. Its siblingCuda_CudnnPagedDispatchWhenEnabledalready has the correct shape.is_supported_paged()admitsdprops.major >= 8, but a planner rejection insidebuild_paged_graph()is anORT_THROW, not a fallback. Unlike the denserun()path, PagedAttention always has FA2/MEA available for the same shape, so an unsupported (arch, shape) under explicit opt-in kills the user's inference instead of degrading.
Suggestions
PagedGraphParams params;leaves 7 padding bytes indeterminate andBytesHashhashes all of them — a miss means the graph is rebuilt every decode step, silently erasing the benefit of this PR.LaunchGetSeqlensKVDecodehardcodespast_seqlens[i] + 1rather than deriving the query count fromcumulative_seqlens_q.build_paged_graph()callscudaGetDevicePropertieson every cache miss purely to build a string used only in the two throw branches.- No CUDA-graph-capture guard on the first-call graph build, in an op that has capture in its contract (
Cuda_FlashSplitKvCudaGraphReplay) and where the XQA path does guard withisCapturing(). docs/contrib_ops/cuda/paged_attention.mdis the normative description of the backend cascade, thesdpa_kernelbits and the env-var matrix; it should gain the new tier, the short-context regression and theORT_ENABLE_CUDNN_FLASH_ATTENTION=0opt-out.
Test coverage gap
Nice that RunIoBindingCase checks against a CPU reference, so these are real numerical tests. But every case where cuDNN actually fires uses batch_size == 1 — precisely the configuration where the two batch-indexed pieces of this change cannot fail (the page-table leading stride max_num_blocks_per_seq, and the per-batch seqlens_kv padding mask). A batch >= 2 GQA case with unequal past_seqlens would exercise both. is_bf16 in run_paged also has no coverage; one c.bf16_query = true case closes that.
Scope
moe_kernels.cu, moe_quantization.cc, QgemmU8X8KernelAvx2.asm, manifest_parser.cc and the deep_gemm/WIN32 cmake edits belong to #32485 — noted that you plan to rebase. The --diag-suppress=20303 change is a CUTLASS/sm_60 arch-list interaction unrelated to this feature; splitting it out would let it be reverted independently when sm_60 leaves the arch list.
Also worth verifying before merge: that the graph cache actually hits across decode steps (count build_paged_graph entries over a ~100-token generation), and the behaviour on an sm_80/sm_86 device with ORT_ENABLE_CUDNN_FLASH_ATTENTION=1.
314d20c to
901939f
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The node-wide readiness probe can disagree with the shape- and thread-specific graph cache, and one regression test can silently skip its target failure.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a cuDNN paged-SDPA decode tier to CUDA PagedAttention, with dispatch gating, graph caching, tests, documentation, and benchmarks.
Changes:
- Implements cuDNN paged-attention graph construction and execution.
- Integrates capability-based dispatch and fallback handling.
- Adds CUDA tests and benchmark coverage.
File summaries
| File | Description |
|---|---|
onnxruntime/contrib_ops/cuda/bert/attention_data.h |
Adds cuDNN execution state. |
onnxruntime/contrib_ops/cuda/bert/paged_attention.h |
Adds cuDNN dispatch configuration and probe state. |
onnxruntime/contrib_ops/cuda/bert/paged_attention.cc |
Integrates eligibility, probing, and dispatch. |
onnxruntime/contrib_ops/cuda/bert/paged_attention_impl.h |
Declares KV-length generation. |
onnxruntime/contrib_ops/cuda/bert/paged_attention_impl.cu |
Implements cuDNN decode execution. |
onnxruntime/contrib_ops/cuda/bert/cudnn_fmha/cudnn_flash_attention.h |
Exposes paged-SDPA APIs. |
onnxruntime/contrib_ops/cuda/bert/cudnn_fmha/cudnn_flash_attention.cc |
Builds, caches, and executes cuDNN graphs. |
onnxruntime/test/contrib_ops/paged_attention_op_test.cc |
Adds dispatch and correctness tests. |
onnxruntime/test/onnx/microbenchmark/paged_attention.cc |
Adds CUDA benchmark variants. |
docs/contrib_ops/cuda/paged_attention.md |
Documents the new dispatch tier. |
cmake/onnxruntime_unittests.cmake |
Includes the benchmark for CUDA builds. |
Review details
- Files reviewed: 11/11 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…sion test; fix QK-Norm docs
There was a problem hiding this comment.
🟡 Changes recommended
Planner failures are repeatedly rebuilt, XQA failure can bypass cuDNN, and capture behavior lacks coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
onnxruntime/contrib_ops/cuda/bert/cudnn_fmha/cudnn_flash_attention.cc:836
- Planner failures are not actually cached here: a null graph is returned without inserting this key. Because
ComputeInternalprobes on every decode step, any statically eligible shape rejected by cuDNN will repeat the expensivevalidate()/build()attempt for every generated token. Cache the negative result as intended, and makerun_pagedreturnfalsewhen it finds a cached null entry instead of dereferencing it.
auto mha_graph = build_paged_graph(params);
if (mha_graph == nullptr) {
return false;
- Files reviewed: 11/11 changed files
- Comments generated: 3
- Review effort level: Balanced
… per-Run probe cache in docs; add cuDNN paged CUDA graph capture/replay test
…ce_cuda # Conflicts: # onnxruntime/contrib_ops/cuda/bert/paged_attention.cc # onnxruntime/contrib_ops/cuda/bert/paged_attention.h
3b930bd to
c745fc4
Compare
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Second review round on head 63c119b5. All eight threads from the previous round are addressed and resolved — the SafeInt narrowing, the FillPagedGraphParams memset, the major >= 9 static gate, the non-throwing build_paged_graph + try_build_paged_graph probe, the capture guard, the cumulative_seqlens_q-derived KV mask, the cudnn_allocator rename, and dropping the unrelated cmake suppression. Thanks for the thorough turnaround.
Two new findings this round (inline), plus notes below.
What I re-verified as correct
- Q / K / V / O / page-table dims and strides match ORT's physical
[num_blocks, block_size, kv_num_heads, head_size]cache layout and the packed decode Q/O layout. head_sinkis covered transitively by!parameters.use_smooth_softmax(the helper setsuse_smooth_softmax = head_sink != nullptr), andv_head_size != head_sizeis only reachable inLATENTmode, which!use_latent_attentionexcludes.CreateConstantSeqLenBufferuses a stream-orderedFill, so the synthesizedseq_len_qbuffer is capture-safe.needs_dense_kvcannot go stale when the late XQA-fallback retry flipsuse_cudnn_paged, becausemea_eligiblerequires!flash_eligibleand the retry requiresfp16_xqa_eligible.num_heads_kv == 0short-circuits before the modulo inis_supported_paged.
Minor, not worth a thread
paged_attention.cc:756— the "no backend available" return now also guards on!use_cudnn_paged, but the message still lists only FlashAttention / MemoryEfficientAttention / paged decode and the threeORT_DISABLE_*env vars. Worth mentioning the cuDNN paged tier andORT_ENABLE_CUDNN_FLASH_ATTENTIONso a user whose only viable backend was cuDNN (and whose planner probe failed) gets an actionable message.- The PR description still documents a
cmake/onnxruntime_providers_cuda.cmake#20303-Dsuppression that was removed from the branch. Please drop that bullet before merge.
CI coverage
is_supported_paged now requires dprops.major >= 9, and every new test that actually exercises the kernel either gates on GetCudaArchitecture() >= 900 or skips when the SdpaKernel=CUDNN_FLASH_ATTENTION marker is absent. On pre-Hopper CI agents only the negative gate tests (RespectsGlobalCudnnDisable, FallsBackWithoutMetadata, FallsBackForMultiTokenBound, FallsBackForQuantizedCache) will execute. That is the right conservative choice, but it does mean the kernel path itself has no automated coverage in CI today — worth calling out in the design doc so a future regression is understood to be caught only by manual sm_90+ runs.
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Will create a follow up PR
Description
Adds a cuDNN paged SDPA tier to the CUDA
PagedAttentiondispatch cascade,sitting between XQA and FlashAttention 2. Decode-only (
max_query_len_bound == 1,token_count == batch_size), unquantized KV cache, standard causalSDPA — no softcap, sliding window, head sink, or bias.
The gate mirrors GroupQueryAttention's cuDNN tier:
UseCudnnFlashAttention()(ORT_ENABLE_CUDNN_FLASH_ATTENTION=1or thesdpa_kernelbit) enables it explicitly.AllowCudnnFlashAttentionAuto()enables it automatically onsm >= 90.ORT_ENABLE_CUDNN_FLASH_ATTENTION=0is the shared kill switch acrossevery cuDNN attention path in the CUDA EP.
No new env var; cuDNN attention configuration stays uniform across
Attention/MultiHeadAttention/GroupQueryAttention/PagedAttention.Motivation and Context
FlashAttention 2's paged split-KV (Flash Decoding) leaves substantial
throughput on the table for GQA at long context. cuDNN 9.5+ ships a paged
SDPA path that is dramatically faster in that regime.
Benchmarks
RTX 5060 Ti, CUDA 12.8, cuDNN 9.12.0.46, sm_120, 15-rep median.
onnxruntime_benchmark BM_PagedAttentionDecode_Cuda/*, cuDNN paged(
ORT_ENABLE_CUDNN_FLASH_ATTENTION=1) vs. FA2 paged(
ORT_ENABLE_CUDNN_FLASH_ATTENTION=0).GQA (
num_heads > kv_num_heads),H=128:MHA (
num_heads == kv_num_heads):GQA at
past >= 2048is the intended target (1.3–5.8× win). MHA longcontext is roughly parity. Short-context decode (
past=512) shows abounded 0.71–0.83× regression (20–25 µs absolute) that only hits very
early in a generation before context grows past 2048. Users that need the
short-context FA2 path there can opt out with
ORT_ENABLE_CUDNN_FLASH_ATTENTION=0.Changes
Kernel + API surface
cudnn_fmha/cudnn_flash_attention.{h,cc}: newis_supported_paged(...)and
run_paged(...)entry points, wrapping cuDNN's frontendSDPA_attributeswith a page-table graph.attention_data.h: three new fields onPagedAttentionDatafor thecuDNN handle, per-batch KV-length scratch, and allocator.
paged_attention.{h,cc}: constructor reads the new gates(
enable_cudnn_paged_,auto_enable_cudnn_paged_);ComputeInternaladds the cuDNN paged tier with metadata-gated eligibility so no D→H
readback is triggered.
paged_attention_impl.{h,cu}:RunPagedAttentionCudnn(...)builds theper-batch KV-length scratch and calls
cudnn_sdpa::run_paged.Build
cmake/onnxruntime_providers_cuda.cmake: suppress nvcc warning#20303-D(CUTLASSsubbyte_reference.huses__nv_atomic_load_nwitha memory-order arg, which nvcc flags as sm_70+ on the sm_60 arch list;
-Werror all-warningsotherwise promotes it to a hard error).cmake/onnxruntime_unittests.cmake: include the microbenchmark in theCUDA build (was WebGPU-only).
Tests + benchmarks
test/contrib_ops/paged_attention_op_test.cc: new cuDNN paged tests andeligibility-gate assertions.
Cuda_AttentionMetadataShape2Compatibility...,Cuda_FlashSplitKvLongContext,Cuda_FlashSplitKvCudaGraphReplay,Cuda_FlashSplitKvSkipsShortReplayRange) now setORT_ENABLE_CUDNN_FLASH_ATTENTION=0. These tests force non-cuDNNbackends by disabling FA/MEA/decoder; after the auto-on-sm≥90 gate they
would otherwise dispatch to cuDNN instead of the intended tier.
test/onnx/microbenchmark/paged_attention.cc: parallel_Cudabenchmark family alongside the existing WebGPU family.
Testing
onnxruntime_provider_test.exe --gtest_filter=PagedAttention.*onWindows / CUDA 12.8 / cuDNN 9.12.0.46 / RTX 5060 Ti (sm_120):
22 pass, 16 skipped (WebGPU + EndToEnd requiring absent runtime),
0 fail.