Skip to content

ggml-cpu : AVX-512 VNNI and AVX2 vec_dot for PTQ1_0 on x86 (4x token generation) - #250

Open
lenny76 wants to merge 1 commit into
PrismML-Eng:prismfrom
lenny76:cpu-ptq1_0-x86
Open

lenny76 wants to merge 1 commit into
PrismML-Eng:prismfrom
lenny76:cpu-ptq1_0-x86

Conversation

@lenny76

@lenny76 lenny76 commented Sep 23, 2026

Copy link
Copy Markdown

Overview

On x86, ggml_vec_dot_ptq1_0_q8_0 is aliased to the generic scalar kernel (arch-fallback.h), so PTQ1_0 runs at under 1 t/s on CPU. This PR adds two SIMD kernels in arch/x86/quants.c:

  • AVX-512BW + AVX-512VNNI: 64 weights per vector (two q8_0 sub-blocks), dpbusd.
  • AVX2: one q8_0 sub-block per vector; dpbusd when AVX-VNNI or AVX512-VL VNNI is available (reuses GGML_DPBUSD_256), else maddubs + madd.

Other x86 CPUs keep the generic kernel. Activations stay Q8_0, so numerics only change by float summation order.

How it works: the element order of a block (see dequantize_row_ptq1_0) is trit nn of qs[0..15] -> 16*nn + m, trit nn of qs[16..23] -> 80 + 8*nn + m, trit nn of qh[h] -> 120 + 2*nn + h. So every 128-bit lane of the unpacked weights is one source run times one power of 3, and the unpack is one in-lane pshufb (no VBMI needed, Cascade Lake has none). Per byte v = b*3^nn mod 256 is two vpmullw plus a blend; the trit (3*v)>>8 is (v >= 86) + (v >= 171); the dot uses t in {0,1,2} and subtracts sum(y).

Additional information

2x Xeon Gold 6262 (Cascade Lake), Linux, GCC 14. AVX-512 build: -DGGML_NATIVE=ON. AVX2 build: -DGGML_NATIVE=OFF -DGGML_AVX2=ON -DGGML_FMA=ON -DGGML_F16C=ON -DGGML_AVX512=OFF (no zmm instruction in libggml-cpu.so). Both runs include #249 (without it, token generation of any vec_dot type is limited on this machine by the src1 conversion, see that PR).

Standalone kernel test (generic kernel copied verbatim as reference, random blocks, n = 128 ... 17408): results within 1.8e-8 relative error (float order only); one-hot activations recover every weight exactly (20000 cases, both kernels). Streaming n = 5120 rows:

kernel 1 thread 24 threads 48 threads
generic 0.46 GB/s 7.1 GB/s 14.2 GB/s
AVX-512 VNNI 2.68 GB/s 52.6 GB/s 66.9 GB/s
AVX2 (256-bit dpbusd) 2.09 GB/s 41.9 GB/s 59.3 GB/s
AVX2 (maddubs) 2.06 GB/s 40.4 GB/s 50.5 GB/s

Ternary-Bonsai-2-27B-PTQ1_0.gguf, llama-bench -ngl 0 -p 64 -n 32:

kernel threads pp64 tg32
generic 48 2.49 0.98
AVX2 24 8.62 4.34
AVX2 48 13.41 4.09
AVX-512 VNNI 24 10.71 4.28
AVX-512 VNNI 48 17.49 3.94

Token generation is memory bound, so both kernels give the same speed there.

Correctness:

  • Greedy 48-token completion: identical text for generic, AVX2 and AVX-512.
  • llama-perplexity -c 512 --chunks 8 on an Italian novel: AVX2 23.4668, AVX-512 23.4743 (PQ2_0 on the same text: 23.5011). The generic kernel is too slow for a full run.
  • Not tested: CPUs with AVX-VNNI (Alder Lake and later) and AMD Zen 4/5. The 256-bit dpbusd path was only run through _mm256_dpbusd_epi32 (AVX512-VL) in the standalone test; it has the same semantics as _mm256_dpbusd_avx_epi32.

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES. Claude Code (Anthropic) designed and wrote the kernels, wrote the standalone test, and ran the measurements above on my machine, under my direction.

🤖 Generated with Claude Code

PTQ1_0 had only the generic scalar vec_dot on x86. Add SIMD kernels:
every 128-bit lane holds one source run of trit bytes times one power
of 3, so the unpack needs one in-lane shuffle (no VBMI). Trit t is
(3*v)>>8, computed with two byte compares; the dot uses t in {0,1,2}
and subtracts sum(y).

- AVX-512BW + VNNI: 64 weights per vector, dpbusd
- AVX2: one q8_0 sub-block per vector, dpbusd with AVX-VNNI or
  AVX512-VL VNNI, else maddubs + madd

Other x86 CPUs keep the generic kernel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bri-prism

Copy link
Copy Markdown
Collaborator

Tested on an Intel laptop. This CPU takes the AVX2 + AVX-VNNI (dpbusd) path; the AVX-512 path wasn't exercised.

Setup: Intel Core Ultra X7 358H (Panther Lake, 16 cores, hybrid; AVX2 + AVX-VNNI, no AVX-512), Windows 11, MSYS2 UCRT64 GCC 16.2, -DGGML_NATIVE=ON, CPU only (-ngl 0). Base = prism @ 3b19c377d; PR merged on top. llama-bench -p 64 -n 32 -r 2 -t 16, two rounds with the variant order reversed in round 2 (values are round 1 / round 2, t/s).

Correctness

  • test-quantize-fns passes, including x86: SSE2/SSSE3 vec_dot for PTQ1_0 and PQ2_0 #248's exact PTQ1_0 vec_dot check (dropped into this tree).
  • Greedy 128-token output on latest-2B-PTQ1_0 diverges from the generic kernel at about token 20 ("principles" vs "laws"); both continuations are coherent. KLD vs base (llama-perplexity -c 512 --chunks 8, 2B PTQ1_0) is at the level of summation-order noise: PPL ratio 1.000128 ± 0.000489, mean ΔPPL 0.0013 ± 0.0049, max KLD 0.0022, same top token 99.2 %, RMS Δp 0.33 %.

Performance, t=16

model base pp64 PR pp64 base tg32 PR tg32
Bonsai 2 27B PTQ1_0 1.70 / 1.67 4.65 / 4.46 1.05 / 1.04 2.00 / 1.93
2B PTQ1_0 40.26 / 27.21 82.37 / 67.33 8.03 / 7.08 10.05 / 9.44

Thread sweep, tg32 (two runs each; t=16 understates the gain on this hybrid CPU):

threads 2B base 2B PR 27B base 27B PR
4 8.28, 7.63 18.35, 18.53 0.66, 0.66 1.90, 2.03
8 9.46, 8.20 14.11, 14.15 0.88, 0.87 2.08, 2.13
16 6.91, 7.06 9.68, 9.73 1.01, 1.02 1.95, 1.95

Best-vs-best: about 2.0x decode on both models (27B: 2.13 vs 1.02 t/s; 2B: 18.5 vs 9.5 t/s) and about 2.7x pp64 on the 27B.

Compared with the overlapping PRs on the same box (all three rewrite ggml_vec_dot_ptq1_0_q8_0 on x86, so they conflict with each other): 27B PTQ1_0 pp64 / tg32 at t=16 is this PR 4.65 / 2.00, #181 2.70 / 1.47, #248 (SSSE3, on its own older base) 3.87 / 1.83. This one is the fastest here.

Not tested: the AVX2-without-VNNI (maddubs) fallback and the AVX-512 path; MSVC builds.

Tested with Claude Code.

@bri-prism

Copy link
Copy Markdown
Collaborator

Follow-up: MSVC builds and the AVX2-without-VNNI (maddubs) fallback, both untested in my first comment.

Setup: Core Ultra X7 358H (AVX2 + AVX-VNNI, no AVX-512), Windows 11, base = prism @ 3b19c377d. Builds:

  • MSVC 19.44 and GCC 16.2, each with -DGGML_NATIVE=OFF -DGGML_AVX2=ON -DGGML_FMA=ON -DGGML_F16C=ON.
  • Each compiler built twice, with GGML_AVX_VNNI=ON and =OFF.
  • GCC native (-DGGML_NATIVE=ON) as a reference.
  • Checked by disassembly: the VNNI=OFF builds contain 0 vpdpbusd, so the maddubs fallback really is the path under test. None of the builds contain AVX-512.

llama-bench -ngl 0 -r 2, two rounds with the order reversed, 0 other llama processes during the run (logged).

Correctness of the fallback

  • test-quantize-fns: the exact PTQ1_0 check passes in all four builds (MSVC/GCC × VNNI on/off).
  • KLD vs the generic kernel (2B PTQ1_0, -c 512 --chunks 8), GCC VNNI=OFF: identical to the VNNI path. PPL ratio 1.000128, max KLD 0.002248, same top 99.216 %. That's expected, since both paths compute the same exact integer dot, so only the float accumulation differs from generic.

Performance, t/s

build 27B PTQ1_0 tg16 (t=8) 2B tg32 (t=4) 2B pp64 (t=4)
MSVC generic 0.36 / 0.36 3.61 / 3.62 5.08 / 5.09
MSVC, this PR, VNNI 2.97 / 2.97 31.60 / 31.54 53.18 / 53.32
MSVC, this PR, maddubs fallback 2.83 / 2.84 30.26 / 30.34 50.32 / 50.51
GCC generic 0.89 / 0.88 7.66 / 7.58 13.32 / 13.20
GCC, this PR, VNNI (native) 2.20 / 2.18 18.01 / 18.32 51.30 / 52.10
GCC, this PR, maddubs fallback 2.19 / 2.26 19.07 / 18.97 56.22 / 56.32
  • Fallback: within about 5 % of the VNNI path on both compilers. GCC's prompt processing is even slightly faster on the fallback.
  • MSVC: about 8x decode / 10x pp64 over the MSVC generic kernel, so the shipped Windows binaries gain the most. It's also about 2x faster than ggml-cpu: add x86 AVX-VNNI dot product for PTQ1_0 #181 under MSVC (ggml-cpu: add x86 AVX-VNNI dot product for PTQ1_0 #181 got 1.45 / 14.7 / 22.1).
  • Unexplained: the MSVC build decodes ~1.35–1.75x faster than GCC for the same kernel (31.6 vs 18.0 t/s on the 2B), while pp64 is about equal. I suspect the threading runtime (MSVC vs libgomp) rather than the kernel, but I haven't verified it. It doesn't affect the PR.

(My first comment's GCC numbers are from before a stray background benchmark on this machine; the ones above are a clean rerun.)

Tested with Claude Code.

@bri-prism

Copy link
Copy Markdown
Collaborator

#248 merged first, so this conflicted with prism. A rebased version is on PrismML-Eng/llama.cpp:rebase/250-on-prism @ 300913b52: one commit, your authorship and message kept, onto prism @ adfffbe41. @lenny76, feel free to take it, or a maintainer can push it to this PR's branch.

How the conflict was resolved: there's now a single ggml_vec_dot_ptq1_0_q8_0, dispatching
AVX512BW && AVX512VNNI (this PR) → AVX2 (this PR; dpbusd with VNNI, maddubs without) → SSE2/SSSE3 (#248's kernel, unchanged) → generic.
Both helper sets are kept. The SSE branch reuses the function's nb, and the generic branch gets UNUSED(nb) (that was a latent -Wunused-variable on non-AVX2 builds).

Verified on a Core Ultra X7 358H, Windows 11:

build x86/quants.c warnings test-quantize-fns KLD vs generic (2B PTQ1_0, -c 512 --chunks 8)
GCC native (AVX2 + AVX-VNNI) 0 0 fail max 0.002248, same top 99.216 %, PPL ratio 1.000128 ± 0.000489
GCC AVX2, VNNI off (maddubs) 0 0 fail identical to the row above
GCC SSE4.2/SSSE3 (#248 path) 0 0 fail max 0.001684, same top 99.314 %
GCC SSE2 only (#248 path) 0 0 fail identical to the row above
MSVC 19.44, AVX2 + VNNI 0 0 fail —
GCC GGML_CPU_ALL_VARIANTS + GGML_FATAL_WARNINGS=ON (as in the Ubuntu CPU release job) all 12 variants build — —

The AVX2 rows match my pre-rebase KLD for this PR to the last digit, so the rebase didn't change behaviour. The icelake/zen4/sapphirerapids variants contain the AVX-512 VNNI path (1,137–2,217 vpdpbusd); it compiles cleanly but isn't run here (no AVX-512 on this CPU). An on-device run on an EPYC Genoa host will follow separately.

What it adds over current prism, where AVX2 hardware gets #248's SSSE3 kernel (interleaved ×2, GCC native): 2B pp64 +38 %, tg32 +19 %; 27B tg16 +20 % (1.14 → 1.37). The absolute numbers here are depressed because the GPU was busy with another workload during the run; the rounds are interleaved, so the ratios hold.

Numerics note (raised by @AlexGabbia on #181): this kernel is not bit-exact with the generic one. It folds d0*d1 and accumulates with fmadd, so a large share of real dot products differ in the last bits (worst relative ~8e-4 per their measurement). The exact check in test-quantize-fns doesn't catch this, because its nb is small. End to end, it's at the float-reordering level shown above: max KLD 0.0022, 99.2 % same top token.

@bri-prism

Copy link
Copy Markdown
Collaborator

AVX-512 VNNI follow-up for the rebased branch (rebase/250-on-prism @ 300913b52), run on an Intel Xeon Gold 6338 (Ice Lake SP: avx512f/bw/vnni), by another session testing for us:

  • Build: prism base, this branch native, AVX2-only and no-SIMD all build.
  • test-quantize-fns: 0 failures (including ptq1_0) on native, AVX2-only and no-SIMD.
  • Ternary-Bonsai-2-27B PTQ1_0, 8 threads, pp32: native (AVX-512) 2.62 t/s vs AVX2-only 1.85 t/s (+42 %), identical over 2 rounds, so the AVX-512 path is the one executing.
  • Not done: a KLD on the AVX-512 tier (the 27B reference pass was too slow on 8 vCPUs). Correctness on that tier rests on test-quantize-fns. There's also no Zen 4 coverage yet.

The rebased branch stays as a reference for you to pull, @lenny76. We won't push it to this PR's branch.

@cheese-cakee

Copy link
Copy Markdown

Test report for rebase/250-on-prism @ 300913b52 in a configuration that isn't covered yet: GPU offload with only the output head on the CPU. There the x86 ggml_vec_dot_ptq1_0_q8_0 runs once per generated token (about 1.3B MACs for Bonsai 2 27B's head), so it sits directly on the decode critical path.

Setup

  • RTX 4050 Laptop GPU (6 GB) + Core i5-13450HX (Raptor Lake, 6P + 4E, AVX2 + AVX-VNNI, no AVX-512). WSL2 Ubuntu 24.04, GCC 13.3, CUDA 12.6.
  • Baseline: prism @ adfffbe41 (x86: SSE2/SSSE3 vec_dot for PTQ1_0 and PQ2_0 #248, SSSE3 path on this CPU). Candidate: 300913b52. The only file that differs is ggml/src/ggml-cpu/arch/x86/quants.c.
  • Both builds: -DGGML_CUDA=ON -DGGML_NATIVE=ON -DCMAKE_CUDA_ARCHITECTURES=89.
  • Ternary-Bonsai-2-27B-PTQ1_0.gguf, llama-bench -ngl 99 -fa on -ot '^output.weight=CPU' -b 64 -ub 64 -ctk q8_0 -ctv q8_0 -p 256 -n 64 -r 3.
  • Three ABBA rounds (baseline, candidate, candidate, baseline) with a 20 s cooldown before each run.

Results, t/s (median over 18 samples per arm)

threads test #248 base this PR ratio per-bracket ratios
8 tg64 15.63 18.22 1.17x 1.99, 0.68, 1.16, 1.16, 1.16, 1.19
8 pp256 382.2 403.3 1.06x 1.09, 1.04, 1.06, 1.04, 1.04, 1.05
16 tg64 16.05 13.00 (see below) 1.26, 0.65, 1.07, 0.62, 1.05, 1.07
16 pp256 371.8 384.3 1.03x 0.95, 1.07, 1.02, 1.06, 1.05, 1.05

With 8 threads, the four clean brackets all show +16-19% on generation. Within-run spread is small (MAD 0.08 base, 0.12 PR).

Run-level slow state (not caused by this PR, but it affects measurements on this box). Some runs sit at about 10 t/s generation for all three repetitions, then the next run is normal:

  • -t 8: baseline run 1 (the session's first run) and PR run 3.
  • -t 16: baseline run 1 and PR runs 2, 3, 7.

Outside that state, the PR's run medians at -t 16 are 17.2-17.8 t/s against 15.2-16.6 for the base. The pooled 0.81x at t=16 comes from how the slow runs fell. My guess is thread placement on the hybrid P/E cores: WSL2 exposes 16 identical logical CPUs, so the guest can't tell P-cores from E-cores. I haven't verified that.

Standalone kernel, same CPU (4096 rows x 5120, single thread, 48 interleaved samples):

  • Generic: 7.84 ms. x86: SSE2/SSSE3 vec_dot for PTQ1_0 and PQ2_0 #248: 2.08 ms. This PR: 0.98 ms.
  • Numerics vs the generic kernel: 647 of 900 random dot products differ in the last bits. The worst relative difference is 1.4e-4 at n = 5120. That is consistent with the numerics note above.

Not tested here: KLD, the maddubs fallback, and MSVC.

Happy to rerun other thread counts or configurations on this laptop if useful.

@AlexGabbia

AlexGabbia commented Oct 1, 2026 •

Copy link
Copy Markdown

Test report for the AVX2 tier of rebase/250-on-prism @ 300913b52: CPU only, Windows, MSVC, on Arrow Lake-HX (AVX-VNNI, no AVX-512), compared directly against the old #181 kernel and with MTP speculative decoding.

Setup

llama-bench, CPU only (-ngl 0 -p 16 -n 32 -r 2, one ABBA round, t/s):

threads test #181 kernel this PR (AVX2 tier) ratio per-pair ratios
24 pp16 5.14 11.74 2.28x 2.18, 2.40
24 tg32 3.56 4.54 1.28x 1.31, 1.24
8 pp16 2.42 6.82 2.81x 2.88, 2.74
8 tg32 1.80 3.23 1.79x 1.83, 1.74

MTP on CPU (llama-server, 96 tokens, temperature 0, 24 threads, one run each): with the #181 kernel, --spec-type draft-mtp --spec-draft-n-max 1 does not pay (2.53 -> 2.40 t/s). With this PR it does (2.99 -> 3.93 t/s, 34/60 drafts accepted); n-max 2 gives 3.10. A batch of 2 tokens costs about 1.19x a single token with this kernel, 3 tokens about 1.47x. The greedy output (first 200 characters compared) is identical between the two kernels and with MTP on or off.

Decode is memory-bound with this kernel on this box. I replaced the kernel with a stub that reads the same weight and activation bytes without decoding, alternating stub and real kernel per token inside one process. Median token time was 276 vs 283 ms at 16 threads and 279 vs 281 ms at 24 threads. The matmuls run at about 25 GB/s effective, so on single-channel RAM further compute savings in the kernel no longer show up in decode.

On the slow-run state reported above: on this laptop, back-to-back runs drift from ~4.3 to ~2.5-3.7 t/s (sustained power/thermal), and the 24 workers often sit on only 17-20 distinct logical CPUs at the end of a graph (GetCurrentProcessorNumber). Alternating the two variants token by token in the same process was the only protocol that gave me stable A/B numbers.

Not tested here: KLD, the maddubs fallback, AVX-512, the exact rebase/250-on-prism branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants