WIP: perf(vk): per-worker pipeline caches, every CPU and no saved pipeline cache for shader preparation on MoltenVK #650
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!650
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/prep-fast"
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?
Type: perf · Risk: medium · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), arm64 and x86_64 under Rosetta · Needs: a Mac game A/B at this head; Windows, Linux (build and start)
Why
On a Mac the first start's shader preparation crawled: 39 of 38,809 shader programs in its first 180 s, an estimated 33
minutes for the compute programs alone. MoltenVK holds one lock per pipeline cache across each shader's conversion to Metal's
language and Metal's compile (
MVKPipeline.mmgetShaderLibrary), and every preparation worker compiled with the one sharedcache, so the 8 workers compiled one pipeline at a time; the preparation also used only half the CPUs, a Windows rule. And a
later start that reads the saved pipeline cache back is no faster on MoltenVK: it compiles every stored Metal library again,
one after another, as the file is read. This PR gives each preparation worker a pipeline cache of its own on MoltenVK, uses
every CPU on macOS, and on MoltenVK neither reads nor writes the pipeline cache's files. It is part of the macOS port. See #420.
What changed
On MoltenVK each worker of every preparation phase compiles with a pipeline cache of its own, asks the shared cache first, and
the phase merges the workers' caches into the shared one after they join; a compile in play goes into a cache of its own that is
merged into the play generation's at once. On macOS the preparation runs on every usable CPU. On MoltenVK the pipeline cache's
files are not read at the start and not written. Elsewhere the code paths are main's: no new lock, no new file behaviour.
gpu/vk/worker_caches.h(new, no Vulkan call of its own):Set(a cache per worker, made withoutEXTERNALLY_SYNCHRONIZED),Scope,create(the shared cache asked withFAIL_ON_PIPELINE_COMPILE_REQUIRED, then a compilewith the worker's own),
merge_into(onevkMergePipelineCachesin slot order),made_alone(play),runs/claim(a programvariant's notes taken whole by one worker, so its libraries compile once).
gpu/vk/vk_draw.cpp:create_cached(the worker path; play's compile alone),worker_caches_on,worker_caches_merge,pcache_use_held; the four phasesprepare_run,precompile_warm(both passes),precompile_at_create,precompile_run(notes sorted by program variant and handed out in runs only with the caches on);
pipeline_cache_saveandpcache_saved_nowhold the merge lock before #190's fetch lock.pcache_mu(a merge into the shared cache needs it to itself,vk.xmldstCacheexternsync) and a play generation's lock are taken only where the worker caches are on. The worker caches are never onwith #190's fetch lock (
pcache_fetch_lock_on: Intel's Windows driver,RX_VK_PCACHE_FETCH_LOCK=1): a merge reads itscaches' data as a fetch does, and #252's gate lets one compile at a time into Intel's compiler there anyway. The order is
Intel's gate, then
pcache_mu, then the fetch lock.prepare_run's compute warm keeps #252's gate around its compile.gpu/vk/prepare_workers.hcount(..., every)andkEveryCpu(__APPLE__), passed byprepare_cpus(
gpu/vk/vk_progcache.cpp); thevk: preparation:line says which rule and whether the caches are per worker.gpu/vk/vk_vendor.hpcache_no_load(andworker_caches),vk_draw.cpppcache_load_off(
RX_VK_PCACHE_LOADdecides when set, the device otherwise),pcache_files_unused:pcache_initreads neither the main filenor the play files, nothing is saved,
play_cache_savewrites no play file, andRX_VK_LOAD=create's.builtmarker isdropped at each start (no later start's cache holds what the warm built). The vendor-fix log line (
gpu/vk/vk_main.cpp) gainstwo fields.
docs/development.mdrows forRX_VK_WORKER_CACHES(new) andRX_VK_PCACHE_LOAD(its default changes on MoltenVK),docs/pipeline-cache-storage.md,gpu/vk/VULKAN.md.tests/test_worker_caches.py(new;vk_draw.cpp's own pieces against a made-up driver whose caches hold a lock acrossa compile, as MoltenVK's do),
tests/test_prepare_every_cpu.py(new), new checks intest_vendor_fixes,test_pcache_stamp,test_pipeline_play_cache(casesaloneandunused, and no lock taken off MoltenVK inroute) andtest_warm_bounded;stand-ins in
tests/pipeline_play_cache_test.cppandtests/pipeline_cache_save_test.cpp.Measurements
Verdict: helps on MoltenVK, measured at the topic's own commits before the rebase: 4.4× the compute and 4.6× the graphics
pipelines a second in LS's own code against one shared cache (16 workers), a later start's pipelines in 1.41 s compiled anew
against 6.65 s through the saved cache, and the first game start reached 230 programs in 180 s against 39. Not measured again at
this head: on MoltenVK the rebase takes the same locks and makes the same Vulkan calls in the same order (#190's fetch lock and
#252's gate are off there), but a game A/B at this head is in Must be tested.
Baseline: main does not run on macOS yet. The GPU check compares the code before this change (one shared cache, 8 workers) with
it, in one process, through LS's own
create_cached,worker_caches_mergeand friends cut from61bfbf1a's source; the gamestarts compare macOS integration builds without the topic (run #2) and with it (run #4,
aefe6b0a).Hardware: macOS 26.7.1, Apple M4 Max (40-core GPU), MoltenVK 1.4.2, 16-core CPU, 64 GB
Scene: the first start's shader preparation (game runs #2 and #4, windowless, defaults); a synthetic 48 compute + 16 graphics
pipeline bench through LS's code (GPU check), Metal's per-user shader cache blocked
Method: GPU check run 01 (
reports/PrepFast-files/gpu-check/runs-01/): one process per route, wall time and pipelines a secondper arm; game: the startup counter at 180 s and the OS log's Metal compiles. One run each
How to reproduce: the GPU check's frozen command in
reports/2026-11-14-prep-fast.md§7; in the game,RX_VK_WORKER_CACHES=0against unset, and
RX_VK_PCACHE_LOAD=1against unset, on the same buildreload-one): the GPU check @ aa8ecba5recompile): the GPU check @ aa8ecba5Reports:
reports/2026-11-14-prep-fast.md(GPU check run 01, its receipts),reports/2026-11-14-prep-speed.md,reports/2026-11-14-mac-startup-04.md(run #4, and run #2 for the before).Flags and settings
One new switch and a new MoltenVK default for an existing one; nothing changes elsewhere.
RX_VK_WORKER_CACHES=0: every compile with the one shared cache;=1: a cache per worker on any driver withpipelineCreationCacheControlRX_VK_PCACHE_FETCH_LOCKis on=1=0only to compareRX_VK_PCACHE_LOAD=0: the main pipeline cache file neither read nor saved;=1: read and saved=0/=1as before on every other driver=1on a Mac only to compareTesting
Tested: on macOS 26.7.1, Apple M4 Max, at this head (main
6c9edffeplus this PR), arm64 with the native compiler andx86_64 with
clang++ -arch x86_64(the fixtures run under Rosetta):test_worker_caches,test_prepare_every_cpu,test_pcache_stamp,test_vendor_fixes,test_warm_bounded, and main'stest_pcache_fetch_lock(#190): pass on both arches;test_intel_compiler_gate(#252, source only) andtest_recorder_sync(arm64): pass.
reports/PubWave-files/PubPrep/p22-head-01/.test_pipeline_play_cacheandtest_pipeline_cache_savefail on main6c9edffeon macOS before they run (posix_fallocatein
pipeline_cache_file.h, fixed by the macOS PR #476), so they ran on this head with #476'spipeline_cache_file.hhunkapplied in a scratch copy: pass on both arches (every case of the play caches,
alone,unusedand #190'slock-usersamong them).
reports/PubWave-files/PubPrep/proof-02/p22-h476-*.6c9edffe(with #476's hunk) and this PR's test files copied in: every one of those fails on both arches(
test_worker_caches: noworker_cachesinvk_vendor.h, no phase wiring;test_prepare_every_cpu: "prepare_cpus() = 8workers of 16 usable CPUs; want 16";
test_pcache_stamp: on MoltenVK the file read, merged, saved and stamped;test_vendor_fixes: noworker_caches/pcache_no_load;test_pipeline_play_cache: noworker_caches.h).reports/PubWave-files/PubPrep/proof-02/p22-base-*.f5905178, the topic's four commits on main, with #476's hunk) and the head's test files:test_worker_cachesfails on both arches on the rework's checks (worker caches on beside the fetch lock,pcache_mutakenoff MoltenVK, the save's lock order), and
test_pipeline_play_cache'sroutecase fails on both (a pipeline is not madewhile
pcache_muis held elsewhere: the lock was taken on every driver).reports/PubWave-files/PubPrep/proof-02/p22-pre-*.tools/ls-suite.py: 129 pass, 13 fail; 11 of them fail onmain
6c9edffeon macOS too (reports/Baseline-files/main-6c9edffe/), and the other 2 (test_precompile_compute_guard,test_shader_module_guard: "Windows structured exceptions required") are skipped on macOS in a full run and fail onlybecause they were named.
reports/PubWave-files/PubPrep/p22-related-02/,p22-p37-related-02-compare.txt.vk_draw.cpp,vk_main.cppandvk_progcache.cppcompiled whole for arm64 and x86_64 with the renderer's flags, with themacOS compile pieces of #476, #490 and #492 applied (main's renderer does not compile on macOS without them): 6 of 6.
reports/PubWave-files/PubPrep/p22-tu-stack-02/.git merge-treeof this head with each open macOS PR's head (#431, #437, #438, #476, #479, #480, #490, #491, #492): noconflict.
Not tested: the game at this head (main does not run on macOS without the other macOS PRs), players' later starts with
Metal's per-user shader cache warm, and Windows and Linux (no change in behaviour there: the new locks, the per-worker caches
and the file rule are MoltenVK-only by
worker_caches_on/pcache_files_unused; not built or run on them: its test requestcovers the build and two starts).
Must be tested:
RX_VK_WORKER_CACHES=0against unset on the same build,expecting about 3-5× the shader programs at 180 s with the caches and 0
[mvk-error]lines (by hand: the parent's GPU slot,the
vk: preparation:and startup counter lines).078c9a7a) and main6c9edffethe same way and start each to the title twice,expecting the
vk: vendor fixesline to saya pipeline cache per worker 0, the pipeline cache's files unread 0, the firststart to save the pipeline cache and the second to read it, with the same preparation lines and timings in line with
main's (by hand: test request T-N598)
Risks
What could break: on MoltenVK, a merge into the shared cache waits for asks of it under way (milliseconds), and a shader
stage shared by different programs may be compiled once per worker on a first start instead of once (MoltenVK's cache-only ask
is per whole pipeline); a later start compiles every pipeline it needs instead of reading the saved cache (with Metal's own
shader cache warm that is mostly conversion; not measured). Other drivers: the vendor-fix log line gains two fields; the
precompile claims its items through
worker_caches::claim(one at a time without the caches, asnext++did).Turning it off:
RX_VK_WORKER_CACHES=0(one shared cache),RX_VK_PCACHE_LOAD=1(read and write the files on MoltenVK),or revert the PR.
Compatibility: Nothing changes for players on Windows and Linux: no shader stamp, cache format or
MKWVK_API_VERSIONchange,and their
cache/longshot_vk_pipelines.binis read and written as before. On macOS the pipeline cache's files are no longerwritten (an old one is left unread; the stamp is untouched).
Reviewer notes
1843f6f8..078c9a7a(5 commits): every CPU on macOS; the worker caches; the header made Vulkan-free (avkDestroy*in a header bypasses the recorder's boundary,
test_recorder_sync); no saved cache on MoltenVK; and the rework on main's#190 and #252 (
078c9a7a), which is where to look for the locks.6c9edffe; no dependency. The topic's four commits were cherry-picked with-xfrommacos/prep-fast(
3573e736 2174e33f aa8ecba5 61bfbf1a). Conflicts in the pick:gpu/vk/vk_draw.cppprepare_run's compute warm: #252 (57f4eaec) putIntelDriverGateandguarded_compute_heldthere;kept, and the caches' path goes through
create_cachedunder that gate (cached_computewould take the gate twice).gpu/vk/vk_draw.cpppipeline_cache_save: #190 (f41a5c59) and #404 (b2911d33) rewrote the fetch asPcacheFetchwithput-offs; main's fetch kept, with the merge lock taken before it.
gpu/vk/vk_draw.cppplay_cache_save: #190 added itswait_ms/queueparameters; main's signature with this PR's earlyreturn.
gpu/vk/VULKAN.md: #321 (76699aad) renamed the file tocache/longshot_vk_pipelines.bin; main's name with this PR'ssentences.
tests/test_pipeline_play_cache.py: #190'slock-userscase and docstring kept besidealoneandunused.078c9a7a: the merge lock and the generation's lock gated to theworker caches, the worker caches off wherever the fetch lock is on, the lock order, and the tests and docs that follow
(
test_pcache_stamp's fixture follows #321'scache_pathand #190'sPcacheFetchinpcache_init).prepare_run,precompile_at_createandprecompile_run(4 blocks, resolved in #652's merge of it,f9f166f7); #644 (descriptor sets forstorage buffer lengths) extends the same vendor-fix
logfinvk_main.cpp: one line with every field (as integration 7 didwith #643's); #652 builds on this branch.
(
git merge-tree).Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main(maine090cc48).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge of every macOS PR built with this one,453eb678: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own excepttest_split_ub_flush, which #658's tree at the time broke and its head fixes;test_collect_widenow passes; the new tests pass,test_worker_cachesandtest_prepare_every_cpuamong them, andtest_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/waveA/.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md). (Added with the A/B at this head.)tests/that reads changed source updated in this PR, not weakened.078c9a7a54eee107e2f1eee107e2f16bc44ad83bFolded into #849 (fix(macos): MoltenVK's precompile and pipelines on Metal)
Pull request closed