Skip to content

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

Description

@marcobambini

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

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

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

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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions