[llvm] Backport ORC dependence propagation performance fix - #23249
Conversation
Test Results 23 files 23 suites 3d 17h 6m 1s ⏱️ For more details on these failures, see this check. Results for commit 2fe3d65. ♻️ This comment has been updated with latest results. |
|
Thanks, the description should be heavily condensed, it's a lot of LLM bla bla |
You mean the commit message, yes? |
…tion.
Backport of upstream LLVM commit db5ffb04ab0b5c4e193d2c31e635e69f42deaaaf
("[ORC] WaitingOnGraph perf: faster dependence propagation.", PR
llvm/llvm-project#183272), which is not present in the LLVM 22.
This fixes the intermittent timeout of rootest-root-io-cpp11Containers-unorderedMap.
The roottest test root/io/cpp11Containers/unorderedMap (and related)
showed a bistable runtime: the same test, in the same
environment, would sometimes complete quickly and sometimes timeout
(300s). With a debug build of LLVM we observed the test to usually
run in ~800 seconds (40 seconds with a Release build) and sometimes take
~18 hours.
This commit replaces the core dependence propagation algorithm in
WaitingOnGraph to avoid worst-case behavior in the common case where
dependence graphs are sparse. This algorithm showed up as the underlying
cause of the bug in #179611.
For each call to MaterializationResponsibility::notifyEmitted,
WaitingOnGraph would build the transitive closure of all SuperNodes
whose "waiting on" relationships were affected by the newly emitted
symbols, then propagate any remaining unemitted dependencies through
this transitive closure graph. This approach is simple, but pushes the
algorithm towards n^2 complexity even for sparse dependence graphs.
The new propagation algorithm:
1. Inverts the edge direction in the SymbolDependenceMap data structure:
SymbolDepMap[SN] now contains the set of SuperNodes that depend on SN,
rather than the set that SN depends upon.
2. Pushes dependencies through the SymbolDepMap iteratively until it
reaches a fixed point.
This updated algorithm converges much more quickly than the original for
the testcase reported in the issue, and for other cases tested so far.
Validation
----------
* Performance on a synthetic dependence chain (time in simplify()):
N old new speedup
500 0.0485s 0.0002s 221x
1000 0.3762s 0.0005s 811x
2000 1.9467s 0.0014s 1376x
4000 21.4621s 0.0034s 6321x
The old implementation scales ~8-11x per doubling of N; the new one
~2.4x.
References
----------
* Upstream commit: db5ffb04ab0b5c4e193d2c31e635e69f42deaaaf
* Upstream PR: llvm/llvm-project#183272
* Upstream issue: llvm/llvm-project#179611
* Follow-up (perf regression infrastructure):
llvm/llvm-project#183251
The commit was released in LLVM 23.
---
Diagnosed, backported and validated with the assistance of Claude Opus 5.
7685bba to
2fe3d65
Compare
|
The commit log has been shortened (core is now the LLVM commit log). |
|
Looks reasonable to me but I'd wait for @hahnjo's review. |
hahnjo
left a comment
There was a problem hiding this comment.
🛑 I tried backporting the linked upstream commit, and it seems the LLM happily modified the code. 🛑
Thanks for shortening the commit message; I think the "Validation" section should be removed as well because it seems to be only validating the upstream change with a synthetic test case that only Claude knows. Please also update the PR summary with a copy of the commit message when done.
| CoalesceToPendingSNs.remove( | ||
| [&](SuperNode *SN) { return SuperNodeDeps.count(SN); }); | ||
| [&](SuperNode *SN) { return ModifiedPendingSNSet.count(SN); }); |
There was a problem hiding this comment.
I see that you / Claude introduced ModifiedPendingSNSet to resolve the conflicts during backporting. I'm not particularly keen on hacking additional variables into this part of the code that don't exist upstream...
| static void hoistDeps(SuperNodeDepsMap &SuperNodeDeps, | ||
| std::vector<std::unique_ptr<SuperNode>> &SNs, | ||
| // Returns true if any dependencies were hoisted. | ||
| static bool hoistDeps(SuperNode *SN, SuperNodeDepsMap &SuperNodeDeps, | ||
| ElemToSuperNodeMap &ElemToSN) { |
There was a problem hiding this comment.
Where does this change come from? It's definitely not part of llvm/llvm-project#183272 and AFAICT this code never existed upstream. What is the motivation for changing this downstream if the commit message and PR summary say "backport"?
There was a problem hiding this comment.
I guess this half applies llvm/llvm-project@7f6ada9, slapped together with this backport?
Backport of upstream LLVM commit db5ffb04ab0b5c4e193d2c31e635e69f42deaaaf ("[ORC] WaitingOnGraph perf: faster dependence propagation.", PR llvm/llvm-project#183272), which is not present in the LLVM 22.
This fixes the intermittent timeout of rootest-root-io-cpp11Containers-unorderedMap.
Analysis
The roottest test root/io/cpp11Containers/unorderedMap (and related) showed a bistable runtime: the same test, in the same environment, would sometimes complete quickly and sometimes timeout (300s). With a debug build of LLVM we observed the test to usually run in ~800 seconds (40 seconds with a Release build) and sometimes take ~18 hours. Profiling attributed essentially all of the extra time to WaitingOnGraph::propagateSuperNodeDeps().
propagateSuperNodeDeps() computed, for every SuperNode, the transitive closure of its dependence set with a per-node DFS. Its total cost is
which means the result depends critically on the order in which nodes are expanded:
The visitation order came from iterating a DenseMap keyed on SuperNode*, and DenseMapInfo<T*>::getHashValue() hashes the raw pointer value:
so the traversal order is a function of ASLR and heap layout. Two runs of the same binary on the same input could therefore land on opposite ends of that complexity range. The algorithm also mutated Deps in place while iterating (Deps = std::move(Reachable)) and de-duplicated work on pop rather than on push, which further inflated the worklist.
A second amplifier was sinkDeps(), which re-expanded the computed closures back into per-symbol dependence maps. This inflated the dependence sets and prevented the Coalescer from merging SuperNodes (coalescing requires exact equality of the dependence sets), so the graph stayed large and every subsequent emit paid the cost again.
Because both traversal orders compute the same closure, this never showed up as a correctness failure -- only as the 800s / 18h runtime split.
Solution
Adopt upstream's rewrite:
Validation
Differential test: 4000 randomised emit/fail scenarios driven through both the old and new implementations produce identical Ready and Failed sets (assertions enabled).
Performance on a synthetic dependence chain (time in simplify()):
The old implementation scales ~8-11x per doubling of N; the new one ~2.4x.
libLLVMOrcJIT.a builds and links cleanly.
References
WaitingOnGraphllvm/llvm-project#179611Build a performance test suite for WaitingOnGraph llvm/llvm-project#183251
The fix first ships in LLVM 23 (verified present at tag llvmorg-23.1.0 and absent from release/22.x as of 22.1.8), so it cannot be picked up from an LLVM 22 point release. This patch can be dropped once ROOT's vendored LLVM moves to 23 or later.
Diagnosed, backported and validated with the assistance of Claude Opus 5.