Repository navigation
Add VectorizeIn::SWAR and MemoryType::GPR: SWAR vectorization and general-purpose register storage - #9596
Add VectorizeIn::SWAR and MemoryType::GPR: SWAR vectorization and general-purpose register storage#9596alexreinking wants to merge 5 commits into
Conversation
691a722 to
4a58e6d
Compare
2eb73b5 to
aafaba9
Compare
aafaba9 to
824c5e8
Compare
|
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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
824c5e8 to
28e7b36
Compare
28e7b36 to
ab9f8a0
Compare
673f5d0 to
c14aa31
Compare
| return; | ||
| } | ||
|
|
||
| if (llvm::Type *word_t = gpr_word_type(op->name, op->type, op->index)) { |
There was a problem hiding this comment.
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?
|
Suggested renamings: GPR -> Scalar (we can promise it's lowered as a scalar, but we can't promise where LLVM will place it) |
2c854f1 to
9f8e29f
Compare
9f8e29f to
7cb2811
Compare
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>
7cb2811 to
cbafab5
Compare
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_twords with shifts and masks.Today Halide can't express this choice:
vectorizealways means SIMD instructions on SIMD registers. This PR separates the two halves of that choice:vectorize(var, factor, VectorizeIn::SWAR)computes the loop's lanes with SWAR arithmetic on packed integer words.store_in(MemoryType::GPR)keeps an allocation as integer words in general-purpose registers.Either may be used without the other.
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:
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::GPRmakes 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.What it does.
enum class VectorizeIn { SIMD, SWAR }andFunc/Stage::vectorize(var, [factor,] VectorizeIn, [tail])overloads.VectorizeIn::SIMDis the existing behavior.ForType::VectorizedSWAR, with anis_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_loopsvectorizes a SWAR loop as usual, then lowers it as SWAR (newVectorizeSWAR.{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:fmov s0, w8; mov.s v0[1], w9on AArch64.VectorSubs::widennow 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 isuint32x4arithmetic on four 4-byte row loads.vectorize_loopsnow takes theTarget, which gives it the width of the general-purpose registers.MemoryType::GPR: likeRegister, 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 likeRegister.User errors:
VectorizeIn::SIMD);MemoryType::GPRallocation wider than a GPR;MemoryType::GPRwith 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):
w-registerldp/and/orr/stpcode with no SIMD instructions.Breaking changes
None. This adds two enum values (
ForType::VectorizedSWARandMemoryType::GPR), a new enum (VectorizeIn) andvectorizeoverloads, and makesvectorize_loops(internal) take aTarget.Checklist
correctness_vectorize_swar,correctness_vectorize_swar_errorsVectorizeIn,MemoryType::GPRandFunc::vectorizedocs)hl.VectorizeIn,hl.MemoryType.GPR,vectorizeoverloads)