You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The ONNX Attention opset-24 external-KV-cache decode path (nonpad_kv_seqlen + in-place TensorScatter, CUDA EP) sizes its Flash split-KV launch from the KV cache buffer length, not from the valid KV length. For a fixed valid length, enlarging the cache buffer makes the same Flash kernel dramatically slower — worst exactly for long-max-context deployments, where a large pre-allocated buffer is the whole point.
This was surfaced by the baseline decode benchmark in #28352 (script #29684).
Evidence (RTX PRO 6000, SM120, fp16, S_past=2048, per decode step)
Shrinking only the cache buffer (--max-seq-len) at a fixed valid length collapses the identical flash_splitkv kernel:
In the nonpad_kv_seqlen path, K/V are the full cache, so total_sequence_length = buffer length. The valid length only exists as the nonpad_kv_seqlendevice tensor, which isn't consumed until LaunchConvertNonpadKvSeqlenToFlashSeqlensK at attention.cc:311-320 — after the split count is already fixed. So the split-KV launch (and its partitioning) is sized for the whole padded buffer and does wasted work over the padding region.
By contrast, contrib GQA sizes its split from a CPU-input valid length (onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc:595), so it never over-sizes.
This is architecture-independent host-side sizing logic, not an SM120 artifact.
It's the single largest term separating the ONNX Attention external-cache decode path from GQA.
Proposed fix (for evaluation)
Size the Flash split from the valid KV length instead of the buffer shape. The valid length is device-side (nonpad_kv_seqlen), which host-side split sizing can't read — mirror GQA and accept the valid length (or a max-valid-length hint) as a CPU input to the opset-24 path, then feed it into get_num_splits_and_buffer_sizes.
Open questions for the reviewing team:
Cleanest way to plumb a CPU-side length/hint through the opset-24 Attention schema + kernel without regressing the general (non-decode) path.
Whether a conservative "max valid length" hint is enough (avoids a device→host sync) vs. needing the exact per-batch length.
Correctness interaction with the existing device-side seqlens_k masking (the split sizing is a perf-only knob; the mask still bounds the math).
Where this sits in the bigger picture
This is step 1 of "close the ONNX-Attention-vs-GQA decode gap" from #28352. It only brings attn_scatter up to the Flash-tier attn_past level (~13-20 µs GPU); it does not by itself reach GQA's XQA/cuDNN decode tier (~10-11 µs GPU). The remaining gap needs (2) fused KV-append prep to cut launch overhead (see the existing TODO at attention.cc:346), and (3) wiring ONNX Attention decode onto a decode-specialized kernel tier — ORT already has a cuDNN SDPA integration used by GQA. Filing this one separately because it's the smallest, most self-contained, highest-ROI item.
Summary
The ONNX
Attentionopset-24 external-KV-cache decode path (nonpad_kv_seqlen+ in-placeTensorScatter, CUDA EP) sizes its Flash split-KV launch from the KV cache buffer length, not from the valid KV length. For a fixed valid length, enlarging the cache buffer makes the same Flash kernel dramatically slower — worst exactly for long-max-context deployments, where a large pre-allocated buffer is the whole point.This was surfaced by the baseline decode benchmark in #28352 (script #29684).
Evidence (RTX PRO 6000, SM120, fp16, S_past=2048, per decode step)
Shrinking only the cache buffer (
--max-seq-len) at a fixed valid length collapses the identicalflash_splitkvkernel:For comparison,
attn_past(opset-23 past/present) — whose K/V shape is the valid length — runs the same-tier Flash at 13.2 µs. Cache layout is ruled out (4D BNSH cache: same ~34 µs). Raw artifacts (CSV / nsys): https://github.com/namgyu-youn/onnxruntime/releases/tag/bench-28352-artifactsRoot cause
onnxruntime/core/providers/cuda/llm/attention.cc:257-260computesnum_splitsfromparameters.total_sequence_length:In the
nonpad_kv_seqlenpath, K/V are the full cache, sototal_sequence_length= buffer length. The valid length only exists as thenonpad_kv_seqlendevice tensor, which isn't consumed untilLaunchConvertNonpadKvSeqlenToFlashSeqlensKatattention.cc:311-320— after the split count is already fixed. So the split-KV launch (and its partitioning) is sized for the whole padded buffer and does wasted work over the padding region.By contrast, contrib GQA sizes its split from a CPU-input valid length (
onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc:595), so it never over-sizes.Impact
Proposed fix (for evaluation)
Size the Flash split from the valid KV length instead of the buffer shape. The valid length is device-side (
nonpad_kv_seqlen), which host-side split sizing can't read — mirror GQA and accept the valid length (or a max-valid-length hint) as a CPU input to the opset-24 path, then feed it intoget_num_splits_and_buffer_sizes.Open questions for the reviewing team:
seqlens_kmasking (the split sizing is a perf-only knob; the mask still bounds the math).Where this sits in the bigger picture
This is step 1 of "close the ONNX-Attention-vs-GQA decode gap" from #28352. It only brings
attn_scatterup to the Flash-tierattn_pastlevel (~13-20 µs GPU); it does not by itself reach GQA's XQA/cuDNN decode tier (~10-11 µs GPU). The remaining gap needs (2) fused KV-append prep to cut launch overhead (see the existingTODOatattention.cc:346), and (3) wiring ONNX Attention decode onto a decode-specialized kernel tier — ORT already has a cuDNN SDPA integration used by GQA. Filing this one separately because it's the smallest, most self-contained, highest-ROI item.References
attention.cc:257-260(split sizing),:311-320(nonpad→seqlens_k conversion)group_query_attention.cc:595(GQA CPU-input length)