vq_rows_p6 — the VQ4P apply — is vectorized behind an #ifdef in
src/model.c:2158 rather than behind the dispatch table, and that is the
structural reason x86 has no kernel for it rather than a narrow one.
#if (defined(__ARM_NEON) || defined(__aarch64__)) && !defined(WASTE_P6_SCALAR)
if (nr == VQ_TILE) {
/* four vqtbl4q_s8 per 16 rows over a vld3q_u8 deinterleave */
} else
#endif
{
/* one row at a time, four scalar table loads per row */
}
The two range kernels that carry the rest of the arithmetic — mvq_rows_f32
and lutb_range — moved behind waste_kernels on 2026-07-28 precisely so
that one binary adapts from CPUID and the ISA lives in its own translation
unit. This one did not move with them, so an x86 build takes the portable
else branch: T[j0] + T[en + j1] + T[2*en + j2] + T[3*en + j3], per row.
That is not a narrower kernel, it is the absence of one, on the same build
where VQ3R's table gets simd_avx512.c.
Successor to #32, which is closed: its documentation half landed in
48b8afa, and this is the
half that needs code.
Scope: who actually meets this
Narrower than #32 claimed, and worth stating before anyone prioritises it.
#32 said VQ4P is what convert.py produces by default. It is not —
--index-bits defaults to 8 and --stages to 3, so a default conversion is
VQ3R, and convert.py:903 refuses --index-bits 6 without
--stages 4 --entries 64. So this is not the path on every x86 build.
What makes it worth doing anyway is who the affected population is. That
shape is exactly the 64-entry crossover docs/LEARNED.md §50 lists as not
measured. So the people most likely to run VQ4P on x86 are the people trying
to answer that open question — and what they would measure is a scalar apply
against a vectorized one, then report an ISA difference as a table-size
result. A wrong number that looks like an answer costs more than a slow path
that announces itself.
Why a kernel is possible here and is not for VQ3R
§41 has the arithmetic. VQ3R's 256-entry stage table is 256 bytes — sixteen
vector registers of the thirty-two that exist — so it fits no byte-permute
primitive on any ISA, and blocking does not make it fit. VQ4P spends the same
24 bits per 8-weight vector at 4x64, and a 64-entry stage table is 64 bytes,
four registers, which is exactly what vqtbl4q_s8 addresses. The premise
that made the gather scalar is false for this format.
The x86 analogue is AVX-512 VBMI's vpermi2b: 128 bytes indexed across
two zmm registers, so 64 entries fit with room to spare. Zen 4 and later, Ice
Lake and later.
The work, in the order that makes it reviewable
-
Add the dispatch slot first, with the ARM body moved into it unchanged.
waste_kernels has no entry for this kernel; add one taking
(int b, int e, void *arg) like the other two, move the NEON body out of
model.c into its own translation unit, and register it from the ARM path.
This step should be bit-identical on ARM by construction and is worth
landing on its own — it is reviewable without an x86 machine, and it is
where a mistake would be cheapest to find.
It also improves the self-check. -DWASTE_P6_SCALAR is a compile-time
lever today; once the kernel is a slot, WASTE_BACKEND=cpu reaches the
portable path at runtime, which is the lever the existing suite check
already uses.
-
WASTE_CPU_AVX512VBMI does not exist yet. src/waste_backend.h:56-57
has AVX512F (bit 11) and AVX512BW (bit 12); VBMI is CPUID leaf 7
subleaf 0, ECX bit 1, and src/backend.c:123 is where its siblings are
detected. Gate on it specifically — AVX512F && AVX512BW at
backend.c:190 is not sufficient for vpermi2b.
-
Then the kernel, in src/simd_avx512.c, which already exists.
Acceptance
- Bit-identity against the portable path, not "close". §43 says why the
bar is that high: an int8 table makes the engine discontinuous, so an
approximate match is not a match. It is reachable — accumulation is int16
within a WASTE_VQ_LUT_BLK block and only folds to fp32 at the end, so
there is no float reordering for a different lane arrangement to expose.
The ARM path already claims this against -DWASTE_P6_SCALAR; x86 has to
clear the same bar against the same reference.
- The check that decides it is
tests/run.sh's SIMD backend matches the
CPU baseline, which runs WASTE_BACKEND=cpu against the dispatched path
and compares logits.
- It has to run on an
--index-bits 6 container. The suite's default
WASTE_REF_MODEL is a VQ3R conversion, which does not execute this kernel
at all; per CLAUDE.md, running the suite on the wrong container shape is
exactly how a load path once stayed broken through green runs. Build one
with convert.py --index-bits 6 --stages 4 --entries 64.
Unmeasured in both directions
Stated before anyone spends a weekend on it:
- The path may be bound by the deinterleave (
vld3q_u8 on ARM, three
interleaved index bytes per row) rather than by the lookup, in which case
the table primitive buys much less than it looks like it should.
- Cross-lane byte permutes are not uniformly cheap across x86
microarchitectures, and vpermi2b's cost differs between Ice Lake and
Zen 4/5.
This is a hypothesis with an obvious first experiment, not a plan.
Nobody here can execute it
This repository has no x86 machine that is not emulated, which is also why
docs/BACKENDS.md records AVX-512 as "compiled and dispatched, never
executed", and why the gap was found by reading rather than by measurement.
Whoever writes this is also the only person who can test it, so the
measurement matters more than the patch — and step 1 above is deliberately
carved out as the part that can be reviewed here.
Related: #11 (a GPU VQ4P implementation exists off-repo and found that
building the table dominated applying it — a caution about where the time
actually goes, on a different vehicle), and #36 (the suite's non-synthetic
path does not run in CI on any platform, which is the reason a container-shape
requirement in the acceptance criteria above cannot be enforced by CI today).
vq_rows_p6— the VQ4P apply — is vectorized behind an#ifdefinsrc/model.c:2158rather than behind the dispatch table, and that is thestructural reason x86 has no kernel for it rather than a narrow one.
The two range kernels that carry the rest of the arithmetic —
mvq_rows_f32and
lutb_range— moved behindwaste_kernelson 2026-07-28 precisely sothat one binary adapts from CPUID and the ISA lives in its own translation
unit. This one did not move with them, so an x86 build takes the portable
elsebranch:T[j0] + T[en + j1] + T[2*en + j2] + T[3*en + j3], per row.That is not a narrower kernel, it is the absence of one, on the same build
where VQ3R's table gets
simd_avx512.c.Successor to #32, which is closed: its documentation half landed in
48b8afa, and this is the
half that needs code.
Scope: who actually meets this
Narrower than #32 claimed, and worth stating before anyone prioritises it.
#32 said VQ4P is what
convert.pyproduces by default. It is not —--index-bitsdefaults to 8 and--stagesto 3, so a default conversion isVQ3R, and
convert.py:903refuses--index-bits 6without--stages 4 --entries 64. So this is not the path on every x86 build.What makes it worth doing anyway is who the affected population is. That
shape is exactly the 64-entry crossover
docs/LEARNED.md§50 lists as notmeasured. So the people most likely to run VQ4P on x86 are the people trying
to answer that open question — and what they would measure is a scalar apply
against a vectorized one, then report an ISA difference as a table-size
result. A wrong number that looks like an answer costs more than a slow path
that announces itself.
Why a kernel is possible here and is not for VQ3R
§41 has the arithmetic. VQ3R's 256-entry stage table is 256 bytes — sixteen
vector registers of the thirty-two that exist — so it fits no byte-permute
primitive on any ISA, and blocking does not make it fit. VQ4P spends the same
24 bits per 8-weight vector at 4x64, and a 64-entry stage table is 64 bytes,
four registers, which is exactly what
vqtbl4q_s8addresses. The premisethat made the gather scalar is false for this format.
The x86 analogue is AVX-512 VBMI's
vpermi2b: 128 bytes indexed acrosstwo zmm registers, so 64 entries fit with room to spare. Zen 4 and later, Ice
Lake and later.
The work, in the order that makes it reviewable
Add the dispatch slot first, with the ARM body moved into it unchanged.
waste_kernelshas no entry for this kernel; add one taking(int b, int e, void *arg)like the other two, move the NEON body out ofmodel.cinto its own translation unit, and register it from the ARM path.This step should be bit-identical on ARM by construction and is worth
landing on its own — it is reviewable without an x86 machine, and it is
where a mistake would be cheapest to find.
It also improves the self-check.
-DWASTE_P6_SCALARis a compile-timelever today; once the kernel is a slot,
WASTE_BACKEND=cpureaches theportable path at runtime, which is the lever the existing suite check
already uses.
WASTE_CPU_AVX512VBMIdoes not exist yet.src/waste_backend.h:56-57has
AVX512F(bit 11) andAVX512BW(bit 12); VBMI is CPUID leaf 7subleaf 0, ECX bit 1, and
src/backend.c:123is where its siblings aredetected. Gate on it specifically —
AVX512F && AVX512BWatbackend.c:190is not sufficient forvpermi2b.Then the kernel, in
src/simd_avx512.c, which already exists.Acceptance
bar is that high: an int8 table makes the engine discontinuous, so an
approximate match is not a match. It is reachable — accumulation is int16
within a
WASTE_VQ_LUT_BLKblock and only folds to fp32 at the end, sothere is no float reordering for a different lane arrangement to expose.
The ARM path already claims this against
-DWASTE_P6_SCALAR; x86 has toclear the same bar against the same reference.
tests/run.sh's SIMD backend matches theCPU baseline, which runs
WASTE_BACKEND=cpuagainst the dispatched pathand compares logits.
--index-bits 6container. The suite's defaultWASTE_REF_MODELis a VQ3R conversion, which does not execute this kernelat all; per
CLAUDE.md, running the suite on the wrong container shape isexactly how a load path once stayed broken through green runs. Build one
with
convert.py --index-bits 6 --stages 4 --entries 64.Unmeasured in both directions
Stated before anyone spends a weekend on it:
vld3q_u8on ARM, threeinterleaved index bytes per row) rather than by the lookup, in which case
the table primitive buys much less than it looks like it should.
microarchitectures, and
vpermi2b's cost differs between Ice Lake andZen 4/5.
This is a hypothesis with an obvious first experiment, not a plan.
Nobody here can execute it
This repository has no x86 machine that is not emulated, which is also why
docs/BACKENDS.mdrecords AVX-512 as "compiled and dispatched, neverexecuted", and why the gap was found by reading rather than by measurement.
Whoever writes this is also the only person who can test it, so the
measurement matters more than the patch — and step 1 above is deliberately
carved out as the part that can be reviewed here.
Related: #11 (a GPU VQ4P implementation exists off-repo and found that
building the table dominated applying it — a caution about where the time
actually goes, on a different vehicle), and #36 (the suite's non-synthetic
path does not run in CI on any platform, which is the reason a container-shape
requirement in the acceptance criteria above cannot be enforced by CI today).