Repository navigation
perf(recompiler): iterate structurizer fixpoints in post-order and bound shared merge splitting growth - #5028
Conversation
20f6cbe to
a7b7efe
Compare
67bb149 to
516e926
Compare
|
A data point from a real shader for the case listed as remaining ("splitting that adds one block per round … still runs all its rounds"). vkQuake (LibreQuake) homebrew PPSA99010 on a RADV (Mesa 26.2) build: compute shader
The title waits 10 s for the dispatch, so 18.4 s still stops it. The change (+15/−2 in I'll open it as a follow-up with |
When every attempt of the structurizer ladder refuses a graph, the title aborts. Rebuild such a graph from a hierarchical decomposition into strongly connected components instead: each original block keeps one copy and runs behind a guard on its own reach flag, and each component becomes a loop with a single latch that repeats it while a back edge set its again flag. Every edge sets the reach flag of its target, so exactly one flag names the next original block and the blocks run in the input's order for every branch decision. Irreducible and non-terminating graphs are handled the same way, and no block is cloned. The ladder runs first and its output is unchanged. The fallback only runs when both attempt series refuse; if it cannot structure the graph, the ladder's error is reported as before. Blocks that start the top-level order unconditionally run unguarded, so the entry block's results reach later blocks without a stale path. APS5_FORCE_STRUCTURIZER_FALLBACK=1 uses the construction for every supported graph, for tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zer fallback Structurize eight graphs the ladder refuses on main (a duplicate merge, a region that wraps around to its dominator, the merge-splitting budget, a conditional without a structured merge, an irreducible entry and three loops that never exit) and the boykopovar#2642 two-latch loop, which the ladder accepts. Each must give structured branches, every original block exactly once and the same executions as the input for every branch decision. Register agc_structurizer_forced_fallback and agc_shader_control_flow_forced_fallback, which run the in-tree graphs and RDNA programs, through Recompile and SPIR-V validation, with APS5_FORCE_STRUCTURIZER_FALLBACK=1. Under that switch the split-kind checks become checks that no block is cloned and a route variable is used, and the nested-selection check names blocks by their original instructions instead of their position. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…und shared merge splitting growth The structurizer's attempts could spend more than 10 s on a graph of 20 blocks before they accepted or refused it; in a title that stalls shader preparation. Nearly all of that time went to recomputing dominators and post-dominator fixpoints while merge splitting grew the graph to hundreds or thousands of blocks. Dominators, post-dominators and the fixpoints in mergeBesideReturns, iterationSelectionMerge and joinWithinIteration now sweep the blocks in reverse post-order (dominators) or post-order (the others) instead of in id order. Splitting appends its new blocks at the end, so id order needed a sweep for almost every new block. Each fixpoint starts from the full set and its equations are monotone, so every sweep order reaches the same greatest fixed point and the result does not change. Shared merge splitting also refuses an attempt once the graph has more than 16 times its block count at the start of splitting (at least 64 blocks). Without it, splits that route through a new junction each round grew some graphs past 4,000 blocks before the round budget ran out. A refused attempt goes to the next attempt or to the reach-flag fallback, as it does after the round budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dget Structurize the 20-block graph whose merge splitting adds one forwarder per round until its round budget runs out, and a 12-block loop whose 2000-word latch is too large to clone, so merge splitting keeps adding junctions. Both must give structured branches, every original block once and the same executions as the input. Give the loop an unsupported exit, which keeps the reach-flag fallback out, and require Structurize to report the growth budget refusal instead of the round budget refusal it reached before after 676 blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
516e926 to
07d4b36
Compare
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
|
…time CPU (#482) * perf(shader): rewrite high-fan-out IR uses in linear time Ported from upstream PR boykopovar#4035 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(shader): index specialization tables by SPIR-V id Ported from upstream PR boykopovar#4772 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(shader): honor disabled optimization during specialization Ported from upstream PR boykopovar#4827 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(shader): discard helper functions unreachable after specialization Ported from upstream PR boykopovar#5064 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(shader): drop module constants nothing references after specialization Ported from upstream PR boykopovar#5315 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(shader): cap SPIR-V scalar replacement Ported from upstream PR boykopovar#4288 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(recompiler): iterate structurizer fixpoints in post-order and bound shared merge splitting growth Includes the stacked upstream commit that structurizes refused control flow with reach flags, which this change builds on. Ported from upstream PR boykopovar#5028 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(shader): emit image_bvh_intersect_ray as one shared function per variant Ported from upstream PR boykopovar#5455 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(shader): avoid redundancy traversal map copies Patches the bundled SPIRV-Tools redundancy elimination (only built with ANYPS5_ENABLE_SPIRV_TOOLS=ON). Ported from upstream PR boykopovar#5511 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(metal): accept a specialized BDA module that dropped one of its two BDA descriptors Specialization removes descriptor variables the final module no longer references. With helper functions now discarded after specialization, a compute module can drop its page-table variable while keeping the fault buffer, and the bridge then rejected the dispatch ("BDA ABI requires page-table and fault-buffer descriptor metadata"). Require at least one of the two roles instead of both; the module's own descriptors are still checked against the prepared plan. Not a port; needed by the port of upstream PR boykopovar#5064. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing this together with #4926, which it builds on, following the request there to hold off on pull requests that do not name a title needing the change while the queue is this long. The branch stays; if a title stops on refused control flow on current |
|
Reopened with #4926; reviewing both now. |
What
The structurizer's attempts can spend seconds to minutes on a small graph before they accept or refuse it. With the oracle described under Tested, about 1,150 of 200,000 random graphs of 2 to 32 blocks take more than 10 s, on
mainand with #4926 (1,142 and 1,149; the count depends on machine load), and some take more than 300 s. One 20-block graph takes 10.3 s. In a title, that time is spent while the shader is prepared.A profile of 41 of these graphs puts 73% of the time in
computeDominatorTree, 13% initerationSelectionMerge, 6% injoinWithinIterationand 5% inmergeBesideReturns, all recomputed while shared merge splitting grows the graph. Every slow attempt ends at the split round budget (4 rounds per block at the start of splitting), in one of two ways:In production code, this PR changes only
Structurizer.cpp:mergeBesideReturns,iterationSelectionMergeandjoinWithinIterationsweep the blocks in reverse post-order from the entry (dominators) or in post-order (the others), unreachable blocks last in id order. They swept in id order, but splitting appends its blocks at the end, so a sweep carried a set only one new block further.computePostDominatorsalready used this order;analysisOrderreplaces its inline copy. Each fixpoint starts from the full set and its equations are monotone, so every sweep order reaches the same greatest fixed point and no output changes.joinWithinIterationalso tests membership with a bit vector instead of a linear search, andmergeBesideReturnsinserts into its already sorted set.splitSharedMergeBlocksrefuses the attempt once the graph has more than 16 times its block count at the start of splitting, or more than 64 blocks when that is larger (CFG shared merge splitting exceeded growth budget). The budget counts blocks, not time. A refused attempt continues exactly as after the existing round budget: the next attempt, then the reach-flag fallback of feat(recompiler): structure every control-flow graph the structurizer refuses #4926. With the sweep-order changes alone, 360 of the 1,220 graphs that took more than 10 s in any run onmainor feat(recompiler): structure every control-flow graph the structurizer refuses #4926 still do.The growth budget is a heuristic. Succeeding attempts grew at most 8.6 times on these 200,000 graphs, 9.4 times on 200,000 graphs of another seed and 10.6 times on 12,000 graphs of up to 128 blocks. An attempt that would succeed only beyond 16 times now gives a later attempt's output or the fallback's, which is valid and equivalent but not the same. No such graph was found, but shaders are larger than these graphs.
Splitting that adds one block per round stays under the growth budget and still runs all its rounds. On 2,000 random graphs of up to 128 blocks, 9 still take more than 10 s with this PR, at most 63.5 s; with the sweep-order changes alone, 28 take more than 10 s and 13 more than 300 s. The technical debt entry of #4926 now records the budget and this limit.
Tested
WSL2 Ubuntu 26.04, GCC 15.2.0, configured like CI (Release, SPIRV-Tools ON, lavapipe), on top of #4926. No title.
In-tree:
agc_structurizergains three cases. The 20-block graph and a 12-block loop whose 2000-word latch is too large to clone, so that splitting keeps adding junctions, must give structured branches, every original block once and the same executions as the input. The same loop with an unsupported exit, which keeps the reach-flag fallback out, must be refused at the growth budget. On feat(recompiler): structure every control-flow graph the structurizer refuses #4926 alone that case fails: it reports the round budget after the graph reached 676 blocks.g++ -O2, the test takes 0.4 s with this PR. On feat(recompiler): structure every control-flow graph the structurizer refuses #4926 alone it fails after 16 s (39 s withAPS5_NO_PERF_ITERATION_SELECTION=1).ctest -R "structurizer|control_flow": 8 of 8 pass in three runs.agc_structurizertakes 0.3 s, and under 1 s in each of its variants.ctestin the CI configuration: 1014 tests, 0 failed.tools/check_skips.pyflags onlyguest_video_out_high_frame_rate, which skips because the WSL display has no 120 Hz mode.tools/check_test_registration.pypasses.tools/check_conventions.py --base origin/mainpasses with both the branch's checker andmain's.maincac4e96 with feat(recompiler): structure every control-flow graph the structurizer refuses #4926: fullctestin the CI configuration, 1032 tests, 0 failed. Then onto 4bb2c7f, where only the technical debt file conflicted: the structurizer, control-flow and shader tests, 18 of 18.agc_structurizertakes 0.5 s and at most 0.9 s in its variants.Out of tree, with the oracle described in #4926 (graph contract, SPIRV-Tools on a skeleton module at SPIR-V 1.3, 1.4 and 1.6, and an exact equivalence decision), on the same 200,000 graphs (seed 1, 2 to 32 blocks, 10 s per graph), built against
mainand against this branch and run on the same machine at 16 jobs:main: 81,078 graphs accepted, 117,780 refused, 1,142 past 10 s. The run took 2,235 s.mainaccepts give byte-identical outputs. Against the run made for feat(recompiler): structure every control-flow graph the structurizer refuses #4926, the 198,851 graphs that feat(recompiler): structure every control-flow graph the structurizer refuses #4926 finished give byte-identical outputs too.mainor with feat(recompiler): structure every control-flow graph the structurizer refuses #4926 in either run give the same outputs as this PR built without the growth budget and run until it finished. That build still took more than 10 s on 360 of them, and up to 203 s (12 graphs at a time).The same code, run earlier on other graphs: on 200,000 graphs of seed 2 (2 to 32 blocks), all 199,892 graphs that a build with the sweep-order changes alone finished within 60 s give the same outputs, and the slowest graph takes 2.3 s. On 2,000 graphs of up to 128 blocks, all 1,987 that build finished within 300 s give the same outputs.
A second check on graphs of seed 7, with this branch at ebcb1068c:
mainaccepts give byte-identical outputs, and so do the 79,632 graphs feat(recompiler): structure every control-flow graph the structurizer refuses #4926 finished within 10 s. None is refused or takes more than 10 s;maintakes more than 10 s on 366. Two runs of 40,000 of these graphs at different job counts give identical outputs, and an AddressSanitizer build gives the same outputs on 5,000 of them without a report.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code