Skip to content

metal: fmt=6 (E8/IQ3) expert decode — GPU path + on-GPU FWHT rotation - #732

Merged
JustVugg merged 1 commit into
JustVugg:devfrom
michael-denyer:m5-metal-fmt6
Jul 31, 2026
Merged

metal: fmt=6 (E8/IQ3) expert decode — GPU path + on-GPU FWHT rotation#732
JustVugg merged 1 commit into
JustVugg:devfrom
michael-denyer:m5-metal-fmt6

Conversation

@michael-denyer

Copy link
Copy Markdown
Contributor

Summary

Metal decode for fmt=6 (E8/IQ3) routed experts. Since #465 the container loads on Apple hardware but every expert decodes through the scalar CPU kernel, which on an M5 Pro costs ~3x the disk time the format saves (measured below). This PR gives the format its first GPU path: a moe_gemv fmt==6 branch that decodes the 98-byte super-blocks in-kernel, a moe_fwht kernel that rotates the GPU-resident down-projection input between silu and the down GEMV, and host-side rotation of the staged gate/up input in colibri.c.

Mechanism

  • moe_gemv fmt==6: one 32-weight sub-block per lane step. Grid magnitudes come from a constant uchar4 E8G[256] table generated from quant.h e8_grid; the j=7 sign closes by odd parity of the other seven; the fp16 block scale and 4-bit sub-scale apply in-kernel, so the saddr per-row multiply is skipped (the .qs rider is a tag, resolved but unused).
  • moe_fwht: block-diagonal FWHT with the exact tiling rule of e8_rot_rows (largest power of two dividing the remainder, capped 32768). Tile sizes are validated at submit; a tile over threadgroup memory rejects to the CPU path instead of splitting differently from the converter, so GPU and container tiling cannot diverge.
  • metal_stage_rot_e8 in colibri.c rotates each distinct staged source row once and copies to duplicates. For S=1 decode that is one FWHT per token. The CPU fallback path keeps its own rotation and is untouched.
  • Sign streams are regenerated in the backend with the same xorshift64* draw as quant.h e8_signs. The two copies cannot drift silently: the metal-test oracle runs the full block path end-to-end against matmul_e8/e8_rot_rows, so any divergence fails the build.

Heeding the #587 lesson about silent CPU fallback: the moe_submit fmt gate is lifted only together with the expert kernel, in one commit, and the oracle exercises the whole submit path. A fmt=6 run should show fallback CPU 0 in the METAL line.

Also included

  • convert_fp8_to_int4.py --jobs N: the e8 encodes of one shard run across a process pool. The python codec measures ~5.5 s per expert matrix single-threaded, which puts a full GLM-5.2 REAP conversion at ~81 h serial; 14 workers bring it to ~6 h. Works on both --indir and the --repo disk-safe path; untested in combination with the new --workers, and the help text says so. Pool output verified byte-identical to serial on the bench fixture (1,135 tensors), re-verified after rebasing onto current dev.
  • Two explicit casts in quant.h so it compiles as C++ (the oracle includes it from ObjC++).

Validation

  • make metal-test: two new fmt=6 moe_block cases (decode nb=8, ragged nb=6) at GLM dims, D=6144 and I=1536 so the multi-tile 512+1024 FWHT path is exercised. nerr 4.8e-7 and 5.1e-7 against the scalar reference. The existing suite, including the metal: grouped-int4 (fmt=4) GEMV support + two latent fmt=4 fixes #457 fmt=4 cases, stays green.
  • make test-c: green.
  • Microbench, M5 Pro, 11-expert block at GLM dims, S=1: fmt=2 1.000 ms/block, fmt=6 1.048 ms/block. GPU wall parity while representing 28% fewer weight bytes, which is the whole point on a disk-bound host.

Pending

An end-to-end A/B on a real GLM-5.2 REAP-504B fmt=6 container is in progress (the conversion is long). Projection from the measured decode profile on this host (28.1 s disk-wait, 7.4 s GPU experts per 128 tokens): ~+18% decode from the byte reduction alone, before the pinned-expert coverage gain in the same RAM budget. I will post measured numbers on this thread when the container is done.

moe_gemv gains an fmt==6 branch decoding the 98-byte super-blocks in-kernel
(grid table generated from quant.h e8_grid; signs close by odd parity; scales
live in-block so the saddr multiply is skipped). A new moe_fwht kernel rotates
the GPU-resident down-projection input between silu and the down GEMV, with
the exact block-diagonal tiling of e8_rot_rows; tiles that exceed threadgroup
memory reject at submit (CPU fallback) rather than splitting differently from
the converter. The staged gate/up input is rotated host-side in colibri.c
(metal_stage_rot_e8, one FWHT per distinct source row). Sign streams are
regenerated with the same xorshift draw as quant.h e8_signs; the metal-test
oracle compares the full block path end-to-end against matmul_e8/e8_rot_rows
(nerr ~5e-7), so codec drift between the copies fails the build.

quant.h: two explicit casts so it compiles as C++ (the oracle includes it).
convert_fp8_to_int4.py: --jobs N runs e8 encodes across a process pool (the
python codec is ~5.5s per expert matrix; 504B serial would be ~81h). Pool
output verified byte-identical to serial on the bench fixture (1135 tensors).
@JustVugg
JustVugg merged commit 618b5e9 into JustVugg:dev Jul 31, 2026
10 checks passed
@michael-denyer

Copy link
Copy Markdown
Contributor Author

End-to-end numbers on Apple Silicon, now that I have a clean container to measure.

Setup: M5 Pro, 64 GB, GLM-5.2 REAP-504B. Two colibri containers built from the same source
weights, identical everywhere (per-row int4 dense and attention) except the routed experts:
one fmt=2 int4, one fmt=6 E8/IQ3. Both warmed to steady-state pinning, .coli_usage frozen
and restored per run, quiet machine, three interleaved pairs. Tuned config for every run:
COLI_METAL=1 COLI_METAL_RESSET=1 DIRECT=1 MTP=0 PIPE=1 PIPE_WORKERS=8 RAM_GB=50 TOPP=0.7,
greedy, 128 tokens.

fmt=6 (E8) fmt=2 (int4)
decode 3.53 tok/s (3.57/3.57/3.44) 2.84 tok/s (2.87/2.82/2.82)
expert hit 72% 64%
expert-disk wait ~14.6 s ~26.9 s
container 180 GB 232 GB
fallback CPU 0 0

fmt=6 is +24% on this hardware, reproducible across all three pairs with low variance. Two
effects compound. At the same RAM budget the smaller 3-bit experts pin denser (72% hit vs
64%), and each cold miss reads fewer bytes, so expert-disk wait roughly halves. Both convert
straight to throughput. fallback CPU 0 on every run confirms the kernel path and the on-GPU
FWHT rotation carry the whole decode.

Two notes for anyone reproducing this on Metal:

  1. The residency set (COLI_METAL_RESSET=1, Metal: rebase MTLResidencySet expert residency onto split runtime #426) is required here, not optional. Without it,
    moe_submit runs the per-submit useResource: walk over every resident expert buffer,
    which stalls the GPU about 8 s per run (expert-matmul 17 s vs 7 s at identical hit) and hides
    the disk win entirely. My first run dropped the flag and measured a false dead heat at ~2.1
    tok/s for both formats. With it restored, the real +24% appears.

  2. Build the fmt=6 container with per-row dense (--group-size 0). The current converter
    defaults dense and attention to grouped int4 (fmt=4), and Metal fmt=4 attention has no kernel
    yet (Metal fmt=4 grouped-int4 decode: attention + routed experts (#585) #587 open), so it falls to CPU and costs ~66 s of a 147 s decode. That is a container
    confound, not a property of fmt=6, but it will wreck the comparison if the two containers
    differ in dense format.

fmt=6 pays on Apple Silicon: faster decode, denser pinning, a 22% smaller container, and per
the upstream ablations equal-or-better quality per bit. I can run other shapes if that helps.

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.

2 participants