Skip to content

ggml-cpu: AVX-512-VNNI dot-products for Q1_0/Q2_0 - #37

Merged
khosravipasha merged 1 commit into
prismfrom
cpu/avx512-vnni-q1q2-prism
Jun 4, 2026
Merged

ggml-cpu: AVX-512-VNNI dot-products for Q1_0/Q2_0#37
khosravipasha merged 1 commit into
prismfrom
cpu/avx512-vnni-q1q2-prism

Conversation

@bri-prism

Copy link
Copy Markdown

What

Adds an AVX-512-VNNI / AVX-512VL fast path for the Q1_0 and Q2_0 CPU dot products. Both formats previously had no x86 vec_dot path — arch-fallback routed the _generic functions to a scalar loop. The scalar path is preserved as the #else fallback (guarded by __AVX512VNNI__ && __AVX512VL__).

Why

The low-bit CPU dot product is the hot kernel for Bonsai inference on CPU backends. Q2_0 in particular was leaving a large amount of x86 throughput on the table.

How

  • helper ggml_hsum_i32_8_vnni to reduce _mm256_dpbusd_epi32 accumulators
  • Q1_0: build a sign mask from the bit field, blend +qy/-qy, accumulate with dpbusd(ones, sel)
  • Q2_0: vectorized 2-bit unpack (replicate-4 + 16-bit shift/mask + pack), then dpbusd(codes, qy) - dpbusd(ones, qy) = sum((code-1)*qy)

Performance (EPYC 9655, AVX-512-VNNI)

Format prefill decode
Q2_0 ~3.9× ~3.0×
Q1_0 ~parity (the ±1 scalar loop already auto-vectorizes)

Scales: holds 3.4–4× through -d 2048; scalar Q2_0 times out at -d 8192.

Correctness

  • Bit-exact vs the scalar reference: standalone unit test (2000+ random blocks) + test-quantize-fns dot-product error identical with/without the path.
  • KL divergence vs FP16 (Bonsai-8B, packed_models_KL_validation flow): Q2_0 mean KLD 0.000135, top-1 99.28%, PPL +0.14% — passes thresholds. Scalar and VNNI builds produce byte-identical KLD/top-1, confirming the kernel preserves outputs end-to-end.
  • Verified to build + run on this prism tree (above numbers reproduced here).

Notes

This is the public-fork counterpart of the approved internal PR (llama.cpp-private#1), landed on prism per review. The one red CI signal — test-quantize-fns on q2_0 — is pre-existing on prism (the {−1,0,1,2} 2-bit format exceeds the generic 2-bit threshold); it reports identical numbers with and without this change.

Q1_0/Q2_0 had no x86 vec_dot path (arch-fallback routed the generic
functions to a scalar loop). Add an AVX-512-VNNI/AVX-512VL fast path
guarded by __AVX512VNNI__ && __AVX512VL__, scalar fallback otherwise:

- helper ggml_hsum_i32_8_vnni to reduce _mm256_dpbusd_epi32 accumulators
- Q1_0: build a sign mask from the bit field, blend +qy/-qy, accumulate
  with dpbusd(ones, sel)
- Q2_0: vectorized 2-bit unpack (replicate-4 + 16-bit shift/mask + pack),
  then dpbusd(codes, qy) - dpbusd(ones, qy) = sum((code-1)*qy)

Q2_0 prefill ~3.9x / decode ~3.0x vs scalar on EPYC 9655; Q1_0 ~parity
(the +/-1 scalar loop already auto-vectorizes). Bit-exact vs scalar
(test-quantize-fns + standalone unit test); KL-divergence vs FP16
unchanged between scalar and VNNI builds.
@khosravipasha
khosravipasha merged commit c9d528d into prism Jun 4, 2026
@pl752

pl752 commented Jun 4, 2026

Copy link
Copy Markdown

@khosravipasha Why are the avx-512 specific changes being put into the generic quants file instead of x86 specific one? Also it seems that q1 part might be not called as x86 dot routes to generic only if neither AVX2/AVX/SSSE3 are available, am I right?

@khosravipasha

Copy link
Copy Markdown
Collaborator

@pl752 I think you might be right that wrong file is being modified, did not realize in the initial pass reviewing.

For the second part will need to double check.

Probably need to undo the commit and redo the PR.

@pl752

pl752 commented Jun 4, 2026

Copy link
Copy Markdown

In order to add x86 specific implementation it is needed to remove alias in x86 section of arch_fallback.h for q2 and define function with impls and call to generic fallback

@pl752

pl752 commented Jun 4, 2026

Copy link
Copy Markdown

Also I will benchmark vnni for zen 4 as I tried to use it, but encountered lower speed than xor-sub (though it's likely that I have used it wrong), also for 256 bit variants there is AVX-VNNI extension of some of the later non AVX-512 cpus which implements dp for AVX2 like ISA (VEX encoding instead of EVEX) in alder lake and later

@bri-prism

Copy link
Copy Markdown
Author

Thanks @pl752 — good catch, you're right about the q1_0 path.

q1_0: correct. On x86, ggml_vec_dot_q1_0_q8_0 lives in arch/x86/quants.c with an #if defined(__AVX2__) implementation and only delegates to ..._generic in the #else. So the __AVX512VNNI__ block I added to the generic q1_0 is unreachable on any AVX2-capable build (including the EPYC 9655 in the table) — which is also why q1_0 shows ~parity rather than a VNNI speedup: the AVX2 path is what actually runs. That hunk is effectively dead on x86 and should be dropped; if we ever want a VNNI tier for q1_0 it belongs next to the AVX2 path in arch/x86/quants.c, not in the generic file.

q2_0 is the reason the change is in the generic file — and there it's on the live path. q2_0 has no x86-specific vec_dot. The x86 block of arch-fallback.h aliases ggml_vec_dot_q2_0_q8_0_generic → ggml_vec_dot_q2_0_q8_0, so the generic function is the symbol the dispatch table calls on x86 — that VNNI block is what produces the ~3.9× / 3.0× q2_0 numbers (confirmed reached: scalar vs VNNI builds are byte-identical in KLD/top-1, and removing the guard regresses to scalar timing). Relocating q2_0 to arch/x86/quants.c would mean registering a new x86 symbol and dropping that alias; generic-with-a-compile-guard was the minimal change that's actually dispatched.

Summary: the q2_0 placement is intentional and on the hot path; the q1_0 VNNI hunk is dead on AVX2 x86 and isn't what's producing the parity number. I'll put up a follow-up to remove the q1_0 block (and can add a q1_0 VNNI tier in arch/x86/quants.c if a measurable win is wanted there — it auto-vectorizes to ~parity today, so probably not worth it). Thanks for the review.

@khosravipasha
khosravipasha deleted the cpu/avx512-vnni-q1q2-prism branch June 10, 2026 02:16
CyberSys pushed a commit to SkyDev-Devision/PrismML_llama.cpp that referenced this pull request Jul 28, 2026
Q1_0/Q2_0 had no x86 vec_dot path (arch-fallback routed the generic
functions to a scalar loop). Add an AVX-512-VNNI/AVX-512VL fast path
guarded by __AVX512VNNI__ && __AVX512VL__, scalar fallback otherwise:

- helper ggml_hsum_i32_8_vnni to reduce _mm256_dpbusd_epi32 accumulators
- Q1_0: build a sign mask from the bit field, blend +qy/-qy, accumulate
  with dpbusd(ones, sel)
- Q2_0: vectorized 2-bit unpack (replicate-4 + 16-bit shift/mask + pack),
  then dpbusd(codes, qy) - dpbusd(ones, qy) = sum((code-1)*qy)

Q2_0 prefill ~3.9x / decode ~3.0x vs scalar on EPYC 9655; Q1_0 ~parity
(the +/-1 scalar loop already auto-vectorizes). Bit-exact vs scalar
(test-quantize-fns + standalone unit test); KL-divergence vs FP16
unchanged between scalar and VNNI builds.

Co-authored-by: Brian <brian@Brians-MacBook-Pro.local>
khosravipasha pushed a commit that referenced this pull request Aug 11, 2026
Port the fork's Q2_0 AVX-512-VNNI dot-product to the group-128 type (142).
Wired ONLY to Q2_0_g128 (fork format); upstream's Q2_0 g64 stays scalar (a g64
VNNI dot would be an upstream contribution — tracked separately). Generic scalar
kept as the fallback via arch-fallback. Verified: test-quantize-fns q2_0_g128
PASS, CPU MUL_MAT q2_0_g128 45 OK / 0 FAIL (VNNI == reference).

Original fork work by Brian / THT / bri-prism (#37, AVX-VNNI fast path).
Co-authored-by: THT <tht@prismml.com>
Co-authored-by: bri-prism <bri-prism@users.noreply.github.com>
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.

3 participants