Triton MoE improvements and chunked GRPO improvements - #1294
Triton MoE improvements and chunked GRPO improvements#1294michaelroyzen wants to merge 6 commits into
Conversation
|
Reported triton-lang/triton#10821 separately, but the patch in this PR serves as a workaround for all B300 code using Triton in Liger. |
Chunked Triton GRPO loss: head-to-head vs existing GRPO loss implementationsBenchmarks a new chunked Triton GRPO loss ( TL;DR
The new kernel is 1.8–2.5x faster than the torch chunked path at the same flat memory profile (2.3x+ from 4k context up), and within ~1.6x of the unchunked Triton kernel at 16k+ contexts while using up to ~18x less memory. Short contexts show a larger relative gap to the unchunked kernel because fixed per-launch overheads dominate there. Setup
MethodologyAll three implementations are measured from the hidden-state boundary with the lm_head GEMM included, since the chunked variants fuse it: the unchunked Triton path is timed as ResultsPer micro-batch of 4 sequences, forward + backward:
Loss values agree across all three implementations to full print precision on identical inputs ( Notes:
CorrectnessCovered by
Kernel design notes (sm_103 / B300)
ReproducePYTHONPATH=src python benchmark/scripts/benchmark_chunked_grpo_loss_head_to_head.py
PYTHONPATH=src:. python -m pytest test/transformers/test_chunked_grpo_loss.py -q --override-ini addopts= |
…chmark Capture vocab size at forward time and fail with a clear diagnostic if the lm_head weight is not materialized in backward (ZeRO-3/FSDP repartitioning), instead of an opaque autograd shape error. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Performance-tuning pass over the fused MoE grouped-GEMM op (
liger_kernel.ops.fused_moe), plus a tensor-core fix in the chunked PPO loss backward.Headline numbers (bf16, Qwen3-30B-A3B expert shape E=128 / H=2048 / I=768 / K=8, vs. the current
mainimplementation):LIGER_FUSED_MOE_MEMORY_EFFICIENT=1saves −288 MB at T=8192 / −1151 MB at T=32768 for a ~10-15% slower backward on H100Full tables in the Performance section below.
Fused MoE (
ops/fused_moe.py,ops/fused_moe_kernels.py).item()sync to size the m-tile buffers. Tile metadata is now allocated at a host-computable upper bound (TK//BLOCK_M + min(E, TK)), the actual tile count stays on device inexpert_tile_offset[E], and GEMM CTAs past it exit early. No host↔device sync anywhere in forward or backward.tl.make_tensor_descriptorover a flattened 2D view when rows are 16-byte aligned (async bulk copies that bypass L1 and free LSU issue slots), with the portable pointer-load path kept for other GPUs and unaligned shapes. The TMA scratch allocator is (re)registered in bothforward()andbackward()because Triton stores it in a ContextVar that does not propagate to the autograd engine's backward thread.BLOCK_Mwas a fixed 64; it is now picked per call from expected tokens-per-expert (clamped to [16, 128]), so low-occupancy shapes (e.g. T=128, K=8, E=128 → 8 tokens/expert) stop running mostly-padded MMAs.BLOCK_M,USE_TMA, and — for the dW kernels — a tokens-per-expert bucket (TPE_BUCKET): the best dW tile is regime-dependent (output-write-bound at small TK/E, K-loop-bound at large TK/E), whichH_dim/I_dimalone cannot see.early_config_prunehook drops tiles wider than the (padded) problem, cutting first-run compile+tune cost dramatically for small shapes such as unit tests.zeros_likememset (grad buffers aretorch.emptynow).dS(router-score grads) accumulates through fp32 atomics instead of bf16 — ceil(I/BLOCK_N) partials per element previously rounded on every add.tl.dotcalls chained through one accumulator. Besides being simpler, this avoids a Triton 3.7.1 miscompile on sm103 under the tcgen05 MMA path that made the two-chained-dot pattern produce silently wrong dx on B300-class parts; the single-GEMM form is used unconditionally (neutral-to-positive on other archs, verified clean on H100).(T, ceil(H/BLOCK_H))so small-T launches still fill the GPU.LIGER_FUSED_MOE_MEMORY_EFFICIENT=1(read at import): backward writes SwiGLU gradients in place over the saved pre-activations and recomputess_k * silu(gate) * upinside dW2 instead of materializingweighted_act— ≈ −1.2 GB peak at T=32768/I=768/bf16 for a ~10-15% slower backward. A second backward over the same graph then raises a version-counter error by design (guarded viatorch.autograd.graph.increment_version).B300 / Blackwell-datacenter-specific optimizations (sm100/sm103)
All Blackwell-only behavior is confined to autotune config selection; kernel code is shared across architectures, and non-Blackwell search spaces stay exactly as tuned on H100.
_blackwell_configand pruned at first kernel launch wheninfer_device_arch()isn't Blackwell datacenter — deliberately not at import, so importing liger_kernel never initializes CUDA (fork-based workers). Consumer Blackwell (sm120) is excluded: no TMEM, smaller smem, the wide tiles would only waste tuning time.TPE_BUCKETkey is architecture-neutral, but it matters most on B300: dW1 wants BK=128-deep tiles at large TK/E and BK=32 at small TK/E, and the old shared cache entry let whichever T tuned first pin the config for every later T. With the key, realistic mixed-shape runs (small T tunes first, as any warmup or unit test would) measure 1.22x / 1.26x backward at T=8192 / 32768 on B300 over the extended config space alone.tl.dotmiscompile (see the Numerics bullet in the Fused MoE section above) mademain's dx gradients silently wrong on B300-class parts under Triton 3.7.1; the single-GEMM dX rewrite fixes it (applied unconditionally on all archs).Net effect on B300: 2.35x full-step at T=32768 vs
main(table below), on top of the gradient correctness fix.Chunked PPO loss (
chunked_loss/fused_linear_ppo.py)The selective-logprob backward's two grad GEMMs ran in fp32, dispatching to SIMT CUDA-core kernels (~20x below the bf16 tensor-core rate) and dominating the backward's runtime. GEMM operands now round to the input dtype while cuBLAS still accumulates each chunk in fp32 internally and the cross-chunk
grad_hidden/grad_weightbuffers stay fp32 — the same rounding a non-chunked autograd backward through a bf16 lm_head applies everywhere. For fp32 inputs the casts are no-ops. Chunk sizes double to 8192×4096 (identical peak memory, ~21% faster measured).Tests & benchmarks
benchmark_fused_moe.pywarmup updated for the new autotune keys (one warmup per distinct BLOCK_M bucket of the sweep).Performance
All numbers bf16 at the Qwen3-30B-A3B expert shape (E=128, H=2048, I=768, K=8), median of
triton.testing.do_bench, after autotune warmup. "before" is the currentmainimplementation, "after" is this PR.H100 80GB SXM — speedup vs
mainMeasured in the tuning environment (triton 3.7.1):
Independently re-verified on torch 2.10.0+cu128 / triton 3.6.0 (same H100 class, same shapes): 1.33x forward / 1.48x full at T=8192, 1.25x forward / 1.47x full at T=32768. The gap vs the table above tracks the Triton version (3.6.0 lacks some of 3.7.1's TMA/pipelining improvements the tuned configs exploit), so treat 1.47x+ on full steps as the conservative floor.
Peak memory (full fwd+bwd, default mode) is unchanged vs
main: 1953 MB at T=8192, 4356→4357 MB at T=32768 (re-verified). Opt-inLIGER_FUSED_MOE_MEMORY_EFFICIENT=1saves −288 MB at T=8192 and −1151 MB at T=32768 (1665 / 3205 MB peaks) at ~10-15% slower backward on H100 (~32% on B300, where faster MMAs make the dW2 recompute relatively more expensive).H100 sustained TFLOPS (this PR)
FLOP model per token-expert pair: forward 6·H·I, backward 12·H·I, full 18·H·I.
B300 (sm103) — speedup vs
main(Note:
main's dx gradients on B300 are silently wrong due to the Triton 3.7.1 sm103 miscompile described above, so on that hardware this PR is a correctness fix as well as a speedup.)Robustness spot checks (H100)
Testing Done
Environment: torch 2.10.0+cu128, triton 3.6.0, Python 3.12, transformers 5.13.0.
Unit tests:
test/transformers/test_fused_moe.py— 22/22 passed (routing invariants, fwd/bwd vs reference in fp32 + bf16, single-token / all-to-one-expert / K=E edge cases, and the three new tests).PPO loss (exercises the
fused_linear_ppo.pychange):test/chunked_loss/test_grpo_loss.py— 976 passed, 614 skipped.Convergence: bf16 MoE mini-models (
qwen3_moe,qwen3_vl_moe,glm4v_moe,qwen3_5_moe,hunyuan_v1_moe) — 5/5 passed. fp32 MoE mini-models — 3 passed, 1 skipped;mini_hunyuan_v1_moeis flaky by one logprob element in full-suite runs, passes in isolation, and reproduces identically on unmodifiedmain(pre-existing, unrelated to this PR).Benchmarks: see the Performance section above (speedup vs
main, TFLOPS sweep, memory, skewed-routing and Mixtral-shape robustness checks).Hardware Type: H100-80G-SXM
run
make testto ensure correctnessrun
make checkstyleto ensure code stylerun
make test-convergenceto ensure convergence@Tcc0403 @Mecoli1219