Vector Legalization lowering pass - #8629
Conversation
|
Regarding the build failure: some simplification rules in I see two possible solutions:
|
|
I think the gpu backends need to handle arbitrary vector sizes. The rest of the compiler is free to make vectors of any size. A shared legalization pass for non-llvm backends that maps from Halide IR to Halide IR with narrower vectors might work, but seems a little tricky to get right for things like vectorreduce nodes. The other approach would be just changing how all ops are printed to handle small bundles of values, but this seems even nastier. Basically I agree with your option 2. |
|
I'd consider this to be greatly out of scope of this bugfix PR. I guess I'll skip that test for now and make an issue? Looking at the IR that triggers the error: let t397 = ramp(.thread_id_x + g$8.s0.x.v17.base.s, g$8.extent.0, 4)
let t398 = concat_vectors(f0$8[t397], f1$8[t397])
let t404 = extract_element(t398, 1)
let t405 = extract_element(t398, 0)which came from this: g$8(g$8.s0.x) = let t374 = slice_vectors(
concat_vectors(f0$8(g$8.s0.x, 0), f0$8(g$8.s0.x, 1), f0$8(g$8.s0.x, 2), f0$8(g$8.s0.x, 3)),
concat_vectors(f1$8(g$8.s0.x, 4), f1$8(g$8.s0.x, 5), f1$8(g$8.s0.x, 6), f1$8(g$8.s0.x, 7)),
1, -1, 2)
in (let t375 = (t374*t374)
in (extract_element(t375, 0) + extract_element(t375, 1))
)It seems that the simplifier rules have done a good job simplifying it, but there is some simplifications missing, or the simplifier rules have gotten stuck in a local minimum. The two ramped loads of size 4 get concatenated into a vector of size 8, to then just take elements 0 and 1 out of it. Very inefficient, compared to just doing two loads (or one ramped load). Of course, this is due to the unnatural way of constructing it with all these explicit shuffles in the test, but perhaps, having Halide simplify this further might be achievable for this PR? @abadams Any ideas on improving the codegen for this? |
|
Aaarrrghghhhh llvm/llvm-project@735209c
Clearly doesn't work for us here... 😢 |
|
n.b. - the "fixes #NN" magic belongs on a single line (one per line if multiple) in either the PR description or the final commit description (in the GitHub interface, which by default concatenates all the intermediate commit messages). It only clutters the PR title. |
b90ae86 to
3b6f14d
Compare
|
Specifically asking for review from @abadams as I uncovered another bug, but this time in the Deinterleaver transformation. This transformation is implemented as a GraphIRMutator, but is not supposed to recurse fully, because it's goal is to produce lane-extracted Exprs from other Exprs, such as let t99 = f0[ramp(0, 1, 8)]
let t100 = shuffle(t99, 0, 1, 2, 3)
let t101 = shuffle(t100, 0, 1)When the deinterleaver extracts So, in my opinion, I don't think this Deinterleaver should ever recurse into shuffle arguments. But as I didn't write this transformation, nor do I know fully what it's purpose is, I am hesitant just deleting the recursion there: Lines 389 to 403 in 85a3b07 I don't understand what the purpose of is of trying to extract odd and even lanes from a shuffle argument if the shuffle is actually just an element extraction. |
3b6f14d to
cf6312e
Compare
|
@derek-gerstmann already reviewed the Vulkan interleave codegen. That part didn't change. This involved the changes in:
I worked together with @abadams on a Shuffle simplification bug in It'd be great if @abadams could look at the following for review:
|
| return name + ".lanes_" + std::to_string(lane_start) + "_" + std::to_string(lane_start + lane_count - 1); | ||
| } | ||
|
|
||
| Expr simplify_shuffle(const Shuffle *op) { |
There was a problem hiding this comment.
Why is this here rather than in the simplifier?
There was a problem hiding this comment.
Because I can't call the simplifier on the shuffle, and expect it to only touch the shuffle. I can only do simplify(...) which runs ALL of the simplifier logic. It's a bit pitty/unintuitive that Simplify_Shuffle.cpp is not accessible as is.
Also, I wasn't too sure I could add that to the general simplifier code either. I could try to merge the two procedures. Perhaps other places benefit from these simplifier rules too then.
There was a problem hiding this comment.
I'll try this tomorrow. It indeed is late.
| // user_error << "Cannot legalize vectors when tracing is enabled."; | ||
| auto event = as_const_int(op->args[6]); | ||
| internal_assert(event); | ||
| if (*event == halide_trace_load || *event == halide_trace_store) { |
There was a problem hiding this comment.
I'm not sure it's a good idea to preserve only trace loads and trace stores, because those are supposed to be nested in other tracing events. Or is the idea that those other events won't see this mutator, because they're scalar?
There was a problem hiding this comment.
The test suite didn't show these to be nested anywhere. They are surrounded by other trace events, such as begin and end of a Func. AFAIK, they weren't nested. To be transparent: I have never ever used the tracing features. I was just looking at IR before and after legalization, to make sure it all seemed reasonable.
There was a problem hiding this comment.
Sorry, by nested I meant they should execute after a begin_realization event (or whatever it's called), and before an end_realization, so it would be bad to drop those outer events.
There was a problem hiding this comment.
IIRC, I think they never are processed here. The begin_realization and end_realization trace calls are never involved in vectorized expressions. So this ExtractLanes mutator will never be ran on those IR nodes. Perhaps I should turn this into an internal_assert() to validate my idea.
There was a problem hiding this comment.
@alexreinking Maybe you can take a look at what I did for the tracing events. You're currently very into these.
| } | ||
| }; | ||
|
|
||
| class ExtractLanes : public IRMutator { |
There was a problem hiding this comment.
How is this different to the deinterleave function in Deinterleave.cpp? Should they be unified?
There was a problem hiding this comment.
Hmm, perhaps. I think I didn't understand what Deinterleave was doing. And I'm not too sure I do now. Deinterleaver and Interleaver are doing a weird dance together which I didn't understand either.
There was a problem hiding this comment.
I'm comparing Deinterleaver and ExtractLanes. Can you have a look at their Load visitors? The alignment gets dropped if the starting lane is not 0. I don't understand why we wouldn't simply update the alignment, like I did in the ExtractLanes version. Is this an oversight in the Deinterleaver, or am I not understanding the rationale behind dropping it?
There was a problem hiding this comment.
The alignment is the alignment of the first lane. I think your logic is only correct if the load is of a ramp with stride 1. If it's some gather of some complex expression, that's not the right way to update it. In the cases we can safely update it, the simplifier can reinfer it very easily, so I thought it best to just leave it to the next simplifier pass.
There was a problem hiding this comment.
I'm still not sure why this can't use the existing Deinterleaver
With #8898, this fail is no longer reproduced with seed=11290674455725750672 . |
Co-authored-by: Gemini 3.1 Pro <gemini@aistudio.google.com>
4e3750b to
08ee445
Compare
|
I think the correctness_simplify failure is legit, but I think it's the expected output that is wrong (i.e. main), not the behavior in this PR. |
|
Oh, oops, that's fixed in the other PR. Hopefully this didn't make too much of a merge conflict. |
# Conflicts: # src/Deinterleave.cpp # src/Simplify_Exprs.cpp # src/Simplify_Shuffle.cpp # src/VectorizeLoops.cpp # test/correctness/CMakeLists.txt # test/correctness/simplify.cpp # test/error/CMakeLists.txt
…interleave. - LegalizeVectors.cpp: add missing Store::make is_streaming arg, and handle the new DeviceAPI::SMEStreaming case (treated like Host: no vector-lane cap, LLVM handles legalization). - fuzz_extract_lanes.cpp: add missing Store::make is_streaming arg, and use Type::to_abi() to construct a Runtime::Buffer<>, since Type no longer implicitly converts to halide_type_t. - deinterleave_vector.cpp: this main-ported test used extract_even_lanes()/extract_odd_lanes(), which this branch replaced with the more general extract_lanes(). Rewrite it in terms of extract_lanes() with equivalent parameters. - test/error/CMakeLists.txt: drop metal_vector_too_large.cpp, a test for a hard error this branch's vector legalization pass makes unnecessary (wide vectors get legalized instead of erroring); this branch had already deleted the underlying test file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Main added a thread_id argument to the Call::trace intrinsic (2e2336b, "Add tracing and event support for parallel tasks") between args[7] (parent_id) and what used to be args[8] (idx), shifting idx/size/tag each one slot to the right. Deinterleave.cpp::extract_lanes_trace still read the old, now off-by-one layout, so idx was misread as thread_id, size was misread as idx, and the trailing string tag was misread as an integer, tripping `internal_assert(size == type_lanes * num_vecs)` (or worse) whenever a legalized/bundled vector's trace call got deinterleaved. Reproduced via HL_FORCE_VECTOR_LEGALIZATION=4 on correctness_specialize, correctness_vectorize_guard_with_if, and correctness_compute_with, all of which hit this assert. Fixes all three (compute_with fully passes; the other two still fail on unrelated, pre-existing "unexpected IR" op-count checks that are a separate, expected side effect of forcing legalization somewhere it doesn't normally run). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AMX tile ops (tile_matmul, tile_load, tile_zero, tile_store, added by ExtractTileOperations.cpp) are opaque, fixed-shape x86 hardware tile instructions, not per-lane-parallel vector ops. Their "lanes" (e.g. a 256-lane int32 accumulator, 1024-lane int8 operand tiles) don't correspond to independently sliceable SIMD lanes the way an ordinary vector op's do, so: - LiftExceedingVectors::visit(Call) asserted that every extractable arg shared the call's own lane count, which tile_matmul's mixed scalar/tile/accumulator args violate by design. The assert didn't actually constrain the mutation logic below it (mutate() handles arbitrary-width args fine), so it's simply removed. - LegalizeVectors::visit(Store) tried to split a store of a wide tile_matmul result into legal-width chunks by calling extract_lanes() on the same atomic call repeatedly, which doesn't make sense for a single hardware instruction that must compute its whole tile at once, and crashed downstream in Deinterleave's ExtractLanes. Stores of these atomic tile calls are now left unsplit. Reproduced via HL_FORCE_VECTOR_LEGALIZATION=4 on correctness_tiled_matmul, which now passes (including value comparisons, not just avoiding the crash). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Simplify::visit(const Shuffle *) has an optimization that turns a strided slice of a scalar-based Ramp into a smaller Ramp directly, but built it via Ramp::make(..., op->indices.size()) without checking that a single-lane slice degenerates to a scalar. Ramp::make asserts lanes > 1, so a 1-lane slice (op->indices.size() == 1) crashed with "Ramp of lanes <= 1". This is a latent simplifier bug (Ramp::make has required lanes > 1 since it was written), not something introduced by vector legalization, but forcing legalization onto ordinary Host loops via HL_FORCE_VECTOR_LEGALIZATION=4 produces single-lane slice Shuffles far more often than normal codegen ever does, which is how correctness_fuzz_schedule (block for halide#8038) tripped it. Reproduced via HL_FORCE_VECTOR_LEGALIZATION=4 on correctness_fuzz_schedule, which now passes. Verified no regressions in correctness_simplify, correctness_deinterleave_vector, correctness_deinterleave4. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #8629 +/- ##
==========================================
- Coverage 70.08% 70.05% -0.03%
==========================================
Files 260 261 +1
Lines 79287 79900 +613
Branches 19327 19506 +179
==========================================
+ Hits 55569 55975 +406
- Misses 17923 18006 +83
- Partials 5795 5919 +124 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…les. HL_FORCE_VECTOR_LEGALIZATION only makes the pass do anything on GPU device loops in normal use, so CI configurations without Vulkan/Metal/ D3D12Compute hardware get essentially no coverage of LegalizeVectors.cpp even though we've now found and fixed three real bugs in it by manually setting the env var. This test closes that CI gap permanently: it sets HL_FORCE_VECTOR_LEGALIZATION itself, in a loop over several (including deliberately awkward, non-power-of-two) maximum lane counts, and runs a battery of 26 small pipelines under each. Each case defines the same pipeline twice -- once unscheduled as a reference, once with an explicit vectorizing schedule that legalization then rewrites -- and compares the results for exact equality. Coverage spans interleaving (select-based channel interleave, a genuine channel-interleaved output store with custom buffer strides, multi-way select), deinterleaving (strided loads, nested upsampling), shuffles (concat, strided slice, reversal), horizontal/partial VectorReduce, a 2D stencil blur, cast/widen/reinterpret chains, predicated stores under multiple tail strategies, boundary conditions, nested split/ vectorize/unroll schedules, explicit odd vectorize widths, 2D tiling, a gather-like dynamic lookup, tuple-valued outputs, and traced stores (exercising the Call::trace argument layout fixed in 9c602ec). Verified this test actually catches regressions, not just runs green: temporarily reverted the Simplify_Shuffle.cpp fix from 203664d and confirmed a new case here (complex_nested_split_fuse_vectorize, adapted from the historical fuzz_schedule.cpp crash that fix addressed) reproduces the exact same crash before the fix and passes after it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test/fuzz/random_expr_generator.h's RandomExpressionGenerator was originally derived from this test's own bespoke random-expression generator (same "cover all Deinterleaver visit methods" design, identical Shuffle interleave/concat/slice construction), but the two had since diverged, with the shared generator ending up strictly richer: on top of everything this test already generated (Broadcast, Ramp, Cast, Reinterpret, the three Shuffle forms, VectorReduce, Add/Sub/Min/Max/absd), it adds Mul/Div/Mod/abs, bitwise ops, and boolean ops. Move test/correctness/fuzz_extract_lanes.cpp to test/fuzz/extract_lanes.cpp, drive it off RandomExpressionGenerator and the shared FuzzingContext/FUZZ_TEST harness (gaining libFuzzer compatibility and a standard seed-driven CLI for free), while keeping this test's own execution-based verification methodology unchanged: JIT-compile via the same custom lowering pass, realize, and compare extract_lanes()'s output against the corresponding lanes of the original expression. No other test/fuzz file executes generated code this way (they're all symbolic), so this is a new kind of coverage for that directory, not a replacement of an existing one. Reusing the richer generator surfaced two real gaps, both fixed here: - The custom InjectExpr lowering pass silently assumed a strict 1:1 correspondence between the expression array and unrolled per-row Store nodes; a mismatch there would previously manifest as a confusing downstream value mismatch instead of a clear failure. Added an explicit count check so any future violation of that assumption fails loudly at the source instead. - The richer generator's Mul can compound across recursion depth to overflow a signed integer type even with small leaves and free variables. Signed overflow is documented, LLVM-nsw-exploited undefined behavior in Halide (unlike unsigned overflow, which wraps deterministically), so such expressions have no well-defined value to compare against and were producing nondeterministic "mismatches" across runs (confirmed uninitialized-memory reads via valgrind) that had nothing to do with extract_lanes() itself. Added might_overflow_signed_int(), which uses Halide's own bounds inference (find_constant_bounds) to conservatively detect and skip such expressions given the fuzzed variables' known range -- this doesn't touch the shared generator, so it can't affect the other, purely-symbolic fuzz tests that already handle this differently (e.g. simplify.cpp's safe_simplify catches InternalError around compile-time constant folding, which never executes generated code in the first place). Verified stability with a 2000-run stress pass (0 mismatches in ~1500 completed iterations before the time budget) and a valgrind pass (0 errors). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two independent issues reported from ARM CI (both arm-64 and arm-32): 1. arm-32 (all LLVM versions): case_reinterpret_cast produced a wrong value (0 instead of 34) specifically at x=0, and specifically for every legalization width except 1. The test's bit pattern at x=0 (17) reinterprets to a denormal float; ARM NEON's floating-point unit flushes denormals to zero per the architecture spec, which is why plain scalar codegen (width 1, no NEON) was unaffected while every NEON-vectorized width was. This was a portability bug in the test itself, not a reinterpret/legalization bug: OR in the bit pattern for 1.0f so the value is always a normal float regardless of x, sidestepping architecture-defined denormal behavior entirely rather than trying to work around it. 2. arm-64 with SVE2, on LLVM main and LLVM 21 (not LLVM 22): forcing legalization to narrow, non-native-SVE lane counts across this test's variety of schedules hits real crashes in LLVM's AArch64 SVE backend, both immediately following a "Vectorization factor is not suitable ... Disabling SVE" warning -- i.e. in SVE's own fallback path, not something Halide controls. Skip the test under SVE2 broadly (matching this repo's existing convention for known LLVM backend limitations, e.g. predicated_store_load.cpp) rather than chasing an exact LLVM version cutoff that isn't even monotonic here (21 and main both fail; only 22 in between passes) and may shift again as LLVM's SVE backend changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Local repro with real LLVM 21 (via compile_to_assembly against a synthetic arm-64-sve2-vector_bits_128 target) showed the crash does NOT go through Halide's "Disabling SVE" fallback path as the previous comment claimed -- check_feasible_vscale's lanes_used check passes for the crashing case. The actual trigger is LegalizeVectors reassembling an unevenly-divisible vector split (40 lanes capped at 16) into a non-power-of-2-width (10 lanes) intermediate slice, which LLVM's AArch64 SVE legalizer has no rule for.
⭐ New feature: Wrote a vector legalization lowering pass that comes near the end of the lowering. For loop Device APIs determine the maximal lane count for the expressions inside that for. Shuffles get lifted into their own variable, such that splitting into groups of lanes is done without recalculations.
There are a few unsupported scenarios, which are reported asinternal_errors:VectorReducewith output lanes > 1.Reinterpretwith input/output having different number of bits per element.error/metal_vector_too_largeis converted intocorrectness/metal_long_vectorsas this is now supported.🧹 Cleanup errors: no newline needed for
HeapPrinter, and helper macrovk_report_errorto print error codes. This trailing newline is pretty much everywhere in the codebase with a 50% probability forinternal_assertandinternal_error. This could use a more broad cleanup.Fixes #8628 (see for details): use
OpVectorShuffleinstead ofOpCompositeInsert.