Repository navigation
perf(agc): allow shader preparation on first use - #5872
Conversation
CnRJay
left a comment
There was a problem hiding this comment.
The memory numbers are a real problem for Demon's Souls. Before an opt-in mode, though, it is worth knowing what the extra 5 GB is.
In lazy mode, the first draw or dispatch of each shader goes through SourceHandleFor → PrepareShaderWithDiagnostics on the thread recording it, while it holds snapshot.prepared->mutex. With a cold disk cache that is a full recompile (SPIR-V generation and optimization) on the queue worker. With a warm one it is a disk-cache load and parse. Both stall the queue in the frame where the shader first appears, which is the stutter the background registration pool was added to avoid (#2583). The PR measures memory only, so the cost on the other side isn't visible. The summary lines of one scene with the opt-in off and on, warm cache, with frame_ms p99/max, would show it, for Demon's Souls or a title that doesn't run out of memory.
On the memory itself:
- If most of the 17 GB is prepared SPIR-V or artifacts held for shaders that are never drawn, a smaller default for the prepared-artifact cap or evicting prepared entries by last use would fix it for every user without a switch.
- If it is the queued
RegisteredPreparations, #5910 removes the decoded program each one holds, which was 638 MiB in Borderlands 4.
A heap profile at the 12 GB / 17 GB points would show which of these it is. As it stands, Demon's Souls still runs out of memory by default, and users would need to know about the variable.
|
Tried this on Resident Evil 2 (PPSA04288), Windows 11, RX 9070 XT, 32 GB. Local integration tree on main 93d69ed; the only difference between the runs is APS5_LAZY_SHADER_PREPARE. Same build, warm shader cache (~8,000 shaders), 300 s boot through the first-boot setup screens, private bytes sampled every 10 s:
So about 1 GB less here, with the same frame rate and the setup screens reached a bit sooner (161 s vs 170 s). Smaller than on Demon's Souls, because most of RE2's footprint at that point is its own direct memory rather than prepared shaders, but it's a clear win with no downside I could see. I didn't reach gameplay in these runs. |
|
Thanks, this was the right question. I measured it, and the answer points at your first option. Short version: with the opt-in off, registration loads the prepared artifact of every registered shader at boot, about 6,300 shaders, while the game has drawn only ~170 by the intro. The extra memory is those undrawn artifacts, and it doesn't show up as private heap. Demon's Souls (PPSA01341) boot, Windows 11, RTX 4070, 32 GB RAM,
What this rules in and out:
So a cap, or evicting prepared entries by last use, should fix this for everyone with no switch, as you suggested. I'll look at that as a follow-up and close this PR once it lands, unless you'd rather it closed now. On the stutter cost: I can't give |
adriadam10
left a comment
There was a problem hiding this comment.
Checked the diff, built it merged onto main and ran the AgcDriver ctest scope (only agc_driver_tessellation fails, it segfaults on main too).
tests/execution/PreparedShaders.cpp:623(LazyRegistration, modelazy): it passes with theShaderRegistry.cppand docs change reverted, so it doesn't test the opt-in. It only checks dispatch output and error reporting, which eager preparation also gives. Please assert something only lazy mode does, e.g. that registration leaves the snapshot deferred (no prepared artifact / no disk-cache load until the first dispatch), and show it fail without the change.lazy_syncdoes fail without it.- @CnRJay's first-use stall question is still open:
frame_msp99/max with the opt-in off and on, warm cache, on a scene that doesn't run out of memory. - Your own measurements point at retention (cache entries mapped for every registered shader), and you said a cap or eviction by last use would fix it without a switch. If that is the plan, this env var is a stopgap for one title; say whether you want this merged meanwhile or closed in favour of the follow-up.
The memory numbers (Demon's Souls 17.1 → 12.2 GB, RE2 15.2 → 14.3 GB by @AnthonyClemens) are clear. Happy to re-review once the test is fixed.
|
Thanks for the review. All three points are addressed below. 1. The
2. First-use stall: Demon's Souls' intro, warm cache, same build (
On this scene the opt-in added no visible hitches. The caveat: with the opt-in off this machine pages (27.8 GB private in a 10.2 GB working set), so some of the off-side spikes are paging. I don't have a scene where the opt-in off fits in RAM here, so this doesn't fully isolate first-use preparation cost on a machine with headroom. 3. Stopgap or close: I'd like this merged as an opt-in stopgap. It's off by default, and for Demon's Souls and RE2 it's the difference between booting and running out of memory. Meanwhile I'll work on the general fix you described (bounding or evicting prepared entries by last use) as a separate PR, and close this one if that change covers the same case without a switch. |
APS5_LAZY_SHADER_PREPARE=1 keeps registration validation and decoding but skips queueing the preparation. Draws and dispatches prepare a deferred snapshot on first use and cache it; explicit ABI resolution still prepares its stages. Titles that register thousands of shaders at boot no longer hold every prepared shader in memory before the first frame. Cover lazy dispatch, metadata and failure cases in both asynchronous and synchronous modes.
The lazy mode of agc_driver_prepared_shaders only checked dispatch output and failure reporting, which eager preparation also produces. Read the registered snapshot through Driver::RegisteredShaders: registration leaves it deferred with no prepared artifact, and the first dispatch prepares it.
f515ed2 to
c8bedff
Compare
|
Demon's Souls (PPSA01342), measured into the level rather than boot only. Windows Server 2025 (26100), RTX 3060 (driver 591.86), Ryzen 9 5900X, 32 GB RAM, 64 GiB pagefile (commit limit 95.9 GiB). Each run is 40 min: boot, the debug walk-through menu, Tutorial (Fort Entrance) picked by scripted pad input, the level load, then idle in Outpost Passage. Memory is sampled every 10 s, in GiB, one run per row. The build is not
¹ Shader disk-cache hits plus misses over the run; "cold" is the misses. ² At the final Cross on the walk-through menu. ³ Mean from the first frame dump showing the level HUD to the end of the run. ⁴ System commit / system commit minus other processes' private bytes (other processes held 15–17 GiB at each run's commit peak). ⁵ Set by short spikes at 1,320–1,660 s, about 15 min after B's last cache miss was compiled (all 4,642 were done by about 450 s). We haven't identified the cause, and A has no such spikes. Five single 10 s samples sit 4–9 GiB above their neighbours, and one of them sets the commit peak. Peak private is the process's peak-commit counter, which also caught larger transients between samples; no 10 s sample of B's private bytes is above 48.7 GiB. Without the five spike samples, B's highest private sample is 43.5 GiB and its peak commit is 71.3 / 56.3 GiB.
On a 32 GB machine, the opt-in is the difference between paging about 22 GiB and about 1 GiB. That supports keeping it as the stopgap until the cap/eviction change lands. Nit (optional): GPUs tested: NVIDIA GeForce RTX 3060, driver 591.86, Windows Server 2025. |
adriadam10
left a comment
There was a problem hiding this comment.
Re-checked. LazyRegistration now asserts the snapshot is deferred with no entries after registration and prepared after the first dispatch; with the lazy branch in ShaderRegistry.cpp disabled, agc_driver_prepared_shaders_lazy and _lazy_sync fail. Built merged onto main: all 11 agc_driver_prepared_shaders* pass; the only other failures (agc_driver_tessellation, agc_driver_preparation_scope_settle) also fail on main.
Memory numbers are backed by @AnthonyClemens (RE2) and @fdfawef324 (Demon's Souls), and the frame_ms data answers the first-use question. Non-blocking: APS5_LAZY_SHADER_PREPARE=0 also enables the mode, same as APS5_SYNC_SHADER_PREPARE. Merging as the opt-in stopgap.
|
Re-checked after main changed Memory numbers are backed by @AnthonyClemens (RE2) and @fdfawef324 (Demon's Souls). Non-blocking: |
|
Re-checked after main moved again: same head, merges cleanly. Built merged onto current main and ran the AgcDriver ctest scope: all 11 Memory numbers are backed by @AnthonyClemens (RE2) and @fdfawef324 (Demon's Souls). Non-blocking: |
1 similar comment
|
Re-checked after main moved again: same head, merges cleanly. Built merged onto current main and ran the AgcDriver ctest scope: all 11 Memory numbers are backed by @AnthonyClemens (RE2) and @fdfawef324 (Demon's Souls). Non-blocking: |
Brings #5872 (shader preparation on first use), main's async driver log and its geometry subgroup checks. The prepared-shader modes are the union of both lists. With this branch, the pipeline ABI of a lazily registered shader it cannot prepare leaves the stage deferred instead of throwing, so LazyRegistration now requires the stage to stay deferred with no prepared entry after that call; its dispatch and shutdown failure checks are unchanged. StderrCapture flushes the async log when it starts and before it reads, since a deferral's log line can otherwise still be queued. The undecodable-state check now matches the decode's VGT_SHADER_STAGES_EN reason rather than one message, which main's subgroup output check now gives first.
What
Adds opt-in
APS5_LAZY_SHADER_PREPARE=1: shaders are still validated and decoded at registration, but prepared on their first draw or dispatch instead of all at boot. The default is unchanged.Why: Demon's Souls (PPSA01341) registers thousands of shaders during boot. Preparing all of them up front pushes private memory to 17 GB and runs a 32 GB Windows machine out of RAM before the first real frame. #5806 reports the same on
mainunder an 18 GiB cap.main+ #5870main+ #5870 + this PR, opt-in on(#5870 fixes an earlier abort on
main; without it the game stops before this point. This PR doesn't depend on it.)Tested
Windows 11 (26200), WinLibs GCC 15.2.0, Release, RTX 4070.
agc_driver_prepared_shaders*: 12 passed, including the newlazyandlazy_syncmodes. These cover dispatch, metadata and failure reporting on first use.^(agc_|windows_backtrace_): 534 passed, 0 failed, 3 skipped (two udmabuf tests are Linux only; the RTX 4070 has no stencil export).PR overlap: #5115 edits a nearby line of
tests/execution/PreparedShaders.cpp. Either can merge first; the rebase is trivial.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)