Repository navigation
Conversation
Benchmark Results
Benchmark PlotsA plot of the benchmark results has been uploaded as an artifact at https://github.com/EnzymeAD/Enzyme.jl/actions/runs/39576052072/artifacts/12341809715. |
|
Two red checks on 864c32b that aren't from this PR:
Generated by Claude Code |
|
Julia 1.13 - windows-latest fails in The cause is that effects = Core.Compiler.decode_effects(ci.ipo_purity_bits)
if Core.Compiler.is_effect_free(effects)
push!(fattrs, StringAttribute("enzyme_ReadOnlyOrThrow"))
endThis keeps the information the body used to give, for every native callee and not just this test. Generated by Claude Code |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3768 +/- ##
===========================================
+ Coverage 49.20% 79.93% +30.72%
===========================================
Files 58 72 +14
Lines 23504 25335 +1831
===========================================
+ Hits 11566 20252 +8686
+ Misses 11938 5083 -6855 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Stack on #3771 |
|
We should also enable this for 1.10 and 1.11. |
|
Rebase the PR on #3771 and enable this for 1.10/1.11 as well. |
|
#3771 had been rewritten (cea484c, 45f6fc0, bbd149d), so I brought it into this branch with a merge commit (65bbfbb). This session isn't allowed to rewrite history, so a real rebase and force-push wasn't possible here. If you want linear history, a local 1.10 and 1.11 were already enabled in 2bfda4d. Two adjustments came with the merge:
Generated by Claude Code |
|
@claude Hook the 2.x path now |
|
The GPUCompiler 2.x path is hooked in ebe6733. Generated by Claude Code |
|
CI status on 7bc78da: every Linux and Windows job for 1.10 through 1.13 passes, including the GPUCompiler 2.x default jobs and the
Generated by Claude Code |
|
@claude rebase |
|
Your PR no longer requires formatting changes. Thank you for your contribution! |
0bf54e8 to
a41eb31
Compare
|
CI on 08d10d6: everything that has finished is green except two checks.
Integration Tests - Lux - Julia 1.11 was cancelled after hanging in "Load Julia packages from cache" before any test ran; I re-ran it. Generated by Claude Code |
08d10d6 to
9625027
Compare
|
Every Julia 1.10 unit job is red on 3656e7f. The failing test is the new The error is "Call parameter type does not match function signature!". In The cause is in Enzyme. When an alloca is moved to the heap, The fix is EnzymeAD/Enzyme#3491. Its lit test reproduces this exact error on LLVM 15 without the fix and passes with it. This PR stays red on 1.10 until an Enzyme_jll release carries that fix, unless the test is gated in the meantime. Generated by Claude Code |
|
On 9d97ea0 the Documentation build itself passes: cross-references, doctests and rendering all succeed. Only the preview deploy fails. Downgrade 1.10 is still the typed-pointer failure from my earlier comment, which needs EnzymeAD/Enzyme#3491 in a released Enzyme_jll. Generated by Claude Code |
…ng them
Calls Enzyme never differentiates (an `EnzymeRules.inactive` rule, or
argument and return types that are all guaranteed const) now resolve to
the native CodeInstance during inference and are bound to the host's
compiled code instead of being re-emitted into the Enzyme module.
- `typeinf_edge` is overridden for `EnzymeInterpreter`: a call of a
derivative-free method resolves to the CodeInstance Julia's native
interpreter infers for it, so neither it nor its callees are inferred
by Enzyme, and the optimizer emits `:invoke mi`.
- `ci_cache_populate` (GPUCompiler 1.x) and `CompilerCaching.get_codeinfos`
(2.x) leave such callees out of the emitted list; `bind_native_callees!`
replaces the specsig stubs codegen emits for them by declarations bound
to the native entry point. The skipped callees live in the session's
EnzymeContext.
- Declarations carry `enzyme_inactive`, `enzymejl_native_inactive` and
`nofree`, plus read-only-or-throw when Julia's effects prove the callee
effect-free. GC-rooted struct returns get an sret type with the tracked
pointers stripped.
- `native_uses_specsig` mirrors Julia's `uses_specsig`; callees with the
`jl_fptr_args` ABI and varargs methods are left out.
- `EnzymeInterpreter` carries a `native_callees` flag, part of
`EnzymeCacheToken`, set by `get_interpreter` from the job's method
tables; host only, and off for the `onehot` job.
- On Julia 1.10 and 1.11 calls go through `jl_invoke`: `check_ir!` records
the native return type on the call as `enzymejl_rt` for `abs_typeof`,
and marks effect-free callees read-only-or-throw. Inference that calls
natively keeps caches of its own on 1.10.
Also:
- Only take the Complex shortcut in activity analysis for a concrete
Complex (Union{ComplexF64, ComplexF32} has no ptreltype).
- Box the batched shadows of a rule argument or return of a non-concrete
type: `BatchDuplicated{T, N}` of a non-concrete `T` holds them in a
boxed `NTuple{N, T}`.
Assisted-by: Claude Code (Opus 5.5)
504c17c to
148fca8
Compare
Requested by Valentin
Most of the code a differentiated function calls is never differentiated: callees with an
EnzymeRules.inactiverule, or whose argument types and return type are all guaranteed const. Enzyme still emits, optimizes, analyzes and code generates it in every thunk, although Julia compiles the same code natively for the primal. In the Enzyme tests of TensorKit.jl (QuantumKitHub/TensorKit.jl#464) that is ~80% of the IR of each thunk:_degeneracystructure,_sectorstructure, LRU cache lookups, fusion-tree iteration, …. Each new sector, element type or tensor shape compiles all of it again.This PR stops inferring and emitting those callees and calls their native code instead. It's always on for host code on Julia 1.12 and later (
Enzyme.Compiler.NATIVE_CALLEES, a constant that is part of the interpreter's cache token). Julia 1.10 and 1.11 keep the old path.How
typeinf_edgeis overridden forEnzymeInterpreter. It resolves a call of a derivative-free method to the CodeInstance Julia's native interpreter infers for it, viareturn_cached_result. The caller gets the native return type and effects, and neither the method nor its callees are inferred byEnzymeInterpreter. This only applies when the method has no rule other thaninactive, isn't a primitive or always-inline function, and Julia would call it through a specialized entry point rather than inline it. The method is inferred natively first and only compiled when it qualifies. Since nothing for that MethodInstance is in theEnzymeInterpretercache, the optimizer emits:invoke mi.ci_cache_populatereturns (jl_emit_native). For an:invokeof a MethodInstance or CodeInstance that is not in the list, codegen emits only a small specsig stub that boxes the arguments andjl_invokes the callee; nothing of its body or its callees.ci_cache_populateis overridden forEnzymeInterpreterto also leave out derivative-free callees thatEnzymeInterpreterdid infer, for example ones whose results were already in its cache (native_callee).bind_native_callees!runs right afteremit_llvm(primal module,nested_codegen!, sugar.jl). It replaces each stub by adeclare_native!declaration bound to the native specsig entry point, asinvoke_codegen!does for custom rules. It runs before the Julia values become symbolic (Keep Julia values symbolic until the module is linked (GPUCompiler 1.8) #3742), because it reads the invoked MethodInstance from the stub's constant global.pgcstack(swiftself).nofree,enzyme_inactiveandenzymejl_native_inactive. With the last one,materialize_native_invokes!leaves them without a body; nested differentiation doesn't differentiate them either.Gotchas found on the way
jl_invoke, not the CodeInstance. Each populate therefore publishes the table of what it left out, and the bind that follows its emission consumes it. That avoids matching a stub against a callee left out by another compilation.uses_specsigwithprefer_specsigoff): an unboxed argument or return, a union return, or all-singleton arguments. With only boxed arguments and a boxed return it uses thejl_fptr_argsABI, althoughjl_read_codeinst_invokestill reports a specialized entry. Calling that through the derived specsig crashed (#_sortperm#29(…, ::Vector{Int})).native_uses_specsigmirrors the rule, and varargs methods are left out as well. Custom rules never hit this, because they always take an unboxed annotation argument.Measurements
After rebasing onto main (3de895b): Julia 1.13.1, with 1.10 for reference
TensorKit.jl#464, single test files run standalone with
CI=true(the CI configuration),main(9e86a1c) vs this PR, all runs concurrently on one machine with the profiler on. All tests pass in every configuration.enzyme/qrenzyme/mul(25260 tests)Where the time goes in
enzyme/mul(sampled CPU time):check_irEnzymeInterpreterinferenceThe PR removes nearly all the work on code that is never differentiated; the AD step itself costs the same. After it, ordinary native compilation of the test harness and primal code is the largest cost (~40% of
mul), which this PR doesn't touch. Julia 1.10 spends about as long as 1.13 without the PR, so it would benefit similarly if the path worked there.The measurements below are from earlier revisions, when an
ENZYME_NATIVE_CALLEESswitch still existed.Latest (all three commits, Julia 1.12.7), three configurations at the same time
ENZYME_NATIVE_CALLEES=0)All 216 calls pass in each configuration. Switching to the native interpreter mainly removes the extra warmup cost of the populate-only version. svd_trunc on Z2 takes 13.2 / 9.1 / 8.0 s.
Earlier (first commit, opt-in)
TensorKit.jl#464
enzyme-factorizationsCI matrix, 216test_reversecalls (each compiles one new thunk), Julia 1.12.7, flag off vs on, run at the same time on one machine:ENZYME_NATIVE_CALLEES=1These numbers were measured before rebasing onto current main; after the rebase, svd_trunc Z2 takes 6.5 s.
Julia 1.13.1
72 calls (Trivial and Z2 sectors, including the ~60 s one-time warmup), flag off vs on at the same time: 340 s → 208 s (−39%), all pass. The only change needed for 1.13 is
codeinfo_for_consttaking the world range and edges of the CodeInstance; GPUCompiler'sci_cache_populateand the stubs codegen emits are the same as on 1.12.Tests
New test
test/native_callees.jl: a@noinlineinactive callee returning a struct with aVectorfield (rooted sret), and the boxed-ABIsortpermcase, in forward and reverse mode.On the final commit, with the feature on (now the default), these groups pass locally on 1.12:
native_callees rules core advanced basic sugar errors callconv mixed typeunstable. That's 15201 pass and 49 broken (the same as with it off), 0 errors.I didn't run the GPU, threads or integration suites with the flag on. The path on Julia 1.10 and 1.11 is unchanged.
Open questions / follow-ups
ci_cache_populate.🤖 Generated with Claude Code