Module forward_prefill_gemm

Module forward_prefill_gemm 

Source
Expand description

GLM routed-MoE prefill through the shared tensor-core grouped W4A16 GEMM.

Β§What this replaces, and why it is not a new kernel

forward_moe’s routed arm runs w4a16_gemv_sw_moe_batchm_mR β€” a software-dequant GEMV with no mma.sync β€” and MOE_ROW_BATCH_MAX_ROWS caps it at 8 rows per launch regardless of AVAROK_GLM_PREFILL_ROWS. A 256-row prefill sub-chunk is therefore 32 separate 8-row sweeps, and each sweep re-reads the weights of every expert its 8 rows selected. Measured share of prefill: 36.9 % (2026-09-02) to 49.3 % (2026-09-06) of the GPU-busy window β€” the single largest bucket in both profiles, and at 94-100 % of its own DRAM roofline, so the win cannot come from more FLOP/s. It has to come from reading each expert’s weights FEWER TIMES.

kernels/gb10/common/moe_w4a16_grouped_gemm.cu::moe_w4a16_grouped_gemm_ptrtable does exactly that: one launch for all experts, mma.sync.aligned.m16n8k16, expert_offsets prefix sum, in-kernel A-gather through sorted_token_ids. Thirteen other MoE models already run their prefill on it through layers::moe::forward_prefill_routed. GLM never did, because glm5next_mlp is a separate bespoke implementation.

Β§πŸ”΄ Layout compatibility β€” VERIFIED, not assumed

The two kernels read the SAME bytes with the same convention. Checked term by term against w4a16_gemv.cu::w4a16_gemv_partial_rows (the GEMV this replaces) and moe_w4a16_grouped_gemm.cu::moe_w4a16_grouped_gemm_ptrtable:

termGEMV (w4a16_gemv.cu)grouped GEMMsame?
packed byte for logical kB_packed[n * (K/2) + k/2] (kk*8 + b, k = kk*16 + 2b(+1))B_expert[gn * (K/2) + gk/2]βœ… N-major [N, K/2]
nibbleeven k β†’ byte & 0xF, odd β†’ byte >> 4(gk & 1) ? (byte >> 4) : (byte & 0xF)βœ…
codebookE2M1_LUTE2M1_LUT_MOE (same 16 values)βœ…
block scaleB_scale[n * (K/16) + k/16], E4M3S_expert[gn * (K/GROUP_SIZE) + k_base/GROUP_SIZE], E4M3, GROUP_SIZE 16βœ…
per-tensor scalescale2_vals[eid], multiplied into the block scalescale2_vals[expert_id], multiplied into the dequantβœ…
remote expert (EP)packed_ptrs[eid] == 0 β†’ return, caller’s zeros standB_expert == 0 β†’ return, caller’s zeros standβœ…
table indexGLOBAL expert id, [num_experts]GLOBAL expert id, [num_experts]βœ…

weight_loader/glm5_next_load/nvfp4_dequant.rs states the same three conventions (ModelOpt direct-multiplier order, GROUP_SIZE = 16 not DeepSeek-V4’s 32, even flat index = LOW nibble). No permutation, no repack, no extra bytes at load β€” the already-uploaded Glm5NextMoePtrTables are passed straight through.

Β§πŸ”΄ What is NOT bit-identical β€” and it is NOT only re-association

Two things move, and the second one is the bigger one:

  1. Association. The GEMM accumulates a k-major mma.sync FP32 chain over K_STEP = 16 tiles; the GEMV accumulates two interleaved FP32 fmaf chains per orig-lane and combines them through a warp shuffle tree.
  2. Operand precision. mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 takes BF16 operands, so the grouped kernel stages its dequantised weight tile as __float2bfloat16(E2M1_LUT[nibble] * e4m3 * scale2) in shared memory. The GEMV keeps that product in FP32 all the way into its fmaf. The grouped path therefore carries an extra ~2^-9 relative rounding PER WEIGHT ELEMENT that the GEMV does not.

MEASURED 2026-09-22, examples/glm5next_moe_grouped_prefill_microtest.rs on n1 (GB10), M=64, top_k=8, 16 experts, N=256, K=512, random NVFP4, against an FP32 host reference:

armmax_absmax_abs / β€–refβ€–βˆžmax_rel
grouped GEMM vs exact FP32 ref1.4620670.0033060.128986
GEMV (production) vs the same ref0.9995420.0022600.003889
grouped GEMM vs BF16-WEIGHT ref0.9985660.0022580.003888
grouped GEMM vs GEMV2.0000000.0045220.130725

The third row is the finding: against a reference that rounds the dequantised weight to BF16 first, the grouped GEMM scores 0.003888 β€” the GEMV’s own 0.003889 to five figures. All of the residual gap is operand precision; none of it is layout. That is what makes the layout claim above evidence rather than assertion.

It is still a numerics change, so it is gated OFF for decode and for the speculative verify (rows <= MOE_ROW_BATCH_MAX_ROWS), which must stay bit-identical, and ON only for prefill β€” which already left the bit-identical tier when the dense projections moved to cuBLASLt above the same width.

Β§Buffer discipline

πŸͺ€ A59: every buffer this path needs is allocated in Glm5NextMlpWorkspace::new, at load, sized from max_rows. Nothing here allocates. The only per-call host traffic is one [num_experts + 1] i32 read of expert_offsets (see max_m_tiles_from_offsets in tile.rs).

Split into tile.rs (tile geometry + env-lever resolution) and dispatch.rs (the grouped-GEMM launch + the prefill driver) to keep each file under the 500-LoC cap; both are re-exported here so callers keep using forward_prefill_gemm::<name> unchanged.