perf(vk): keep program stages' SPIR-V packed, make shader modules only per pipeline (~1.5 GB less RAM on NVIDIA) #2309
Open
DUDUYG wants to merge 3 commits from
DUDUYG/perf/spirv-packed into main
pull from: DUDUYG/perf/spirv-packed
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
2 participants
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2309
Loading…
Reference in a new issue
No description provided.
Delete branch "DUDUYG/perf/spirv-packed"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The problem
Every translated program keeps a
VkShaderModulefor each of its stages for as long as the program lives.Measured on this PC in play, before this PR:
In the memory map, two of the driver's ~1 GB allocations held 11,970 and 8,430 SPIR-V headers.
Microbenchmark with the game's own 20,400 modules (1636 MB of SPIR-V), driver 621.14:
The fix
Stages keep their SPIR-V packed. Each stage keeps its SPIR-V packed with zstd in
Stage::code, andStage::moduleis gone.Modules exist only while a pipeline is made. A
StageModuleunpacks the code and makes the module just for the pipeline being made, then destroys it.The new code,
stage_code()andStageModule, is ingpu/vk/vk_stage_code.cpp.Where the modules are made now:
build_graphicsmakes only the modules of the stages that the pipeline, or the library part, takes.FAIL_ON_PIPELINE_COMPILE_REQUIRED;Destroys removed. The module destroys are gone from:
program_cache_borrow;seed_variant_of;The code now belongs to the program.
zstd.
third_party/zstd/lib/compress/comes from the same zstd 1.5.7 release that is already vendored. It is unmodified, BSD, and has the same tarball SHA-256 as recorded inPROVENANCE.md.-DZSTD_MULTITHREAD=0was removed fromnx2/build.py: zstd tests it with#ifdef, so defining it as 0 turned multithreading on.zstdmt_compress.cis vendored with the rest of the folder but not built.⚠️ Costs and things to know before merging
Every player translates the program cache once more.
gpu/vk/vk_shader.cppis part of the translator stamp, so the first start after updating translates all programs again.hookshot_programs.bincompared record by record, 98,200 of 98,200 modules the same. So nothing in the driver's caches goes stale.vk_shader.cppitself (removingmake_moduleand the destroys) could not be avoided.Only NVIDIA was measured (RTX 5070, driver 621.14, Windows 11).
-fPIClike the decoder's.Translation and loading do a little more work.
Precompile time:
These are not a clean A/B: "before" ran from a copy on an NVMe disk and "this PR" from the SATA SSD. Both used the same
mkw_vk_pipelines.bincontents and the same driver cache.The recorder.
vk_stage_code.cppdoes not includevk_work.h, so its module destroys are no recorder boundary: no recorded command uses a shader module. It is listed intest_recorder_sync'sBOUNDARY_OKwith that reason, likevk_shader.cpp's destroys before it. SoRX_VK_RECORDER=deferand=threadneither drain nor wait for them, also when the dispatch's ask makes and destroys a module on the replay thread.Overlap with #1630 (compressed program cache, open).
nx2/build.py, andtests/program_cache_cleanup_test.cpp.third_party/zbic's single-file zstd into the game. That file'sPROVENANCE.mdsays it carries GPLv2 parts from Atmosphere-NX and is not linked into the game. This PR instead vendors the official encoder next to the decoder already used.Measured
Test setup:
--prepare-shaders.mainfrom the same day, plus open PRs #2198, #2222 and #2193, plus local logging (present times, memory map). The comparison build is the same thing without this commit.One run each:
About the frames over 20 ms:
A discarded run. A first run of this PR (not in the table) had 10 frames over 33 ms, the worst 102 ms.
Tests
tests/program_cache_cleanup_test.cppandtests/test_program_cache_cleanup.pynow check which program cache records are taken and that each taken stage keeps its code whole, through the production loader, with astage_codestand-in that fails when told to. They used to count module lifetimes. Cases: first or second stage not kept, truncated, empty, answers past the stages or slots, valid, mixed.tests/test_precompile_compute_guard.py's fixture has aStageModulestand-in. The module must be alive during the create call and gone after it.tests/test_mesh_fallback_failed_once.pyfinds the failure block after the compile, which may now also be the module's.tests/test_shader_stamp_cache.pyandtests/test_dlss_sdk_cache.pyknow the zstd encoder sources andzstd_users.tests/test_recorder_sync.pylistsvk_stage_code.cppinBOUNDARY_OK(its module destroys), andvk_shader.cpp's reason no longer names modules. The first CI run failed here: my local run had the new file still untracked, so the test did not see it.Checked:
RX_VK_ONLY=1 nx2/build.py --standalone), and the whole game builds withmake_game.py.tests/run_all.py: 311 passed, 60 skipped.test_precompile_compute_guardandtest_fpx_stickywere run with the MSVC headers andlld-linkand pass.main(49f75dc8):nx2/test_nup_read_name.pyandtests/test_bake_portability.pyneed thezstandardmodule;nx2/test_nup_paths.pyneeds the symlink privilege.Also seen while measuring (not changed here)
With the shipped state lists,
mkw_vk_pipelines.binstayed at exactly 999,490 KB while the precompile compiled its 15,456 graphics pipelines, so it was never saved again. Those pipelines came from NVIDIA's own disk cache:hookshot.exe, the first one or two starts took 135-137 s.I haven't found why the app cache stops growing near 1 GB. It is a separate issue.
🤖 Generated with Claude Code
Nice work. The memory map is good evidence, and so is the one-at-a-time microbenchmark. I went through the lifetimes and found nothing wrong:
StageModulelives in the scope of its create call. That coversbuild_graphics(themodulesvector lives to the end of the function, aftergraphics_with, which is synchronous), the mesh fallback, the warm, the precompile, the dispatch's ask and its queuedbuildlambda (the module is made inside the lambda, on the worker), and the dispatch's own compile. No recorded command or later link uses a module: GPL linking takes the library pipelines, not modules. So theBOUNDARY_OKentry is right.Stage::codeis only read after a program is built. The pack context and the unpack buffer arethread_local, and the log counter is atomic. Parallel prepares and cache reads (16 threads) share nothing new.build_graphicsmatches thepart_stagesfilter below it. Parts 0 and 3 now make no modules, and parts 1 and 2 make only their own stages. That is also a small win, because before, every part got every stage's module.gpu/vkchainsVkShaderModuleCreateInfointo the stage info. Every path goes through a module, so no maintenance5 path is affected.kept), and you checked that the SPIR-V is byte-identical. Sohookshot_programs.binandmkw_vk_pipelines.bin/driver caches stay valid. The one cost is the stamp bump: every player translates everything again once (~3.5 min on a 9800X3D, more on slower CPUs). @NotRexed should decide whether that's fine, and also how this fits with #1630's zstd.ZSTD_MULTITHREAD=0catch is a good one.Before this goes in:
glslang/spirv-valwere missing and the appimage job stopped at System packages. But that means nothing has compiled the new zstd encoder on Linux yet, in CI or locally. Please rerun the jobs so the appimage/Linux build goes green.DISABLE_OPTIMIZATIONretries) now create the pipeline twice from the same live module. That should be fine, but no one has run it.One nit inline.
@ -0,0 +53,4 @@return;}static std::atomic<int> told{0};const size_t n = ZSTD_decompress(words.data(), words.size() * sizeof(uint32_t), st.code.packed.data(), st.code.packed.size());nit:
ZSTD_decompressmakes and frees a wholeZSTD_DCtxon the heap on every call (ZSTD_HEAPMODEis 1 by default), so that's one ~100 KB malloc/free per module made. Athread_local ZSTD_DCtxwithZSTD_decompressDCtxwould match the pack side'sCtx. It's small next tovkCreateShaderModule, but this now runs on every pipeline compiled in play.The size is vendored zstd: about 23,350 lines of
third_party/zstd/lib/compress/, byte-identical to the official v1.5.7 tarball (the SHA-256 matchesPROVENANCE.md), BSD-licensed. The real change is about 290 lines. EveryStageModulelives within its create call, nothing outsidegpu/vkusesStage::module, and dropping-DZSTD_MULTITHREAD=0is right.Still open:
Re-review at
67bb1f7. The only change since my last review is the merge of main, and it is resolved correctly:program_cache_borrowmoved tovk_progcache.cpp(#2323). They now keep the code withstage_codeand destroy no modules,make_moduleis gone fromvk_progcache.h, and theBOUNDARY_OKreason forvk_progcache.cppis updated to match.zstd_jobstakesencoder=Falseby default, so the runtime's decoder job from #2270 builds as before. Only the Vulkan back end gets the encoder.Stage::module. Main has moved 53 commits since this merge. Those commits add no new uses of it, and a three-way merge of the files both sides touch (vk_draw.cpp,vk_shader.cpp/.h,nx2/build.py,test_recorder_sync.py,seed_variant_cleanup_test.cpp) applies cleanly.Still open from last time:
ZSTD_DCtxnit inline is unchanged. It's still only a nit.I agree with Joey-Magnus that this should ship together with the other PRs that force a full re-translation, so players re-prepare once. The stamp bump and how this fits with #1630's zstd are still for @NotRexed to decide.
Nit: the runtime's decoder job (
nx2/build.py, the"zstd_"call) still passes-DZSTD_MULTITHREAD=0. That's harmless for the decoder alone, but now that the docstring says the flags leave it out, you could drop it there too for consistency.Can you do re-measurement and see if the frame pacing is worst?
I re-measured with the two builds alternated A B A B A B on the same disk. Frame pacing is not worse with this change, and the earlier 157 vs 236 gap was noise.
Setup:
The two arms:
4123715e).ZSTD_DCtxchange (test buildc05ce25b).Both are on the same older
main(ebbba2e), with the same extra PRs (#2198, #2222, #2193) and the same logging. The only difference is this PR's change.¹ the mean change in frame time from one frame to the next.
Reading it:
Also pushed
c251eab2:vk_stage_code.cpp: athread_localZSTD_DCtxwithZSTD_decompressDCtx, like the pack side's context (your inline nit).nx2/build.py: the runtime's decoder-onlyzstd_jobscall without-DZSTD_MULTITHREAD=0.The renderer builds (
RX_VK_ONLY=1 nx2/build.py --standalone, llvm-mingw, no warnings). These pass:shader_stamp_cache,dlss_sdk_cache,program_cache_cleanup,seed_variant_cleanup,precompile_compute_guard,recorder_sync,game_font,mods_size_tableandmesh_fallback_failed_once. The B runs above include the DCtx change. Their prepare made all 15,456 graphics and 1,200 compute pipelines through it, with no unpack or module error.Still not done: a run on AMD or Intel. The Linux CI is queued.
Tested on
RTX 5090(32 GB VRAM, Windows 11, 98 GB RAM, NVIDIA 621.14) withRX_RELEASE=1(17,888programs,25,303graphics and799compute pipelines precompiled at startup), as part of isolatingmain's+5.86 GBPrivate Bytes /+2.51 GBWorking Set increase since49f75dc8.1. Where
main's+5.86 GBPrivate /+2.51 GBWorking Set increase goesComparing
49f75dc8(15,004 MBPeak WS /26,280 MBPeak Private) to currentmain(17,510 MBPeak WS /32,140 MBPeak Private) in Kokiri Forest (2048x1152uncapped,RX_VK_FRAMEGEN=0,HOS_FPS_MAX=240):+2,944 MB(2.88 GB) Private Bytes / VRAM (0 MBWorking Set): Strandedimages_pool()startup pre-allocation (#2438,867ddd7f), which pre-allocatesminBlockCount = 24(3,072 MB) inimage_pooleven thoughRX_VK_COMPACT(textures_pool()) andRX_VK_TARGET_BLOCKS(target_images_pool()) are on by default (23 of 24 blocks = 2,944 MBstay permanently empty; covered by #2711 / #2759).+2,916 MB(2.85 GB) Private Bytes and+2,506 MB(2.45 GB) Working Set: Expanding the shipped pipeline state list from15,290 -> 25,303graphics pipelines (+10,013pipelines) and11,748 -> 17,888loaded programs (+6,140programs), each holding unpackedStage::codeSPIR-V and liveVkShaderModulehandles across the run. Replacinghookshot_pipeline_states_shipped.binonmainwith49f75dc8's list (15,290pipelines) +RX_VK_IMAGE_PREALLOC=0bringsmaindown to14,727 MBPeak WS /25,863 MBPeak Private (-2,783 MBWS /-6,277 MBPrivate).2. #2309 A/B on
main(RX_RELEASE=1, all 25,303 graphics pipelines precompiled)RX_RELEASE=1, 25,303 pipelines)mainmain17,888progs /25,303pipes)p95/p99ms)maincontrol (c544828f)2,048 mscache read)17.07/19.61)DUDUYG)2,374 mscache read + zstd pack)16.81/18.68)Verdict:
17,888-program /25,303-pipeline precompile onmain, #2309 saves-2,389 MB(-2.33 GB) of Peak Working Set (recovering 95% ofmain's entire+2,506 MBWorking Set growth since49f75dc8) and-2,537 MB(-2.48 GB) of Peak Private Bytes (recovering 87% of the+2,916 MBprecompile Private Bytes growth), while keeping all 25,303 precompiled pipelines.+0.53 s(3.16 svs2.63 s), time to first frame is unchanged (4 s), first-load stutter is0frames over 50 ms, and frame rate/pacing in Kokiri Forest is at least as fast (68.6 fps,p99 = 18.68 ms).The re-measurement settles the frame-pacing question. Three alternated A/B pairs on the same disk show B no worse, and the 157 vs 236 gap was noise. The
ZSTD_DCtxnit is fixed with a per-thread context (vk_stage_code.cpp).It conflicts with main again, in one place. #2525 added
st.phis = fallback_size::spirv_phis(spirv...)toprogram_cache_read(vk_progcache.cpp:~431), andfallback_compute_pipelinesreadsst.phison RADV GCN. When you resolve it, keep that line beforestage_code(st.code, spirv), because the SPIR-V isn't kept afterwards. I resolved it that way locally.tests/run_all.pypasses apart fromtest_lift_cache, which fails on main here, andtest_recorder_syncpasses when run alone. I also compiled the vendoredlib/compresswith gcc on Linux, since no CI run has built it yet.#2760 and #2800 (open) still use
Stage::module, so whichever lands second needs porting. AMD and Intel are still untested. The stamp bump should ship together with the other PRs that force a re-translation (#2306, #1975).