Add fused CuTe-DSL forward+backward kernels for the CSA Compressor gated pooling (THD)#5984
Add fused CuTe-DSL forward+backward kernels for the CSA Compressor gated pooling (THD)#5984zkyue wants to merge 4 commits into
Conversation
The gated softmax pooling region of Compressor._forward_thd (gather-index
build -> gather -> +APE -> overlap-window transform -> fp32 softmax ->
gated weighted sum -> bf16 cast) decomposes into ~39 forward and ~51
backward kernel launches per call at compress_ratio = 4, materializes
(total_comp, 2*ratio, 1, head_dim) window intermediates that are also
saved for autograd, and its backward pays an indexing_backward plus a
radix sort for the gather.
Add one forward and one backward kernel written in the CuTe Python DSL
(nvidia-cutlass-dsl, JIT-compiled on first use) covering exactly that
region for the THD packed path, coff in {1, 2}:
* fp32 arithmetic with a single final bf16 rounding; mul/fma pinned in
PTX so results do not depend on compiler FMA contraction. dKV/dScore
are bit-identical to an fp32-intermediate eager reference; against an
fp64 oracle the fused kernels max abs error is <= the eager path
error on every tested output.
* Backward is atomic-free for dKV/dScore: every consumed input element
belongs to exactly one pooling window, so gradient stores are
disjoint; unconsumed elements keep exact zeros from the
zero-initialized grad buffers, matching autograd. dAPE is reduced
with one fp32 atomic per (k, dim) per CTA and is therefore not
bitwise run-to-run deterministic (forward, dKV, dScore are); the
backward raises under torch.use_deterministic_algorithms(True).
* Static-capacity padding rows (fixed_total_comp) are supported:
forward computes them exactly like the eager code (gather from token
0 with first-in-segment semantics), backward ignores incoming
gradients on padding rows.
* CUDA-graph capture compatible after a one-step eager warmup per
(ratio, head_dim, coff, device) configuration; a first call that
would JIT under capture raises a clear RuntimeError instead of
corrupting the capture.
* A cached-launch fast path snapshots the DSL launch state once per
(kernel, config, device, thread) and replays it with in-place
argument mutation, removing per-call host wrapper overhead; any
structural mismatch on a future nvidia-cutlass-dsl upgrade raises at
construction and the wrapper permanently falls back to the regular
launch path.
Addresses NVIDIA#5968.
Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
Additive fast path: maybe_compress_thd_fused() returns the pooled tensor for the supported configuration (THD non-pre-grouped path, compress_ratio == 4, bf16 kv/score, fp32 ape, compute capability 10.0, int32 flat offsets) and None otherwise, in which case the eager region runs unchanged. SBHD and the pre-grouped CP-prep path are untouched. The dispatch also keeps the eager path under torch.use_deterministic_algorithms(True) (dAPE fp32 atomics) and under torch.compile tracing (raw-pointer launch path is not traceable). MCORE_CSA_FUSED_COMPRESSOR=0 disables the dispatch entirely. compress_ratio == 128 is functionally supported by the explicit op API but stays on the eager path until tuned (see NVIDIA#5968). Addresses NVIDIA#5968. Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
Covers numerics vs an fp32-intermediate eager reference (bitwise dKV/dScore), vs the verbatim upstream eager numerics (tolerance) and vs an fp64 oracle (fused error <= eager error) over ragged THD packs including segments shorter than ratio; coff == 1 (compress_ratio == 128) functional correctness; fixed_total_comp static-capacity padding rows (including a pack whose leading segment is shorter than ratio, and that nonzero padding-row gradients are ignored); run-to-run determinism of forward/dKV/dScore; CUDA-graph capture -> replay (including replay with new data and a smaller device-side true row count) and the loud JIT-under-capture error; dispatch gating and eager fallback (kill switch, ratio 128, non-bf16, layout, empty output, deterministic mode), plus Compressor._forward_thd-level integration. The module is marked launch_on_gb200 for the CC 10.0 CI lane and skips cleanly without CUDA, nvidia-cutlass-dsl, or compute capability 10.0. Addresses NVIDIA#5968. Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
Standalone harness (not collected by pytest) behind the numbers in issue NVIDIA#5968 and the PR: --check compares fused fwd+bwd against the verbatim upstream eager region, an fp32-intermediate reference and an fp64 oracle; --bench measures CUDA-event wall clock of the replaced region; --nsys provides a cudaProfilerApi-gated loop with NVTX FWD/BWD ranges for pure-kernel-time capture. The eager reference reuses the upstream batch_of_row and Compressor._overlap_transform_thd implementations. Addresses NVIDIA#5968. Signed-off-by: zky <51477259+zkyue@users.noreply.github.com>
|
Hi @zkyue Thanks for your contribution! I think it is better to put the kernels in cudnn-frontend like other CSA/HCA fused kernels. Is it convenient for you to do so, or do you need our help to port the kernel? |
|
@hxbai Sure, happy to do that — the kernels are self-contained CuTe DSL and should map cleanly onto the cudnn-frontend python API structure (we've contributed there before). Plan: I'll open a PR against cudnn-frontend porting the fwd+bwd kernels (APIBase-style wrapper, THD semantics, tests), then rework this PR into a thin dispatch that uses the cudnn-frontend implementation when available, keeping the eager fallback. Will link the kernel PR here once it's up. |
|
@hxbai The cudnn-frontend kernel PR is up: NVIDIA/cudnn-frontend#427 (fwd+bwd kernels, APIBase-style wrapper, THD semantics, 40 tests; outputs bit-identical to the kernels in this PR). Once it lands I'll rework this PR into the thin dispatch that uses the cudnn-frontend implementation when available, keeping the eager fallback. |
What
Addresses #5968 (contribution invited by the maintainers in
#5968 (comment)).
The
Compressorused by the CSA/HCA experimental attention variants implements its gatedsoftmax pooling in eager PyTorch. In
_forward_thd, the region between the two projectionGEMMs and the RMSNorm — gather-index build, gather,
+ APE, overlap-window transform(
coff == 2), fp32 softmax over the window, gated weighted sum — decomposes into ~39forward and ~51 backward kernel launches per call (at
compress_ratio = 4), materializes(total_comp, 2*ratio, 1, head_dim)window intermediates (also saved for autograd), andits backward pays an
indexing_backwardplus a radix sort for the gather.This PR adds an additive fused fast path for exactly that region: one forward and one
backward kernel written in the CuTe Python DSL (
nvidia-cutlass-dsl, JIT-compiled atfirst call), keeping the existing eager implementation as the semantic reference and
fallback. Forward+backward replace the ~90 eager launches with 1 + 1 kernels (plus 3
grad-buffer zero-fills in backward).
Files
megatron/core/transformer/experimental_attention_variant/csa_fused_compressor.py— new,self-contained: import-guarded DSL imports, the two kernels, a cached-launch fast path,
the autograd wrapper, the explicit op API (
compress_thd_fused) and the dispatch helper(
maybe_compress_thd_fused).megatron/core/transformer/experimental_attention_variant/csa.py— minimal insertion in_forward_thd: try the fused path, fall into the (unchanged, re-indented) eager regionwhen it returns
None.tests/unit_tests/transformer/experimental_attention_variant/test_csa_fused_compressor.py— unit tests.tests/unit_tests/transformer/experimental_attention_variant/bench_csa_fused_compressor.py—the standalone benchmark/correctness harness behind the numbers below (not collected by pytest).
Dispatch / gating (initial)
The fast path engages only for: THD packed non-pre-grouped path,
compress_ratio == 4(
coff == 2), bf16kv/score, fp32ape, compute capability 10.0 (the onlyvalidated architecture; the kernels use no TMA/tcgen05/MMA features, so wider coverage is
likely straightforward but stays opt-in until validated), and int32 flat offsets
(
total_tokens * coff * head_dim < 2**31). Everything else — SBHD, the pre-grouped CP-prepinput path,
compress_ratio == 128, missing DSL, other devices — keeps the eagerimplementation.
MCORE_CSA_FUSED_COMPRESSOR=0disables the dispatch entirely.compress_ratio == 128(coff == 1) is functionally supported by the explicit op API andcovered by the tests, but stays on eager until tuned (currently only ~3.1× fwd / ~1.2× bwd
wall-clock there). The dispatch also keeps the eager path under
torch.use_deterministic_algorithms(True)(seedAPEbelow) and undertorch.compiletracing (the raw-pointer launch path is not traceable; eager lets the compiler fuse the
region itself).
Numerics
All arithmetic is fp32 with a single final bf16 rounding;
mul.rn.f32/fma.rn.f32arepinned in PTX so results do not depend on compiler FMA contraction. Fresh numbers from the
included harness on the ported code (B200, details below):
rounding):
dKV/dScorebit-identical on every tested shape; forward differs on<0.006% of elements with max abs difference 3.9e-3–7.8e-3.
itself rounds intermediates to bf16 (
torch.softmax(...).to(kv.dtype), bf16 multiply).Against an fp64 oracle the fused kernels' max abs error is equal to or lower than
eager's on every tested output (table in the harness output / issue).
dKV/dScore(disjoint stores; unconsumed elements keepexact zeros, matching autograd). Forward,
dKV,dScorereplay bitwise identically runto run.
dAPEis accumulated with fp32 atomics and is not bitwise run-to-rundeterministic (max run-to-run deviation 1.5e-5–6.9e-5 over 10 replays per shape in the
issue measurements, fp32); the dispatch therefore falls back to eager under
torch.use_deterministic_algorithms(True), and the explicit op raises in backward inthat mode. A deterministic per-CTA-partials reduction is possible follow-up work if
required.
CUDA graphs &
fixed_total_comp(new vs the issue)Both items the issue left open are implemented and tested here:
per
(ratio, head_dim, coff)configuration; a first call that would JIT under captureraises a clear
RuntimeErrorinstead of corrupting the capture. The unit test capturesfwd+bwd in one graph and replays with new data and a smaller device-side true row
count (static capacity, changed
cu_seqlenscontents), checking bitwisedKV/dScoreagainst the eager reference.
code (they gather the window from token 0 with first-in-segment semantics, as the eager
gather does); backward ignores incoming gradients on padding rows (they are tail
padding, not consumed downstream) — tested, including that nonzero padding-row gradients
change nothing and including a pack whose leading segment is shorter than
ratio.Performance
Measured on 1×B200 (CC 10.0, driver 590.48.01); PyTorch 2.13.0a0 (CUDA 13.3),
nvidia-cutlass-dsl4.6.1; bf16kv/score(total, 1, coff*head_dim), fp32ape;compress_ratio = 4,coff = 2; THD packs of 8192-token sequences. Both implementationsrun on identical inputs over exactly the replaced region (projection GEMMs, RMSNorm, RoPE
excluded — unchanged). Numbers re-measured on this PR's code with the included harness.
Isolated GPU kernel time (nsys, sum of kernel durations per iteration, 50 iterations
after 20 warmup; no launch/host overhead; backward includes its grad-buffer fills):
End-to-end wall clock of the same region (CUDA events, median of 100, includes launch
overhead and, for backward, autograd engine overhead — not comparable to the kernel-time
numbers above):
(The fused wall numbers go through the explicit op API of the harness and therefore
include its host-side argument validation, a few µs per call; the production dispatch
path performs its cheaper gating checks instead. They are slightly below the issue's
wall numbers, which were measured on the pre-review version of the wrapper without the
added validation; the isolated kernel times above are unchanged.)
Tests
tests/unit_tests/transformer/experimental_attention_variant/test_csa_fused_compressor.py(all skip cleanly without CUDA / DSL / CC 10.0):
dKV/dScore), vs verbatim upstream eager (tolerance),vs fp64 oracle (fused error ≤ eager error), over ragged multi-segment packs including
segments shorter than
ratio;coff == 1(compress_ratio == 128) functional correctness;fixed_total_compstatic-capacity padding (fwd semantics + ignored padding grads,including a pack with a leading segment shorter than
ratio);dKV/dScore;maybe_compress_thd_fused), including thedeterministic-mode fallback (and the explicit op raising in backward under
torch.use_deterministic_algorithms(True)), plusCompressor._forward_thd-levelintegration (fused engages, matches eager, gradients flow).
The module carries
pytestmark = pytest.mark.launch_on_gb200so the CC 10.0 CI laneexercises it; everywhere else the tests skip cleanly (no CUDA / no DSL / other archs).
Also ran the existing related suites with the fused dispatch live:
test_attention_variant_csa.py,test_csa_cp_layout_kernels.py,test_csa_cp_utils.py—136 passed, 1 skipped (needs ≥2 ranks), 0 failed.
Notes
MCORE_CSA_FUSED_COMPRESSOR=0kill switch), matching the scope proposed in [ENHANCEMENT] Fused forward+backward CUDA kernel for the CSA/HCACompressorgated pooling (THD path) #5968. Itdoes change training numerics relative to the current eager code (more accurate against
the fp64 oracle, but not bit-identical) — happy to flip this to opt-in if you prefer.
per-call host overhead; any structural mismatch on a future
nvidia-cutlass-dslupgrade raises during construction and the wrapper permanently falls back to the
regular launch path (it never launches from a partially built snapshot).
MCORE_CSA_FUSED_COMPRESSOR_FAST_LAUNCH=0disables just this optimization.dAPEreduction, tuning forcompress_ratio == 128, wider arch enablement after validation.