perf(prepare): build fewer predicted pipelines, size the pools to the usable CPUs, and keep translated programs across cache-code changes #2323
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2323
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/faster-prepare"
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?
What changes
A fresh prepare (first start after a build or an update, or the Builder's
--prepare-shaders) is mostly the driver compiling pipelines. This PR makes it build fewer pipelines that no draw can use, run its thread pools at the right size, and stop throwing away every player's translated programs when only the cache code changes. It also fixes a gap where some predicted pipelines were never built, and adds clearer log lines.graphics_keyputs the input types in the key), so the other two pipelines could never be looked up. Each of them was still a full driver compile with its own modules.make_profileturnssupport_scaled_attributesoff on every GPU.predict_runskips a prediction whosepipeline_key(with the program's mesh or mesh-fallback form) was already built for that program key. Some vertex classes differ only in dynamic state (exact topology, stride), and mesh programs have no vertex layout, so those predictions were the same pipeline.program_cache_load(..., keep=true)) now also includes the records already read into memory (disk_taken). Before, the predicted pipelines of programs that the state list had loaded were silently never built: 64,797 predicted, 64,325 made, and nothing in the log said why. Now "made" plus skipped duplicates equals "predicted".program_cache_load_somedoes). It was one record at a time on one shared handle under a lock.hardware_concurrency), not the ones its affinity mask allows: started on 12 of 24 CPUs, the prediction and the precompile ran 24 threads each.usable_cpus()counts the process's affinity mask (GetProcessAffinityMaskon Windows,sched_getaffinitythrough host/posix on Linux). The prediction, the precompile and the program cache readers use it. The translation, the state list and the stage libraries keep half the PC's CPUs, but never more thanusable_cpus()(half_cpus()); halving the usable count instead made the translation about 30% slower in a test.hookshot_programs.bin(6.9 GB with the whole game) is only valid for the build's translator stamp, andnx2/build.pyhashes all ofvk_shader.cppfor it. The cache's reads, the prediction's reads and the pool sizes lived in that file too, so any change to how the cache is read or scheduled (items 3-5 above, for example) made every player translate the whole game again.vk_shader.h(Program::Answer, the questions,Stage::c1, the head's words), which the translator stamp does not hash, is pinned bystatic_asserts besidekProgramCacheFormat: a layout change stops the build until the format is raised.gpu/vk/vk_progcache.cpp/.h, outside the hashed files:disk_init, the record and index format,program_cache_save,_read,_load,_load_some,_borrow,_keys,_each,usable_cpus,half_cpus.hookshot_prepared.binare now stamped withprogram_cache_stamp(): the translator's stamp (translator_stamp()invk_shader.cpp, the same inputsdisk_stamphad) mixed with a newkProgramCacheFormat. A change to what a record holds, or to how the file or its index is laid out, must raisekProgramCacheFormat. The old cache is then not read, and the next start prepares again, as after a translator change.nx2/build.py's stamp block and its mirrors innx2/bake.py/nx2/bake_mimo.pyare unchanged (same file list). The seed reader (seed_open) and the seed's translation drivers stay invk_shader.cpp: they parse stage code and decide what is translated.hookshot_prepared.binsays so. A start closed (or ended) during the prediction left the rest unbuilt for good: the next start found the programs prepared and skipped the prediction, so play compiled those pipelines when they were first drawn.predict_runnow keepshookshot_pipelines.bin.predictingfrom its start to its end, and the Builder's run, the start-up screen and the console start run the prediction again when it is there. What the driver kept from the first try comes back from its cache quickly. After two starts that ended inside it (a driver that hangs or ends the process on one of these pipelines), the marker is let go and the rest are left to play, asprecompile_rundoes with its notes. Closing the window during the prediction does not count toward those two: the marker keeps its count, and the next start goes on.predict_runkeeps a sample of 32 of them (hookshot_pipelines.bin.predicted), and the next game start asks the driver for each on an idle thread behind the game, without compiling and as play asks:vk: the driver's shader cache kept N of M predicted pipelines asked. It needspipelineCreationCacheControl(as play's own asks do) and does not run in a--prepare-shadersrun, which exits right after. The driver cache folders' size is logged at the start and after the prediction.RX_VK_NO_PREDICT_CHECK=1turns the check off.vk: program 79116b290d46542f not translated: its NVN stage 5 (code 79116b290d46542f) the branch at instruction 16404 (+0x40140) goes outside the program.prepare_guesscounted one predicted pipeline per seed program (38,809). The prediction now makes 56,588 with the shipped list, so the guess is one and a half per program. Before, the bar ran ahead through the translation and then slowed down once the real count replaced the guess.The log line of the prediction reports the new counts, for example:
56168 made, 420 the same pipeline as another ... left out as no draw could ask for them: 8209 by their vertex input types, 0 by their colour output types (7555 program variants with none left; 0 notes whose own types do not fit; 0 took a less common layout or targets that fit).New tests:
tests/test_predict_types.py(the SPIR-V type reader and fit rule, compiled on hand-made SPIR-V, plus source checks for items 2-3),tests/test_prepare_cpus.py(usable_cpus/half_cpusagainst stand-in masks, and which pools use them),tests/test_program_cache_format.py(runsnx2/build.py's real stamp block: an edit tovk_progcache.cpp/.hkeeps the stamp, an edit tovk_shader.cppchanges it; and the stamps' sources),tests/test_predict_resume.pyandtests/test_predicted_check.py(source checks for items 7 and 8). The tests that read the cache code fromvk_shader.cppnow readvk_progcache.cpp/.hbeside it.Note for anyone working on the stamp or the bake (#1707): the translator stamp's file list did not change.
HOOKSHOT_SHADER_STAMPstill coversvk_shader.cpp,vk_bank.cpp,vk_state.h,vk_api.hand the recompiler folders. What changed is the runtime stamp that the program cache, the notes andhookshot_prepared.bincarry:program_cache_stamp() = mix(translator_stamp(), kProgramCacheFormat), wheretranslator_stamp()is the olddisk_stamp(). The record layout (put_head/get_head,put_info/get_info,program_cache_save/_read) now lives invk_progcache.cppand is versioned bykProgramCacheFormat(1).Because this PR changes
vk_shader.cppand the cache stamp, existing installs prepare once more after updating. Later changes tovk_progcache.cppor the scheduling invk_draw.cppno longer cause that.Relation to #2248
#2248 (klimovich008) and item 1 fix the same bug, and they edit the same lines of
predict_pipelinesandFaceinvk_predict.cpp, so only one of them should land. The bug: the prediction's fallback gave a program the vertex layout most common among shaders with the same input locations, whatever their types, so some predicted pipelines had vertex attribute formats of another numeric type than the shader's inputs (VUID-08733, which #2248 reports on an RX 9070 XT).5d34f63e) keys the fallback by a signature of locations and numeric types, and gives an input interface it cannot classify (grouped decorations, component-packed inputs, other widths) only its exact module's notes. On their AMD subset that took the predictions from 276 to 4, so most of the prediction goes.tests/test_predict_sees_loaded.pycompilesvk_predict.cppwith stand-ins and would need stand-ins for what this branch adds there (Locations,PredictCounts,attribute_type_of,note_color_types).Measured
Setup: RTX 3090, Ryzen 9 7900X, Linux, NVIDIA driver. Each timed run was a cold
hookshot --prepare-shadersin a reflinked copy of a game folder built from main: no program cache, notes, pipeline cache or computes list, an empty__GL_SHADER_DISK_CACHE_PATH, headless. The shipped list was 25,251 pipeline states (15,330 notes). Every timed run held the exclusive timing lock, which gives it 12 logical CPUs (6 cores) and moves builds to the other CPUs.The PC was shared with other work all night and the next day (other test games and prepares running beside some runs, load averages of 20-50), so the seconds are noisy; base and new runs were interleaved. The translation step runs below normal priority, so it suffers most from that load. The main numbers are from the night's runs on main
fab8cb37and were not taken again for the final head. Main's later commits merged here (#2042 switches, #2195 the prediction's stall guard, #1823 the driver-fault quarantine, #2363 the driver cache folder) change neither the work the prepare does nor its thread counts, but the final head's run is a single run on a loaded PC.Whole prepare (wall), seconds:
fab8cb37)62711af6, items 1-10 and the review fixes)By step (the same runs; main / items 1-4 / items 1-5):
62711af6Predicted pipelines, shipped list (the prediction's log line; the same in every run):
pipeline_keyas another (item 2)The 8,209 left out are the kind of pipeline VUID-08733 is about: their vertex attribute formats are of another numeric type than the shader reads (scaled formats not counted), so no draw can ask for them (
graphics_keyputs the input types in the program key). This branch builds none of them; main built all of them.The 7,555 program variants left with no prediction are the bone-input variants whose signature's notes all have the other input types: no note anywhere has a layout of their own types (the fallback asks the fit before it picks, and found nothing to take for any of them: the "less common class" count is 0). Main gave each a pipeline that no draw can look up. What that warmed in the driver's cache was the variant's own vertex module, which no noted draw has used; a draw that ever does needs a layout of these types, which no note has to predict from.
Thread count for the prediction alone (programs kept, cold driver cache, same 12 CPUs): 6 threads 302.0 s, 12 threads 271.0 s, 24 threads 313-315 s. One thread per logical CPU is best here, so the prediction and precompile use
usable_cpus(). Grouping the predicted pipelines by fragment module on one worker (to reuse the driver's per-stage cache) took 283.3 s against 271.0 s: no gain, not kept.Program cache stamp (item 6), each a run of
--prepare-shaderson a folder prepared by this branch:vk_progcache.cpp:46676 program keys kept, 6790077 KB, nothing translated, no prediction; the whole run took 28 s.vk_shader.cpp:0 program keys kept, the translation starts again.0 program keys kept, a full preparation with the same counts as a cold one (38,809 programs, 46,666 records, 56,588 predicted pipelines).Play after the prepare: a headless start from a Kokiri Forest save, A pressed until play, then the Debug teleports to areas the shipped list covers less (Hyrule Field, Dodongo's Cavern, Jabu-Jabu's Belly, Bottom of the Well), about 40 s in each. This branch (final code, with main merged) ran with its own prepare's driver cache; main (
fab8cb37) ran from its prepared folder (the 15,330 shipped notes) with an empty driver cache, so for main only the counts compare, not the milliseconds:Both had draws left out in one 2-second window, at Jabu-Jabu's load. The 21 pipelines there are neither noted nor predicted by main or this branch. The start's check said
the driver's shader cache kept 32 of 32 predicted pipelines asked (... in 216 ms).Items 7 and 8 at the next start (final code, main merged):
RX_VK_PREPARE_AGAIN=1) was ended withkill -940 s into the prediction (9,247 of 56,588 built), which lefthookshot_pipelines.bin.predictingat 1. The next--prepare-shadersrun translated nothing (0 new), loggedthe last start did not finish building the predicted pipelines (it ended in them): they are built again, built all of them again (56168 made, 420 the same pipeline as another, 120.6 s, with the first run's driver cache, 263 MB at its start) and removed the marker.kept 32 of 32 predicted pipelines asked (... in 216 ms). At a start with an empty driver cache,kept 1 of 32(the start's precompile had just compiled the 15,330 noted pipelines into it, and one sampled pipeline matched one of them: same modules and state under another program variant).Possible future improvement (not in this PR)
About 77% of the translation step is the driver compiling compute pipelines: 9,925 of them, for 4,127 compute programs and their texture-answer variants (each program is tried with every texture answer that changes its SPIR-V, up to 64 variants). Across 39 local play folders, only 606 of the 4,127 compute programs were ever dispatched. Building every variant only for programs on a shipped "dispatched" list, and the base variant alone for the rest, would remove about 4,800 compute compiles: roughly 70 s of a fresh prepare and 0.45 GB of program cache. The risk is a hitch in play if an unlisted program is used with a non-base variant. This PR keeps compiling everything.
Also for later, from review: the driver-cache check runs at idle priority but borrows programs through the program cache's lock, which play's threads take too, so a busy PC could hold play up behind it;
driver_cache_logwalks the whole driver cache folder twice per prepare (cheap on NVIDIA's few large files, maybe not on Mesa's); and on Windows PCs with more than 64 logical CPUsusable_cpus()reads one processor group only.Also for later: stage libraries (
RX_VK_GPL) would cover every vertex layout and target set for two compiles per program. The program cache holds 17,247 distinct vertex and 23,155 distinct fragment SPIR-V modules for about 43,000 graphics programs; how many of those share a descriptor layout (which a library also depends on) was not counted here. Translating the seed on all usable logical CPUs instead of half (the start-up screen holds the game anyway) needs a measurement on a whole PC, which the shared timing slot (6 cores) could not give.Not tested
usable_cpus()readsGetProcessAffinityMask, which covers one processor group: on PCs with more than 64 logical CPUs it falls back to all of them.kill -9) during the prediction, not with the window closed. Closing the window (the marker keeps its count) is covered by the source tests only.%LOCALAPPDATA%\NVIDIA\GLCache,AMD\VkCache,Intel\ShaderCache) are a best guess of where those drivers keep it.driver_cache_roomstays Linux-only: NVIDIA on Windows takes it from the control panel, and nothing here could check an environment variable there) and__GL_SHADER_DISK_CACHE_SKIP_CLEANUP(it stops the driver from ever trimming its cache, which would grow by about 2 GB with every update's preparation).Results on AMD, after the merge: RX 580 (Polaris, Mesa 26.2.6 RADV, no mesh shaders), i7-9700K, 15 GB. Main
7759a6f3against this PR's final head12f835d0, one run each.Cold prepare
On RADV the dropped pipelines were also the expensive ones: 26.4 ms per pipeline on main, 19.8 ms here.
VUID-VkGraphicsPipelineCreateInfo-Input-08733, Khronos validation layer 1.4.363, counted during the prediction:
All of main's are vertex attributes of an integer format read as float, or the other way round, at locations 6 and 7: the skinned-bone variants. Both builds also report the same 2 × VUID-VkShaderModuleCreateInfo-pCode-08737, which this PR doesn't touch.
Play (Kokiri, then a teleport to Hyrule Field, about 60 s each; the precompile was skipped in both, see below):
Separate finding, not from this PR: on this card the precompile runs out of memory on main and here alike, past 24 GB. One or two mesh-fallback compute pipelines never finish compiling in RADV/ACO on Polaris. That's being looked into separately.
github-sync referenced this pull request2026-11-12 06:22:54 +00:00