[Common] Pass cu_seqlens and token-unit ragged offsets directly to cuDNN SDPA fprop#3186
[Common] Pass cu_seqlens and token-unit ragged offsets directly to cuDNN SDPA fprop#3186egilliam-nv wants to merge 8 commits into
Conversation
Greptile SummaryThis PR eliminates 1-2 CUDA kernel launches per varlen forward pass by passing
Confidence Score: 5/5The core logic is well-guarded and tested on H100 and Blackwell; the SM_120 stats-shape fix is a genuine correctness improvement with no backward-pass impact. The RaggedOffsetMultipliers refactor faithfully reproduces every layout-group case from the old kernel and eliminates the pre-existing cross-thread read race on tail entries. Stats and Max multiplier calls are correctly gated inside use_ragged_stats. The two open questions are the missing runtime escape hatch documented in the PR description and whether set_cu_seq_len_q/kv is safe for non-THD padding configs - both are non-blocking. The use_cu_seqlens_directly activation condition in fused_attn_f16_arbitrary_seqlen.cu lines 100-109 warrants a second look to confirm that non-THD, non-ragged padding configs are handled correctly by the unified engine on cuDNN >= 9.24. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[fused_attn_fwd_impl called] --> B{use_cu_seqlens_directly}
B -- YES --> C{is_padding?}
C -- YES --> D[seq_q/seq_kv shape b+1, set_cu_seq_len_q/kv]
B -- YES --> F{is_ragged_q?}
F -- YES --> G[Q and O set_ragged_offset_multiplier]
B -- YES --> H{is_ragged_kv?}
H -- YES --> I[K and V set_ragged_offset_multiplier]
B -- YES --> J{use_ragged_stats?}
J -- YES --> K[Stats and Max set_ragged_offset_multiplier]
B -- NO --> L{is_padding?}
L -- YES --> M[Launch cu_seqlens_to_actual_seqlens kernel]
B -- NO --> N{is_ragged_q or is_ragged_kv?}
N -- YES --> O[Launch cu_seqlens_padded_to_offsets kernel]
D --> P[mha_graph execute]
G --> P
I --> P
K --> P
M --> P
O --> P
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[fused_attn_fwd_impl called] --> B{use_cu_seqlens_directly}
B -- YES --> C{is_padding?}
C -- YES --> D[seq_q/seq_kv shape b+1, set_cu_seq_len_q/kv]
B -- YES --> F{is_ragged_q?}
F -- YES --> G[Q and O set_ragged_offset_multiplier]
B -- YES --> H{is_ragged_kv?}
H -- YES --> I[K and V set_ragged_offset_multiplier]
B -- YES --> J{use_ragged_stats?}
J -- YES --> K[Stats and Max set_ragged_offset_multiplier]
B -- NO --> L{is_padding?}
L -- YES --> M[Launch cu_seqlens_to_actual_seqlens kernel]
B -- NO --> N{is_ragged_q or is_ragged_kv?}
N -- YES --> O[Launch cu_seqlens_padded_to_offsets kernel]
D --> P[mha_graph execute]
G --> P
I --> P
K --> P
M --> P
O --> P
Reviews (7): Last reviewed commit: "[Common] Suppress fn_size lint on fused_..." | Re-trigger Greptile |
8ae2ca9 to
06ae482
Compare
sudhakarsingh27
left a comment
There was a problem hiding this comment.
Just found some grammar nits, otherwise looks good to me
7c30c38 to
775cf39
Compare
…DNN SDPA fprop
cuDNN >= 9.24 SDPA (unified engine) accepts cumulative sequence lengths
directly (cu_seq_len_q/kv) and can scale ragged offsets stored in coarser
units back to elements via a per-tensor ragged offset multiplier. Use both
in the f16/bf16 forward to skip the two conversion kernels
(cu_seqlens_to_actual_seqlens and cu_seqlens_padded_to_offsets) that
previously ran before every varlen fprop:
- Bind the user's int32 cu_seqlens buffers as CU_SEQ_LEN_Q/KV for the
padding mask, and the token-unit cu_seqlens_padded buffers as ragged
offsets for Q/K/V/O/Stats with elements-per-token multipliers.
- Gate on cudnn >= 9.24 and !dropout (the FE rejects dropout together with
generated stats on the unified engine; TE always generates stats).
CU_SEQ_LEN inputs pin implementation selection to the unified engine.
- Keep the true batch size on the direct path: cuDNN reads the user's
[actual_b+1] buffers, so the quantized max_b graph batch would read out
of bounds. Token-dim bucketing (max_t) is unaffected.
- No conversion workspace is needed on the direct path.
- Factor the layout-group -> multiplier mapping into RaggedOffsetMultipliers
(utils.h), shared by the graph builder and the legacy conversion kernel so
the two cannot drift. The kernel rewrite also removes a cross-thread read
(offsets_v[tid] = offsets_k[cu_seqlens_id]) that raced for quantized-batch
tail entries with interleaved layouts.
- Backward is unchanged (no backend support yet).
NVTE_FUSED_ATTN_DIRECT_SEQLENS=0 disables the new path (testing aid, to be
removed before merging).
Validated on H100 and Blackwell against cuDNN 9.25: test_dpa_softmax_thd
15/15 in both modes, and direct-vs-legacy fused outputs/grads match for all
THD layouts (thd_thd_thd, t3hd, th3d, thd_t2hd, thd_th2d) x MHA/GQA x
padding/padding_causal x pad_between_seqs {false,true}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil Gilliam <egilliam@nvidia.com>
Extend the direct-seqlens path to the FP8/MXFP8 forward: bind the user's int32 cu_seqlens buffers as CU_SEQ_LEN_Q/KV for the padding mask instead of converting them to per-batch lengths with the cu_seqlens_to_actual_seqlens kernel before every call. (Unlike the F16 path, the FP8 path has no THD/ragged support, so this is the only conversion kernel there.) FP8/MXFP8 on the unified engine requires cuDNN >= 9.25 and cuDNN frontend >= 1.26. The frontend is header-only, so its version is a compile-time property; the gate uses a constant-folded CUDNN_FRONTEND_VERSION check (all referenced symbols exist in 1.25, so no preprocessor guards are needed). Dropout with generated stats stays on the legacy path, same as F16. Backward is unchanged (no backend support yet). Validated against cuDNN 9.25 + frontend 1.26 (test_dpa_fp8_vs_f16, padding configs, direct path on with no fallback): 56 passed on H100 (delayed + current scaling), 168 passed on Blackwell (adds MXFP8); zero failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil Gilliam <egilliam@nvidia.com>
for more information, see https://pre-commit.ci
Address review feedback and a version-mix bug found in testing:
- Remove the NVTE_FUSED_ATTN_DIRECT_SEQLENS env override (unnecessary; the
version gates fully determine the path).
- Check the compile-time CUDNN_VERSION in addition to the runtime version.
The cuDNN frontend gates cu_seq_len support on min(compile-time, runtime)
version, so e.g. a binary built against 9.24 headers running on a 9.25
library must take the legacy path; a runtime-only check let it attempt
the direct fp8 graph, which the frontend rejects ("No suitable
implementation") with no fallback.
- Add a (currently redundant) CUDNN_FRONTEND_VERSION >= 1.25 check to the
f16 gate for symmetry with the fp8 gate.
- Raise the fp8 frontend floor from 1.26 to 1.27: 1.26 suffices for this
C++ API use, but 1.27 is the floor for the python FE API's fp8 cu_seq_len
support (exposed post-1.26-cut), and a single version story per feature
avoids a silent gap when TE moves to the python FE API.
Smoke-tested on H100: f16 THD 15/15 (direct path, cuDNN 9.24), fp8 padding
subset 56 passed via legacy on 9.24, and 56 passed via legacy on the
9.24-compile/9.25-runtime mix that previously failed 56/56.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil Gilliam <egilliam@nvidia.com>
Per TE team discussion: 1.26 is all the C++ FE API needs for fp8 + cu_seqlens (the support surface made the 1.26 cut; SDPA_fp8_attributes has had the setters since 1.25). Keep a comment noting that the python FE API requires 1.27 (its sdpa_fp8 binding gained cu_seq_len_q/kv post-1.26-cut), so a future migration to the python FE API knows to raise the floor. Smoke-tested on H100: f16 THD 15/15 (direct, cuDNN 9.24), fp8 padding subset 56 passed via legacy on 9.24 and on the 9.24-compile/9.25-runtime mix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil Gilliam <egilliam@nvidia.com>
use_ragged_stats excludes sm120, but the forward Stats declaration used the weaker condition (is_ragged_q && cudnn >= 9.6). On sm120 with THD, fwd therefore declared the ragged-style [b][s][h] stats stride with a null ragged offset (i.e. dense token-major), while bwd read the stats tensor as dense [b][h][s] -- a fwd/bwd layout mismatch. It would also have let the direct-seqlens path set a ragged-offset multiplier on a null ragged offset, a frontend validation error. Use use_ragged_stats for the fwd declaration so fwd and bwd agree, and give the stats allocation the same sm120 exception Max already has: without it the buffer is [num_tokens_q, h, 1], undersized for the dense [b, h, s_q, 1] graph whenever num_tokens_q < b * s_q. Pre-existing issue, independent of the direct-seqlens work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil Gilliam <egilliam@nvidia.com>
Clearer name for the flag controlling whether cu_seqlens buffers are passed straight to cuDNN SDPA; comment wording updated to match. No functional change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil Gilliam <egilliam@nvidia.com>
The direct-seqlens additions push the function to 508 non-comment lines, over cpplint's 500 limit. Per TE team, refactoring this long-standing function is beyond the scope of this PR, so suppress with NOLINT for now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Emil Gilliam <egilliam@nvidia.com>
5f6e16d to
65edde2
Compare
Description
cuDNN >= 9.24 SDPA (unified engine) accepts cumulative sequence lengths directly (
cu_seq_len_q/kv) and can scale ragged offsets stored in coarser units (e.g. tokens) back to element offsets via a per-tensor ragged offset multiplier. This PR uses both features in the fused-attention forward passes to skip the conversion kernels that previously ran before every varlen call:cu_seqlens_to_actual_seqlensand (f16 THD only)cu_seqlens_padded_to_offsets.f16/bf16 forward (cuDNN >= 9.24): binds the user's int32
cu_seqlensbuffers asCU_SEQ_LEN_Q/KVfor the padding mask, and the token-unitcu_seqlens_paddedbuffers as ragged offsets for Q/K/V/O/Stats with elements-per-token multipliers. Both conversion kernels are skipped and no conversion workspace is allocated.fp8/mxfp8 forward (cuDNN >= 9.25 and cudnn-frontend >= 1.26): same for
cu_seqlens(the fp8 path has no THD support, so only the one conversion kernel existed there). The frontend is header-only, so its version is a compile-time property; the gate is a constant-foldedCUDNN_FRONTEND_VERSIONcheck — all referenced symbols exist in 1.25 (SDPA_fp8_attributesis an alias ofSDPA_attributes), so no preprocessor guards are needed and the fp8 direct path is simply dead code on 1.25 builds.Notes for reviewers:
CU_SEQ_LEN_*inputs pin implementation selection to the unified engine (the composite support surface rejects them), so the gates keep composite-served configs on the legacy path: training with dropout (the frontend rejects dropout together with generated stats on the unified engine, and TE always generates stats), and for f16 the direct path only engages for THD/ragged configs, which backend selection restricts to unified-capable hardware.max_b: cuDNN reads the user's[actual_b+1]cu_seqlens buffers, so a quantized batch would read out of bounds. Token-dim bucketing is unchanged.RaggedOffsetMultipliers(utils.h), shared by the graph builder and the legacy conversion kernel so the two cannot drift. The kernel rewrite also removes a cross-thread read (offsets_v[tid] = offsets_k[cu_seqlens_id]) that raced for quantized-batch tail entries with interleaved layouts.NVTE_FUSED_ATTN_DIRECT_SEQLENS=0disables the new path (kept as an escape hatch for A/B testing; can be removed on request).Validation against cuDNN 9.25, no-fallback design so passes prove the unified-engine path:
test_dpa_softmax_thd15/15 with the direct path on and off (H100 + Blackwell), matching pre-change baseline; direct-vs-legacy fused outputs/grads match for all THD layouts (thd_thd_thd,t3hd,th3d,thd_t2hd,thd_th2d) x MHA/GQA x padding/padding_causal x pad_between_seqs {false, true} on H100 and Blackwell (14/14 each).test_dpa_fp8_vs_f16padding configs — 56 passed on H100 (delayed + current scaling), 168 passed on Blackwell (adds MXFP8), zero failures.Type of change
Changes
cu_seqlens/cu_seqlens_paddeddirectly to cuDNN (unified engine) on cuDNN >= 9.24, skipping both conversion kernels. (Also cudnn-frontend must be >= 1.25, but this is now required across the board by TransformerEngine so no need to check for this in the fp16 code.)cu_seqlensdirectly on cuDNN >= 9.25 + cudnn-frontend >= 1.26, skipping the conversion kernelRaggedOffsetMultipliershelper used by both the graph builders and the legacy conversion kernelChecklist:
🤖 Generated with Claude Code