[CUDA] PagedAttention: add SM<80 fp16 fallback via memory-efficient attention
Description
Adds a CUTLASS memory-efficient attention (MEA) fallback to the CUDA PagedAttention op, enabling the operator on sm<80 (Turing / Volta / Pascal) with fp16 for the first time. On sm>=80 the default FlashAttention path is unchanged; MEA is reachable via ORT_DISABLE_FLASH_ATTENTION=1 or the sdpa_kernel CUDA provider option for debugging and perf comparison.
| Environment | Before | After |
|---|---|---|
| sm<80 + fp16 | ❌ error | ✅ MEA |
| sm<80 + bf16 | ❌ error | ❌ error (MEA requires sm>=80 for bf16) |
| sm>=80 + fp16/bf16 (default) | ✅ FA | ✅ FA (unchanged) |
sm>=80 + ORT_DISABLE_FLASH_ATTENTION=1 / sdpa_kernel=EFFICIENT_ATTENTION | ❌ error | ✅ MEA |
Motivation and Context
The original PagedAttention PR (#24595) landed with the title "CUDA SM80 support" — the op errors out immediately whenever FlashAttention isn't available (sm<80 or USE_FLASH_ATTENTION=0 builds). During that review, @tianleiwu flagged that the interface was too FlashAttention-specific ("not good for other EP like WebGPU, CPU etc.") and @aciddelgado agreed the FA-specific dependencies could be lifted at the kernel level.
This PR closes that gap for sm<80 fp16 by mirroring the exact pattern established in #20012 ("Packed QKV and Rotary Embedding Support for sm<80 GQA"). The same CUTLASS memory-efficient attention backend that covers GQA's sm<80 path now covers PagedAttention.
Related work:
- #20012 — direct pattern template (sm<80 GQA MEA fallback)
- #24595 — original PagedAttention PR
- #27516 — MS canonical FA → MEA → Unfused cascade ordering
- #27880 — ONNX Attention CUDA fallback coverage gaps
- #27992 — MEA decode + unfused softcap work (same flavor)
Implementation
Dispatch cascade in paged_attention.cc: FlashAttention preferred; fall back to MemoryEfficientAttention via has_memory_efficient_attention(sm, is_half, is_bf16, head_size, head_size). No custom head-size or dtype bounds hardcoded — MEA's own helper gates fp16 sm>=53 / bf16 sm>=80 / head_size <= 1024 and % 8 == 0. This keeps us forward-compatible with any future expansion of MEA's supported range.
MEA path (UnfusedAttention<T>):
- Reuses existing preprocessing:
LaunchGetCumulativeSeqlensKV(hoisted topaged_attention.ccso both FA and MEA paths consume a pre-populated buffer — single-producer refactor), rotary, packed-QKV unpack,ReshapeAndCache. - New
GatherAndExpandPagedKVCacheCUDA kernel walksblock_tableto gather paged K/V into a packed-varlen[total_kv_tokens, num_heads, head_size]buffer, folding in GQA head expansion (so downstream MEA seesnum_headsuniformly). - Dispatches to
run_memory_efficient_attentionin varlen mode viaseqstart_q_ptr = cumulative_seqlens_q+seqstart_k_ptr = cumulative_seqlens_kv(andhas_custom_right_padding = false). No padding required; layout matches the kernel's expected[total_tokens, num_heads, head_size]with BSNH strides.
Scratch allocation: the MEA path D->H syncs cumulative_seqlens_kv[batch_size] via a pinned buffer to obtain total_kv_tokens on the host for tight gathered_key / gathered_value / fmha_buffer allocation. This adds a forward-per-call cudaStreamSynchronize — acceptable for a compatibility fallback (FA remains the hot path on supported hardware). Over-allocation (the no-sync alternative) would consume B × max_num_blocks_per_seq × block_size × num_heads × head_size × 2 × sizeof(T), which reaches GB-scale for realistic GQA models and was rejected.
fmha_buffer is sized with sizeof(float) (matching the GQA EfficientAttention pattern at group_query_attention.cc:482) because MEA's output accumulator is fp32 regardless of input dtype.
Testing
New TestPagedAttentionMEA class in test_paged_attention_cuda.py runs the existing parity matrix (rotary on/off, rotary_interleaved on/off, packed-QKV on/off, local window on/off, softcap 0/50, varied head sizes/shapes) against the MEA path via the sdpa_kernel CUDA provider option set to EFFICIENT_ATTENTION (=2, from AttentionBackend enum). Using a per-session provider option instead of an env var means both FA and MEA test classes coexist in the same pytest process — each InferenceSession creates its own CUDA EP with its own attention_kernel_options_.
The existing TestPagedAttention class is skipped wholesale on sm<80 by its has_flash_attention() gate, so without the new MEA class the fallback path would have no CI coverage.
Local verification (NVIDIA A100 80GB, CUDA 12.8, GCC 13.3):
TestPagedAttention: 24/24 passed (~60s) # FA baseline — no regression
TestPagedAttentionMEA: 24/24 passed (~59s) # new MEA path
Tolerance: rtol = atol = 5e-3 against the same torch reference used by the FA parity test. All combinations match.
sm<80 hardware coverage: I don't have local Turing / Volta / Pascal hardware, so real-SM coverage relies on MS CI. The code path exercised on A100 via sdpa_kernel=EFFICIENT_ATTENTION is the same one taken on sm<80; only the underlying CUTLASS kernel (run_memory_efficient_attention_sm50/70/75/80) differs per SM, and those are upstream and unmodified by this change.
Build note: built with --cmake_extra_defines CMAKE_CUDA_ARCHITECTURES=80 CMAKE_CXX_STANDARD=20. The explicit C++20 define was needed because the initial configure resolved CMAKE_CXX_STANDARD=17, under which ort_version_check.h's consteval usage fails to compile. Unrelated to this change.