Skip to content

TypeAnalysis: skip redundant remove/insert in AugmentWithJuliaObjectType - #3199

Open
maximilian-gelbrecht wants to merge 1 commit into
EnzymeAD:mainfrom
maximilian-gelbrecht:mg/augment-julia-object-type-fastpath
Open

maximilian-gelbrecht wants to merge 1 commit into
EnzymeAD:mainfrom
maximilian-gelbrecht:mg/augment-julia-object-type-fastpath

Conversation

@maximilian-gelbrecht

@maximilian-gelbrecht maximilian-gelbrecht commented Sep 9, 2026 •

Copy link
Copy Markdown

This is something that Fable found when analysing compile times of SpeedyWeather gradients in Julia 1.12. I chatted with @vchuravy about this.

Claude summary:

TypeAnalysis: skip redundant remove/insert in AugmentWithJuliaObjectType

What

AugmentWithJuliaObjectType (enzyme/Enzyme/TypeAnalysis/TypeAnalysis.cpp) is called from
TypeAnalyzer::getAnalysis, i.e. on every type query, when EnzymeJuliaAddrLoad is set. It marks Julia object
pointers in the TypeTree:

  • for a tracked pointer type: remove({-1}); remove({0}); insert({-1}, Pointer),
  • for each Julia pointer field of a struct/array type: remove({off}); insert({off}, Pointer).

It does this unconditionally, although after the first call the tree already carries exactly that result.
TypeTree::insert iterates over the whole offset map ("check if there is an existing match"), so every later query
on a large tree — a pointer to a big Julia struct has one entry per field offset — pays O(map size) for nothing.

This PR checks the exact keys first (an O(log n) map::find) and returns early when the post-condition already
holds:

  • tracked pointer: {-1} is Pointer and there is no {0} entry,
  • struct field: {off} is already Pointer.

The resulting tree is identical to what the unconditional remove/insert produces; only the redundant work is skipped.
About 15 added lines, no new state.

Why

With #3186/#3187/#3188 in place, profiling reverse-mode compilation of a Julia atmospheric model
(SpeedyWeather.jl, Julia 1.12, LLVM 18) shows activity analysis dominated by

forceActiveDetection → isConstantValue → isValueInactiveFromUsers → TypeAnalyzer::getAnalysis (66 %)
  → AugmentWithJuliaObjectType (53 %) → TypeTree::insert (49 %)

i.e. half of the activity-analysis time is this redundant re-augmentation.

Measured

SpeedyWeather.jl BarotropicModel time_step!, reverse mode, Julia 1.12.6, Apple M3, Enzyme core v0.0.292 +
#3186/#3187/#3188 as the baseline:

compile time gradient
baseline 123.5 s `sum
with this change 111.0 s bit-identical

On the same model's larger PrimitiveWetModel step the change is one of two needed to get through activity analysis
at all (the other being a memoised isNoNeed in calculateUnusedValuesInFunction, sent separately).

Testing

  • Gradients bit-identical on the two SpeedyWeather test cases above.
  • lit suite: not yet run by me — please run in CI.

AugmentWithJuliaObjectType runs on every getAnalysis call. For a tracked
pointer type it unconditionally did remove({-1}); remove({0});
insert({-1}, Pointer), and for each Julia pointer field of a struct
remove({off}); insert({off}, Pointer). TypeTree::insert walks the whole
offset map, so on large TypeTrees (pointers to big Julia structs) each
query paid O(map) although the tree already carried the result after the
first augmentation.

Check the exact keys first (O(log n)) and return early when the tree
already has the post-condition. The end state of the tree is unchanged.

Measured on SpeedyWeather.jl (BarotropicModel time_step!, reverse mode,
Julia 1.12, on top of EnzymeAD#3186/EnzymeAD#3187/EnzymeAD#3188): 123.5 s -> 111.0 s, gradient
bit-identical.
@vchuravy

vchuravy commented Sep 9, 2026

Copy link
Copy Markdown
Member

The other alternative is #3197 which is caching the types

This branch has not been deployed

No deployments
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