Give TypeAnalysis caller context for dispatch-table callees - #3089
Draft
vchuravy wants to merge 1 commit into
Draft
Give TypeAnalysis caller context for dispatch-table callees#3089vchuravy wants to merge 1 commit into
vchuravy wants to merge 1 commit into
Conversation
flang lowers a type-bound procedure call to a load out of the derived
type's binding table followed by an indirect call, so the bound procedures
have no direct call site at all -- their address only ever appears as a
`ptrtoint` entry inside the table. Enzyme reaches them from
GetOrCreateShadowConstant walking that table, and GetOrCreateShadowFunction
then seeded every pointer argument with an empty TypeTree, leaving
TypeAnalysis to recover the layout of each argument from the body alone.
Two additions, both in terms of a new pair of utilities in Utils.cpp:
* getDevirtualizedCallee constant-folds an indirect callee expression
through loads out of provably immutable memory (LLVM's
ConstantFoldLoadFromConstPtr only folds constant globals with a
definitive initializer), so a non-null answer is guaranteed. TypeAnalyzer
::visitCallBase uses it so that visitIPOCall applies to such calls.
* getIndirectCallCandidates over-approximates the target set of a
`call (inttoptr (load (base + const offset)))` by collecting, out of
every constant dispatch-table global in the module, the function at that
same offset -- the base itself need not be known, which is what makes it
apply to a dispatch off a runtime class descriptor.
GetOrCreateShadowFunction uses it to find the call sites that may reach
the function it is about to differentiate and takes the *meet* of their
argument types: an over-approximated call site only weakens the result,
and if any matching-signature indirect call cannot be bounded we fall
back to no information at all.
Enzyme-created shadow globals are tagged with "enzyme_shadow_of" so that a
shadow table is not mistaken for a source-level one. The whole thing can be
turned off with -enzyme-devirtualize=0.
On a whole-program flang module containing DVODE this takes the
"cannot handle unknown binary operator" count from 27 to 19.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
flang lowers a type-bound procedure call to a load out of the derived type's
binding table followed by an indirect call, so the bound procedures have no
direct call site at all — their address appears only as a
ptrtointentryinside the table. Enzyme reaches them from
GetOrCreateShadowConstantwalkingthat table, and
GetOrCreateShadowFunctionthen seeded every pointer argumentwith an empty
TypeTree, leaving TypeAnalysis to recover the layout of eachargument from the body alone.
Motivating case: a whole-program module built from Fortran containing the ODE
solver DVODE, where
dvindy,dvjac,dvjust,dvstep,dvhinanddvodeare each referenced exactly three times — their own definition plus two
binding-table entries — and nowhere called directly.
The change
Two new utilities in
Utils.cpp:getDevirtualizedCalleeconstant-folds an indirect callee through loadsout of provably immutable memory. Loads go exclusively through LLVM's
ConstantFoldLoadFromConstPtr, which only succeeds on constant globals witha definitive initializer; that, plus traversing nothing but Load/GEP/Cast, is
what makes a non-null result a guarantee rather than a guess.
TypeAnalyzer::visitCallBaseuses it so that the existingvisitIPOCallapplies to such calls.
getIndirectCallCandidatesover-approximates the targets of acall (inttoptr? (load (base + const offset)))with the function at thatoffset in every constant dispatch table in the module.
baseneed not beknown, which is what reaches a dispatch off a runtime class descriptor —
the shape flang actually emits, and the one plain constant folding cannot
resolve.
GetOrCreateShadowFunctionuses the latter to find the call sites that mayreach the function it is about to differentiate, and takes the meet of
their argument types.
Why this is sound
The type information constrains the shadow of
fn, notfn, and a shadow isonly reachable through the shadow table Enzyme writes into the same module — an
external caller invokes the original. So those really are all of the shadow's
call sites, despite
fnitself having external linkage.Erring towards less information is therefore the safe direction, and both
approximations do: a candidate set that is too large only adds call sites,
which the meet then weakens against, and an indirect call that cannot be
bounded at all makes us give up and return no information.
Enzyme-created shadow globals are tagged with
enzyme_shadow_ofso a shadowtable is not counted as a source-level one (otherwise every slot would have two
candidates once shadows exist). The whole thing is gated on
-enzyme-devirtualize, default on.Tests
test/TypeAnalysis/flangvtable.ll— hand-reduced fromflang -O1output.@callerreaches the table through a constant descriptor: asserts thedispatch resolves, that the callee is analyzed interprocedurally at all
(before this, it never appears in the output), and that the caller's
%rwork: {[-1]:Pointer, [-1,0]:Float@double}reaches the callee's%yh.@runtime_callerreaches it through a runtime descriptor, andRUNTIME-NOTpins that nothing is concluded there.
test/Fortran/ForwardMode/type_bound_procedure.f90— end-to-end forward-modedifferentiation through a real
class(...)/procedure ::dispatch.Passes before and after, so it is a regression guard rather than a
discriminator: a program this small always recovers its types from the body.
Results
On the DVODE module,
cannot handle unknown binary operatordrops from 27 to19 (all of the difference in
mul nuw nsw i64index arithmetic that neededcaller context) and total
error:lines from 95 to 85.-enzyme-devirtualize=0reproduces the old numbers exactly.check-typeanalysisandcheck-enzymefailure sets are byte-identical tobaseline;
check-typeanalysisgains the one new passing test. Verified againstLLVM 24, where both suites have many pre-existing environmental failures
(removed
-opaque-pointersflag, typed-pointer CHECK lines), so the comparisonis failure-set diffs rather than absolute counts.
Notes for review
llvm-split-branchinst(needed to buildagainst LLVM 24 locally); this branch is the same commit cherry-picked onto
main, so CI here is the first build againstmain.A table in another TU could hold a different function at the same slot; the
argument for why that is acceptable is the shadow-reachability one above, and
is the part most worth a second opinion.
getNewFromOriginalassertion asbefore this change — a separate pre-existing failure.
🤖 Generated with Claude Code