Repository navigation
Lower small table lookups to tbl/pshufb/swizzle - #9581
alexreinking wants to merge 4 commits into
Conversation
|
@copilot Please fix the clang-tidy failure. |
Added |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## alexreinking/arm-field-shift #9581 +/- ##
================================================================
+ Coverage 73.13% 73.21% +0.08%
================================================================
Files 263 263
Lines 82586 82810 +224
Branches 20195 20272 +77
================================================================
+ Hits 60397 60633 +236
+ Misses 15894 15867 -27
- Partials 6295 6310 +15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| const string intrin = target.bits == 64 ? | ||
| "llvm.aarch64.neon.tbl" + std::to_string(regs) + (slice_lanes == 8 ? ".v8i8" : ".v16i8") : | ||
| "llvm.arm.neon.vtbl" + std::to_string(regs); | ||
| value = codegen_dynamic_shuffle(op, regs * reg_lanes, slice_lanes, [&](Value *table, Value *index) { |
There was a problem hiding this comment.
I think instead of the lambda arg and simple_call_intrin this should just use the vector slicing already done by call_intrin
| Expr index = mutate(op->index); | ||
| ConstantInterval ib = constant_integer_bounds(index, bounds); | ||
| if (ib.is_bounded() && ib.min < ib.max && ib.max - ib.min < max_n) { | ||
| // The shape of an embedded buffer is fixed at compile time (it |
There was a problem hiding this comment.
I think that this block of code should be abstracted (or just documented) as can_prove_index_span_safe_to_load(op, ConstantInterval), and should handle pipeline-internal fixed-size allocations in addition to embedded buffers.
| }; | ||
|
|
||
| class OptimizeSmallTableLookups : public IRMutator { | ||
| const std::function<int(const Type &)> &max_table_size; |
There was a problem hiding this comment.
It would be great if this could be a lowering pass. If it really does depend on internal codegen state, I think it would clearer if this was a CodeGen_LLVM * pointer that you call the virtual method on, and if that's the case it should live in CodeGen_Internal.cpp/h
2d1f0f9 to
64a0c90
Compare
64a0c90 to
7783cf3
Compare
7783cf3 to
ee89658
Compare
ee89658 to
25cbc80
Compare
25cbc80 to
c7324f6
Compare
Add a pass, optimize_small_table_lookups, run from CodeGen_LLVM::compile_func,
that turns vectorized lookups into small tables into
dynamic_shuffle(table, index, 0, n - 1) calls when the target says it can do
them profitably, via a new CodeGen_LLVM::max_dynamic_shuffle_table_size hook.
Two sources are recognized:
- mux(index, {c0, ..., cn-1}) with constant values. The table becomes a
constant vector; if the index isn't provably in range it is clamped to
n - 1 as an unsigned value, matching mux semantics.
- Loads from embedded Buffers whose index is provably within a window of at
most n elements inside the buffer's allocation. The table is a dense load of
that window. Buffer contents are not assumed to be constant (JIT passes
embedded buffers by reference), only their shape, which is asserted on
entry to the pipeline, so there is no padding or out-of-bounds read.
dynamic_shuffle is then lowered to:
- AArch64 NEON tbl1-tbl4 (tables up to 64 bytes), ARMv7 vtbl1-vtbl4 (up to
32 bytes).
- x86 pshufb (SSE4.1 targets; AVX2/AVX-512 forms with the table replicated per
128-bit lane) for tables up to 16 bytes, and AVX-512 VBMI vpermb for tables
up to 64 bytes.
- WebAssembly i8x16.swizzle for tables up to 16 bytes.
Other targets (including SVE, and Hexagon, which has its own
dynamic_shuffle lowering) keep their existing codegen. CodeGen_LLVM also gains
a generic (slow but correct) lowering of dynamic_shuffle.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: alexreinking <169273+alexreinking@users.noreply.github.com>
…nd-trip The round-trip JIT-compiles a deserialized copy of the pipeline, whose embedded buffers are copies, so updating the originals is not visible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move table lookup optimization into target-driven lowering, support fixed-size internal allocations, and use call_intrin vector slicing. Preserve C++ emission and extend regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
c7324f6 to
8441b83
Compare
Vectorized lookups into small tables currently compile poorly on CPUs: a vectorized
mux(idx, {c0, ..., cn-1})becomes a chain of n compares and selects, and a load from a smallBufferbecomes a gather (one scalar load per lane). Most CPUs have a register table-lookup instruction that does either in one instruction per vector: ARMtbl, x86pshufb/vpermb, WebAssemblyi8x16.swizzle. This matters for quantized-weight formats that decode 4-bit codes through a 16-entry int8 codebook (e.g. GGML's mxfp4 and iq4_nl), which GGML's NEON kernels decode with a singlevqtbl1q_s8per 16 codes.This PR adds a pass,
optimize_small_table_lookups(inOptimizeShuffles.{h,cpp}), that turns such lookups intodynamic_shuffle(table, index, 0, n - 1), and lowersdynamic_shuffleon ARM, x86 and WebAssembly.What is recognized.
mux(index, {c0, ..., cn-1})where every value is a constant. The table is a constant vector. Ifconstant_integer_boundscan't prove the index lies in[0, n - 1], it is clamped (as unsigned) ton - 1, which matchesmux's semantics: any out-of-range index, including a negative one, selects the last value.Loadfrom aBufferembedded in the pipeline (imagedefined, noParameter) whose index is proven to lie in a window of at most n elements inside the buffer's allocation. The table is a dense vector load of that window, and the index is rebased to it.Where. The pass runs at the start of
CodeGen_LLVM::compile_func, the same place #9087 runsoptimize_shufflesfor SVE2 and Hexagon runs its own. The query is a new virtual,int CodeGen_LLVM::max_dynamic_shuffle_table_size(const Type &elem) const: the largest table the target can profitably index, or 0 to disable. The default is 0, so every target without an override keeps today's code. That includes Hexagon, which has its owndynamic_shufflelowering, and SVE, which #9087 targets. So recognition is generic, and lowering is per target.Lowering.
tbl1–tbl4(8- or 16-lane results)vtbl1–vtbl4pshufbvpshufbymm/zmm, table replicated per 128-bit laneavx512_cannonlake)vpermbwasm_simd128i8x16.swizzleCodeGen_LLVM::codegen_dynamic_shuffle, does the slicing and padding.CodeGen_LLVMalso gets a generic, slow but correct lowering ofdynamic_shuffle: one variableextractelementper lane.Relationship to #9087. That draft PR generalizes
optimize_shuffles(the bounded indirect-load →dynamic_shufflepass) to SVE2 and lowersdynamic_shuffleto SVE2tbl. This PR is complementary and is designed so the two can converge:dynamic_shuffle(table, u8 index, 0, n - 1)form, and the backends also cast the index tou8in codegen as Support Call::dynamic_shuffle for LUT in SVE2 #9087 does.optimize_shuffles.CodeGen_ARM::max_dynamic_shuffle_table_sizereturns 0 under SVE (with a TODO), so Support Call::dynamic_shuffle for LUT in SVE2 #9087's SVE2 lowering can claim constant tables there too.Effect. In a Halide reimplementation of GGML's mxfp4 kernels (
apps/ggmlon a branch), the codes are now decoded throughmux(nibble, kvalues_mxfp4)instead of arithmetic. That now compiles to onetblper 16 codes, as in GGML's NEON code (vqtbl1q_s8).vec_dotinner loop, instructions per 2 blocks (64 codes)vec_dotspeed relative to GGML, K = 4096Nothing else in the app's kernels changed: rebuilding its ~340 AOT objects with this patch, but without the app change, gave byte-identical objects.
Tests
New tests:
correctness_small_table_lookup(JIT) covers:muxwith an in-range index, a let-bound index, an unboundedu8index, and a possibly negativei32index;minand a sub-range window, and two dimensions;Param), which must be left alone;It runs these for table sizes 2–65, vector widths 8/16/64,
int8/uint8, plusuint16/float, which are not transformed. It also checks the genericdynamic_shufflefallback.Targets: the host, plus host-without-NEON on ARM, plus SSE4.1-only and baseline on x86 hosts, plus no-SIMD on wasm.
simd_op_check_{arm,x86,wasm}:tblwith 1–4 table registers on arm-64, andvtbl.8with 1/2/4 registers on arm-32;pshufb,vpshufbymm/zmm, andvpermb;i8x16.swizzle, replacing a TODO.Commands run (macOS arm64 host, LLVM 22.1,
cmake --preset macOS):ctest -j 8 -R "^(correctness_(mux|gather|aligned_split_mux_phase|many_inlined_selects|multi_way_select|unroll_huge_mux|non_vector_aligned_embeded_buffer|vector_cast|vector_math|simd_op_check_.*|register_shuffle|hexagon_scatter|lazy_convolution|convolution|decompose_vector_shuffle|vector_shuffle|fallback_vscale_sve|sve_codegen_predicated|sme_streaming|small_table_lookup)|performance_clamped_vector_load|generator_aot_shuffler)$"HL_JIT_TARGET=wasm-32-wasmrt-wasm_simd128 ./test/correctness/correctness_small_table_lookup./test/correctness/correctness_simd_op_check_hvx,..._powerpc,..._riscvpre-commithooks on commit (clang-format, keep-sorted, codespell, CMake checks)Not run:
simd_op_check_x86checks the generated assembly only, and the correctness test's x86 target variants run only on x86 hosts.Fixes: no tracking issue.
Breaking changes
None. No public API changes. Generated code changes only on ARM (NEON), x86 (SSE4.1+) and WebAssembly (SIMD128), for vectorized 8-bit lookups into small constant tables and embedded buffers; results are unchanged.
Checklist
Internalpass and hook are documented in their headers.Co-authored-by: Copilottrailer.