Skip to content

Add VectorizeIn::SWAR and MemoryType::GPR: SWAR vectorization and general-purpose register storage - #9596

Open
alexreinking wants to merge 5 commits into
alexreinking/const-lut-shufflefrom
alexreinking/vectorize-gpr
Open

alexreinking wants to merge 5 commits into
alexreinking/const-lut-shufflefrom
alexreinking/vectorize-gpr

Conversation

@alexreinking

@alexreinking alexreinking commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Some short vectors of narrow integers are better computed in general-purpose registers than in SIMD registers: "SIMD within a register" (SWAR). A typical case is unpacking a few bytes with shifts and masks and handing the result to scalar code or to a few lanes of a vector. Doing that in Neon or SSE costs round-trips between the two register files and occupies the vector pipes. Hand-written kernels use SWAR for exactly this. For example, GGML decodes q4_K's twelve bytes of 6-bit scales and mins as three uint32_t words with shifts and masks.

Today Halide can't express this choice: vectorize always means SIMD instructions on SIMD registers. This PR separates the two halves of that choice:

  • Compute, a property of the vectorized loop: vectorize(var, factor, VectorizeIn::SWAR) computes the loop's lanes with SWAR arithmetic on packed integer words.
  • Storage, a property of the Func: store_in(MemoryType::GPR) keeps an allocation as integer words in general-purpose registers.

Either may be used without the other.

f.split(k, j, i, 4).unroll(j).vectorize(i, 4, VectorizeIn::SWAR);

SWAR is a loop type rather than a storage choice because it describes how a loop's vectors are computed, not where a Func lives. The same Func can have stages vectorized either way, and a SWAR loop's results can be handed straight to SIMD consumers. It also composes with SIMD vectorization. A SWAR loop may sit inside a SIMD loop, so that each SIMD lane holds a word of packed lanes ("SWAR within a vector register"). For example, this decodes four blocks' scales at once:

f.split(k, j, i, 4).unroll(j).vectorize(i, 4, VectorizeIn::SWAR).vectorize(b, 4);

The storage choice is needed separately because SWAR lowering is followed by simplification. If a stage only copies bytes (staging a block's index bytes for a table lookup, say), the simplifier folds it back to an ordinary vector load and store. LLVM then keeps the stage in a SIMD register and extracts each byte with a lane move. MemoryType::GPR makes codegen store each dense vector of the allocation as one integer, so LLVM keeps it in GPRs and the lookups extract their indices with shifts and masks. It does this whether the stage was vectorized as SIMD or SWAR.

idx.compute_at(g, b).store_in(MemoryType::GPR).split(k, ko, ki, 8).unroll(ko).vectorize(ki, 8);

What it does.

  • enum class VectorizeIn { SIMD, SWAR } and Func/Stage::vectorize(var, [factor,] VectorizeIn, [tail]) overloads. VectorizeIn::SIMD is the existing behavior.
  • ForType::VectorizedSWAR, with an is_vectorized() helper used at the sites that check for vectorized loops. Every earlier lowering pass sees an ordinary vectorized loop, so sliding window, storage folding, unrolling and bounds inference are untouched.
  • vectorize_loops vectorizes a SWAR loop as usual, then lowers it as SWAR (new VectorizeSWAR.{h,cpp}). The lanes of each vector are packed into one GPR (up to the target's word, 32 or 64 bits), and lane-wise ops become integer instructions:
    • dense loads and stores as whole words;
    • bitwise ops;
    • uniform and per-lane-constant shifts;
    • masked-carry adds and subtracts;
    • power-of-two multiplies, divides and mods;
    • blends for selects.
    • Predicated loads and stores in vector tails, and conditions that vary across the lanes at runtime, are done one lane at a time.
  • Consumers vectorized in SIMD registers read the result as usual. The hand-off is a register move, e.g. fmov s0, w8; mov.s v0[1], w9 on AArch64.
  • A SWAR loop nested inside a SIMD loop (in the same Func, or in a Func computed at the SIMD loop) is lowered first, to scalar code on words, which the SIMD loop then vectorizes like any other scalars. This leaves dense vector accesses in the SIMD loop's body. To support them, VectorSubs::widen now repeats each lane of a vector in place (matching how ramps, broadcasts, loads and reinterprets are widened), and accesses to allocations inside the SIMD loop keep ramp indices dense. The generated code for the four-block decode above is uint32x4 arithmetic on four 4-byte row loads.
  • vectorize_loops now takes the Target, which gives it the width of the general-purpose registers.
  • MemoryType::GPR: like Register, it needs a constant size and is never moved to the heap. In LLVM codegen, each dense vector load and store of it is a single integer load or store, bitcast to and from the vector. The C backend treats it like Register.

User errors:

  • an operation that can't be done lane-wise in a GPR (naming the Func, loop and expression, and suggesting VectorizeIn::SIMD);
  • a vector too wide for a GPR;
  • a SIMD-vectorized loop nested inside a SWAR one;
  • a SWAR loop on a GPU or other device API;
  • a dense vector access to a MemoryType::GPR allocation wider than a GPR;
  • MemoryType::GPR with a dynamic size, or inside a GPU loop.

Benchmarks. These were measured on an Apple M3 Max with one thread, using apps/ggml (#9511, higher in this stack).

q4_K's 6-bit scale decode (SWAR compute):

k.in(readers).compute_at(acc, ryo).split(k, kw, kb, 4).unroll(kw).vectorize(kb, 4, VectorizeIn::SWAR);
  • Decode: it compiles to pure w-register ldp/and/orr/stp code with no SIMD instructions.
  • q4_K vec_dot: 130 → 118 instructions per block (76 → 72 SIMD), 7–12% faster (1.04× GGML's Neon kernel).
  • q5_K: vec_dot is 4–7% faster, and its single-row mul_mat 4–9%.

Breaking changes

None. This adds two enum values (ForType::VectorizedSWAR and MemoryType::GPR), a new enum (VectorizeIn) and vectorize overloads, and makes vectorize_loops (internal) take a Target.

Checklist

  • Tests added or updated: correctness_vectorize_swar, correctness_vectorize_swar_errors
  • Documentation updated (VectorizeIn, MemoryType::GPR and Func::vectorize docs)
  • Python bindings updated (hl.VectorizeIn, hl.MemoryType.GPR, vectorize overloads)
  • Benchmarks are included here if the change is intended to affect performance.
  • Commits include AI attribution where applicable (see Code of Conduct)

@alexreinking
alexreinking added this pull request to stack #9597 October 23, 2026 00:08
@alexreinking
alexreinking removed this pull request from stack #9597 October 23, 2026 00:38
@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from 691a722 to 4a58e6d Compare October 23, 2026 00:38
@alexreinking
alexreinking added this pull request to stack #9598 October 23, 2026 00:38
@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch 2 times, most recently from 2eb73b5 to aafaba9 Compare October 23, 2026 05:16
@alexreinking
alexreinking removed this pull request from stack #9598 October 23, 2026 05:16
@alexreinking
alexreinking added this pull request to stack #9600 October 23, 2026 05:17
@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from aafaba9 to 824c5e8 Compare October 23, 2026 14:43
@alexreinking
alexreinking removed this pull request from stack #9600 October 23, 2026 14:43
@alexreinking
alexreinking added this pull request to stack #9605 October 23, 2026 14:43
@mcourteaux

Copy link
Copy Markdown
Contributor

Conclusion from dev-meeting: SWAR should be a loop-type, to be able to vectorize a SWAR loop. If it's triggered by a memory type, you say "put it in GPR", which is fundamentally not compatible with then vectorizing it into real vector registers.

@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.16129% with 216 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.96%. Comparing base (c7324f6) to head (824c5e8).

Files with missing lines Patch % Lines
src/VectorizeGPR.cpp 59.96% 139 Missing and 62 partials ⚠️
src/VectorizeLoops.cpp 91.80% 2 Missing and 3 partials ⚠️
src/BoundSmallAllocations.cpp 90.62% 2 Missing and 1 partial ⚠️
src/IRPrinter.cpp 0.00% 2 Missing and 1 partial ⚠️
src/ScheduleFunctions.cpp 70.00% 2 Missing and 1 partial ⚠️
src/CodeGen_C.cpp 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                        Coverage Diff                         @@
##           alexreinking/const-lut-shuffle    #9596      +/-   ##
==================================================================
+ Coverage                           72.76%   72.96%   +0.20%     
==================================================================
  Files                                 263      264       +1     
  Lines                               82458    83065     +607     
  Branches                            20164    20365     +201     
==================================================================
+ Hits                                60001    60609     +608     
- Misses                              16006    16076      +70     
+ Partials                             6451     6380      -71     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from 824c5e8 to 28e7b36 Compare October 24, 2026 15:43
@alexreinking
alexreinking removed this pull request from stack #9605 October 24, 2026 15:44
@alexreinking
alexreinking added this pull request to stack #9609 October 24, 2026 15:45
@alexreinking alexreinking added the skip_buildbots Do not run buildbots on this PR. Must add before opening PR as we scan labels immediately. label Oct 24, 2026
@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from 28e7b36 to ab9f8a0 Compare October 24, 2026 16:39
@alexreinking alexreinking changed the title Add MemoryType::GPR: vectorize in general-purpose registers (SWAR) Add vectorize(var, factor, VectorizeIn::GPR): SWAR vectorization in general-purpose registers Oct 24, 2026
@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from 673f5d0 to c14aa31 Compare October 25, 2026 21:05
@alexreinking
alexreinking removed this pull request from stack #9609 October 25, 2026 21:05
@alexreinking
alexreinking added this pull request to stack #9613 October 25, 2026 21:05
@alexreinking alexreinking changed the title Add vectorize(var, factor, VectorizeIn::GPR): SWAR vectorization in general-purpose registers Add VectorizeIn::SWAR and MemoryType::GPR: SWAR vectorization and general-purpose register storage Oct 25, 2026
Comment thread src/CodeGen_LLVM.cpp
return;
}

if (llvm::Type *word_t = gpr_word_type(op->name, op->type, op->index)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test case that reaches this? If a vectorized Func stored in a GPR is an error earlier in lowering, I'm not sure how we could reach here. Unaligned struct loads lowered as small vector loads?

@abadams

abadams commented Oct 26, 2026

Copy link
Copy Markdown
Member

Suggested renamings:

GPR -> Scalar (we can promise it's lowered as a scalar, but we can't promise where LLVM will place it)
VectorizeIn:: -> Vector::

@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from 2c854f1 to 9f8e29f Compare October 26, 2026 19:58
@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from 9f8e29f to 7cb2811 Compare October 26, 2026 23:05
@alexreinking
alexreinking removed this pull request from stack #9613 October 28, 2026 03:25
@alexreinking
alexreinking added this pull request to stack #9629 October 28, 2026 03:25
alexreinking and others added 5 commits November 2, 2026 18:48
A loop type for vectorized loops whose lanes are packed into a single
general-purpose integer register instead of a SIMD register. It is
parallel and synchronous like ForType::Vectorized, and is printed as
"vectorized_gpr". Add is_vectorized() to test for either kind of
vectorized loop, use it at the sites that check for vectorized loops, and
handle the new type in serialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
VectorizeIn chooses which class of registers holds the lanes of a
vectorized loop. VectorizeIn::SIMD is the existing behavior.
VectorizeIn::GPR ("SIMD within a register") packs the lanes into one
general-purpose integer register, as wide as the target's word.

Such loops are vectorized as usual, then lower_gpr_vectors rewrites every
vector in the loop body as an unsigned integer word with lane i in bits
[i * bits, (i + 1) * bits). Dense loads and stores become reinterprets of
the word, broadcasts and constant vectors become word constants, bitwise
ops apply to the word directly, shifts (and multiplication, division and
modulo by powers of two) are masked so that bits don't cross lanes, and
adds and subtracts keep carries out of the next lane with the usual SWAR
masking of the lane MSBs. Selects become blends with a lane mask.
Predicated loads and stores in vector tails, and conditions that vary
across the lanes at runtime, are done one lane at a time. Anything else,
e.g. a gather, a shift by a per-lane amount, or a multiply, is a
user_error naming the Func and the offending expression, and suggesting
VectorizeIn::SIMD. GPR vectorization is CPU-only: it's an error inside a
loop on a GPU or other device API. Nesting GPR and SIMD vectorized loops is
also an error.

Includes Python bindings for the enum and the new overloads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
correctness_vectorize_gpr checks GPR-vectorized Funcs bit-exactly against
the same algorithm without vectorization, and checks that no vector
values are left in their producers. It covers GGML's q4_K/q5_K 6-bit
scale and min decode with four lanes per 32-bit word and eight lanes per
64-bit word, the decode feeding a SIMD consumer (which must keep its
vectors), the decode in a wrapper computed at an RVar of a SIMD update,
GPR-vectorized update stages next to a SIMD sibling, lane-wise adds and
subtracts, shifts, bitwise ops, multiplication, division and modulo by
powers of two, signed lanes, uint16 lanes, ramps, selects, uniform
runtime shift amounts, and tail strategies.

correctness_vectorize_gpr_errors checks that unsupported operations,
vectors that don't fit in a register, unsupported operations in an
update stage, mixed nesting with SIMD
vectorization, and GPU loops are rejected with clear errors. It is skipped without exceptions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A loop vectorized in a general-purpose register may now sit inside a
loop vectorized in SIMD registers, either in the same Func or in a Func
computed at the SIMD loop. VectorizeLoops lowers the GPR loops within a
SIMD loop first, to scalar code on words of packed lanes, and the SIMD
loop then vectorizes those words like any other scalars. Each SIMD lane
holds one word ("SWAR within a vector register"), e.g. to decode the
6-bit scales of four q4_K blocks at once:

    f.split(k, j, i, 4).unroll(j).vectorize(i, 4, VectorizeIn::GPR).vectorize(b, 4);

The reverse nesting, a SIMD loop inside a GPR loop, remains an error.

The GPR lowering leaves dense vector loads and stores of the packed
lanes in the body handed to VectorSubs, which previously only ever saw
scalars. Two changes keep these correct and dense:

- VectorSubs::widen now repeats each lane of a vector in place, matching
  the layout used by Ramp, Broadcast, Load, and Reinterpret (one vector
  per original lane), instead of concatenating copies of the vector.
  This was unreachable before: VectorSubs never widened a vector that
  wasn't already a scalar.
- RewriteAccessToVectorAlloc keeps a ramp index a ramp, so accesses to
  an allocation inside the SIMD loop stay dense.

Tests: KScales decode across SIMD lanes and in a Func computed inside
a SIMD consumer, plus the whole elementwise suite inside a 4-wide SIMD
loop, checking that the producer computes on SIMD vectors of words
rather than of the narrow lanes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Vectorizing a loop "in a GPR" conflated two choices: how the lanes are
computed on (SIMD instructions, or SWAR arithmetic on packed integer
words) and where the result is stored (SIMD registers, or integer words
in general-purpose registers). Separate them:

- VectorizeIn::GPR is renamed VectorizeIn::SWAR (and ForType::
  VectorizedGPR to VectorizedSWAR, VectorizeGPR.{h,cpp} to
  VectorizeSWAR.{h,cpp}). It controls only the compute.

- MemoryType::GPR is a new CPU-only storage type: like Register, but
  each dense vector load or store of the allocation is done as a single
  integer word, so LLVM keeps the allocation in general-purpose
  registers and a consumer reading single elements extracts them with
  shifts and masks rather than lane moves out of a SIMD register. A
  dense access wider than the target's word size, a dynamic size, or
  use inside a GPU loop is a user error.

SWAR lowering is followed by simplification, which folds a pure-copy
stage back to an ordinary vector load and store, so before this there
was no way to ask for the words to survive to codegen.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@alexreinking
alexreinking removed this pull request from stack #9629 November 2, 2026 23:50
@alexreinking
alexreinking force-pushed the alexreinking/vectorize-gpr branch from 7cb2811 to cbafab5 Compare November 2, 2026 23:50
@alexreinking
alexreinking added this pull request to stack #9654 November 2, 2026 23:50

This branch has not been deployed

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

Labels

skip_buildbots Do not run buildbots on this PR. Must add before opening PR as we scan labels immediately.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants