fix(vk): bound precompile thread concurrency via half_cpus (#1593) #1610
Open
oscarlosu-lab wants to merge 1 commit from
oscarlosu-lab/fix/precompile-concurrency into main
pull from: oscarlosu-lab/fix/precompile-concurrency
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!1610
Loading…
Reference in a new issue
No description provided.
Delete branch "oscarlosu-lab/fix/precompile-concurrency"
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?
This PR bounds worker thread concurrency in
precompile_run()using #2323's affinity-awarehalf_cpus()helper and honorsRX_VK_PRECOMPILE_JOBS(#1593). Without a cap, systems with high thread counts (especially on Linux using Mesa RADV / ACO) can spawn dozens of concurrent pipeline compiles simultaneously and exhaust system RAM.Why
In
gpu/vk/vk_draw.cpp(precompile_run()), the thread pool previously spawnedstd::min(usable_cpus(), 64u)compiler threads. On high-core-count systems, compiling many Vulkan graphics/compute pipelines concurrently can build excessive per-thread resident memory and trigger the Linux OOM-killer (#1593).Changes
std::clamp(half_cpus(), 1u, 16u), building on #2323'shalf_cpus()budget (already honoring process affinity viausable_cpus()) while keeping the compiler cap deliberately lower thanpredict_run()/state_list_run()'s 32 cap.RX_VK_PRECOMPILE_JOBS(capped at64u), leavingHOOKSHOT_JOBSstrictly for build-time concurrency so global build settings do not throttle runtime startup.tests/test_prepare_cpus.pysoprecompile_runis checked forhalf_cpus().Scope
Related to #1593 (closed, tracked in #1072). This bounds how many pipeline compiles run side by side, which is what #1593's RADV report showed. It does not fix the Intel ANV out-of-memory that mooki000 measured on a UHD 770: there a few ~250,000-word compute programs compiled together exhaust RAM even with 4 workers. That needs the large compiles serialized on their own, which is a separate follow-up.
#2414 caps the same pools by physical cores and keeps every smaller stage ceiling, so its cap would sit above this one: the two compose rather than conflict.
Actually maybe we should do CPU*0.75?
Maybe a slider in builder too like RAM would be good
Personally, I prefer that certain things be hardcoded and that the end user doesn't have so many controls that could generate instability, something more typical of a product that goes into production, I think the best approach would be to make a game that is compatible with all systems without each user having to adjust it.
Tested on an Intel UHD 770 (Raptor Lake-S, no mesh shaders), Mesa 26.2.5 ANV, Arch Linux, i9-13900K (32 threads). I merged this into main
872cb68and ran it headless with the game capped at 32 GB. On this machine it doesn't get the start-up precompile through.Same save-load scene and the same caches for both runs, start-up precompile on:
872cb68start-up: 702 of 17346)start-up: 675 of 17346)With this PR the game's memory sat at 9.6-15 GB for two minutes. Then it went to 20 GB and 30 GB in the last 30 s.
The worker count isn't what decides it here. A few compute programs are about 250,000 SPIR-V words each (
53caac5f38279735,f55579c6c7b39d05,a54f19e927d8faef,dad2a8fbef3fd020), and ANV takes minutes and several GB to compile each of them. They come one after another in the list, so any pool of four or more workers compiles them at the same time. I ran the build's shader preparation with only 4 workers. Memory stayed at 3-4 GB for 16 minutes. Then those four started compiling, and it went 3.5 → 10 → 25 → 32 GB in about ten minutes and was killed. With 16 workers (the default on this CPU) the same preparation was killed after 5 minutes.So this cap may well be right for RADV (#1593). On Intel it only changes how soon the OOM kill happens. What would help there is keeping the very large compiles from running at the same time, or leaving them out of the precompile. I haven't tested this on RADV.
Thanks a lot for testing this thoroughly on Intel ANV and sharing the exact numbers and shader keys (
53caac5f38279735,f55579c6c7b39d05,a54f19e927d8faef,dad2a8fbef3fd020), @mooki000 — that pinpoints a very clear second bottleneck.Your 4-worker preparation run confirms that thread-count clamping alone cannot prevent ANV OOM on oversized compute shaders (~250k SPIR-V words): because they appear back-to-back in
computes, any pool with>= 4workers still compiles multiple ~250k-word pipelines concurrently.I'd like to keep this PR (
#1610) focused on bounding default worker concurrency (16cap +RX_VK_PRECOMPILE_JOBS/HOOKSHOT_JOBS) for#1593/ RADV, and address the oversized SPIR-V concurrency issue cleanly in a dedicated follow-up PR — e.g., recording shader SPIR-V word count and serializing pipelines above a large-shader threshold (via a dedicated lock across bothprecompile_run()and shader preparation) so at most one heavyweight compile runs at a time regardless of worker count.Sounds good to me: #1610 for the worker cap, and the very large compiles in their own PR.
I have a local branch on a similar idea, but it only changes builds in play, and I'm not offering it as it is. On Intel, main already builds every compute pipeline in play one at a time (#496). The branch records each stage's SPIR-V word count and keeps only programs with a stage of 100,000 words or more on that thread. The others go to the shared workers (4 on this machine). It doesn't touch
precompile_run()or the shader preparation. The program cache here has 54 compute programs of 100,000 words or more, and 335 if the no-mesh fallback's programs are counted.On the UHD 770 it didn't work out. Both runs started with no caches, with the shader preparation and the start-up precompile switched off (
RX_VK_NO_WARM=1 RX_VK_NO_PREDICT=1 RX_VK_PRECOMPILE=0), loaded the same save, and were allowed 480 s with the game capped at 32 GB:49f75dc8More compute work ran with the branch. But the smaller builds on the shared workers, next to the one big build, still took the game to the cap. A second run of the branch, with #2110 merged as well, was killed after 306 s. The picture was wrong too, blown out to white with coloured bands. Main's picture on this GPU is also wrong, but in a different way. I haven't found the reason, so the branch stays local.
What our runs say about a threshold:
53caac5f38279735) hadn't finished compiling after 7 min 49 s. The process went from 0.6 to 8.2 GB in that time and was still growing. So one at a time keeps the memory well under the cap, but each of these compiles is very slow on ANV.So your idea and our branch do different things. Your lock covers the precompile and the shader preparation, which our branch leaves alone. Our branch only changes builds in play on Intel, where main already builds every compute pipeline one at a time, and lets the small ones run side by side. Both use a word count. I can't say yet how far one of these compiles grows by the time it finishes, because it was still growing when my run ended.
When your follow-up is up, I'll run it on the UHD 770 with the same scene and post the numbers.
Added 2026-11-11 16:46 UTC: I have since found why the branch's picture was wrong; a frame dump shows it. The four shared workers also translate the graphics shaders, and with the branch they were busy with compute builds. Eight of those took 25 to 61 s each (their largest stages are about 28,000 to 66,000 words; the machine was busy, load average 100 to 120). From the save load to the end of the run the queue stayed at 449-516 jobs and shader translation hardly moved: 2,846 ms in all when the save loaded, 2,921 ms at the end. Near the end, 2,350-2,390 draws a frame were still left out because their shaders weren't ready. The dumped frame has 230 draws with the branch and 1,170 without it, so the world wasn't drawn and only the sky reached the upscale. The coloured bands are main's own false colours on this GPU, in both runs. For a threshold this adds: in play, compute compiles of that size already block everything that waits behind them on shared workers here, well below 100,000 words.
Main still sizes the precompile pool as
min(usable_cpus(), 64)(vk_draw.cpp:8084), so the cap isn't on main yet. But the conflict is more than text: #2323 replacedhardware_concurrency()withusable_cpus()/half_cpus(), which honour process affinity on Windows. A rebase should build onhalf_cpus()rather than going back tohardware_concurrency().Two things decide whether this goes in at all:
precompile_runincluded, so taking both would stack two policies. One of them should go.Smaller point:
HOOKSHOT_JOBSis the build's jobs variable, so reusing it throttles play for anyone who exports it globally. I'd keep onlyRX_VK_PRECOMPILE_JOBS.Thanks for the review, @Joey-Magnus! Updated in
e3a158b1with both changes:half_cpus()):precompile_run()now usesstd::clamp(half_cpus(), 1u, 16u)so it builds directly on #2323's affinity-awarehalf_cpus()budget instead of rawusable_cpus() / 2orhardware_concurrency(), andtests/test_prepare_cpus.pyis updated to checkprecompile_runagainsthalf_cpus().HOOKSHOT_JOBSfallback: Keeps onlyRX_VK_PRECOMPILE_JOBSfor runtime precompile overrides so global build-timeHOOKSHOT_JOBSsettings do not throttle startup/play.As noted, #2414 vs this
half_cpus()/16-worker cap is a single-policy call for maintainers, and the follow-up for serializing oversized SPIR-V compiles (~250k words) from @mooki000's UHD 770 findings stays separated from this PR.Re-review at e3a158b. Both of my earlier nits are handled:
HOOKSHOT_JOBSis gone, so onlyRX_VK_PRECOMPILE_JOBSthrottles the runtime pass, and the description now says the 16 cap is deliberately lower than the 32 used by the other pools. Joey-Magnus's rebase point is done too. The default is nowclamp(half_cpus(), 1, 16), so it follows process affinity like the other pools since #2323, andtest_prepare_cpus.pychecks forhalf_cpus()inprecompile_run. The code looks correct to me: a garbage or zero override falls back to the default, and the override is still capped at 64. Mergeable, and CI is green except the builder package job, which is still pending.The one open question is the one Joey raised: #2414 (still open) caps the same
precompile_runpool with a different policy (physical cores throughcompile_worker_count()) and edits the same lines. Itstest_compile_threads.pyalso expectsprecompile_runto use that helper. So only one of the two can go in as written, and the other would need reworking. Choosing between them is a policy decision for @NotRexed, so I'm leaving this as a comment rather than an approval.For the record, mooki000's UHD 770 runs show this cap only delays the OOM on ANV (124 s to 199 s). The cause there is the very large compute programs compiling side by side. The author has scoped that to a follow-up, and mooki000 agreed, so I don't count it against this PR. The description should just not suggest this fixes Intel.
Nit (inline): the new variable isn't documented anywhere.
Nit:
RX_VK_PRECOMPILE_JOBSis the only knob for this pool now, but nothing mentions it. A word in the comment abovePre, next toRX_VK_PRECOMPILE_STALL_S, would help. A one-linelogfof the worker count would also make tester logs easier to compare (mooki000 had to infer it). Callingatoionce into a local would also read better.Rebased onto
eaef1460in9c17e2b2. hckrman101's inline nit is in:RX_VK_PRECOMPILE_JOBSis named in the comment abovePrenext toRX_VK_PRECOMPILE_STALL_S,atoiis read once intoenv_n, and the pass logsvk: precompile: N workers (M pipelines)so tester logs can be compared. docs/development.md has the row.The description now says what this does and does not cover: it bounds concurrent compiles (the #1593 RADV case) and does not fix the Intel ANV OOM from the ~250k-word compute programs, which needs those compiles serialized on their own (follow-up).
On #2414: it caps the pools by physical cores and keeps every smaller stage ceiling, so this PR's 16-worker clamp would sit below its cap. They compose; which policy you want as the default is your call.
Note on the current CI failures on
9c17e2b2— all of them come fromorigin/mainrather than this PR (tests/test_prepare_cpus.py, Windows Vulkan,appimage,dav1d,SPIR-V fixtures, andPython source checks (ubuntu-24.04, part 1/2)all passed):Python source checks (windows-2022, part 1/2): fails only intests/test_env_on.py(HOS_FPS_LOGinhost/oot_high_fps.c:15, from8fcc8c22/ #2470) andtests/test_psychov35.py(std::scanfunder-Werror,-Wdeprecated-declarations, missing_CRT_SECURE_NO_WARNINGS, from #2481).Python source checks (windows-2022, part 2/2): fails only intests/test_pcache_header_read.py(fopenunder-Werror,-Wdeprecated-declarations, missing_CRT_SECURE_NO_WARNINGS, from #2488).Happy to rebase cleanly once those are fixed on
main.The rebase onto half_cpus() is done and HOOKSHOT_JOBS is gone, so my earlier points are handled. I merged
9c17e2binto current main (clean) and ran test_prepare_cpus and test_env_on, and both pass. The three red Python jobs on this head come from main's breakage yesterday (test_env_on on HOS_FPS_LOG, and test_psychov35 and test_pcache_header_read on Windows). Current main has fixed those, so a re-run should clear them.One small thing: hookshot_env.h has env_int(name, def) for numeric switches.
env_int("RX_VK_PRECOMPILE_JOBS", 0)could replace the getenv and atoi pair at vk_draw.cpp:8142.The open question is policy, not code. #2414 is still open and caps the same precompile_run pool by physical cores, and its test_compile_threads expects its own helper there, so only one of the two can go in as written. As the description says, this doesn't fix mooki000's UHD 770 out-of-memory kill. That needs the very large compiles serialized.
Rebased cleanly on latest
main(51da0464) and adoptedenv_int("RX_VK_PRECOMPILE_JOBS", 0)andenv_int("RX_VK_PRECOMPILE_STALL_S", 30)fromhookshot_env.has suggested. Local checks (test_prepare_cpus.py,test_env_on.py,test_run_all_declarations.py) all pass.