flint_cpu_opt / benchmarks /q41-summary.md
HelloSun's picture
Add Q4_1 tiled VNNI GEMM for prefill: pp256 +34%, pp2048 +42% vs vec_dot patch
616131c verified
|
Raw History Blame Contribute Delete
3.71 kB
# RecursiveSI/flint-int4-gguf (flint-int4-q4_1.gguf) with this patch
The Q4_1 file (24.2 GiB, every weight tensor Q4_1; token_embd and the router are F32) is not hosted here; download it from
[RecursiveSI/flint-int4-gguf](https://huggingface.co/RecursiveSI/flint-int4-gguf). The patch adds an AVX512-VNNI `vec_dot` for
Q4_1 x Q8_1 (the x86 path had AVX2 only) and puts Q4_1 into the tiled VNNI GEMM used for prefill.
`llama-bench -t 16 -fa on -lm mlock -b 1024 -ub 1024`, 3 interleaved rounds, median t/s (raw: `q41-ab-interleaved.csv` for the
vec_dot kernel, `q41-tiled-ab-interleaved.csv` for the tiled GEMM),
Xeon Platinum 8559C, same host as the other benchmarks (shared, +-8 % per run).
| | pp256 | pp2048 | tg128 |
|---|---|---|---|
| stock llama.cpp (de5122d) | 182.7 | 188.7 | 48.3 |
| this patch, vec_dot only | 198.8 | 208.5 | 54.0 |
| this patch, tiled GEMM | **266.5** | **297.2** | **55.7** |
| gain vs stock | +46 % | +57 % | +15 % |
| gain vs vec_dot only | +34 % | +42 % | +3 % (noise) |
Decode is unchanged by the tiled path (single-row work stays below the tiled row threshold); the small tg difference is noise.
## Tiled Q4_1 GEMM (prefill)
Q4_1 tiles carry no super-block scale: each 32-wide block of a 256-element slab has its own f32 `d` (scale) and `m` (min).
The tiled kernel gains `sd`/`sm` arrays (per-32-block f32, indexed per tile row) and a `Q41` template flag; activations use the
existing Q8_1 `vec_dot_type`, so no new quantizer was needed. Per 32-block the kernel computes
`d_a * (d_w * dot(q, code) + m_w * sum(code))`: the integer dot and integer code sum are exact and only the f32 accumulation
order differs from `vec_dot`. VNNI uses `vpdpbusd` with the 0..15 weight nibbles as the unsigned side. The driver scales the
k-block byte offset by `256/blck_size` (8 for Q4_1) instead of assuming one block per 256 elements.
## vec_dot kernel (decode)
Two 32-wide blocks per iteration. The two blocks' nibbles are packed into one ymm with memory-source `vinserti128`
(no shuffle-port work), low and high nibbles each go through one `vpdpbusd` against the matching Q8_1 halves, the four fp16
header values of the two blocks are converted at once, and the per-block `d*dy` scale is spread with a single `vpermps`.
A 512-bit version that re-ordered nibbles with cross-lane shuffles was slower than stock (shuffle-port bound) and was dropped.
Prefetch distance `GGML_Q41_PF` (blocks, default 192; 192-320 equal in the cold-MoE microbenchmark).
Microbenchmarks (16 threads, K=2048): cache-resident mul_mat 183 -> 220 GB/s, cold MoE decode (`mul_mat_id`) 43 -> 57 GB/s.
## Accuracy
wikitext-2 test, 32 x 512, tiled Q4_1 vs the vec_dot path of the same patch (logits baseline):
| | KLD | same top-1 |
|---|---|---|
| tiled Q4_1 vs vec_dot (`q41-tiled-kld.log`) | 0.0372 +- 0.0026 | 94.2 % |
| vec_dot patch vs stock | 0.0364 +- 0.0029 | 93.8 % |
| stock with `-fa off` (noise floor) | 0.0385 +- 0.0027 | 93.9 % |
The tiled path is inside the patch's own noise floor. Graph-level `mul_mat` / `mul_mat_id` error versus a float reference is
identical for tiled and vec_dot (`5.29e-3` vs `5.29e-3` for T=1, `5.35e-3` vs `5.39e-3` for T=64; Q8_0 reference 5.35e-3).
Integer dot products are exact; only float accumulation order differs.
## Versus the Q4_K_XL file (same patch, same host)
| file | pp256 | pp2048 | tg128 |
|---|---|---|---|
| flint-UD-Q4_K_XL (25.1 GB), patched | 296 | 307 | 42.8 |
| flint-int4-q4_1 (26.0 GB), patched | 266 | 297 | 55.7 |
Q4_1 decodes about 30 % faster (0.625 B/weight and a simple layout) and now prefills within about 10 % of Q4_K_XL (the tiled
Q4_1 GEMM closed most of the earlier gap). PPL of the two files is similar (18.26 vs 18.36 stock).