Skip to content

fix(shader): release specialized modules that no result uses - #10473

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
milescoviello:fix/release-unused-specialized-modules
Oct 25, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
milescoviello:fix/release-unused-specialized-modules

Conversation

@milescoviello

@milescoviello milescoviello commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What

Three caches in Recompiler.cpp keep every specialized module for the rest of the run: specializeModule's map (by variant and specialization constants), each prepared binding plan's modules, and registrationModule's map. Since #8340, the SPIR-V interning table holds every module's words as well. A module's words outlive the results that used them, so the memory only grows. Once the draw caches start evicting (#9129's byte budget, for example), most of it belongs to no result.

In Ghost of Tsushima (PPSA02225), with a diagnostic that counts the interned modules held only by these caches (Linux, RTX 4070 Ti, the stack the title needs to reach its opening plus #8340):

time interned modules MiB held only by the module caches
4 min 843 223.5 271 modules, 85.8 MiB
6 min 1,350 384.1 803 modules, 170.0 MiB
8 min 1,679 464.4 1,035 modules, 220.1 MiB
10 min 2,183 578.9 1,658 modules, 344.8 MiB
12 min 2,197 588.6 1,548 modules, 323.2 MiB

The title was killed by its 20 GB cap at 633 s in its opening. The same count in lighter titles: MGS4 (PPSA18194) 24 MiB idle of 44 MiB, MXGP 26 (PPSA30839) 2 of 110 MiB, Super Meat Boy 3D (PPSA31162) 7 of 123 MiB, at 300 s each.

How

  • SharedSpirv::Weak is a reference that does not keep the words alive: Lock() returns them while any SharedSpirv still holds them, and an empty module after that.
  • InternSpirv keeps weak references and drops the expired ones from the bucket it searches. Equal words are still shared while a holder lives, as fix(shader): share the words of identical specialized modules #8340 does.
  • SpecializedModule keeps the interface: its bindings, push data use, specializationId, the content hash of its words, and a weak reference to the words. buildSpecializedModule returns the module together with the words it built.
  • specializeModule, registrationModule and the prepared plan's module cache return the words with the module. When the words were released, they are built again from the same key under the module's mutex. That gives the same module, because the key carries the variant and every specialization constant, including the vertex input classes. The content hash is checked (a mismatch throws), so the specializationId that names the module's pipelines stays valid.

Tested

Ubuntu 26.04.1, GCC 15.2, Release, RTX 4070 Ti, NVIDIA 615.71.09. The commit was tested as 09a7d25150 on main 9d4fb4f and is unchanged (git range-diff) on the current head 611daf97e8, which @adriadam10 rebased onto main aeeaefa and which adds 611daf97e8: two test helpers (DenormalFlushElimination.cpp, RedundantValueElimination.cpp) bound a reference to the words of a temporary result, which this change now frees. The 38 tests below pass 3 of 3 on aeeaefa and on 611daf97e8; on the earlier head 460029a65a they passed 3 of 3 on main 379b1dc too.

  • New verifySpecializedModuleRelease in agc_shader_memory: a cached pixel request that gets specialized is recompiled. A weak reference to its result's words expires once the result is gone. Recompiling it then gives the same specializationId and the same words, and a second live result shares them. New testInternedSpirvReleasesUnusedWords in agc_spirv_module: interned words are released after their last holder, and interning them again gives the same words.
  • With the interning table changed to pin every module (test/specweak-mut), both fail 3 of 3: specialized module release: the module caches kept the words of a module no result uses and the SPIR-V interning table kept words no holder uses.
  • The 38 tests matching ^agc_shader_memory$|^agc_spirv_module$|^agc_shader_|shader_disk_cache|^agc_driver_graphics$|pipeline_journal pass 3 of 3 on main 9d4fb4f and on 09a7d25150.
  • Khronos validation layer (LunarG SDK 1.4.363), the 5 tests matching ^agc_shader_memory$|^agc_spirv_module$|^agc_shader_disk_cache$|^agc_driver_graphics$|^agc_driver_pipeline_journal$: 5 of 5 on main 9d4fb4f and on 09a7d25150. Neither log reports the layer missing, and neither has a VUID.
  • Windows 11 VM (GeForce GT 1030, driver 582.66, WinLibs GCC 15.2.0 r7, Release): the same 5 pass 3 of 3 on main at the merge base and on 09a7d25150.
  • Ghost of Tsushima with this change and fix(shader): key the device's capabilities and extensions by a digest #10438 (the cache key digest) on the same stack, seeded with the same pipeline cache, against the run above. The title was killed by its 20 GB cap at 1,260 s instead of 633 s. RssAnon at 4 / 6 / 8 / 10 / 12 min: 4,577 / 5,533 / 5,833 / 6,266 / 6,745 MB against 4,892 / 6,152 / 6,990 / 7,763 / 8,095 MB, up to 1.5 GB lower in that pair. A third run with both changes (19 GB cap, killed at 913 s) came out between the two at 6 to 12 min, so the per-minute difference is within this title's run-to-run spread; the run lengths and the direct count above are the firmer evidence. The two changes are measured together. NVIDIA's own shader cache was warmer for the second run, which helps its progress, not its memory.

Not checked by me: AMD, Intel, macOS.

Overlaps: #6959 (JJenkx) adds a specialization list to SpecializedModule, read from the built words, and copies it into each result, in the same functions this PR splits. Under this PR, that list belongs with the interface the module keeps: a rebuild gives the same words, so it stays valid. Whichever lands second carries it across. #9989 (mine) adds a test next to the same line of ShaderMemory.cpp.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none (it lowers the memory Ghost of Tsushima, PPSA02225, needs in its opening)
  • GPUs tested: NVIDIA GeForce RTX 4070 Ti, driver 615.71.09, Ubuntu 26.04.1; NVIDIA GeForce GT 1030, driver 582.66, Windows 11 VM
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed the exact head bb9c75b3546e76ae175c55cd30be9d9d2857f338, including the weak interning path, module-cache rebuild path, lifetime tests, and the reported main/change validation. I found no code-level blocker in those paths. The branch is based on db6af9e0fd8c75366dabc2066dbf90117f7490d0, while current main is 379b1dca32ecc4e3570ad0a53c69f019b70dbe59 (30 commits ahead); the repository requires PR branches to stay up to date with main. Please refresh the branch and rerun the focused shader tests on the updated main and change, then I can re-review. I did not independently run the GPU tests in this review.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

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.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6959 core/shader/recompiler/Recompiler.cpp L746, L1115-1116, L1156
PR 9508 core/shader/recompiler/Recompiler.cpp L1140-1141
PR 10803 core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp L462, L479

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 4278 core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp
PR 5021 core/shader/recompiler/Recompiler.cpp
PR 5371 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5609 core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 5925 core/shader/recompiler/Recompiler.hpp
PR 6188 core/shader/recompiler/Recompiler.hpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.hpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
PR 6539 core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 6858 core/shader/recompiler/Recompiler.cpp
PR 6924 core/shader/recompiler/Recompiler.cpp
PR 7201 core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 7410 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 7642 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 7684 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7689 core/shader/recompiler/Recompiler.hpp
PR 8004 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 8077 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 9020 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 9335 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 9356 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 9387 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 9748 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 9889 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 9897 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.hpp
PR 10049 core/shader/recompiler/Recompiler.hpp
PR 10193 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.cpp
core/shader/recompiler/Recompiler.hpp
PR 10233 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp

@github-actions github-actions Bot added shader core/shader agc libSceAgc* labels Oct 24, 2026
@adriadam10 adriadam10 self-assigned this Oct 24, 2026
@milescoviello
milescoviello force-pushed the fix/release-unused-specialized-modules branch from bb9c75b to 460029a Compare October 24, 2026 02:51
@milescoviello

Copy link
Copy Markdown
Contributor Author

@MarcZX10 refreshed: rebased onto main 379b1dc as 460029a65a, the commit unchanged (git range-diff). On that pair (Linux, RTX 4070 Ti, 615.71.09) the 38 tests matching ^agc_shader_memory$|^agc_spirv_module$|^agc_shader_|shader_disk_cache|^agc_driver_graphics$|pipeline_journal pass 3 of 3 on main and 3 of 3 on this head, the two new release tests among them. The Tested section names the new head.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

A caveat on the Ghost of Tsushima comparison above, from a third run. This one had both changes, plus a third that is inert on a fresh cache version, at a 19 GB cap instead of 20 GB. It was killed by the cap at 913 s. RssAnon at 4 / 6 / 8 / 10 / 12 min: 4,260 / 4,916 / 6,123 / 7,537 / 7,528 MB, against 4,577 / 5,533 / 5,833 / 6,266 / 6,745 MB in the earlier run with the changes and 4,892 / 6,152 / 6,990 / 7,763 / 8,095 MB without them.

So the run-to-run spread in this title's opening is about as large as the per-minute difference I reported, and those numbers shouldn't be read as the saving. The run lengths point the same way: 633 s without the changes, 1,260 s and 913 s with them, the last at the lower cap. The count of interned modules held only by the module caches (323 to 345 MiB at 10 to 12 min) is a direct count, not a comparison.

AI-assisted: yes (Claude Code)

milescoviello and others added 2 commits October 24, 2026 10:22
The specialized module caches (by variant and constants, by binding plan
and by registration key) and the SPIR-V interning table held every
module's words for the rest of the run, after the results that used them
were gone. They now keep a module's interface (bindings, push data,
specialization ID and content hash) and only a weak reference to its
words; a lookup after the words were released builds them again from the
same key, checks the content hash, and keeps the specialization ID that
names the module's pipelines. The interning table keeps weak references
and drops expired ones from the bucket it searches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the fix/release-unused-specialized-modules branch from 460029a to 611daf9 Compare October 24, 2026 08:52
@milescoviello

Copy link
Copy Markdown
Contributor Author

@adriadam10 thank you for the rebase and for 611daf9. Those two helpers bound a reference to the words of a temporary RecompileResult; with this change nothing else keeps the words alive, so the reference dangled. I searched the branch for the same pattern: every other Recompile(...).spirv.Words() is read within one full expression, and GetPreparedArtifact returns a reference to a stored artifact, so no other site holds words past their result.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

@MarcZX10 the branch is refreshed: @adriadam10 rebased it onto main aeeaefa and added 611daf97e8, which keeps the result alive in two test helpers that read words from a temporary. On that pair (Linux, RTX 4070 Ti, 615.71.09, GCC 15.2), the 38 tests matching ^agc_shader_memory$|^agc_spirv_module$|^agc_shader_|shader_disk_cache|^agc_driver_graphics$|pipeline_journal pass 3 of 3 on the merge base aeeaefa and 3 of 3 on 611daf97e8, the two new release tests among them. Could you take another look?

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

A check on @adriadam10's 611daf97e8 (Linux, RTX 4070 Ti, 615.71.09): on this change alone (0e1767b24b), agc_redundant_value_elimination fails 3 of 3, and agc_denormal_flush_elimination happens to pass. With 611daf97e8, both pass 3 of 3. So the reference to the temporary's words did dangle once nothing else kept them alive, and their fix is needed with this change.

AI-assisted: yes (Claude Code)

@adriadam10
adriadam10 dismissed MarcZX10’s stale review October 25, 2026 08:40

Dismissing a stale review: the requested changes were addressed after it (new head) and the merge queue blocks on it. Please request changes again if anything is still open.

@adriadam10
adriadam10 merged commit 66e6772 into boykopovar:main Oct 25, 2026
12 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Rechecked against current main (#6010, #8632 and #10496 touch other parts of Recompiler.cpp, no overlap with the module caches or the interning table). The weak interning path, the rebuild paths in specializeModule, registrationModule and the prepared plan's module cache, and both new tests were checked earlier. Our commit keeps the recompile result alive in the two test helpers that read its words. The full Build on this head is green; merged.

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks for the recheck against the newer Recompiler.cpp changes and for merging, @adriadam10.

AI-assisted: yes (Claude Code)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants