Skip to content

feat: Hopper DeepSeek-V4.1 training kernels (indexer top-k, MoE experts bf16/FP8, mHC, sparse attention bwd) - #15

Draft
samsja wants to merge 15 commits into
mainfrom
feat/hopper-dsv41-kernels
Draft

samsja wants to merge 15 commits into
mainfrom
feat/hopper-dsv41-kernels

Conversation

@samsja

@samsja samsja commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Hopper (SM90) training kernels for DeepSeek-V4.1 Flash. They port what cuDNN frontend 1.30 ships for the DeepSeek-V4 family on Blackwell only. All three are Python-only (Triton / Gluon), arch = ["9.0"].

  • dsa_indexer_topk: Lightning Indexer forward fused with top-k and the two-level candidate-block filter. It is a drop-in for prime-rl's dsv41_index_topk: same arguments, same picks up to ties. It never builds the (queries, entries) score matrix. A dense FP8 pass keeps per-group maxima, the best groups are rescored, and candidate consumer layers score only their candidates. Forward only, because the indexer is frozen.
  • moe_experts: expert MLP of a rank's local experts: grouped GEMMs with the clamped SwiGLU fused into a warp-specialized epilogue, forward and backward. Only x, gate and up are saved. Weight gradients use torch._grouped_mm.
  • moe_experts(..., fp8=True): DeepSeek-V3 blockwise FP8 (1x128 activation/gradient scales, 128x128 weight blocks, FP8 forward, dgrad and wgrad). It runs DeepGEMM's grouped FP8 GEMMs with the quantization fused into the surrounding passes:
    • one pass over x (and dout) writes the row-quantized copy and the transposed column-quantized copy that wgrad reads;
    • the SwiGLU forward writes FP8 h, and the SwiGLU backward writes FP8 dgate|dup;
    • there is no unpack pass.
  • dsa_sparse_attn_bwd: SM90 backward for V4.1's sparse attention (64 heads × 512, shared latent K = V, attention sink), a drop-in for cuDNN's DSA backward.
    • A Triton preprocess computes delta, the sink-aware LSE and dsink, and compacts each query's non-empty slots.
    • A CuTe DSL wgmma main kernel loops over queries, with dKV accumulated by fp32 vector atomics.
    • It is 1.20–1.23x faster than cuDNN at V4.1 shapes, and its error against an fp32 reference matches cuDNN's to 3 digits.
  • mhc_projection: mHC projection (RMS + gate logits + collapse of the streams by the previous sublayer's pre) in one read of the streams per direction, plus fused gate activations (sigmoid, Sinkhorn). dW is deterministic.

Each has a torch.library custom-op surface with fake impls and works under torch.compile(fullgraph=True). prime-rl picks each kernel up behind prime_kernels.is_available and keeps its own path otherwise.

Results (H200)

kernel today's prime-rl path this PR
indexer, 16k tokens, ratio 1 / ratio 2 6.06 / 3.54 ms 4.19 / 3.04 ms
indexer, CP 131k (rank 7/8): plain / candidate source / consumer 48.3 / 90.7 / 35.6 ms 24.1 / 27.6 / 9.05 ms
MoE experts fwd+bwd, EP=96 load (98k rows) 34.3 ms 32.0 ms
mHC fwd+bwd per sublayer, 16k tokens (GPU time) 1.52 ms 0.75–0.80 ms
MoE experts FP8 fwd+bwd, EP=96 load (same run: bf16 kernel 31.7 ms) 31.8 ms (DeepGEMM + separate quantization) 22.5 ms
  • Indexer picks: match prime-rl's on 100% of tokens in the benchmark. Peak extra memory per call drops from 1.3–4.6 GB to 0.5–1.0 GB.
  • End to end: on a 6-layer CP=8 131k proxy, the indexer alone takes the step from 9.5 s to 8.7 s. On 12-node 16k training the three kernels together gain about 1%, because that run is bound by communication.

FP8 weight cache, split backward, fused mHC stream gradient

  • moe_experts_fp8_quantize_weights / moe_experts_fp8_forward_quantized: quantize expert weights once per optimizer step and reuse them across micro-batches (+8.7% on a PP2×4 V4.1 proxy, less memory).
  • moe_experts_fp8_backward_accumulate: accumulate FP8 weight gradients straight into a caller's fp32 buffer such as FSDP's (+9.1% on the same proxy).
  • Split backward ops (data gradient first, weight gradients after), so a caller can send the data gradient before the weight-gradient GEMMs run. prime-rl uses this to hide DeepEP's backward combine (−2.5 ms per layer at EP48, bitwise-identical gradients).
  • mhc_projection_streams / hyper_connection_streams: return the streams as a view so the post-update's gradient is added inside the projection backward (grad_x_addend), bit for bit the sum autograd computes, instead of a separate full-size add (−0.47 ms per V4.1 layer). tests/mhc_projection gains one test; the module's 16 tests pass.

Sparse-attention backward and FP8 dual cast (DeepSeek-V4.1 pipeline stage)

  • dsa_sparse_attn_backward(..., out_tail=): for a forward output whose per-head last channels the caller rewrote in place (prime-rl's in-place RoPE on the attention output), out_tail holds the original channels so delta = rowsum(dO * O) stays exact. tests/dsa_sparse_attn_bwd gains one test.
  • dKV MMAs of the second warpgroup read P^T / dS^T from registers instead of shared memory: main kernel 4.41 → 4.33 ms at the V4.1 stage shape (8k tokens, 64 heads × 512, 640 slots). dq and dsink are bitwise; dkv is bitwise per contribution, so only the existing atomic order differs. KERNEL_OPTS["rs"] = 0 restores the old path.
  • New fp8_dual_cast (CuTe): per-token and transposed per-token e4m3 casts in one pass, with an optional inverse RoPE. prime-rl uses it for the query-gradient path and every Float8BlockwiseLinear grad_output: cast kernels per V4.1 layer 0.93 → 0.77 ms. FP8 bytes and scales are bitwise-identical to the separate casts.

These three were validated on H200 inside prime-rl's stage bench (bitwise checks as above). The module tests were not rerun after rebasing them onto this branch.

Testing

tests/{dsa_indexer_topk,moe_experts,mhc_projection}: forward and backward against fp32/fp64 torch references on small odd shapes and V4.1 shapes, plus meta/compile checks. 52 passed on H200 (FP8 outputs and grads also checked against an exact torch emulation of the recipe and against prime-rl's DeepGEMM path). Each tests/<name>/bench_<name>.py reproduces its numbers.

Known gaps

  • Only validated on SM90.
  • moe_experts uses Gluon, which is experimental in Triton 3.7.
  • mhc_projection autotunes on first use per shape.

🤖 Generated with Claude Code

samsja and others added 6 commits October 5, 2026 02:05
…op-k for Hopper

Triton drop-in for prime-rl's dsv41_index_topk (same signature, same picks up to
ties) that never materializes the (queries, entries) fp32 score matrix:

- a dense FP8 GEMM (per-token UE8M0 quantization, as prime-rl) keeps only the max
  of every doc-local group of entries and of every 4 groups;
- a persistent per-query kernel selects the best groups by their max (the top
  topk_blocks pinned candidate blocks on candidate source layers);
- a persistent per-query kernel rescores only the entries of the top topk groups
  and radix-selects; candidate consumer layers score only their candidate entries.

The top-k groups by max provably contain the top-k entries, so this is exact.
Forward only (the indexer is frozen). Tests vs an fp64 reference of the FP8 math,
plus a benchmark against prime-rl's op at V4.1 Flash shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The expert MLP of a rank's local experts for training on SM90: tokens already grouped by
expert, DeepSeek-V4's clamped SwiGLU (gate clamped to max=l, up to [-l, l]), bf16, forward and
backward. It is the Hopper-feasible core of cuDNN's MegaMoE expert compute; dispatch and the
routing scores stay with the caller.

The token-major grouped GEMMs are persistent, warp-specialized Gluon kernels: two consumer
warpgroups run the wgmma K loop of a 128x256 tile (issuing TMA loads STAGES-1 steps ahead,
across tiles) and park the bf16 tile in shared memory, and an epilogue warpgroup finishes it
while the next tile's K loop runs:

- fc1: gate and up of the same columns in one 256-wide MMA, SwiGLU in the epilogue,
- down,
- dh: the SwiGLU backward in the epilogue (dgate, dup) plus the recomputed h,
- dx: dgate @ gate_proj + dup @ up_proj in one K loop.

Only x, gate and up are saved for backward. Weight gradients use torch._grouped_mm, which beat
a Gluon version. Exposed as a torch.library custom op with fake and autograd registrations;
separate or packed gate/up weights.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	README.md
#	prime_kernels/kernels.toml
Triton forward and backward for the V4.1 manifold-constrained hyper-connection
projection on SM90:

- forward: one pass over the (tokens, hc, d) streams computes the per-token
  RMS statistic, the bf16 x bf16 -> fp32 projection to the (2 + hc) * hc gate
  logits, and the collapse of the streams by the previous sublayer's pre gate
- backward: TMA-fed kernel writing dx (projection, RMS and collapse terms),
  deterministic split-token dW partials, and d(pre_mix) as the diagonal of a
  tensor-core grad_collapsed @ x.T
- gates: sigmoid pre/post and Sinkhorn comb, forward and backward, in one
  kernel each

Exposed as torch.library custom ops with fake impls; hyper_connection chains
them. Tests compare against fp64/fp32 torch references; the benchmark compares
against prime-rl's DeepseekV41HyperConnection + collapse_streams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	README.md
#	prime_kernels/kernels.toml
samsja and others added 4 commits October 5, 2026 07:00
…ation)

moe_experts(..., fp8=True) runs the expert MLP in DeepSeek-V3 blockwise FP8 for the
forward, the data gradients and the weight gradients: e4m3, fp32 scales, 1x128 groups
along K for activations/gradients (per 128 tokens of each channel for the weight-gradient
operands), 128x128 blocks for weights, fp32 accumulation; SwiGLU math in fp32 as in bf16.

The GEMMs are DeepGEMM's m-grouped (1d2d) and k-grouped (1d1d) FP8 GEMMs. Around them:
- one Gluon/TMA pass over x (and dout) writes both the per-row FP8 copy in a 128-row padded
  layout and the per-column, transposed copy the k-grouped weight-gradient GEMM reads,
- the SwiGLU forward pass writes FP8 h; the SwiGLU backward pass writes FP8 dgate|dup per
  row and per column and FP8 h per column,
- the down and dx GEMMs write out/dx in place at the tokens' rows; the up-to-127 head rows
  per expert that land in a block starting in the previous expert are recomputed by a
  small head GEMM and scattered (no unpad pass),
- weights are quantized once per call with their transposed copies in the same pass,
- no host synchronization.

Tests compare against an fp32 reference and an exact torch emulation of the FP8 recipe;
the benchmark adds the FP8 variants, prime-rl's DeepGEMM path, a per-kernel breakdown and
an accuracy report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@samsja samsja changed the title feat: Hopper DeepSeek-V4.1 training kernels (indexer top-k, MoE experts, mHC) feat: Hopper DeepSeek-V4.1 training kernels (indexer top-k, MoE experts bf16/FP8, mHC) Oct 5, 2026
…ttention

Drop-in for prime-rl's dsv41_sparse_attn_backward (FlashMLA forward + cuDNN
SM90 DSA backward today): shared K = V latent buffer, per-query slot lists
with -1 empty slots, per-head attention sink, 64 heads x 512.

- Triton preprocess: delta = rowsum(dO * O), sink-aware LSE, dsinks, and
  compaction of each query's non-empty slots to the front.
- CuTe DSL main kernel (SM90, wgmma): persistent, one query per iteration,
  two warpgroups. WG0: S / dP / softmax / dQ[:, :256] with dS from
  registers; WG1: dQ[:, 256:] and dKV in 64-column chunks scattered with
  fp32 v4 atomics (4 rows x 128 B per instruction after a quad swap).
  The single-buffered KV tile is refilled half by half as soon as each
  half's last reader is done; the next query's Q / dO halves are
  TMA-loaded while the current query's last dKV chunks still run.
- Triton postprocess: undo the atomics' column permutation, cast to bf16.
- torch.library op prime_kernels::dsa_sparse_attn_bwd with a fake impl.

At V4.1 shapes (16k queries, 640 slots, 16k + 8k kv rows) on H200: 8.71 ms
vs cuDNN 10.42 ms (1 doc, 1.20x) and 8.26 vs 10.15 ms (4 packed docs,
1.23x); results within 1e-4 / 3e-4 / 3e-6 (dq / dkv / dsinks, rel L2) of
cuDNN and at cuDNN's error vs an fp32 reference. The dKV atomics
(~21 GB per call) bound the kernel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@samsja samsja changed the title feat: Hopper DeepSeek-V4.1 training kernels (indexer top-k, MoE experts bf16/FP8, mHC) feat: Hopper DeepSeek-V4.1 training kernels (indexer top-k, MoE experts bf16/FP8, mHC, sparse attention bwd) Oct 5, 2026
samsja and others added 4 commits October 9, 2026 00:52
…fused stream-gradient sum

moe_experts (FP8, Hopper):
- `moe_experts_fp8_quantize_weights` / `moe_experts_fp8_forward_quantized`: quantize expert
  weights once per optimizer step and reuse them across micro-batches.
- `moe_experts_fp8_backward_accumulate`: accumulate weight gradients straight into a caller's
  fp32 buffer (FSDP's), skipping a separate add.
- Split backward ops (data gradient first, weight gradients after) so a caller can send the data
  gradient before the weight-gradient GEMMs run.

mhc_projection:
- `mhc_projection_streams` / `hyper_connection_streams`: return the streams as a view so the
  post-update's gradient is added inside the projection backward (`grad_x_addend`), bit for bit
  the sum autograd would compute, instead of a separate full-size add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hannels were rewritten in place

The forward's caller applies the inverse RoPE in place to each head's last channels; out_tail
carries the original channels so the backward's delta = rowsum(dO * O) stays exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WG1 loads P^T and dS^T once per tile with ldmatrix.trans and runs its six dKV chunks as RS wgmmas,
instead of re-reading both from shared memory for every chunk (-96 KB of shared memory traffic per
tile), and hands P / dS back to WG0 right after dQ_hi. Two rotating dKV accumulators instead of three
make room for the fragments. Bitwise identical per contribution (checked with unique kv rows, where
the atomics are deterministic); KERNEL_OPTS rs=0 restores the old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one pass, optional inverse RoPE

One CTA per [128 tokens x 128 channels] tile reads the bf16 matrix once and writes both of prime-rl's
casts (per_token_cast_to_fp8_triton and per_token_cast_to_fp8_tp_triton) bit for bit, optionally of
dsv4_rope(x, inverse=True) (the DeepSeek-V4.1 query gradient). 8192 x 32768: 383 us vs 509 us for
the three Triton passes (no RoPE: 319 vs 463).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant