Skip to content

Vector Legalization lowering pass - #8629

Open
mcourteaux wants to merge 73 commits into
halide:mainfrom
mcourteaux:fix-vulkan-interleave
Open

Vector Legalization lowering pass#8629
mcourteaux wants to merge 73 commits into
halide:mainfrom
mcourteaux:fix-vulkan-interleave

Conversation

@mcourteaux

@mcourteaux mcourteaux commented May 20, 2025

Copy link
Copy Markdown
Contributor

⭐ 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 as internal_errors:
    1. VectorReduce with output lanes > 1.
    2. Reinterpret with input/output having different number of bits per element.
  • This lowering pass was stress tested by running all tests in the test suite with artificial vector lane limit of 4 (this artificial limit is disabled again in this PR).
  • The test error/metal_vector_too_large is converted into correctness/metal_long_vectors as this is now supported.

🧹 Cleanup errors: no newline needed for HeapPrinter, and helper macro vk_report_error to print error codes. This trailing newline is pretty much everywhere in the codebase with a 50% probability for internal_assert and internal_error. This could use a more broad cleanup.


Fixes #8628 (see for details): use OpVectorShuffle instead of OpCompositeInsert.

@mcourteaux mcourteaux added code_cleanup No functional changes. Reformatting, reorganizing, or refactoring existing code. gpu labels May 20, 2025
Comment thread src/runtime/opencl.cpp
Comment thread src/CodeGen_Vulkan_Dev.cpp
Comment thread src/runtime/vulkan_internal.h
@mcourteaux

mcourteaux commented May 20, 2025

Copy link
Copy Markdown
Contributor Author

Regarding the build failure: some simplification rules in Simplify_Shuffle.cpp rewrite shuffles and thereby exceed the maximal vector length on the GPU backend (triggered by my randomized shuffle indices test). I hate this, as my PR was supposed to be simple, and now I'm uncovering more bugs than I signed up for 😝

I see two possible solutions:

  1. Limit the rules to never exceed the maximal vector length per backend. I don't like this for several reasons:
    • These simplification rules are embedded in the general simplify() call, which can happen at any point, and is not yet aware of selected GPU APIs.
    • This makes the simplification rules restricted, and might limit finding better simplifications if the internal rewriting logic cannot exceed some arbitrary vector size.
  2. The GPU backends need to handle arbitrary vector size expressions, just like the LLVM backend does. You can .vectorize(x, 17) and it just works. This might not be easy to achieve, but I feel like this is the better solution, as it keeps the simplifier as is, and brings the scheduling possibilities in line with the LLVM-based codegen backends. This might be one shared lowering pass for GPU backends with a max-vector-size parameter that rewrites these expressions again. Not sure if that's easy to do...

Opinions @abadams @zvookin ?

@abadams

abadams commented May 20, 2025

Copy link
Copy Markdown
Member

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.

@mcourteaux

mcourteaux commented May 22, 2025

Copy link
Copy Markdown
Contributor Author

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?

@mcourteaux mcourteaux added skip_buildbots Do not run buildbots on this PR. Must add before opening PR as we scan labels immediately. and removed skip_buildbots Do not run buildbots on this PR. Must add before opening PR as we scan labels immediately. labels May 22, 2025
@mcourteaux

Copy link
Copy Markdown
Contributor Author

Aaarrrghghhhh llvm/llvm-project@735209c

The following Auto-Upgrade rules are used to maintain compatibility with
IR using the legacy intrinsics:

  • llvm.nvvm.barrier0 --> llvm.nvvm.barrier.cta.sync.aligned.all(0)

Clearly doesn't work for us here... 😢

@alexreinking alexreinking changed the title Fix Vulkan interleave two vectors bug. Fixes #8628. Fix Vulkan interleave two vectors bug May 22, 2025
@alexreinking

Copy link
Copy Markdown
Member

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.

@mcourteaux

Copy link
Copy Markdown
Contributor Author

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 t340.odd_lanes. The bug in this instance was due to recursing too deeply:

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 t101.even_lanes, it realizes that the resulting type has 1 lane (i.e., scalar). But then it recurses into t100, and incorrectly assumed t100 was of the same type as t101.

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:

Halide/src/Deinterleave.cpp

Lines 389 to 403 in 85a3b07

// If this is extracting a single lane, try to recursively deinterleave rather
// than leaving behind a shuffle.
if (indices.size() == 1) {
int index = indices.front();
for (const auto &i : op->vectors) {
if (index < i.type().lanes()) {
ScopedValue<int> lane(starting_lane, index);
return mutate(i);
}
index -= i.type().lanes();
}
internal_error << "extract_lane index out of bounds: " << Expr(op) << " " << index << "\n";
}
return Shuffle::make(op->vectors, indices);

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.

@mcourteaux
mcourteaux force-pushed the fix-vulkan-interleave branch from 3b6f14d to cf6312e Compare May 24, 2025 13:06
@mcourteaux mcourteaux changed the title Fix Vulkan interleave two vectors bug Fix Vulkan interleave two vectors bug + Vector Legalization lowering pass May 27, 2025
@mcourteaux mcourteaux added enhancement New user-visible features or improvements to existing features. release_notes For changes that may warrant a note in README for official releases. labels May 27, 2025
@mcourteaux

Copy link
Copy Markdown
Contributor Author

@derek-gerstmann already reviewed the Vulkan interleave codegen. That part didn't change. This involved the changes in:

  • CodeGen_Vulkan_Dev.cpp
  • vulkan_internal.cpp
  • vulkan_resources.cpp

I worked together with @abadams on a Shuffle simplification bug in Simplify_Shuffle.cpp, so that part is already "reviewed" (pair programmed, you could say).

It'd be great if @abadams could look at the following for review:

  • LegalizeVectors.cpp
  • The small change in CSE.cpp to not lift Calls if they are not pure (with the exception for CallType::Halide, which can be lifted, according to a test case).
  • My bugfix in Deinterleave.cpp

Comment thread src/LegalizeVectors.cpp Outdated
return name + ".lanes_" + std::to_string(lane_start) + "_" + std::to_string(lane_start + lane_count - 1);
}

Expr simplify_shuffle(const Shuffle *op) {

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.

Why is this here rather than in the simplifier?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll try this tomorrow. It indeed is late.

Comment thread src/LegalizeVectors.cpp
Comment thread src/LegalizeVectors.cpp Outdated
// 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) {

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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@alexreinking Maybe you can take a look at what I did for the tracing events. You're currently very into these.

Comment thread src/LegalizeVectors.cpp Outdated
}
};

class ExtractLanes : public IRMutator {

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.

How is this different to the deinterleave function in Deinterleave.cpp? Should they be unified?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?

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.

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.

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.

I'm still not sure why this can't use the existing Deinterleaver

Comment thread src/Simplify_Let.cpp Outdated
@stevesuzuki-arm

Copy link
Copy Markdown
Contributor

@alexreinking @stevesuzuki-arm Can you check if the failures on ARM are related to the work done in #8898 ? As in: does that PR fix the case below?

With #8898, this fail is no longer reproduced with seed=11290674455725750672 .
But with seed=10736607054766902027, LLVM crash happens: llvm/llvm-project#187557

@mcourteaux
mcourteaux force-pushed the fix-vulkan-interleave branch from 4e3750b to 08ee445 Compare March 21, 2026 23:13
mcourteaux added a commit that referenced this pull request Mar 22, 2026
… of Broadcast(Vector).

fix(simplifier): Cherry-picked fix by @abadams in #8629.

Co-authored-by: Andrew Adams <andrew.b.adams@gmail.com>
Co-authored-by: Gemini 3.1 Pro <gemini@aistudio.google.com>
@abadams

abadams commented Apr 6, 2026

Copy link
Copy Markdown
Member

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.

@abadams

abadams commented Apr 6, 2026

Copy link
Copy Markdown
Member

Oh, oops, that's fixed in the other PR. Hopefully this didn't make too much of a merge conflict.

@mcourteaux mcourteaux changed the title Fix Vulkan interleave two vectors bug + Vector Legalization lowering pass Vector Legalization lowering pass Apr 10, 2026
mcourteaux and others added 5 commits August 21, 2026 11:00
# 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

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.29506% with 215 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.05%. Comparing base (5c21c82) to head (34d94db).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/Deinterleave.cpp 68.32% 67 Missing and 41 partials ⚠️
src/LegalizeVectors.cpp 71.00% 43 Missing and 46 partials ⚠️
src/Simplify_Shuffle.cpp 87.05% 4 Missing and 7 partials ⚠️
src/IR.cpp 0.00% 0 Missing and 2 partials ⚠️
src/CodeGen_ARM.cpp 0.00% 0 Missing and 1 partial ⚠️
src/CodeGen_Hexagon.cpp 0.00% 0 Missing and 1 partial ⚠️
src/CodeGen_LLVM.cpp 50.00% 0 Missing and 1 partial ⚠️
src/Lower.cpp 66.66% 0 Missing and 1 partial ⚠️
src/Simplify_Let.cpp 80.00% 0 Missing and 1 partial ⚠️
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.
📢 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.

mcourteaux and others added 5 commits August 21, 2026 23:58
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code_cleanup No functional changes. Reformatting, reorganizing, or refactoring existing code. dev_meeting Topic to be discussed at the next dev meeting enhancement New user-visible features or improvements to existing features. gpu release_notes For changes that may warrant a note in README for official releases.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vulkan shuffle bug: invalid SPIRV.

5 participants