perf(cutedsl): add Blackwell fast path to RMSNorm - #1388
Merged
Conversation
RMSNorm was the only generic CuTe DSL op with no Blackwell-specific path.
swiglu has packed-f32x2 SFU math and a TVM-FFI direct-call path, rope has a
TMA kernel tuned on B200, and cross_entropy dispatches on
infer_device_arch() == "blackwell". rms_norm.py had no arch query at all.
Its kernels were not the problem. On a B200 the vector forward already beat
Triton on device time (5.8us vs 7.8us at bf16 4096x2048), as did the fused
backward (26.3us vs 32.4us). Wall clock said the opposite because every call
first marshalled its tensors into cute.Tensor handles (from_dlpack + memref
construction), costing ~48us per launch. The op was host-bound at every shape
up to 16384x4096: measured wall time tracked host time (~54us), not the kernel.
Two changes:
* Compile the vector forward and fused backward with --enable-tvm-ffi against
abstract tensors, so PyTorch tensors are passed straight to the compiled
function. eps/offset stay runtime scalars, so neither forces a recompile.
Forward host cost 54us -> 15us, backward 74us -> 26us. This is a calling
convention, not an arch feature, so it applies wherever apache-tvm-ffi is
importable; without it the original marshalling launch still runs.
* Use packed-f32x2 SFU math (fma/mul/add) for the fused backward's dX/dW inner
products, which issue two fp32 lanes per instruction. Gated to sm_10x and to
n_cols > 4096, the 16-warp regime where the kernel is issue-bound rather than
memory-bound; measurements at each width are recorded in _use_packed_math.
The forward deliberately stays scalar: it is DRAM-bound (6.6 TB/s at
16384x8192) and the packed variant measured within +-0.5% at every width and
dtype, so it would be complexity that buys nothing.
Both halves are switchable for A/B via LIGER_RMS_FORCE_NO_FFI and
LIGER_RMS_FORCE_NO_PACKED.
Measured on B200, bf16, llama mode, op-level (cutedsl vs Triton):
shape fwd before fwd after bwd before bwd after
1024x2048 0.65x 2.36x 0.81x 2.36x
4096x2048 0.66x 2.32x 0.81x 2.29x
4096x4096 0.69x 2.33x 0.82x 1.51x
4096x8192 0.66x 1.52x 1.08x 1.17x
16384x2048 0.67x 1.65x 1.73x 1.75x
16384x4096 0.90x 1.12x 1.75x 1.75x
16384x8192 1.24x 1.24x 1.17x 1.29x
32768x4096 1.11x 1.12x 1.84x 1.83x
fp32 moves the same way (forward 0.66-1.08x -> 0.99-2.29x, backward
0.80-1.73x -> 1.15-2.58x). No shape regressed in either dtype.
Adds apache-tvm-ffi to the cutedsl extra. It is optional at runtime, but the
whole forward win depends on it, and the existing swiglu kernel already has
the same latent dependency.
Tests: three new cases cover ground the parity suite cannot reach -- it only
goes up to 4096 wide, so it never compiles the packed backward, and it always
runs on the default stream. The FFI path is checked bit-identical to the
marshalling path (affine and non-affine, the latter pinning the dummy-W
layout), the packed backward against the scalar one, and stream binding via
CUDA graph capture. Both comparison tests assert that the two variants were
really compiled, so they cannot degenerate into comparing a path with itself.
Full cutedsl suite on B200: 579 passed, identical 33-failure set before and
after (pre-existing bf16/fp32 parity gaps in cross_entropy and RMSNorm's
bf16-llama dW), so this introduces no new failures.
lancerts
approved these changes
Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds a Blackwell fast path to the CuTeDSL RMSNorm op (
ops/cutedsl/ops/rms_norm.py), the only generic CuTeDSL op with no arch-specific path (e.g.swiglu.pyhas packed-f32x2 math and a TVM-FFI direct call;rope.pyhas a TMA kernel tuned on B200; andcross_entropy.pydispatches oninfer_device_arch() == "blackwell", whilerms_norm.pyhad no arch query at all).This PR changes how the two vector fast paths —
_launch_fwd_vectorand_launch_bwd_fused— are launched, and the instruction mix of the backward. Their tiling, their CTA-level reduction, and the op's public API are unchanged.1. TVM-FFI direct-call launch (host-side; the bulk of the win)
_launch_fwd_vectorand_launch_bwd_fusedgain a fast branch that skips thefrom_dlpack/ memref marshalling entirely and hands PyTorch tensors straight to the compiled function:_make_fwd_vector_ffi/_make_bwd_fused_fficlosures capture every constexpr, so the compiled signature is just tensors + fp32 scalars._get_fwd_vector_ffi/_get_bwd_fused_fficompile those once per (dtype, geometry, device) against abstract tensors viamake_fake_tensor, cached in a new_ffi_compile_cache.eps/offsetstay runtime scalars, so changing either does not trigger a recompile.make_fake_stream(use_tvm_ffi_env_stream=True), so it drops out of the call signature and resolves from the caller's env stream — no per-call stream query from Python.Result: forward host cost 54us -> 15us, backward 74us -> 26us.
2. packed-f32x2 math in the fused backward (device-side)
_rms_norm_bwd_fused_vector_kernelgains aPACKED_MATH: cutlass.Constexprbranch that computes the dX/dW inner products pairwise withfma_packed_f32x2/mul_packed_f32x2/add_packed_f32x2(two fp32 lanes per instruction), keeping two accumulators that fold before the CTA reduction. Selected by the new_use_packed_math, which requires sm_10x andn_cols > 4096— the 16-warp regime where the kernel is issue-bound rather than memory-bound. Per-width measurements justifying that threshold are in its docstring.Behavior when the fast path does not apply
apache-tvm-ffithe original marshalling launch runs;LIGER_RMS_FORCE_NO_FFI/LIGER_RMS_FORCE_NO_PACKED.Why the op was slow in the first place
The kernels were already faster than Triton on device time (forward 5.8us vs 7.8us at bf16 4096x2048; fused backward 26.3us vs 32.4us). Wall clock disagreed because each call spent ~48us marshalling tensors into
cute.Tensorhandles before it could enqueue, so the op was host-bound at every shape up to 16384x4096 — measured wall time tracked host time (~54us), not kernel time.Results
1x B200, bf16, llama mode, op-level, cutedsl vs Triton (>1.0 = cutedsl faster):
fp32 moves the same way: forward 0.66-1.08x -> 0.99-2.29x, backward 0.80-1.73x -> 1.15-2.58x. No shape regressed in either dtype. "before" is measured by running the same build with both toggles off, so it is exactly the old code path.
The packed-math half accounts for the wide-row backward gains specifically: at bf16 16384x8192 it is 245.8us -> 222.0us (-9.7%) and at fp32 16384x6144 251.3us -> 194.6us (-22.6%), device time.
Validation
Three new tests in
test/cutedsl/test_rms_norm.pycover ground the existing parity suite cannot reach — it only goes up to 4096 wide (so it never compiles the packed backward) and always runs on the default stream:test_rms_norm_ffi_matches_marshalling_path— bit-identity between the two launch paths, affine and non-affine; the non-affine case is what pins the dummy-W layout.test_rms_norm_packed_backward_matches_scalar— packed vs scalar backward at 8192.test_rms_norm_runs_on_the_current_stream— stream binding via CUDA graph capture.Both comparison tests assert the two variants were really compiled, so they cannot degenerate into comparing a path against itself. Mutation-checked: corrupting the packed dW accumulation or the non-affine dummy-W layout makes them fail.
Full cutedsl suite on B200: 579 passed with an identical 33-failure set before and after (pre-existing bf16/fp32 parity gaps in
cross_entropyand RMSNorm's bf16-llama dW) — no new failures.ruff check/ruff formatclean.Packaging note
apache-tvm-ffiis added to thecutedslextra. It stays optional at runtime, but the entire forward win depends on it and the existing swiglu kernel already has the same latent dependency. Happy to drop this if you would rather keep the extra minimal.