Size Flash split-KV from valid KV length in ONNX Attention opset-24 external-cache path (CUDA) - #29689
Size Flash split-KV from valid KV length in ONNX Attention opset-24 external-cache path (CUDA)#29689Ti-Tai Wang (titaiwangms) with Copilot wants to merge 3 commits into
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR optimizes the CUDA EP implementation of the ONNX-domain Attention op (opset 24) in the external KV-cache decode path by sizing FlashAttention split-KV work (num_splits) using the valid KV length (from nonpad_kv_seqlen) rather than the buffer length (total_sequence_length). This avoids wasted work over padded cache regions as the preallocated cache grows.
Changes:
- Read back
nonpad_kv_seqlen(device → host) to compute a per-batch max valid KV length and use it to size Flash split-KV launches. - Keep
seqlen_kunchanged (= buffer length) to preserve KV stride correctness while adjusting only the split heuristic. - Add CUDA fp16 test cases with large cache buffers but small valid lengths to exercise the new sizing branch.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
onnxruntime/core/providers/cuda/llm/attention.cc |
Size FlashAttention split-KV from max valid KV length (from nonpad_kv_seqlen) instead of cache buffer length. |
onnxruntime/test/python/transformers/test_onnx_attention/test_tensorscatter_attention.py |
Add fp16 CUDA tests for large-buffer/small-valid external-cache decode scenarios to validate correctness in the new sizing regime. |
Review synthesis (multi-model review team)Thanks for the fix — it targets the right root cause from #29686, and the numerical correctness is solid (see below). But there's one Critical integration problem that should block merge as-is: the per-step host sync breaks CUDA Graph capture. Critical — the unconditional
|
Evaluation: not adopting, closingThanks for the submission. After a design evaluation we've decided not to take this PR, for the following reasons:
Full analysis is captured in #29686. Closing this PR. |
…el (Phase 1) (#29715) ### Description Adds a cuDNN SDPA tier to the ONNX `Attention` (opset 23/24) CUDA kernel, making the effective dispatch cascade **cuDNN → Flash → MEA → Unfused**. The tier is narrowly gated to the opset-24 external-KV-cache single-token decode path (its actual value proposition) and takes the valid KV length as a device `int*`, so it stays CUDA-graph-capturable — unlike the host-readback approach that sank #29689. This is a **draft** landing Phase 1 only; correctness-sensitive follow-ups are gated out and flagged inline. - **Cascade (`attention.cc` `ComputeInternal`)** — new cuDNN eligibility block before the Flash block. Hard gate (all required): `nonpad_kv_seqlen != nullptr`, `past_key == nullptr`, `is_causal`, `q_sequence_length == 1`, no `attn_mask`/`output_qk`/`softcap`, fp16/bf16, `cudnn_sdpa::is_supported`. Not built-guarded (relies on the wrapper's `CUDNN_MAJOR` stubs + `is_stable()`). - **`RunCudnnSdpaAttention`** — honors existing ONNX-Attention semantics: rank-branched `qkv_format` (`Q_K_V_BSNH` for 3-D, mixed `Q_K_V_BSNH_BNSH_BNSH` for 4-D with Q transposed BNSH→BSNH), BSNH output scratch → transpose to BNSH, device int32 seqlens via the existing `LaunchConvertNonpadKvSeqlenToFlashSeqlensK`, `sequence_length_kv = total_sequence_length` (capacity) + per-batch mask, `mask_sequence_lengths_q = nullptr`, separate `present_key`/`present_value` population. - **Fully-masked-batch guard** — `LaunchZeroOutputForFullyMaskedBatches` after `run()`. cuDNN emits NaN for a `nonpad==0` row where every other tier defines `0`; this is a spec-equivalence requirement, not defense-in-depth (can't be a host-side gate without a D2H sync). - **Option plumbing** — reuses the shared `UseCudnnFlashAttention()` / `AllowCudnnFlashAttentionAuto()` (no new option key); adds `enable_/auto_enable_cudnn_flash_attention_` members mirroring GQA. - **Debug-info wiring** — adds the `AttentionKernelDebugInfo` dispatch block the op previously lacked, recording which tier ran. - **Tests** — cuDNN decode parity test forcing cuDNN via `sdpa_kernel`, covering MHA/MQA/GQA, fully-masked and heterogeneous `nonpad`; falls back to unfused (still parity-correct) where cuDNN is unavailable. **Deferred / needs validation (inline TODOs):** Phase 2 (internal `past_key`/`past_value`) and Phase 3 (prefill `s_q>1`) are gated out. Cold cuDNN plan-cache miss under graph capture relies on the warmup-before-capture invariant (documented in the function header); wrapper-API enhancement + a C++ cold-cache capture test are follow-ups. CUDA was not buildable in this environment — compilation and GPU tests need validation on cuDNN 9.3+/SM≥90. ### Motivation and Context The standard ONNX `Attention` CUDA kernel has no decode-specialized tier, capping decode latency at the Flash floor (~13 µs) versus GQA's cuDNN/XQA tiers (~10 µs). Fixing Flash split-sizing (#29686, wontfix) can't close this structural gap. cuDNN SDPA is the widest-eligibility, already-linked option and reads valid length device-side, so it reaches GQA-class decode latency without breaking CUDA-graph capture. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f38b9775-a6b6-4422-8f42-9e48e626a358 Copilot-Session: 1d32dce6-edfc-4d15-9d94-0bc65713f01d Copilot-Session: a1011908-d09e-411a-8979-fcf0dc18e20e
Description
In the opset-24 external-KV-cache decode path (
nonpad_kv_seqlen, CUDA EP),RunFlashAttentionsized the Flash split-KVnum_splitsfromparameters.total_sequence_length. On this path K/V are the full cache, sototal_sequence_lengthis the buffer length, not the valid KV length — over-partitioning the split-KV launch and doing wasted work over the padding region. The same kernel gets slower as the cache buffer grows, exactly the opposite of what long-max-context deployments want.onnxruntime/core/providers/cuda/llm/attention.cc): read the per-batch max valid length from the device-sidenonpad_kv_seqlenonto the host (onebatch_size-element copy) and feed it intoget_num_splits_and_buffer_sizes. Clamped to the buffer length and floored at 1.nonpad_kv_seqlenFlash path; the non-decode paths keeptotal_sequence_lengthsizing.seqlen_kstays= total_sequence_length— it drives the KV-cache strides (k_batch_stride = seqlen_k*h*d) andn_blocks_per_split. Device-sideseqlens_kmasking still bounds the math per batch, sonum_splitsis a perf-only knob and outputs are unchanged.test_tensorscatter_attention.py): added CUDA fp16 large-buffer / small-valid cases (buffer 2048, valid 64/96/128) to exercise the new sizing branch acrossnum_splitsvalues.Tradeoff: reading the valid length host-side costs one small device→host copy + stream sync, scoped to the external-cache Flash path. Valid lengths change every decode step, so cross-call host caching does not apply. A
nonpad_kv_seqlen-as-CPU-input design (mirroring GQA) would avoid the sync but is a larger, cross-cutting change touching all three dispatch paths and input memory placement — left for reviewer input per the issue's open questions.Motivation and Context
The
nonpad_kv_seqlen+TensorScatterdecode path is meant to reach near-parity with contrib GQA, but buffer-sized split launches make it measurably slower (~+66% e2e at buffer 8192, worse as the buffer grows). This is architecture-independent host-side sizing logic, and the single largest term separating the ONNX Attention external-cache decode path from GQA. It bringsattn_scatterup to the Flash-tierattn_pastlevel; the remaining GQA decode-tier gap (fused KV-append prep, decode-specialized kernel) is out of scope here.