Size MoE activation kernel grid to the row count (+3.79% decode throughput) - #6328
Draft
shanmugamr1992 wants to merge 1 commit into
Draft
Size MoE activation kernel grid to the row count (+3.79% decode throughput)#6328shanmugamr1992 wants to merge 1 commit into
shanmugamr1992 wants to merge 1 commit into
Conversation
The row-parallel Triton activation kernels launched a fixed `min(M, 512)` CTAs. At decode shapes on Qwen3-30B-A3B (batch 256, top-8 => ~2048 live rows) that leaves a GB200 SM array roughly 22% occupied, so `_silu_mul_bounded_kernel` ran about 10x its memory-bandwidth floor: 15.77us per layer against a 1.57us floor for the 9.44MB it actually moves. Size the grid to the row count instead, so each CTA owns one row, capped at a power of two. The cap must stay a small set of fixed powers of two because NUM_BLOCKS is a tl.constexpr and each distinct value costs a JIT compile. The grid remains static per capture, so CUDA-graph replay is unaffected. Measured +3.79% end-to-end decode throughput on 1 node x 4xGB200 (EP4/TP1, BS256, OSL1024), 22833 -> 23699 tok/s, with all ten ON iterations beating all ten OFF iterations. The kernel is elementwise with no cross-CTA reduction, so the row-to-CTA mapping cannot change per-element math: output is bit-exact. Set MCORE_MOE_ACTIVATION_GRID_CAP=512 to restore the previous geometry. Signed-off-by: shanmugamr1992 <shanmugamr1992@gmail.com>
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.
What changed and why
The row-parallel Triton activation kernels in
megatron/core/inference/moe/activations.pylaunched a hardcodedNUM_BLOCKS = min(M, 512)CTAs. At MoE decode shapes that starves the SM array:on a GB200 (148 SMs), 512 CTAs × 128 threads = 65,536 threads against a capacity
of ~303,000, so roughly 22% occupancy.
The consequence is that
_silu_mul_bounded_kernel— a pure elementwise op with noalgorithm to be slow at — ran about 10× its memory-bandwidth floor: 15.8 µs per
layer for the 9.44 MB it actually moves, against a 1.57 µs floor at this machine's
measured 6.081 TB/s streaming ceiling. Other tuned elementwise kernels in the same
trace cost 1.26–2.60 µs, which is what identified this as launch geometry rather
than an inherent cost.
This sizes the grid to the row count instead, one CTA per row, capped at a power of
two.
Two constraints shape the fix:
NUM_BLOCKSis atl.constexpr, so every distinct value costs a fresh JITcompile. The cap therefore stays a small set of fixed powers of two rather than
the raw row count.
capture-time buffer bound
M, never from a per-step token count. Graph replay isunaffected.
Measured gain
+3.79% end-to-end decode throughput, 22833.56 → 23698.83 tok/s.
Baseline is the OFF arm of the same allocation, not a previously recorded number
(see Protocol).
Protocol
oci-hsg), Qwen3-30B-A3B, BF16.--transformer-impl inference_optimized, NVLS dispatcher,full-iteration CUDA graphs.
num_input_tokens_avg = 60.86.timed numbers.
5931387), server restarted per arm.
Arm separation:
min(ON) = 23519.8 > max(OFF) = 22882.8over all 20 timediterations — every ON iteration beats every OFF iteration, so the arms do not
overlap. Pairwise deltas are +4.06% (1→2) and +3.52% (3→4).
Arms were run in one allocation deliberately: this same code measured 0.66%
slower in a different session/node for identical binaries, which is the same order
as many candidate wins.
Where the time went
Confirmed with a second Nsight Systems capture under the change:
Launch count is unchanged and only
gridXmoved, which is the signature of ageometry retune rather than a fusion.
Predicted-vs-measured conversion:
The kernel-to-e2e conversion is 0.90, consistent with the kernel sitting on the
serial chain. The shortfall against the gate was the gate's optimism about
achievable kernel time — it assumed ~2× the bandwidth floor and the kernel landed
at 4.9×.
Correctness
Bit-exact. The kernel is elementwise with no cross-CTA reduction, so the
row-to-CTA mapping cannot change per-element math; bit-exactness is structural
rather than empirical.
Verified anyway:
zero differing elements. 5 shapes × 6 seeds, covering
M= 1024/2048/4096/16384and live-row counts both below and above the old 512 cap.
across all four A/B arms (md5
52f4690b327f59de2e0689c0c3b63b64), so ON isindistinguishable from OFF end to end.
No ulp budget is needed because there is no numerical difference to bound.
Kill switch
MCORE_MOE_ACTIVATION_GRID_CAP=512restores the previous geometry exactly. Thevariable accepts any power of two; the default is 8192.
Scope and risks
bounded_silu_mulpath (--swigluMoE with--transformer-impl inference_optimized) at BS256 decode on GB200._squared_relu_kernelshares the samemin(M, 512)pattern but is notchanged here, to keep one mechanism per PR. It is the obvious follow-up.
hardware-neutral in principle — it removes a cap, it does not tune to an SM
count — but the 22%-occupancy figure that motivates it is GB200-specific.
n_rowsbound at small batch.At very small batch the old geometry may be marginally better; the cap is the
escape hatch if that ever shows up.
Artifacts
EXP-01inskills/run-qwen-model/EXPERIMENTS.md.runs/exp01-swiglu-grid-20260806-120408/arm_{1,2,3,4}_{OFF,ON}.*.logruns/qwen-30b-nsys-20260806-113411/mcore_profile.{nsys-rep,sqlite},this change
runs/exp01-nsys2-124241/mcore_exp01.{nsys-rep,sqlite}/lustre/fsw/portfolios/coreai/users/shanmugamr/agents-space/