Skip to content

Add vq_rows_p6 dispatch slot and AVX-512 VBMI kernel - #41

Merged
marcobambini merged 2 commits into
sqliteai:mainfrom
Ultron09:fix/vq4p-dispatch-x86
Aug 23, 2026
Merged

Add vq_rows_p6 dispatch slot and AVX-512 VBMI kernel#41
marcobambini merged 2 commits into
sqliteai:mainfrom
Ultron09:fix/vq4p-dispatch-x86

Conversation

@Ultron09

@Ultron09 Ultron09 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Move VQ4P apply out of an #ifdef\ in \src/model.c\ into the engine's kernel dispatch table (\waste_k.vq_rows_p6), matching the pattern of other arithmetic kernels in sqlite-vector/WARP.

Summary of Changes

  • Dispatch slot: Added \�oid (*vq_rows_p6)(int b, int e, void *arg);\ to \waste_kernels\ in \src/waste_backend.h.
  • CPU baseline: Exported portable C baseline \waste_vq_rows_p6\ in \src/model.c\ and registered it in \waste_kda_register_cpu\ in \src/kda.c.
  • NEON backend: Moved NEON vectorized implementation \�q_rows_p6_neon\ to \src/kda_neon.c\ and registered it in \waste_kda_register_neon.
  • CPUID feature detection: Added \WASTE_CPU_AVX512VBMI\ capability bit in \src/waste_backend.h\ and detection for CPUID leaf 7 subleaf 0 ECX bit 1 in \src/backend.c.
  • AVX-512 VBMI kernel: Implemented \�q_rows_p6_avx512\ in \src/simd_avx512.c\ using _mm512_permutexvar_epi8\ for 16-row vector unpack and 64-byte stage table lookups, registered in \waste_register_avx512.
  • Build flags: Added -mavx512vbmi\ to CFLAGS for \src/simd_avx512.o\ and \src/simd_avx512.pic.o\ in \Makefile.

Verification

  • Built full suite via \make && make test.
  • Ran \ ests/run.sh: all 42 checks passed (0 failed).
  • Verified bit-identity between \waste_vq_rows_p6\ (CPU baseline) and the dispatched kernel across randomized fixtures.

Closes #38

Move VQ4P apply out of an #ifdef in model.c into the engine's kernel
dispatch table (waste_k.vq_rows_p6), matching the pattern of other
arithmetic kernels in sqlite-vector/WARP.

- Add vq_rows_p6 function pointer slot to waste_kernels in src/waste_backend.h.
- Export portable C implementation waste_vq_rows_p6 in src/model.c and register it in waste_kda_register_cpu in src/kda.c.
- Move NEON vector implementation vq_rows_p6_neon to src/kda_neon.c and register it in waste_kda_register_neon.
- Add WASTE_CPU_AVX512VBMI CPUID detection in src/backend.c and src/waste_backend.h.
- Implement AVX-512 VBMI kernel vq_rows_p6_avx512 in src/simd_avx512.c using _mm512_permutexvar_epi8 for 16-row vector unpack and 64-byte stage table lookups.
- Add -mavx512vbmi to CFLAGS for src/simd_avx512.o and src/simd_avx512.pic.o in Makefile.

Resolves sqliteai#38
@Ultron09

Copy link
Copy Markdown
Contributor Author

cc @marcobambini — PR is ready for review. Implemented both the VQ4P dispatch slot refactor and the AVX-512 VBMI kernel with all tests passing and verified for bit-identity against the baseline.

@mfethe1

mfethe1 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Not a maintainer — but I have a native x86 box (Ryzen 7 3700X, Zen 2, Windows 11, MSYS2 UCRT64, gcc 16.2.0) stood up for #37/#38 work, so I built and tested this. Offering what the hardware can actually settle, and being explicit about what it can't.

Result

Builds clean, no new warnings. Full suite: 44 passed, 0 failed, 13 skipped — byte-for-byte the same board as main on this machine. No regression from the refactor.

Caveat that matters more than the result: on Zen 2 there is no AVX-512 and no VBMI, so I cannot execute vq_rows_p6_avx512. Everything below about that function is inspection, not measurement.

The dispatch refactor looks right

  • waste_kda_register_cpu is called unconditionally at src/backend.c:175 before any ISA backend, and each backend overwrites only what it implements, so waste_k.vq_rows_p6 can never be NULL. That was the failure mode I went looking for and it isn't there.
  • Hoisting vqp_arg and the P6_J* macros into simd.h is the right call. The macros' own comment says they exist so the paths "cannot drift apart"; with three implementations that is now load-bearing rather than aspirational.

The AVX-512 kernel, by inspection

I worked through the index math and it holds up:

_mm512_maskz_loadu_epi8(0x0000FFFFFFFFFFFF, …) zeroes bytes 48–63, so unpack_mask's byte-63 slot contributes 0 and each dword becomes a clean 24-bit b0 | b1<<8 | b2<<16. From there j0..j3 reduce exactly to P6_J0..P6_J3. Good.

Two things worth a comment in the source, because both look wrong on first read and aren't:

  1. _mm512_permutexvar_epi8(j0, T0) returns garbage in three of every four bytes. j0's dword lanes have zero upper bytes, so those select T0[0]. The following slli_epi32(…,24) / srai_epi32(…,24) discards them and sign-extends byte 0, which is what makes it correct. A reader who doesn't spot the shift pair will file a bug.

  2. The accumulator width differs from the other two pathsint32 here, int16 in NEON and scalar. It is still bit-identical: two's-complement addition mod 2³² reduced mod 2¹⁶ at the end equals accumulating mod 2¹⁶ throughout. (And moot in practice, since the existing comment bounds the sum at 16256.) Given this project's standard is bit-identity rather than closeness, that equivalence is worth stating where someone will find it.

The thing I'd want settled before merge

This kernel is never executed by make check.

tools/make_test_container.py has no --index-bits option and tests/run.sh never passes one, so the synthetic container is always index_bits=8 and vq_rows_p6 is unreachable in the suite. My 44/0/13 above — and any CI green on this PR — says nothing whatsoever about the code being added.

CLAUDE.md names this exact trap:

the same applies to --index-bits 6, which exercises a different kernel and a different record fmt

running the suite on one is how the Q4G load path stayed broken through green runs

I tried to close it myself and couldn't, which I think is the useful data point. convert.py needs --index-bits 6 --stages 4 --entries 64 together (good error message, incidentally). I converted a Kimi-Linear layer that way, but --layers 1 writes a manifest with one entry in layers against a config declaring 27, and the engine correctly refuses it as malformed container. So exercising this path at all requires a full conversion at --index-bits 6 — hours, and source weights on disk. That is a high bar for a reviewer, and it means this kernel would land untested by anything automated.

Cheapest fix I can see: teach make_test_container.py --index-bits 6 and add a VQ4P arm to the engine block in run.sh. Then the existing "SIMD backend matches the CPU baseline" check covers this for free on every platform. I'm happy to send that as a separate PR if it would help — it's orthogonal to your change and would give it a home to be verified in.

One hardening suggestion, tested rather than asserted

-mavx512vbmi is added to src/simd_avx512.o for the whole translation unit, and that TU also holds mvq_rows_f32_avx512 and lutb_range_avx512 — which are dispatched on AVX512F+BW alone. Skylake-SP and Cascade Lake have F+BW and no VBMI, so in principle the compiler could place a VBMI instruction in a function that runs there.

I checked instead of speculating. Compiled your simd_avx512.c with your exact flags and disassembled:

$ gcc -O2 -Wall -Wextra -mavx512f -mavx512bw -mavx512vbmi -c src/simd_avx512.c
$ objdump -d simd_avx512.o | grep -E 'vpermb|vpermi2b|vpermt2b'
      5  <vq_rows_p6_avx512>:

Five VBMI instructions, all inside the guarded kernel, none anywhere else. So this does not misbehave today with gcc 16.2.0 at -O2.

But that outcome is the compiler's choice, not the source's — a different gcc, -O3, or a later edit to the neighbouring kernels could change it, and the failure would be a SIGILL on exactly the used-Xeon-Scalable hardware this engine is most attractive on. __attribute__((target("avx512vbmi"))) on the single function, or moving it to its own TU, makes the guarantee structural. Your call whether that's worth it now or a TODO.

Small one

#if defined(__AVX512VBMI__) means a build that doesn't get the Makefile's flag silently omits the kernel and falls back to scalar rather than failing. Fine as long as the Makefile is the only build path, but it's a quiet downgrade if anyone ever compiles this TU directly.


Happy to re-run anything on this box, or to send the make_test_container.py --index-bits 6 change if you'd like this to have a test to sit behind. Nice work on the refactor — the dispatch-slot half is clearly right and useful independently of the VBMI kernel, which is what #38 said too.

@Ultron09

Copy link
Copy Markdown
Contributor Author

@mfethe1 Thank you for the outstanding review, hardware verification on Ryzen 7 / Zen 2, and disassembly analysis!

  1. Target Attribute & In-Source Documentation Added:

    • Pushed commit c3e9ae3 adding __attribute__((target("avx512vbmi"))) directly to vq_rows_p6_avx512 so the compiler's instruction generation for VBMI is structurally isolated to this kernel and cannot leak into neighboring F+BW functions in the translation unit on Cascade Lake / Skylake-SP.
    • Added clear explanatory comments in simd_avx512.c detailing why the _mm512_permutexvar_epi8 + slli_epi32/srai_epi32 shift pair cleanly discards unselected upper bytes and why 32-bit addition truncated mod $2^{16}$ is bit-identical to 16-bit accumulation.
  2. Test Coverage & Container Support:

    • That point about make_test_container.py and tests/run.sh lacking --index-bits 6 is spot on. If you'd like to open a PR adding --index-bits 6 to make_test_container.py and wiring up the VQ4P engine check in run.sh, that would be hugely appreciated and give automated CI coverage for all three backends (AVX-512, NEON, and scalar baseline)!

Thanks again for the thorough review and testing!

@mfethe1

mfethe1 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Opened the follow-up: #48--index-bits 6 in make_test_container.py, a VQ4P engine section in run.sh, and test_container reading the WQ_VQ4P fmt byte. Default containers stay byte-identical (verified by sha256, so the rotary fixture's container_sha256 gate is untouched); the new arm ran clean at exactly baseline+4 on both macOS arm64 (NEON) and Windows/Zen 2 (scalar), no prior PASS moved. Once this kernel's AVX-512 path lands, it gets the same CI coverage on Cascade Lake / Skylake-SP for free.

@marcobambini

Copy link
Copy Markdown
Member

Merged — and this repository has a VQ4P container, so the half that could be
verified here was verified rather than reasoned about.

The dispatch-slot refactor is bit-identical on ARM against a real
index_bits: 6 container.
Same 8 ids, same forward pass, merge-base against
this branch:

kimi-linear-vq4p (VQ4P, 4 stages / 64 entries, index_bits 6)   identical
kimi-linear-4x64 (CTRL, 4 stages / 64 entries, index_bits 8)   identical

That is the claim the PR makes about step 1 — the NEON body moved into a slot,
unchanged — and it now has a container behind it instead of an argument. The
VBMI kernel is a different matter: no machine here can execute an AVX-512
instruction, so it lands under the same standing caveat docs/BACKENDS.md
records for the rest of that file.

@mfethe1's review is why this went in as it stands. The objdump check instead
of a speculation about instruction placement, and the __attribute__((target))
that followed it, turn "gcc 16.2.0 at -O2 happens to do the right thing" into a
structural guarantee — and the failure it prevents is a SIGILL on Skylake-SP
and Cascade Lake, which is exactly the used-Xeon hardware this engine is most
attractive on.

The test gap is real, and it is worse than "no coverage"

You are both right that tools/make_test_container.py has no --index-bits,
so the synthetic container is always 8-bit, vq_rows_p6 is unreachable from
the suite, and a green CI run on this PR says nothing about the code it adds.
That should be closed and neither of you should be blocked on the other: send
the make_test_container.py --index-bits 6 change whenever suits, it is
orthogonal and welcome.

But adding a VQ4P arm to "SIMD backend matches the CPU baseline" will not
work, and I can now say that from this side rather than quoting #36 at you.
Same two containers, dispatched path against WASTE_BACKEND=cpu:

container index bits NEON vs CPU
CTRL 8 max diff 4.53e-06, 0/163840 over 1e-3
VQ4P 6 max diff 0.414783, 162233/163840

@GTSUltear measured 1.22853 and 163297/163840 on AVX2 with the same
single-variable control. Two different ISAs, same structure, same
conclusion
— so the divergence is not an x86 kernel defect, it is the int8
LUT quantization being discontinuous, and that check cannot pass on an
index_bits: 6 container on any platform.

Which means the coverage this kernel needs is bit-identity of the int8 table,
or of the routes, against the scalar path
— not a tolerance on logits, since
a genuinely broken kernel and a single entry rounding the other way both land
around 1. LEARNED.md §43 arriving somewhere concrete, as #37 put it. Worth
settling before vpermi2b gets a second kernel beside it.

Thanks to you both — one for the refactor and the target attribute, one for the
review that found the thing the CI could not.

@marcobambini
marcobambini merged commit 619b666 into sqliteai:main Aug 23, 2026
9 checks passed
mfethe1 added a commit to mfethe1/warp that referenced this pull request Aug 23, 2026
…--index-bits 6 and a VQ4P arm in run.sh

The VQ4P apply — and the AVX-512 kernel PR sqliteai#41 adds to it — was unreachable
from make check: the synthetic container is always index_bits 8, and a real
--index-bits 6 conversion needs the source weights and hours. So a green run
asserted nothing about it, on any platform.

--index-bits 6 writes the one combination the engine accepts at 6 (4 stages,
64 entries, packed 4x6 into 3 bytes, same packing as convert.py's
block_indices_packed). The default path emits byte-identical containers:
manifest.json gains no key (the engine reads absence as 8), and the rotary
fixture's container_sha256 gate still passes.

The VQ4P arm in run.sh runs the same four self-comparisons the engine block
runs on VQ3R: records through the C structs (test_container now accepts the
WQ_VQ4P fmt byte), chunked == token-at-a-time, SIMD vs CPU baseline, expert
cache bit-identity. It builds in milliseconds, so it runs everywhere CI does
— ARM takes the NEON vq_rows_p6 path, x86-64 without VBMI takes the scalar
one, and PR sqliteai#41's kernel gets the same checks for free once it lands.
marcobambini added a commit that referenced this pull request Aug 24, 2026
Almost none of this is the engine. It is Windows, real containers, and the four
feasibility gates that were still open, and most of it was found by people
running hardware this project does not own. GATES.md has no open gate for the
first time, and two of the three that closed this cycle closed against the
change they were proposing.

No ABI move: src/waste.h changed only in a comment this cycle, unlike 0.6.8.
waste_kernels gained vq_rows_p6 but that lives in src/waste_backend.h and is
not public.

Added: the VQ4P apply behind a dispatch slot with an AVX-512 VBMI kernel beside
it (#41), bit-identical on ARM against a real index_bits 6 container and still
never executed on any machine here; WASTE_DUMP_SCORES (#45) and
WASTE_CCR_LAMBDA (#46); the working set printed by `waste plan`, which was
quoted by the line below it and never shown; and a test for the fp8 block-scale
mapping whose only stub had replaced it with the identity (#40).

Fixed: the five gaps of #36 and the two that survived the first attempt —
VirtualLock bounded by the minimum working set rather than the maximum, so
WASTE_MLOCK wired nothing on Windows twice over; diskbench truncating its
offset through a 32-bit off_t at its own default file size, where the quiet
half read the wrong place successfully and kept the working set inside an SSD's
SLC cache; container JSON through Python text mode, which made the same
conversion produce byte-different containers on Windows; a missing cmp
reporting `expert cache changes results` on a clean checkout (#42); plus #35,
#30, and the CLI help that said one thread per core when it is one per logical
CPU (#44).

Recorded as measured and not adopted, which is the half of this file that is
easy to drop: the budget resolver's quantum stands after Gate 7 (§63);
cache-conditional routing stays a knob because it clears README's KL bar and
not its continuation bar; the thread default did not move despite 16 threads
measuring 1.6x below the plateau; no budget policy fixes two K3 opens on 64 GB
(§64, #31 closed, #49 opened for the residue); and VQ4P is not a throughput
upgrade over VQ3R on a GB10 despite complete and exact CUDA coverage (§62).

53 passed, 0 failed, 6 skipped against Kimi-Linear and K3.
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.

VQ4P's apply is an #ifdef in model.c, not a dispatch slot — which is why x86 has no kernel

3 participants