Skip to content

add helpers for backedge updates on CI equivalence swaps and copies#62217

Open
vtjnash wants to merge 6 commits into
masterfrom
jn/62022-more-edges
Open

add helpers for backedge updates on CI equivalence swaps and copies#62217
vtjnash wants to merge 6 commits into
masterfrom
jn/62022-more-edges

Conversation

@vtjnash

@vtjnash vtjnash commented Jun 25, 2026

Copy link
Copy Markdown
Member

No description provided.

@vtjnash vtjnash requested a review from topolarity June 25, 2026 19:55
@topolarity

topolarity commented Jun 25, 2026

Copy link
Copy Markdown
Member

This was the originally claimed reason for #62022, which turned out to be wrong

Do you mean #62021 (comment) ?

I never claimed any reason for #62022, which is a distinct MWE

FWIW I think this was hit in the wild in timholy/Revise.jl#994, if that helps generate an MWE

Comment thread src/gf.c Outdated
}
}
// after storing new backedges, use cached if it is still legal (covers wider world range)
return jl_atomic_load_relaxed(&cached->max_world) >= jl_atomic_load_relaxed(&callee->max_world);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is somewhat awkward that this leaves your max_world potentially incoherent with your forward edges

That seems likely to mislead us, e.g. we can now serialize a CI thinking (correctly) that it is still valid (.max_world == typemax) and then discover during de-serialization that it ends up invalid (despite loading in exactly the same method / binding state)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. Maybe we should throw this method in the world_lock, so that ordering isn't so important here (and also prevents concurrent update attempts to edges).

@topolarity topolarity left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code seems to have a number of serious bugs / confusion still:

  1. src can end up coming from callee but compiled and attached to cached if !ci_has_invoke(cached) which is quite illegal. We also forget to update code_cache(workqueue.interp)[mi] if link_ci_equiv! fails validation
  2. collectinvokes! seems to be confused about whether it should be walking the old or re-linked :invoke graph (JIT does not need to walk the re-linked graph, but IIUC AOT does)
  3. jl_link_ci_equiv does not check that cached->min_world <= callee->min_world so this can still create invalid-to-invoke code at lower world ages
  4. cached is likely to fail validation often in bounded-world environments for reasons unrelated to concurrency, since jl_get_ci_equiv does not check that cached->max_world >= callee->max_world

It also doesn't resolve the MWE in #62021 (which I'm quite sure is this bug)

FWIW having no great test strategy for bounded-world inference / IPO divergence seems like a real problem, although not one we have to fix right away.

The testability barrier here seems to mean we can only catch cache bugs like the above by inspection or by crowd-sourcing them to our users which is a problem we should try to fix with some smart design eventually I think, like the ability to mock the cache perhaps.

@vtjnash

vtjnash commented Jun 28, 2026

Copy link
Copy Markdown
Member Author

collectinvokes! seems to be confused about whether it should be walking the old or re-linked :invoke graph (JIT does not need to walk the re-linked graph, but IIUC AOT does)
jl_link_ci_equiv does not check that cached->min_world <= callee->min_world so this can still create invalid-to-invoke code at lower world ages
cached is likely to fail validation often in bounded-world environments for reasons unrelated to concurrency, since jl_get_ci_equiv does not check that cached->max_world >= callee->max_world

Oooh, right. We should perhaps do 2 lookups here (first one try one that can replace the invoke, but if that fails for AOT, opportunistically compile something else)? That code in jl_get_ci_equiv was removed in a8216e0, but could be restored.

src can end up coming from callee but compiled and attached to cached if !ci_has_invoke(cached) which is quite illegal. We also forget to update code_cache(workqueue.interp)[mi] if link_ci_equiv! fails validation

Why is that illegal? Do you just mean we might have set up the edges inaccurately? We intended to show the CI are equivalent, but originally from 608a06c might never have done that

@topolarity

Copy link
Copy Markdown
Member

Oooh, right. We should perhaps do 2 lookups here (first one try one that can replace the invoke, but if that fails for AOT, opportunistically compile something else)? That code in jl_get_ci_equiv was removed in a8216e0, but could be restored.

Yeah, that might be better. I've also been wondering if it might be worth handling this "re-linking" as part of the re-inference / re-validation that we do when loading pkgimages? That might make it possible to implement this as an atomic edge update, rather than having to preserve callee as a valid CodeInstance while also sort of avoiding its usage due to not having code / being cached / etc.

Why is that illegal? Do you just mean we might have set up the edges inaccurately?

Yeah essentially - link_ci_equiv! looks like it sets up cached to be called instead of callee, but this CodeInfo swap can mean that you invoke callee's code instead of cached which seems to require the edge in the other direction

@vtjnash

vtjnash commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

link_ci_equiv!

Okay, that's fine then. The premise under which I was going to add that was wrong anyways (this is just preparing a dissociated list of compilation jobs--there's no implied connection until linking, and JIT/AOT already has the logic to add the edge when it is actually needed)

@topolarity

topolarity commented Jun 29, 2026

Copy link
Copy Markdown
Member

link_ci_equiv!

Okay, that's fine then. The premise under which I was going to add that was wrong anyways (this is just preparing a dissociated list of compilation jobs--there's no implied connection until linking, and JIT/AOT already has the logic to add the edge when it is actually needed)

Hmm not sure I understand

My concern is that if cached gains an invoke generated from the src of callee, then the edges on the cached CI are now out-of-sync with the code its invoke is pointing to. The "re-link" is implicit in the swap in the compilation queue, and it bypasses the explicit JIT / AOT link logic since the CI itself has been re-linked.

@vtjnash vtjnash force-pushed the jn/62022-more-edges branch from 233d557 to 31d3acc Compare June 29, 2026 20:50
@vtjnash vtjnash force-pushed the jn/62022-more-edges branch from 31d3acc to 8f4fa11 Compare July 8, 2026 19:12
vtjnash and others added 5 commits July 15, 2026 19:28
…ence swaps

When a CodeInstance `callee` is replaced by an equivalent `cached` CI
(via `jl_get_ci_equiv` in `add_codeinsts_to_jit!` and `compile!`) or when a
new CI is created as a copy (via `copy_to_mi_cache`), the edges/backedges
were not updated. As a result, any caller whose edges svec contained
`callee` by identity would not be invalidated when `cached` was
invalidated, since `callee` was not registered in any MethodInstance's
backedges.

Fix by introducing `link_ci_equiv!` on the Julia side, which:
1. Appends `cached` to `callee.edges` so that `_invalidate_backedges`'s
   pointer-identity check (`edge == replaced_ci`) matches when `cached`
   is being invalidated.
2. Registers `callee` in `cached`'s MI's backedges (via `store_backedges`)
   so the invalidation walk can reach `callee` when `cached` is invalidated.
3. Promotes `callee` to the current world via `jl_promote_ci_to_current`
   so it participates in the normal validity regime.

For `copy_to_mi_cache` on the C side, resolve the long-standing TODO by
adding `store_backedges_c` (a C mirror of the Julia `store_backedges`
function that parses the forward-edges svec format) and calling
`jl_promote_ci_to_current` on the newly created CI.

Also add `jl_codeinst_set_edges` as an exported C helper to atomically
update a CodeInstance's edges field with the proper write barrier.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend the CI equivalence edge/backedge fix to the C-side sites where
equivalent CodeInstances are found and substituted. Adds jl_link_ci_equiv,
a C-level mirror of link_ci_equiv! from typeinfer.jl, and calls it from:

 - aot_link_output (get_ci_equiv_compiled): AOT call-target routing
 - linkOutput (findCompatibleCI): JIT call-target routing

In both cases, when a callee CI is routed through a cached/equiv CI,
jl_link_ci_equiv ensures the callee ends up in the equiv's MI backedges
and that the equiv appears in the callee's forward edges. Without this,
invalidation of the equiv would not propagate to the callee.

The jl_link_ci_equiv call in linkOutput is made with LinkerMutex
temporarily released, since jl_alloc_svec may GC.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
warning: this make sysimage 160 -> 190 MB, which needs fixing (due to
bad edges list)
@vtjnash vtjnash force-pushed the jn/62022-more-edges branch from 8f4fa11 to 6e41e8b Compare July 15, 2026 19:39
@vtjnash vtjnash changed the title fix missing edge and backedge updates on CI equivalence swaps and copies add helpers for backedge updates on CI equivalence swaps and copies Jul 15, 2026
@vtjnash vtjnash dismissed topolarity’s stale review July 15, 2026 20:37

No longer includes that part of the change, only some of the helper methods for re-visiting this later after some other concurrent work is resolved

@vtjnash vtjnash added the merge me PR is reviewed. Merge when all tests are passing label Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge me PR is reviewed. Merge when all tests are passing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants