quant: AVX2 matmul_e8 — fmt=6 decode was 92% of the time on a real model - #654
Merged
Conversation
fmt=6 shipped with only its scalar reference kernel, and on a real model that
dominated everything else: on GLM-5.2 (744B, E8 container) a 32-token decode
spent 206.6s of 224.1s — 92% — inside matmul_e8, with disk service fully
overlapped (1.1s of actual wait). The format's own arithmetic was the wall, not
I/O and not the GPU. This is the same shape of problem AVX2 matmul_i3 fixed for
int3-g64, where the scalar path cost more than half the achievable rate.
The format suggests the vectorisation: one 8-weight lane is two 4-dim grid rows
(8 contiguous codebook bytes) plus 8 signs, which is exactly one AVX2 register.
So a lane decodes straight into a register and FMAs against x, instead of being
expanded into a stack buffer and read back:
- the 8 grid bytes widen with a single vpmovzxbd;
- the sign byte (the 7 stored bits plus the parity-derived 8th) expands to 8
lane masks with AND/CMPEQ against a bit-select vector and applies as an XOR
of the float sign bit. That removes the per-weight branch, which is what made
the scalar expansion expensive;
- the grid's half-unit convention folds into the sub-scale, so the magnitudes
need no separate scaling;
- two accumulators over a super-block's 32 FMAs keep it off one dependency
chain, and one horizontal add per 256 weights replaces one per 32.
The scalar path stays for the ragged tail and for non-AVX2 hosts; the converter
enforces whole 256-blocks, so the fast path is the one real containers take.
Kernel: 280.8 -> 4141.3 Mw/s single-threaded, 14.7x (44.8 -> 3.0 ms per
[2048,6144] expert tensor). tests/test_e8_kernel green against its fixture
(worst rel 1.46e-06); AVX2 vs scalar agree to 2.2e-07 relative L2, which is
accumulation order.
End to end on GLM-5.2 (RX 9070 box, 32-token greedy decode, cold cache, single
drive), same text produced in every configuration:
prefill decode expert-matmul
CPU before 91.3s 224.1s (0.14 tok/s) 206.6s
CPU after 16.7s 38.6s (0.83 tok/s) 18.7s
VK before 91.6s 177.3s (0.18 tok/s) 161.0s
VK after 17.2s 33.6s (0.95 tok/s) 16.8s
5.3-5.9x decode, 5.5x prefill. Decode is now disk-bound again (34.9s service,
3.4s wait) rather than compute-bound, which is the regime the streaming design
targets, and the VK tier's share rose from 22.7% to 31.0% of expert hits.
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.
fmt=6 (E8/IQ3) landed its container (#465, #501) and a CUDA kernel (#627), but on the CPU it still runs the scalar reference kernel — and on a real model that dominates everything else.
Measured on GLM-5.2 (744B, E8 container), 32-token greedy decode:
206.6 s of a 224.1 s decode — 92% — inside
matmul_e8, with disk service fully overlapped (1.1 s of actual wait). The format's own arithmetic was the wall, not I/O. This is the same shape of problem AVX2matmul_i3fixed for int3-g64.The kernel
The format suggests its own vectorisation: one 8-weight lane is two 4-dim grid rows (8 contiguous codebook bytes) plus 8 signs — exactly one AVX2 register. So a lane decodes straight into a register and FMAs against
x, instead of being expanded into a stack buffer and read back:vpmovzxbd;The scalar path stays for the ragged tail and for non-AVX2 hosts. The converter enforces whole 256-blocks, so the fast path is the one real containers take.
Numbers
Kernel, single-threaded: 280.8 → 4141.3 Mw/s (14.7×) — 44.8 → 3.0 ms per
[2048, 6144]expert tensor.End to end on GLM-5.2 (RX 9070 box, 32-token greedy decode, cold cache), identical text produced in every configuration:
5.9× decode, 5.5× prefill. Decode becomes disk-bound again (40.3 s service, 3.3 s wait) rather than compute-bound, which is the regime the streaming design targets.
Correctness
tests/test_e8_kernelgreen against its existing fixture (worst rel 1.46e-06). AVX2 and scalar agree to 2.2e-07 relative L2 over a 512-row random container — that is accumulation order, not a semantic difference.make check220.