Skip to content

Mark make_zero/make_zero! shadow-init builtins inactive in forward mode (#3135)#3137

Merged
wsmoses merged 2 commits into
EnzymeAD:mainfrom
ChrisRackauckas-Claude:inactive-make-zero-shadow-builtins
Jun 4, 2026
Merged

Mark make_zero/make_zero! shadow-init builtins inactive in forward mode (#3135)#3137
wsmoses merged 2 commits into
EnzymeAD:mainfrom
ChrisRackauckas-Claude:inactive-make-zero-shadow-builtins

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

Summary

Follow-up to #3135 (per @wsmoses: "Open the pr for those two functions to start; we can do the eqtable stuff in a follow up").

make_zero / make_zero! emit two bookkeeping builtins while initializing the inner-reverse shadow:

  • jl_field_isdefined_checked — the isdefined(prev, i) field walk over undef-able / abstract fields (make_zero.jl).
  • jl_idset_peek_bp — the prev in seen IdSet aliasing check (make_zero!.jlbase/idset.jl).

Enzyme forward mode had no rule for either, so nesting forward-over-reverse (HVPs/Hessians) over a value with an undef-able/abstract field failed with EnzymeNoDerivativeError.

This PR adds the jl_/ijl_ variants of both to nofreefns and inactivefns in src/llvm/attributes.jl, plus a forward-mode regression testset.

What this fixes (verified locally)

On a dev'd checkout of main (0670cc2, Julia 1.11.9), both isolated reproducers fail on main and pass with this change, returning the correct derivative:

Builtin Reproducer main this PR
jl_field_isdefined_checked isdefined(m, i) field walk, fwd-mode No forward mode derivative found for jl_field_isdefined_checked (2.0,)
jl_idset_peek_bp v in Base.IdSet, fwd-mode No forward mode derivative found for jl_idset_peek_bp (12.0,)

The two named errors are gone from the issue's MWEs as well.

Scope / what's deferred

This is not sufficient for the full forward-over-reverse HVP yet. With the two builtins suppressed, the make_zero / make_zero! seen-table path cascades to the table-mutation builtins:

  • MWE1 (make_zero, IdDict seen) → jl_eqtable_get / jl_eqtable_put
  • MWE2 (make_zero!, IdSet seen) → jl_idset_put_key / jl_idset_put_idx

jl_eqtable_get/jl_eqtable_put have explicit erroring custom forward rules in src/rules/llvmrules.jl, so list membership alone won't cover them. That's the agreed follow-up (forward rules for the jl_eqtable_* / jl_idset_put_* family, or making the seen bookkeeping provably inactive at the activity-analysis level).

Tests

Added testset "forward inactivity of make_zero shadow-init builtins" in test/make_zero.jl, exercising each builtin in isolation (without the deferred mutation builtins). Full test/make_zero.jl passes locally on Julia 1.11.9:

make_zero     | 72157  72157
make_zero!    | 42690  42690
remake_zero!  | 42691  42691
forward inactivity of make_zero shadow-init builtins |  2  2

Note on formatting

src/llvm/attributes.jl is not Runic-formatted globally (full-file Runic proposes ~1100 line changes — the whole Set blocks use 4-space indent where Runic wants 8). The new entries match the surrounding 4-space style rather than introducing 8-space stragglers in an otherwise 4-space block. If a green Runic check is required, the file needs a separate full reformat — happy to do that here or in its own PR, your call. (The test/make_zero.jl additions are Runic-clean.)


Please ignore until reviewed by @ChrisRackauckas. Opened as a draft.

🤖 Generated with Claude Code

`make_zero` / `make_zero!` emit `jl_field_isdefined_checked` (the
`isdefined(prev, i)` field walk over undef-able / abstract fields) and
`jl_idset_peek_bp` (the `prev in seen` IdSet aliasing check). Forward mode
had no rule for either, so forward-over-reverse nesting (HVPs/Hessians) over
a value with an undef-able/abstract field failed with EnzymeNoDerivativeError.

Add `jl_`/`ijl_` variants of both to `nofreefns` and `inactivefns` in
src/llvm/attributes.jl, and a forward-mode regression testset exercising
each builtin in isolation.

This removes the two named errors; the full forward-over-reverse HVP still
needs forward handling for the `seen`-table mutation builtins (`jl_eqtable_*`,
`jl_idset_put_*`), tracked as a follow-up. See EnzymeAD#3135.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@wsmoses wsmoses marked this pull request as ready for review May 30, 2026 16:09
@github-actions
Copy link
Copy Markdown
Contributor

Your PR requires formatting changes to meet the project's style guidelines.
Please consider running Runic (git runic main) to apply these changes.

Click here to view the suggested changes.
diff --git a/src/llvm/attributes.jl b/src/llvm/attributes.jl
index c240c1a2..467bea99 100644
--- a/src/llvm/attributes.jl
+++ b/src/llvm/attributes.jl
@@ -214,11 +214,11 @@ const nofreefns = Set{String}((
     "ijl_eqtable_get",
     "cuCtxGetApiVersion",
     "cuCtxSetCurrent",
-    # make_zero / make_zero! shadow-init bookkeeping (IdDict/IdSet seen-table queries)
-    "jl_field_isdefined_checked",
-    "ijl_field_isdefined_checked",
-    "jl_idset_peek_bp",
-    "ijl_idset_peek_bp",
+        # make_zero / make_zero! shadow-init bookkeeping (IdDict/IdSet seen-table queries)
+        "jl_field_isdefined_checked",
+        "ijl_field_isdefined_checked",
+        "jl_idset_peek_bp",
+        "ijl_idset_peek_bp",
 ))
 
 const inactivefns = Set{String}((
@@ -349,11 +349,11 @@ const inactivefns = Set{String}((
     "jl_array_to_string",
     "ijl_array_to_string",
     "pcre2_jit_compile_8",
-    # make_zero / make_zero! shadow-init bookkeeping (IdDict/IdSet seen-table queries)
-    "jl_field_isdefined_checked",
-    "ijl_field_isdefined_checked",
-    "jl_idset_peek_bp",
-    "ijl_idset_peek_bp",
+        # make_zero / make_zero! shadow-init bookkeeping (IdDict/IdSet seen-table queries)
+        "jl_field_isdefined_checked",
+        "ijl_field_isdefined_checked",
+        "jl_idset_peek_bp",
+        "ijl_idset_peek_bp",
     # "jl_"
 ))
 

@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.96%. Comparing base (0670cc2) to head (4de6472).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3137      +/-   ##
==========================================
- Coverage   70.62%   69.96%   -0.67%     
==========================================
  Files          66       66              
  Lines       21339    21958     +619     
==========================================
+ Hits        15071    15363     +292     
- Misses       6268     6595     +327     

☔ View full report in Codecov by Sentry.
📢 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.

@wsmoses
Copy link
Copy Markdown
Member

wsmoses commented Jun 2, 2026

@ChrisRackauckas-Claude your test doesn't work yet per:

Error in testset forward inactivity of make_zero shadow-init builtins:
Error During Test at /home/runner/work/Enzyme.jl/Enzyme.jl/test/make_zero.jl:792
Test threw exception
Expression: Enzyme.autodiff(Forward, idset_membership, Duplicated(2.0, 1.0)) == (12.0,)
EnzymeRuntimeException: Enzyme execution failed.
Enzyme: Not yet implemented forward for jl_eqtable_get

Stacktrace:
[1] get
@ ./iddict.jl:102 [inlined]
[2] in
@ ./iddict.jl:192 [inlined]
[3] haskey
@ ./abstractdict.jl:17 [inlined]
[4] in
@ ./idset.jl:20 [inlined]
[5] idset_membership
@ ~/work/Enzyme.jl/Enzyme.jl/test/make_zero.jl:785 [inlined]
[6] fwddiffejulia_idset_membership_62141wrap
@ ~/work/Enzyme.jl/Enzyme.jl/test/make_zero.jl:0
[7] macro expansion
@ ~/work/Enzyme.jl/Enzyme.jl/src/compiler.jl:6855 [inlined]
[8] enzyme_call
@ ~/work/Enzyme.jl/Enzyme.jl/src/compiler.jl:6325 [inlined]
[9] ForwardModeThunk
@ ~/work/Enzyme.jl/Enzyme.jl/src/compiler.jl:6225 [inlined]
[10] autodiff
@ ~/work/Enzyme.jl/Enzyme.jl/src/Enzyme.jl:688 [inlined]
[11] autodiff
@ ~/work/Enzyme.jl/Enzyme.jl/src/Enzyme.jl:577 [inlined]
[12] autodiff(mode::EnzymeCore.ForwardMode{false, EnzymeCore.FFIABI, false, false, false}, f::typeof(Main.var"##make_zero#116056".MakeZeroTests.idset_membership), args::EnzymeCore.Duplicated{Float64})
@ Enzyme ~/work/Enzyme.jl/Enzyme.jl/src/Enzyme.jl:549
[13] macro expansion
@ /opt/hostedtoolcache/julia/1.10.11/x64/share/julia/stdlib/v1.10/Test/src/Test.jl:670 [inlined]
[14] macro expansion
@ ~/work/Enzyme.jl/Enzyme.jl/test/make_zero.jl:792 [inlined]
[15] macro expansion
@ /opt/hostedtoolcache/julia/1.10.11/x64/share/julia/stdlib/v1.10/Test/src/Test.jl:1582 [inlined]
[16] top-level scope
@ ~/work/Enzyme.jl/Enzyme.jl/test/make_zero.jl:790
ERROR: LoadError: Test run finished with errors
in expression starting at /home/runner/work/Enzyme.jl/Enzyme.jl/test/runtests.jl:42

remove the test for now

@wsmoses
Copy link
Copy Markdown
Member

wsmoses commented Jun 3, 2026

@ChrisRackauckas

The `idset_membership` test exercised `prev in Base.IdSet` expecting it to
route through `jl_idset_peek_bp`, but that is only the Julia 1.11+ IdSet
implementation. On 1.10 `Base.IdSet` is backed by an `IdDict`, so `in` routes
through `jl_eqtable_get` — which has an erroring forward rule — and the test
fails with "Not yet implemented forward for jl_eqtable_get" on CI.

Drop the idset_membership function and its assertion, keeping the
jl_field_isdefined_checked regression test. The jl_idset_peek_bp attribute
entries remain (they are correct on 1.11+); covering the membership path
in a test belongs with the deferred jl_eqtable_*/jl_idset_put_* forward work.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor Author

Done — removed the idset_membership test in 4de6472.

Confirmed the root cause locally on Julia 1.10.11: Base.IdSet is backed by an IdDict on 1.10, so v in seen routes through jl_eqtable_get (which has an erroring forward rule) rather than the 1.11+ jl_idset_peek_bp path the test was targeting — hence the "Not yet implemented forward for jl_eqtable_get" CI failure. The original local run passed only because it was on 1.11.9.

I kept the jl_idset_peek_bp attribute entries (correct on 1.11+) and the jl_field_isdefined_checked regression test, which passes on 1.10:

Test Summary:                                        | Pass  Total  Time
forward inactivity of make_zero shadow-init builtins |    1      1  2.0s

Covering the IdSet membership path in a test belongs with the deferred jl_eqtable_* / jl_idset_put_* forward work.

@wsmoses wsmoses merged commit f9fdef3 into EnzymeAD:main Jun 4, 2026
58 of 66 checks passed
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.

3 participants