Skip to content

chore(train): engine-10 continued: the SwiGLU limit fused, routed experts through a ring of bulk copies - #76

Merged
marcospaulo merged 3 commits into
mainfrom
train/engine-10
Sep 28, 2026
Merged

marcospaulo merged 3 commits into
mainfrom
train/engine-10

Conversation

@marcospaulo

Copy link
Copy Markdown
Member

The shared branch's commits after #75:

  • 640a12c perf(cuda): GLM-5.3's SwiGLU limit, a clamp on the gate and one on up before the GLU, now fuses into the quantized mat-vec's GLU epilogue. Before, the two clamp nodes kept the gate/up fusion from matching. On an RTX 5080, GLM-5.3-Flash's routed FFN in place (8 layers of 32 experts, CUDA graphs and PDL) takes 807/808 us at 1 token against 865/857; 3 tokens are unchanged. GGML_CUDA_GLU_LIMIT_FUSE_LEGACY=1 restores the separate clamps.
  • b8d44d4 perf(cuda): routed experts at 1-8 tokens stream through mmvq-moe.cu. It runs one block per SM. A producer warp lists the distinct experts once, so a shared expert is read once, and streams their row tiles through a shared-memory ring of cp.async.bulk copies to two teams of consumer warps. The ring takes IQ3_XXS at any token count and Q8_0 past 1 token, where it measured faster. In place: IQ3_XXS 815 vs 818 us at 1 token and 1,823 vs 1,953 at 3; Q8_0 2,265 vs 2,382 at 3. IQ4_XS measured slower and stays on mul_mat_vec_q. GGML_CUDA_MMVQ_MOE_LEGACY=1 turns the ring off.
  • 217bbd7 docs(torad): the TORAD.md rows for both.

Checks:

  • test-backend-ops: MUL_MAT_ID 1066/1066 and MUL_MAT_VEC_FUSION 1030/1030, with GLM-5.3-Flash's shapes through the ring.
  • b8d44d4 built alone in a clean tree: its IQ3_XXS cases pass (7/7 and 8/8).
  • Five mutants each fail the tests: the ring's clamp dropped, mul_mat_vec_q's clamp dropped, the ids row stride dropped, a tile one row short, and the tile counter never reset.

…ec's GLU

GLM-5.3 clamps before the GLU (a SwiGLU limit of 10, in its routed experts, shared experts and dense FFN):
llama-graph puts a CLAMP of the gate to [-inf, 10] and of the up projection to [-10, 10] between the mat-vecs and the
SWIGLU, and those two nodes kept the gate/up mat-vec + GLU fusion from matching, so a decode ran the two mat-vecs
apart, then two clamps and the GLU: five launches where the fusion takes one. ggml_cuda_can_fuse now takes {MUL_MAT,
CLAMP, MUL_MAT, CLAMP, GLU} and its MUL_MAT_ID twin (either mat-vec first: the GLU's first source is the gate's clamp)
when the clamps are exactly a limit L (gate [-inf, L], up [-L, L], L finite and above 0), nothing sits between a
mat-vec and its clamp, and the GLU is a SwiGLU; mul_mat_vec_q applies the limit in its epilogue as one float,
glu_limit. The PQ2_0 tensor-core path takes no limit and stays unfused. GGML_CUDA_GLU_LIMIT_FUSE_LEGACY=1 leaves the
clamps as nodes.

In place (moe-graph: GLM-5.3-Flash's routed FFN, 8 layers of 32 experts, CUDA graphs and PDL; RTX 5080, legs
A-B-B-A): 807/808 us at 1 token against 865/857 with the clamps as nodes. 3 tokens are unchanged (1,956/1,955 against
1,953/1,974): the fused mat-vec takes one column, and a verify's experts run apart as before.

test-backend-ops: MUL_MAT_VEC_FUSION builds the limit as llama-graph does, at 0.5 so that most products clamp and a
kernel that drops it fails, for Q8_0, IQ3_XXS and Q4_0, with and without ids, at 1 and 3 columns and with a batch.
…ared expert read once

A decode's routed experts are a few thousand rows each (GLM-5.3-Flash: 8 of 288, gate and up 2,048 rows of 4,096
weights at IQ3_XXS, down 4,096 of 2,048), and mul_mat_vec_q reads them a row or two a block: a block's loads are its
only bytes in flight, and at an MTP verify every token reads its experts again where the tokens share them. mmvq-moe.cu
runs one block an SM: a producer warp lists the distinct experts once, each with every token/slot pair that routes to
it, and streams tiles of their rows through a ring of shared-memory slots with cp.async.bulk, while two teams of eight
warps take the dot products for all of a tile's pairs. An IQ3_XXS warp takes its lanes' fragments of a tile into
registers and frees the slot before the math (vecdotq.cuh: iq3_xxs_frag), so the slots stay in flight, and keeps the
grid in shared memory a copy a lane. The last tiles go by tickets on the stream's tile counter
(ggml_cuda_pq2_tile_counters), so the blocks end within a tile of each other.

It takes what it measured faster at in place (moe-graph: GLM-5.3-Flash's routed FFN on 32 experts, CUDA graphs and
PDL; RTX 5080, legs A-B-B-A, ring against mul_mat_vec_q):
- IQ3_XXS, 8 layers: 815/814 us against 818/879 at 1 token, 1,823/1,833 against 1,953/1,992 at 3.
- Q8_0 (an MTP layer's experts), 4 layers: past 1 token only, 2,270/2,260 against 2,384/2,381 at 3; at 1 token it
  stays on mul_mat_vec_q (988/991 against 979/977), its dot products holding the slot.
- IQ4_XS not at all: 527/525 against 506/505 at 1 token, 1,243/1,240 against 1,199/1,200 at 3.
GGML_CUDA_MMVQ_MOE_LEGACY=1 turns it off.

Per-tile %globaltimer stamps put its steady state at DRAM's ceiling (a 12.5 KB tile every 1.15 us an SM, ~913 GB/s);
what it loses is each launch's start: a team's first q8_1 vector is loaded from global memory behind the stream's
queues, 3-6 us, as is a down projection's at each new expert.

The IQ3_XXS dot product takes its signs as byte masks from ksigns64, 3 integer ops for 4 weights where __vcmpne4 and
__vsub4 are emulated (GGML_CUDA_IQ3_XXS_SIGNS_LEGACY builds the old ones), in mul_mat_vec_q too, and
get_vec_dot_q_cuda and get_vdr_mmvq move to vecdotq.cuh for both kernels.

test-backend-ops: GLM-5.3-Flash's shapes through the ring, MUL_MAT_ID at 1 and 3 tokens (gate/up and down) and
MUL_MAT_VEC_FUSION gated at a SwiGLU limit; MUL_MAT_ID 1066/1066, MUL_MAT_VEC_FUSION 1030/1030.
@marcospaulo
marcospaulo merged commit a786bcd into main Sep 28, 2026
5 checks passed
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