Skip to content

cpu kernels: gathered decode on the cores where it is faster, AVX-VNNI rows, IQ3_S one-token kernel - #863

Closed
Hardin22 wants to merge 3 commits into
Niko1221:mainfrom
Hardin22:pr/cpu-iq-kernels
Closed

Hardin22 wants to merge 3 commits into
Niko1221:mainfrom
Hardin22:pr/cpu-iq-kernels

Conversation

@Hardin22

@Hardin22 Hardin22 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Three changes to the AVX2 expert kernels. Each one gives the same bits as the scalar path, except (c), which follows the #152 rule.

  1. Gathered grid decode, chosen per core (STRATA_IQ256_GATHER: unset = auto, 0 = off, 1 = every core).
    • Fmt32<118>/<121>/<122> become the only gathered decodes. They replace the in-decode branch from dd24a55.
    • They also gather the IQ3_XXS sign masks and the IQ2_S grid.
    • Auto gathers on Intel P-cores of Alder Lake / Sapphire Rapids and newer:
      • GenuineIntel with AVX-VNNI is the generation marker. That excludes Haswell/Broadwell and the Downfall-microcode generations.
      • The E-core-only parts are excluded by model.
      • On a hybrid CPU, only a thread on a core of CPUID 1Ah type 40h.
      • AMD keeps the scalar decode.
  2. AVX-VNNI: vpdpwssd / vpdpbusd forms of the i-quant, IQ4_NL and Q2_0 rows. Integer sums, so the same bits.
    • Gated by cpu_avxvnni_ok(), which lives in expert_layout.cpp (not compiled for AVX2) and respects STRATA_FORCE_ISA and STRATA_ISA_FLOOR. STRATA_NO_AVXVNNI=1 turns it off.
    • On GCC/Clang only the VNNI copies get target("avxvnni"), and only if CMake finds the intrinsics. No file gets -mavxvnni.
  3. IQ3_S takes the multi-token kernel for a lone token wherever its grid is gathered. Native CPU expert results depend on verifier width (plain/MTP divergence) #152 sent one token to ggml's dot because the kernel was no faster there; with the gather it is much faster. This changes the rounding of a lone token's IQ3_S gate/up rows to the group's. On these CPUs that makes IQ3_S independent of the window size. STRATA_IQ_MT_MIN keeps its meaning.

Test: iq_avx2_parity, a CPU-only CTest. Random blocks for IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S and IQ4_XS gate/up rows and IQ4_NL / Q2_0 down rows. Every variant at 1..8 tokens must match the scalar variant bit for bit; the scalar rows are checked against ggml's vec_dot (Q2_0 against a double reference).

  • 0 failures on an i9-14900KF, also with STRATA_IQ256_GATHER=0, STRATA_IQ_MT_MIN=1, STRATA_NO_AVXVNNI=1 and STRATA_FORCE_ISA=avx2, and pinned to a P-core and to an E-core.
  • With STRATA_FORCE_ISA=avx it skips cleanly.
  • A deliberately broken gather was caught.

Measured (iq_avx2_parity --bench --dispatch, i9-14900KF, one thread, more expert blobs than L3, median of 5)

Engine path, ms per expert (gate/up + down) at 1-4 tokens. Upstream main is shown at its default (STRATA_IQ256_GATHER unset = 0) and with =1:

format (gate/up / down) core tokens main main, gather=1 this PR (auto) vs main
IQ3_S / IQ4_NL P 1 0.431 0.405 0.257 −40%
IQ3_S / IQ4_NL P 2 0.493 0.380 0.312 −37%
IQ3_S / IQ4_NL P 4 0.642 0.535 0.455 −29%
IQ3_XXS / IQ4_NL P 1 0.271 0.272 0.276 +2% (noise)
IQ3_XXS / IQ4_NL P 2 0.362 0.325 0.291 −20%
IQ2_S / Q2_0 P 2 0.377 0.380 0.298 −21%
IQ2_XXS / Q2_0 P 4 0.419 0.405 0.369 −12%
IQ3_S / IQ4_NL E 2 1.060 1.472 0.971 −8%
IQ3_XXS / IQ4_NL E 4 1.196 1.741 1.046 −13%
IQ2_S / Q2_0 E 4 1.252 1.264 1.138 −9%

The full grid has 5 formats × 1-4 tokens × P/E core. Nowhere is it slower than main beyond noise. On E-cores, gather=1 is 40-75% slower, which is why auto leaves them alone.

End to end: Swift 1.5 IQ3_XXS on an RTX 5080 alone (most of the window is CPU experts) showed no difference beyond noise. That is expected: its CPU experts mostly see one token, and IQ3_XXS at one token is unchanged. I don't have an IQ3_S model here, so the end-to-end effect on IQ3_S, where one token goes from 0.43 to 0.26 ms per expert, is not measured. A report from an IQ3_S rig would settle it.

Not tested

  • The GCC/Clang path: no GCC or Clang on this PC. The target("avxvnni") copies and the CMake check have never been compiled. If the check fails, the build simply leaves the VNNI copies out.
  • AMD CPUs: auto keeps them on today's path by construction.

…re it is faster (STRATA_IQ256_GATHER auto)

dd24a55 reads the IQ3 grid words with one gather under STRATA_IQ256_GATHER=1, on every core, and keeps it off by
default because AVX2 gather throughput depends on the core. This makes the gathered decode its own Fmt32 (118, 121,
122), chosen once per call instead of a branch in every decode, and takes it further:

- IQ3_XXS: the four even-sign masks by one 64-bit gather as well;
- IQ3_S: the 9th index bit by one sllv and one and; the sign bytes by one broadcast and one shuffle;
- IQ2_S: the four 10-bit grid indices built in a register and read by one 64-bit gather, the signs as IQ3_S's.

Each is its format's scalar decode bit for bit: the same words land in the same lanes. IQ2_XXS measured 4% slower
gathered and IQ2_XS has its own sign decode, so both keep the scalar one.

STRATA_IQ256_GATHER: unset = auto, 0 = the scalar decode everywhere (as before), 1 = the gathered one everywhere.
Auto gathers on a thread whose core is an Intel performance core of Alder Lake / Sapphire Rapids or newer:

- GenuineIntel with AVX-VNNI (CPUID 7.1:EAX[4]) as the generation marker. That leaves out Haswell/Broadwell (slow
  gathers) and Skylake through Ice Lake / Tiger Lake (gathers slowed by the Downfall/GDS microcode);
- not the E-core-only parts, which are not hybrid: Alder Lake-N / Twin Lake, Grand Ridge, Sierra Forest and
  Clearwater Forest, by model;
- on a hybrid CPU only a thread on a P-core (CPUID 1Ah core type 40h): the E-cores gather slower than they assemble.

AMD keeps the scalar decode (Zen 2/3 gather slowly; Zen 4/5 take the AVX-512 kernels). STRATA_FORCE_ISA turns auto
off. The probes are in expert_layout.cpp, which is not compiled for AVX2, and the core type is asked once per thread
(the pool pins its workers).

Measured on an i9-14900KF, gate/up rows of one expert at three tokens on one P-core, scalar -> gathered: IQ3_S 0.40
-> 0.27 ms, IQ2_S 0.31 -> 0.27, IQ3_XXS 0.26 -> 0.24. On its E-cores the gathered decode was slower.

iq_avx2.cpp has no runtime initializer again (016ea2e): the switch and STRATA_IQ_PREFETCH are read on first use.

iq_avx2_parity, a CPU-only CTest: random blocks (every grid index and sign pattern occurs) for IQ2_XXS, IQ2_XS, IQ2_S,
IQ3_XXS, IQ3_S and IQ4_XS gate/up rows and IQ4_NL / Q2_0 down rows; every variant, 1..8 tokens, each token's rows bit
for bit the scalar variant's rows for that token alone; the scalar rows against ggml-cpu's vec_dot (Q2_0: a double
reference). --bench: ms per expert on one thread, each variant, ggml's per-token dot and the engine's dispatch, over
more expert blobs than an L3 holds.
…s (cpu_avxvnni_ok)

Alder Lake, Sapphire Rapids and later have AVX-VNNI on every core: vpdpwssd does the madd and the add of an i-quant
row in one uop, and vpdpbusd the maddubs + madd(ones) of the IQ4_NL and Q2_0 down rows. The sums are integer, so the
results are bit for bit those of the AVX2 forms: vpdpwssd wraps like madd + add, and the maddubs it replaces in the
down rows cannot saturate (IQ4_NL |w| <= 127, Q2_0 codes 0..3).

The row kernels move to iq_avx2_rows.inl and q2_avx2_rows.inl, which iq_avx2.cpp and q2_avx2.cpp include twice: once
as they were (namespace plain) and once with AVX-VNNI (namespace vnni). In the second copy every function is declared
with target("avxvnni") under GCC/Clang, so the intrinsics compile there and nowhere else: neither file gets
-mavxvnni, which would let the compiler use the extension in the plain path. MSVC takes the intrinsics under
/arch:AVX2. CMake checks that the compiler has them (GCC 11, Clang 12) and otherwise builds without the copies
(STRATA_AVXVNNI=0).

cpu_avxvnni_ok() in expert_layout.cpp, which is not compiled for AVX2: cpu_avx2_ok() and CPUID 7.1:EAX[4], off under
STRATA_FORCE_ISA (a CPU that stops at AVX2), off in a STRATA_ISA_FLOOR build, and STRATA_NO_AVXVNNI=1 turns it off.
The VNNI copies are reached only through the AVX2 kernels' entry points, which the engine already calls only behind
cpu_avx2_ok().

Measured on an i9-14900KF on top of the gathered decode: another 4-10% on a P-core's i-quant gate/up rows, 15-25% on
an E-core's Q2_0 down rows.

iq_avx2_parity runs the VNNI variants as well (iq4nl256_down_rows_v, q2_0_gguf_rows_multi_avx2_v): every combination
bit for bit against the scalar one-token rows, 1..8 tokens.
…its grid is gathered

Niko1221#152's rule gives a lone token's gate/up rows to ggml's dot, because the multi-token kernels were no faster at one
token. With the gathered decode that no longer holds for IQ3_S on the Intel cores that gather it: an expert's gate/up
rows for one token on an i9-14900KF P-core take 0.34 ms with ggml's dot and 0.19 ms with the AVX-2 kernel. And in
decode most CPU experts serve one token of the window.

So on a CPU without AVX-512 where cpu_gather_fast() holds (Intel, Alder Lake or later, see the gather commit) and
STRATA_IQ256_GATHER is not 0, IQ3_S takes the multi-token kernel from one token on. This only pays together with the
gathered decode: on the same CPU's E-cores, which keep the scalar decode, it is 0.71 -> 0.68 ms, about even. That is
why it follows the gather's machine-wide rule and not the per-thread one. Measured in the engine with Swift 1.5
IQ3_XXS (whose IQ3_S layers this reaches): the CPU expert time per verify window -4%.

This changes results. A lone token's IQ3_S gate/up rows now round like the multi-token kernel's (the order of the
float additions), no longer like ggml's dot. They are the same rows the token gets in a group, so on these CPUs IQ3_S
experts no longer depend on how many drafts a window held (Niko1221#152), and every core rounds them alike. Down rows and
the other formats keep the default rule. STRATA_IQ_MT_MIN set (2 is the old default) keeps its rule for every format.

The decision is native_gu_mt_min(): every probe is in expert_layout.cpp and cpu_avx2_ok() comes first. Nothing
compiled for AVX2 runs before that check, including on an older-CPU build (STRATA_ISA_FLOOR) on an AVX-only CPU.
iq_avx2_parity checks native_gu_rows wherever native_gu_mt_min is 1: each token's rows must match alone and in groups
of 2..8. docs/DETAILS.md notes the new default next to STRATA_IQ_MT_MIN.
txiki739 added a commit to txiki739/Strata3090-5060TI that referenced this pull request Oct 5, 2026
…is tree)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
txiki739 added a commit to txiki739/Strata3090-5060TI that referenced this pull request Oct 5, 2026
Niko1221 added a commit that referenced this pull request Oct 6, 2026
…T1=1); the default keeps 0.1.39's rounding

Items 1-2 of #863 (gathered decode per core, AVX-VNNI rows) are bitwise and stay. Item 3 changes a lone token's IQ3_S
rounding on Intel hybrid CPUs, so it needs the switch. The #706 bit-plane path stays in front of the VNNI/plain rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Niko1221

Niko1221 commented Oct 6, 2026

Copy link
Copy Markdown
Owner

In 0.1.40: items 1 and 2 (gathered decode per core, AVX-VNNI rows) are in. Item 3 (IQ3_S one-token kernel) is opt-in as STRATA_IQ3S_MT1=1, since it changes a lone token's rounding. A GCC/Clang build log and an end-to-end IQ3_S result on an Intel hybrid CPU are still welcome.

@Niko1221 Niko1221 closed this Oct 6, 2026
Niko1221 added a commit that referenced this pull request Oct 6, 2026
…T1=1); the default keeps 0.1.39's rounding

Items 1-2 of #863 (gathered decode per core, AVX-VNNI rows) are bitwise and stay. Item 3 changes a lone token's IQ3_S
rounding on Intel hybrid CPUs, so it needs the switch. The #706 bit-plane path stays in front of the VNNI/plain rows.
fragtion added a commit to fragtion/StrataPool that referenced this pull request Oct 6, 2026
…/ IQ2_S decode on the cores where it is faster (STRATA_IQ256_GATHER auto)

(merged with Niko1221#851's q8k_quant_avx2 in the same files)
fragtion added a commit to fragtion/StrataPool that referenced this pull request Oct 6, 2026
Upstream merged Niko1221#863, Niko1221#732, Niko1221#869, Niko1221#931 and Niko1221#846 (our copies dropped) and took its own form of Niko1221#970 (Niko1221#804).
Kept: Niko1221#851, Niko1221#652, Niko1221#855, Niko1221#813, Niko1221#789. A pool turns off what it cannot run yet: --pipeline-windows, --batch-mtp,
--kv-grow, session files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
fragtion added a commit to fragtion/StrataPool that referenced this pull request Oct 6, 2026
…/ IQ2_S decode on the cores where it is faster (STRATA_IQ256_GATHER auto)

(merged with Niko1221#851's q8k_quant_avx2 in the same files)
txiki739 added a commit to txiki739/Strata3090-5060TI that referenced this pull request Oct 6, 2026
…iant knobs (opt-in)

Port of Niko1221#930 (sanastasiou) onto the gathered-variant layout of Niko1221#863: the scalar IQ3_S and IQ2_S decodes take
their grid indices from a constexpr high-bit table (one punpcklbw instead of a shift, mask and or per index),
their sign vectors from one broadcast load and one pshufb, and their (2s+1) scales from constexpr tables.
Bit-identical: iq_avx2_parity passes (every variant, 1..8 tokens, against ggml-cpu).

On a Ryzen 7 5700X (one thread, IQ3_S gate/up rows, ms an expert) the scalar decode goes 0.403 -> 0.278 at one
token, 0.393 -> 0.370 at three; the gathered decode (Fmt32<121>, unchanged) stays ahead at 2, 4 and 5 tokens.

Two opt-in knobs to measure the mix (unset: the behaviour so far):
- STRATA_IQ256_GATHER_NT=2,4,5: the gathered decode only for those token counts;
- STRATA_IQ_DOWN_MT_MIN: the IQ4_NL down rows' multi-token rule apart from STRATA_IQ_MT_MIN (ggml's one-token
  dot is faster for down on this CPU).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anon761 pushed a commit to anon761/Strata-upstream that referenced this pull request Oct 6, 2026
…T1=1); the default keeps 0.1.39's rounding

Items 1-2 of Niko1221#863 (gathered decode per core, AVX-VNNI rows) are bitwise and stay. Item 3 changes a lone token's IQ3_S
rounding on Intel hybrid CPUs, so it needs the switch. The Niko1221#706 bit-plane path stays in front of the VNNI/plain rows.
sskver pushed a commit to sskver/Strata that referenced this pull request Oct 7, 2026
…T1=1); the default keeps 0.1.39's rounding

Items 1-2 of Niko1221#863 (gathered decode per core, AVX-VNNI rows) are bitwise and stay. Item 3 changes a lone token's IQ3_S
rounding on Intel hybrid CPUs, so it needs the switch. The Niko1221#706 bit-plane path stays in front of the VNNI/plain rows.
eddoursul pushed a commit to eddoursul/Strata that referenced this pull request Oct 12, 2026
…is tree)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.

2 participants