Repository navigation
Replay the stores through a Julia array's data pointer when rematerializing it - #3375
Merged
Merged
Conversation
…lizing it A Julia array allocated in a function is rematerialized in the reverse pass: the allocation is redone and the stores into it are replayed. The use walk in computeForwardingProperties stops at loads, so the array's data pointer (an addrspace(13) pointer the allocator set up, loaded from the object) and everything through it were invisible: the data stores were never recorded as stores to replay, and the data loads never as loads that must be reloadable. The reverse pass then re-ran the data pointer load on the fresh object and read its unfilled buffer. This went unnoticed while the reads were cached by other means. A load through a pointer loaded from memory was cached whenever any write at all followed the call in the caller. Since #3325 only unknown writes count, so the callee reloads, and Turing's demo_dot_assume_observe_index model on Julia 1.10 silently returned a wrong gradient: sqmahal's `x .- μ` temporary was recreated empty before invwsumsq's reverse read it. The same happens without any callee once nothing follows the loads in the function. Walk through an addrspace(13) pointer loaded from the allocation as if it were pointer arithmetic on it, as already happens for julia.gc_loaded on newer Julia versions where this worked. The data stores are then replayed before the reverse pass reads the data, and the usual checks apply to the data loads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G84mmXoaait2MBqCH5BKNb
… enabled Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G84mmXoaait2MBqCH5BKNb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the wrong gradient behind the Julia 1.10 Turing failure in EnzymeAD/Enzyme.jl#3734 (
demo_dot_assume_observe_index, reverse mode with runtime activity). The bug is older than that; #3325 only exposed it.Cause
A Julia array allocated in a function differentiated in split mode is rematerialized in the reverse pass: the allocation is redone and the stores into it are replayed. The use walk in
computeForwardingPropertiesstops at loads, so the array's data pointer (an addrspace(13) pointer the allocator sets up, loaded from the object) and everything reached through it were invisible. The fill stores were never recorded as stores to replay, and the data loads never as loads that must be reloadable. The reverse pass re-ran the data pointer load on the fresh object and read its unfilled buffer.This went unnoticed while the reads were cached by other means. A load through a pointer loaded from memory was cached whenever any write at all followed the call in the caller. Since #3325 only unknown writes count, so the callee reloads. In the Turing model,
sqmahal'sx .- μtemporary is recreated empty bysqmahal's reverse beforeinvwsumsq's reverse reads it (sqmahal's later writes are all to its sret and allocas).The same happens without any callee once nothing follows the loads in the function. On Julia 1.10 with Enzyme_jll 0.0.299 this already returns uninitialized memory:
Julia 1.11+ is unaffected: its data pointer flows through
julia.gc_loaded, a call the walk already follows, so the stores are replayed there.Change
Walk through an addrspace(13) pointer loaded from the allocation as if it were pointer arithmetic on it, as already happens for
julia.gc_loaded. The data stores are then replayed before the reverse pass reads the data, and the existing checks (a store that may execute after a load, writing calls) apply to the data loads and stores.Also tried: refusing to promote such an allocation when its primal is needed in the reverse pass, so it is cached whole. That fixes the model too, but changes
rematprimal.lland keeps arrays alive longer, so it was not kept.Tests
ReverseMode/julia_array_data_remat.ll, reduced from the Julia 1.10 IR of the reproducer above (the array fill in a split-mode callee of a combined function). Without this change the reverse ofjulia_midreallocates the array and re-runs the fill loop without its stores, then reloads the data from the fresh buffer; the CHECK lines fail at the missing load/store. With it the fill loop is replayed.partial_int_window.ll. lit (LLVM 20): no new failures against a build of main.🤖 Generated with Claude Code
https://claude.ai/code/session_01G84mmXoaait2MBqCH5BKNb