Skip to content

[Kernel] DSv4 flashmla norm-rope: K-tokens-per-block ILP to hide load latency - #33358

Closed
Rainchar9119 wants to merge 3 commits into
sgl-project:mainfrom
Rainchar9119:perf-flashmla-ilp-tokens-per-block
Closed

Rainchar9119 wants to merge 3 commits into
sgl-project:mainfrom
Rainchar9119:perf-flashmla-ilp-tokens-per-block

Conversation

@Rainchar9119

@Rainchar9119 Rainchar9119 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Motivation

fused_norm_rope_flashmla (the DSv4 FlashMLA norm-rope-store kernel: RMSNorm over
head_dim=512 + RoPE on the trailing 64 dims + write to the paged KV cache; shared
by the default FP8-UE8M0-quant store path and the kBf16Store bf16 path) is
memory-latency-bound, not bandwidth-bound. NCU on the baseline shows
long_scoreboard (warps stalled waiting on global loads) as the dominant stall
(~15 cyc/issue) while DRAM sits at only ~5–7% of peak and the SM throughput is
~25–41%. The baseline processes one token per block and consumes each input
load immediately, so nothing is in flight to cover the ~hundreds-of-cycles load
latency — the lever is instruction-level parallelism on the loads, not the math.

Modifications

Pure launch/ILP restructuring of the flashmla kernel; the RoPE, RMSNorm reduction
tree, UE8M0 quant, and store byte layout are untouched. The indexer and fp4
paths are not modified.

  1. K tokens per block (K=4 at large N). A block now processes K tokens
    back-to-back. Stage A resolves all K plans first (K independent 16 B plan
    loads in flight), stashing position / out_loc / valid. Stage B then issues
    all K input (+ rope-warp freqs) loads back-to-back — addresses are already
    resolved, so the K global loads have no mutual dependency and stay in flight
    together, covering the load latency the 1-token layout stalled on. The weight
    vector is loaded once (shared across the K tokens).
  2. Streaming input load via __ldcs (evict-first / read-only path). Input is
    read exactly once, whereas weight/freqs are reused; streaming the input keeps
    it from evicting the reused data from L1. This drives long_scoreboard down
    further than the K-ILP alone (16384 decode: 15.1 → ~6.3 cyc/issue).
  3. Small-N dispatch. At K=4 small num_tokens is grid-starved (fewer blocks
    than SMs → occupancy collapses), so the launcher drops to K=1 below a
    cutoff (kFlashmlaSmallNCutoff = 2048). Per-token math/store are identical
    across K, so this is purely a scheduling choice.
  4. RoPE complex-multiply pinned to __fmaf_rn. With the K-loop unrolled, nvcc
    would otherwise pick different fp-contraction forms for a*b - c*d across
    iterations and produce 1-ULP drift. Pinning the fma keeps the exact rounding of
    the 1-token baseline.

Each (token) is a fully self-contained work-item (its 512-dim reduction, RoPE, and
store depend only on its own input/plan), so which SM / block / K-grouping runs it
does not change its output bits. Output is therefore bitwise-identical to the
previous kernel on both store paths — default FP8 quant and kBf16Store.

kFlashmlaTokensPerBlock=4 and kFlashmlaSmallNCutoff=2048 are sm_100 (B200)
autotune values, kept as kernel constexpr (easy to retune for other SM counts).

Accuracy Tests

New test/registered/kernels/ops/attention/test_dsv4_flashmla_norm_rope.py checks
both store paths against an independent torch reference (RMSNorm-512 + trailing-64
RoPE + bf16/FP8 store), across batch sizes spanning the K=1 small-N branch and the
K=4 large-N branch, decode mode.

$ python -m pytest test/registered/kernels/ops/attention/test_dsv4_flashmla_norm_rope.py -v
...
test_flashmla_norm_rope_bf16_store[1/8/64/256/2048]  PASSED
test_flashmla_norm_rope_fp8_store [1/8/64/256/2048]  PASSED
======================== 10 passed, 2 warnings in 8.24s ========================

The FP8 nope dims are checked by dequantizing the 448 fp8-e4m3 bytes with the 7
per-64-element-group UE8M0 exponents and comparing to the reference at
rtol=1/16 (2⁻⁴, the round-to-nearest bound for fp8-e4m3's 3 mantissa bits);
the rope bf16 tail and the whole bf16 path use rtol=atol=2e-2, plus NaN/Inf
checks. (num_tokens ∈ {1,8,64,256} hit the K=1 launcher branch, 2048 hits K=4;
5 shapes × 2 store paths = 10 tests.)

Additionally verified byte-exact against the pre-change kernel across
N ∈ {256, 1024, 2048, 4096, 8192, 16384} × {extend, decode} × ordered/permuted
out_loc, for both FP8 and bf16 paths: q/kv 0 bytes differ, no NaN/Inf, skipped
slots untouched (to_kaiyuan/correctness_full.txt).

Speed Tests and Profiling

CUDA-event median, L2-flushed, on B200 (sm_100), baseline = pristine upstream
kernel, ratio = this-PR / baseline (<1 is faster):

bf16-store path

N extend ratio decode ratio
256 0.91 1.00
1024 1.00 1.00
2048 0.89 1.00
4096 0.84 0.84
8192 0.82 0.88
16384 0.75 0.80

FP8 quant path

N extend ratio decode ratio
256 0.95 0.90
1024 0.99 0.95
2048 1.00 1.00
4096 0.85 1.00
8192 0.94 0.99
16384 0.85 0.92

Small N stays at parity (the grid can't fill the SMs; the K=1 dispatch avoids the
K=4 slowdown there). The benefit grows once work fills the GPU. The bf16 path
gains the most (up to ~1.33× at N=16384); the FP8 path gains less — its per-warp
abs_max reduce + quant ALU make it less latency-bound, diluting the ILP win.
Reproduce: cd to_kaiyuan && python verify_pr.py [--bf16-store].

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): ❌ Run #30873411815
Latest PR Test (Extra): ❌ Run #30873411698

Rainchar9119 and others added 2 commits August 3, 2026 14:22
… latency

The fused_norm_rope_flashmla kernel (head_dim=512 RMSNorm + tail-64 RoPE +
paged KV-cache store, FP8-quant and bf16 store paths) is memory-latency-bound:
NCU shows long_scoreboard as the dominant stall (~15/issue) with DRAM at only
~5-7% of peak, i.e. it stalls waiting on global loads rather than on bandwidth.

The 1-token-per-block layout issues one input load and immediately consumes it,
so nothing hides the ~hundreds-of-cycles load latency. This makes a block
process K tokens back-to-back (K=4): resolve all K plans first, then issue all
K input loads before consuming any, keeping multiple independent global loads
in flight. Input is streamed via __ldcs (evict-first) so it doesn't evict the
reused weight/freqs from L1. The per-token reduction tree and store bytes are
unchanged, so output is bit-identical to the original on both store paths.

Small num_tokens is grid-starved at K=4 (fewer blocks than SMs), so the
launcher drops to K=1 below a cutoff (2048). The RoPE complex multiply is
pinned to an explicit fma so the unrolled K-loop keeps the baseline's rounding.

Correctness: whole-kvcache byte parity vs the unmodified kernel across
extend/decode x N in {256..16384} x ordered/permuted out_loc, both FP8 and
bf16 paths -> 0 byte diffs, no NaN/Inf, skipped slots untouched.

Perf (median, L2-flushed, ratio=new/old, <1 is faster): bf16 store large-N
~0.75-0.88 (up to ~1.33x); FP8 store large-N ~0.85-0.92; small-N neutral.

Co-Authored-By: Claude <noreply@anthropic.com>
Covers both store paths of compress_norm_rope_store (head_dim=512): the
default FP8-UE8M0-quant path and the bf16-store path, each against an
independent torch reference (RMSNorm-512 + per-dim bf16 weight + trailing-64
RoPE). Batch sizes {1,8,64,256,2048} exercise both the K=1 small-N and K=4
large-N launcher branches. FP8 nope dims are checked by dequantizing the
per-64-group UE8M0 scales (fp8-e4m3 round-to-nearest bound rtol=1/16); the
rope bf16 tail and the bf16 path use rtol=atol=2e-2, plus NaN/Inf checks.

Co-Authored-By: Claude <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@Rainchar9119

Copy link
Copy Markdown
Contributor Author

/rerun-failed-ci

Hi maintainers — this is my first contribution, so the pr-gate step failed before
any kernel tests could run (I believe CI needs the run-ci label from a
trusted contributor). Could a maintainer add the label / trigger CI when you get
a chance? Happy to sign the CLA if one is required.

Quick summary for reviewers:

  • Pure launch/ILP restructuring of fused_norm_rope_flashmla — the RoPE, RMSNorm
    reduction tree, UE8M0 quant, and store byte layout are untouched.
  • Output is bitwise-identical to the previous kernel on both store paths (default
    FP8 quant and kBf16Store); the new torch-reference test covers both paths ×
    {1,8,64,256,2048} tokens (K=1 small-N and K=4 large-N launcher branches),
    10/10 passing locally.
  • kFlashmlaTokensPerBlock=4 / kFlashmlaSmallNCutoff=2048 are sm_100 (B200)
    autotune values kept as kernel constexpr; small N stays at parity, large N
    gains up to ~1.33× (bf16) / ~1.18× (FP8).

cc @BBuf @DarkSharpness @HydraQYH @celve @yuan-luo — thanks for taking a look!


@b8zhong b8zhong mentioned this pull request Aug 5, 2026
41 tasks
@Rainchar9119

Copy link
Copy Markdown
Contributor Author

Hi maintainers, could someone please help review this PR when you have a chance?
This is a focused performance optimization for the DSv4 FlashMLA norm-rope kernel: it processes multiple K tokens per block to hide global-memory load latency, while retaining the original per-token math and store behavior. The small-token path falls back to the baseline configuration to avoid reduced GPU occupancy.
I’ve also added coverage for correctness. Thanks!

Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 8, 2026
…project#32035 sgl-project#33656 sgl-project#32183 sgl-project#33145)

Applied PRs (latest from GitHub):
  sgl-project#33288  Indexer logits OOM fix
  sgl-project#30393  HiCache packed/sidecar draft caches
  sgl-project#31170  DPA prefix_affinity load balancing
  sgl-project#33795  DSpark compact ragged-verify CUDA graph JIT race
  sgl-project#32467  C128 plan-kernel warp barrier
  sgl-project#33865  DSpark x prefill CP unblock
  sgl-project#30371  SWA state pool sizing (storage page)
  sgl-project#33358  FlashMLA norm-rope K-tokens-per-block ILP
  sgl-project#33872  num_draft_tokens clamp + extend_len==0 skip (supersede sgl-project#32183)
  sgl-project#34002  Sidecar backup vacuously-successful fix (replaces sgl-project#33656, with tests)
  sgl-project#33862  Reclaim redundant host mirrors after storage backup
  sgl-project#31315  Avoid repeated Mooncake gets after stale hits
  sgl-project#32327  Q8KV8 sparse MLA prefill backend (flashmla_sparse_q8)
  sgl-project#31668  Fix sidecar pool life-time (use-after-free on prefetch abort)
  sgl-project#31195  TP0 verify-token-budget broadcast (adapted to get_schedule() API)

Dropped (per user request or superseded):
  sgl-project#32771  IndexCache C4 top-k reuse — has bug
  sgl-project#32035  DSpark C128 online compressor — has bug
  sgl-project#33656  Superseded by sgl-project#34002 (same fix + unit tests)
  sgl-project#32183  Superseded by sgl-project#33872 (included in supersede PR)
  sgl-project#33145  Base f01f706 already has superior reasoning-effort profile system

Conflicts resolved:
  sgl-project#31195: adapted to base get_schedule().disable_overlap_schedule API
  sgl-project#32327: path remapped jit_kernel/ -> kernels/jit/ and kernels/ops/attention/
  sgl-project#31668: applied cleanly on top of sgl-project#30393+sgl-project#34002+sgl-project#33862 modifications

@b8zhong b8zhong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this? Although the unit speedup is good, norm+rope kernel should be really small at these large num tokens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants