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. 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).