You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a custom rule executes host code that was compiled into the differentiated module by nested_codegen! (src/rules/customrules.jl:1704), any @cfunction in that code produces a trampoline whose ABI disagrees with what Julia 1.12's runtime cfunction converter hands back, leading to a segfault. Found while debugging the long-red CUDA CI "Reverse Kernel" testset; this is the next failure after the jl-inst-simplify fix (EnzymeAD/Enzyme#2919).
Reproducer
Julia 1.12.6, Enzyme main + EnzymeAD/Enzyme#2919 (via deps/build_local.jl), CUDA v6:
using CUDA, Enzyme
functionsquare_kernel!(x)
i =threadIdx().x
x[i] *= x[i]
returnnothingendfunctionsquare!(x)
@cuda blocks =1 threads =length(x) square_kernel!(x)
returnnothingend
A =CuArray(collect(Float32, 1:64))
dA = CUDA.ones(64)
Enzyme.autodiff(Reverse, square!, Duplicated(A, dA)) # SIGSEGV
CUDACore's augmented_primal calls Enzyme.tape_type at runtime; the nested compile's optimize! invokes LLVM.jl custom passes, and the process crashes inside module_callback (LLVM.jl newpm.jl:140). The identical tape_type call works at top level — only the copy of run! compiled into the differentiated module misbehaves.
Root cause
nested_codegen! compiles LLVM.jl's run! into the module using GPUCompiler's CodegenParams(gcstack_arg = false) (GPUCompiler jlgen.jl:791), so specsig functions there do not take pgcstack as an argument.
Julia 1.12 lowers the @cfunction(module_callback, ...) inside run! to the abi-converter pattern (gen_cfun_wrapper): a jlcapi_module_callback_* trampoline, cache slots, and a jl_get_abi_converter guard. Under gcstack_arg=false the trampoline's indirect converter call passes only (ptr, ptr) — no pgcstack (verified in the post-AD module dump; cfuncdata flags have specsig=1).
At runtime jl_get_abi_converter (runtime_ccall.cpp:445-449) finds module_callback's code instance compiled by the stock JIT (gcstack_arg=true, pgcstack in swiftself/r13), sees an exact declrt/sigt match, and returns the raw specsig pointer.
The trampoline calls it without pgcstack. gdb at the fault: rdi = LLVM Module, rsi = thunk, and r13 = leftover thunk pointer — the callee treats r13 as pgcstack, pushes a GC frame into the CustomPassState object, and segfaults.
Not Enzyme-specific in principle: any GPUCompiler-compiled host code that executes an @cfunction on 1.12 has this mismatch. Enzyme is the main consumer that actually runs nested-compiled host code, because rules pull their whole call graph (here: the entire Enzyme.jl compiler plus LLVM.jl's pass machinery — the post-AD module grows from 647 to 3615 functions) into the differentiated module.
Ruled out
GC rooting inside rule-executed code is sound (WeakRef + GC.@preserve probe inside a custom rule passes).
The registered callback pointer in the emitted IR is the correct in-module trampoline; the bad pointer comes from the runtime converter at world-guard refresh.
Fix options
(a) Julia: encode gcstack_arg in the cfuncdata flags so jl_get_abi_converter refuses the raw-specsig fast path when the caller's ABI differs from the native JIT's. The runtime currently assumes all callers share the stock ABI.
(b) GPUCompiler gcstack_arg=true for host-executable targets: tested, not viable — Enzyme.jl fails its own precompilation with "AssertionError: Swiftself attribute coming from differentiable context is not supported" (lower_convention).
(c) Enzyme.jl: stop compiling rule-reachable compiler code into the differentiated module and call rules through a dynamic-dispatch boundary. This would also remove the exposure that made the jl-inst-simplify miscompile (nondeterminstic segfault during compilation #2919) reachable: unsound-but-latent pass behavior currently gets applied to the whole Enzyme/GPUCompiler/LLVM.jl compiler stack on every rule compile.
Happy to file the corresponding JuliaLang/julia issue for (a) if that is the preferred direction.
Summary
When a custom rule executes host code that was compiled into the differentiated module by
nested_codegen!(src/rules/customrules.jl:1704), any@cfunctionin that code produces a trampoline whose ABI disagrees with what Julia 1.12's runtime cfunction converter hands back, leading to a segfault. Found while debugging the long-red CUDA CI "Reverse Kernel" testset; this is the next failure after the jl-inst-simplify fix (EnzymeAD/Enzyme#2919).Reproducer
Julia 1.12.6, Enzyme main + EnzymeAD/Enzyme#2919 (via
deps/build_local.jl), CUDA v6:CUDACore's
augmented_primalcallsEnzyme.tape_typeat runtime; the nested compile'soptimize!invokes LLVM.jl custom passes, and the process crashes insidemodule_callback(LLVM.jlnewpm.jl:140). The identicaltape_typecall works at top level — only the copy ofrun!compiled into the differentiated module misbehaves.Root cause
nested_codegen!compiles LLVM.jl'srun!into the module using GPUCompiler'sCodegenParams(gcstack_arg = false)(GPUCompilerjlgen.jl:791), so specsig functions there do not take pgcstack as an argument.@cfunction(module_callback, ...)insiderun!to the abi-converter pattern (gen_cfun_wrapper): ajlcapi_module_callback_*trampoline, cache slots, and ajl_get_abi_converterguard. Undergcstack_arg=falsethe trampoline's indirect converter call passes only(ptr, ptr)— no pgcstack (verified in the post-AD module dump; cfuncdata flags have specsig=1).jl_get_abi_converter(runtime_ccall.cpp:445-449) findsmodule_callback's code instance compiled by the stock JIT (gcstack_arg=true, pgcstack in swiftself/r13), sees an exact declrt/sigt match, and returns the raw specsig pointer.rdi= LLVM Module,rsi= thunk, andr13= leftover thunk pointer — the callee treats r13 as pgcstack, pushes a GC frame into theCustomPassStateobject, and segfaults.Not Enzyme-specific in principle: any GPUCompiler-compiled host code that executes an
@cfunctionon 1.12 has this mismatch. Enzyme is the main consumer that actually runs nested-compiled host code, because rules pull their whole call graph (here: the entire Enzyme.jl compiler plus LLVM.jl's pass machinery — the post-AD module grows from 647 to 3615 functions) into the differentiated module.Ruled out
GC.@preserveprobe inside a custom rule passes).Fix options
gcstack_argin the cfuncdata flags sojl_get_abi_converterrefuses the raw-specsig fast path when the caller's ABI differs from the native JIT's. The runtime currently assumes all callers share the stock ABI.gcstack_arg=truefor host-executable targets: tested, not viable — Enzyme.jl fails its own precompilation with "AssertionError: Swiftself attribute coming from differentiable context is not supported" (lower_convention).Happy to file the corresponding JuliaLang/julia issue for (a) if that is the preferred direction.
🤖 Generated with Claude Code