Skip to content

Fix unsound icmp fold for loads from fresh allocations in jl-inst-simplify#2919

Merged
wsmoses merged 16 commits into
mainfrom
vc/jlinstsimplify-alloc-load
Jul 6, 2026
Merged

Fix unsound icmp fold for loads from fresh allocations in jl-inst-simplify#2919
wsmoses merged 16 commits into
mainfrom
vc/jlinstsimplify-alloc-load

Conversation

@vchuravy

@vchuravy vchuravy commented Jul 5, 2026

Copy link
Copy Markdown
Member

The bug

arePointersGuaranteedNoAlias (Utils.cpp) treats a pointer loaded from a fresh allocation as guaranteed-unequal to another pointer, scanning for clobbering writes only between the load and the compare. That interval is wrong: the loaded SSA value is fixed at load time, so the scan proves nothing. If a store into the allocation happens before the load, the loaded value is an arbitrary captured pointer — including the compared-against one. jl-inst-simplify then folds the icmp to a constant.

Minimal shape (folds to false before this patch, even though it is always true):

%box = call noalias ptr addrspace(10) @julia.gc_alloc_obj(...)
%slot = addrspacecast ptr addrspace(10) %box to ptr addrspace(11)
store ptr addrspace(10) %v, ptr addrspace(11) %slot
%contents = load ptr addrspace(10), ptr addrspace(11) %slot
%cmp = icmp eq ptr addrspace(10) %contents, %v   ; always true, folded to false

Julia hits this whenever a value round-trips through a Core.Box (captured + reassigned local): x !== nothing folds to true for x === nothing.

How it surfaced

Enzyme.jl's CUDA CI ("Reverse Kernel" testset) fails with

AssertionError: codegen_i=2 > length(codegen_types)=1 ... source_typ=CuDeviceVector{Float32, 1}

CUDACore's augmented_primal rule calls Enzyme.tape_type at runtime; nested_codegen! compiles the rule's whole call graph — including Enzyme.Compiler.lower_convention — into the differentiated module, where this fold rewrites returnRoots = returnRoots !== nothing into an unconditional store jl_true (returnRoots is a Core.Box there). classify_arguments then sees has_returnroots=true for a Nothing-returning kernel and asserts.

The fix

Scan for clobbers between the allocation and the load instead. If nothing wrote to the loaded location in that interval, the loaded value is undef or an allocator-installed fresh pointer, and the fold remains legal; otherwise bail. The previously unguarded isa<Argument>(end) fast path had the same hole and now sits behind the same check.

Testing

  • New lit test enzyme/test/Enzyme/JLSimplify/boxedroundtrip.ll: the boxed roundtrip compare must survive; the no-store variant must still fold. Fails against the unpatched pass, passes with this patch.
  • All existing JLSimplify tests produce bit-identical output with and without the patch (verified with LLVM 21 opt); in particular loads.ll's intended fresh-array-data fold still fires.
  • End-to-end with Enzyme.jl v0.13.173 on Julia 1.12.6 via deps/build_local.jl: a CPU-only reproducer (custom rule shape with a Core.Boxed Union{Nothing,Type} local) errors on the released jll and computes the correct gradient with this patch; the CUDA "Reverse Kernel" assertion above no longer triggers.

🤖 Generated with Claude Code

…plify

arePointersGuaranteedNoAlias treated a pointer loaded from a fresh
allocation as unequal to any other pointer, scanning for clobbering
writes only between the load and the compare. That interval is wrong:
the loaded SSA value is fixed at load time, and a store INTO the
allocation before the load (e.g. julia lowering a captured, reassigned
local into a Core.Box) makes the loaded value an arbitrary captured
pointer. jl-inst-simplify then folded `x !== nothing` to true for
x === nothing whenever x round-tripped through a Core.Box, e.g. in
Enzyme.jl's own lower_convention when compiled into a differentiated
module via nested_codegen! of a custom rule (EnzymeAD/Enzyme.jl CUDA
"Reverse Kernel" CI failure:
  AssertionError: codegen_i=2 > length(codegen_types)=1 ...).

Scan for clobbers between the allocation and the load instead: if
nothing wrote to the loaded location in that interval, the loaded value
is undef or an allocator-installed fresh pointer and the fold is legal;
otherwise bail. All existing JLSimplify tests produce identical output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vchuravy vchuravy requested a review from wsmoses July 5, 2026 19:51
@wsmoses wsmoses merged commit c61e011 into main Jul 6, 2026
33 checks passed
@wsmoses wsmoses deleted the vc/jlinstsimplify-alloc-load branch July 6, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants