Repository navigation
perf(agc): build the pipelines of a submission ahead on a compile pool - #5925
adriadam10 wants to merge 3 commits into
Conversation
… next one A graphics pipeline the store builds or a compute pipeline a dispatch builds is appended to pipelines.journal next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants) plus the recipe the pipeline was built from: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the Pipeline constructor reads. When a VulkanDevice is created with a window (a headless device waits for its first draw or dispatch, since the window's device usually replaces it first) the journal is loaded and its pipelines are built on a pool of threads while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident instead of compiling it on the queue worker. The pipeline stores key on RecompileResult::Identity, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (SharedSpirv::ContentHash, computed once per shared module), rather than on the process-lifetime variant ids; a generated stage is keyed like any other. CachedPipeline builds outside the store's mutex and the library parts and link are built outside the library store's mutex, so replay threads and queue workers build in parallel; a concurrent build of the same key keeps the first inserted. A replayed pipeline enters the store at its least recently used end and a full store takes none, and its optimized relink waits for the first draw that binds it, so the title's own pipelines come first in both. The compute pipeline creation moves into VulkanDevice::createComputePipeline, shared by dispatches and the replay. The store key also covers the color component mapping, the depth target's sample count and a depth bias clamp, which the constructor reads. The file names the device it was written for (the properties the driver's pipeline cache file is named by) and starts over for another; a torn tail is cut at a record boundary; two runs sharing the directory take turns on a lock file and keep each other's records; a read failure never starts the file over. A device cancels its replay before it is replaced or destroyed, and a replaced device destroys its pipelines then rather than at the driver's stop. APS5_NO_PIPELINE_JOURNAL=1 disables it; APS5_PIPELINE_JOURNAL_THREADS, APS5_PIPELINE_JOURNAL_ENTRIES (the first recorded), APS5_PIPELINE_STORE_ENTRIES and APS5_PIPELINE_JOURNAL_MAX_MIB bound it; APS5_PROFILE_DRAW prints a [pipeline-journal] summary. APS5_VERIFY_SET_LAYOUTS=1 now also checks a dispatch's set layout against the one described from its shader. The lavapipe test suite, which shares one cache directory, runs with the journal off except for the journal's own tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P6hw3kRAuQdfcU9YCZgaoK
…a pipeline boykopovar#5861 added both to the pipeline state and its store key; the recipe dropped them, so a replayed pipeline keyed another configuration. Co-Authored-By: Claude <noreply@anthropic.com>
On the first run of a title, every draw and dispatch of a new configuration created its Vulkan pipeline on the queue worker, under GuestMemory::GpuMutex. Driver::resolveGroupAhead, which resolved only the dispatch groups of the compute queues, now also walks the graphics queue and the draws: at the start of a submission the worker walks its packets with a scratch QueueState, decodes every draw and dispatch whose draw-cache or dispatch-cache key is new (the materialized stage results go to the recompile cache, which serves the worker again at the packet) and hands their pipeline builds to a pool of hardware_concurrency() / 2 threads the VulkanDevice owns (PipelineLookaheadPool). Driver::draw gets the resolve mode Driver::dispatch has: it stops before the GPU lock and hands over the state and the stages. A graphics pipeline is built through Graphics::BuildDrawPipelineAhead, the derivation Draw makes (the masked blend states and proxied targets, now shared with Draw as pipelineStateOf, the vertex input layout, the described set layouts and the attachment layout of a recorded draw) ending in CachedPipeline; a compute pipeline through VulkanDevice::ReplayComputePipeline, the journal replay's hook. Both find-or-insert: a worker that reaches the packet first builds the pipeline itself and the pool's copy is dropped; the worker never waits for the pool. On the graphics queue the walk reads memory as it is at the submission's start, continues past waits, conditional ranges and memory-writing packets, and adopts nothing: a stale decode costs a pipeline that is never bound. On the compute queues the walk keeps its group-capture rules and adopts the resolved dispatches as before; it now also executes SET_*_REG_INDIRECT packets from their submit-time register lists. The pool is cancelled and drained before the device is replaced and at its teardown, before the journal replay. APS5_NO_SUBMIT_LOOKAHEAD=1 disables it; APS5_SUBMIT_LOOKAHEAD_THREADS sets the pool's threads; APS5_PROFILE_DRAW prints a [lookahead] line per queue and one for the pool. On lavapipe, a submission of 24 draws and 24 dispatches of new configurations with pixel shaders of 200 instructions completes in 2315 to 2555 ms instead of 2851 to 3319 ms, 64 of each in 5760 ms instead of 6361 ms, with every pipeline found built by the pool; with trivial pipelines it is neutral. Co-Authored-By: Claude <noreply@anthropic.com>
|
I ran Astro's Playroom (PPSA01325) on Linux with an RTX 5080 (NVIDIA 615.78.08):
GPU busy per present is the same (50.8 vs 51.2 ms). With
|
|
Windows Server 2025, RTX 3060 (NVIDIA 591.86), WinLibs GCC 15.2.0 r7, Release, head 3339ddd. This head has the
The lookahead is not needed for it, so the fix belongs in #5875 and this branch needs it after a rebase. Without a debugger, Not tested: a title, Linux, and the AI-assisted: yes |
|
PS5 FSR4 Showcase (#6118) on AMD, as @MarcZX100 suggested. The first run still gives up at Windows 11 (26300), Radeon 860M (RDNA 3.5, driver 32.0.13058.8001), WinLibs GCC 15.2.0, Release. Catalog 01.000.010, relinked with Tree. This PR conflicts with current Each side starts with a new run folder and an empty
Not tested: this PR on current AI-assisted: yes (Claude Code). |
|
Not merging this head:
|
|
@adriadam10 following the #5648 reference in @LeandroLP23's FSR4 report: our buffer-tuple fix is merged as Two source-based coverage suggestions for the rebased head:
We have a Windows11 Enterprise26200 RTX5080 (610.62/32.0.16.1062) and Radeon(TM) Graphics (32.0.21036.18) available for the updated head. A useful hardware comparison would keep correctness checks separate from cold pipeline-build time and warm worker/frame cost, with the same scene and cache setup. Our current Sonic replay ends during startup, so it supplies no steady-gameplay performance result for this PR. This is source review of |
|
I tried the FSR4 Showcase (PPSA99011) cold A/B on this head for the warm-up question in #6118, but the head is too old for that title. Setup: 3339ddd with #5648 (23dcb82) cherry-picked, since it doesn't merge into current Every run stops 4 s in with I'll rerun the comparison once the branch is rebased: cold, lookahead on and off, counting runs until all 12 chapters play. On AI-assisted: yes (Claude Code) |
What
On the first run of a title, the queue worker finds the pipelines of a submission already built instead of creating them under
GuestMemory::GpuMutexat each draw and dispatch.Driver::resolveGroupAhead(DispatchAhead.cpp), which resolved only the dispatch groups of the compute queues, now also walks the graphics queue and the draws. At the start of every submission the worker walks the packets with a scratchQueueState, decodes every draw and dispatch whose draw-cache or dispatch-cache key is new (the materialized stage results go to the recompile cache, which serves the worker again at the packet) and hands their pipeline builds to a pool ofhardware_concurrency() / 2threads theVulkanDeviceowns (PipelineLookaheadPool).Driver::drawgets the same resolve modeDriver::dispatchhas: it stops beforelockForDrawand hands the state and the stage results over.Graphics::BuildDrawPipelineAhead, the derivationGraphics::Drawmakes (masked blend states and proxied targets, now shared withDrawaspipelineStateOf, the vertex input layout, the described set layouts and the attachment layout a recorded draw uses) ending inCachedPipeline; a compute pipeline throughVulkanDevice::ReplayComputePipeline, the journal replay's hook. Both find-or-insert: a worker that reaches the packet first builds the pipeline itself and the pool's copy is dropped. Nothing waits for the pool.APS5_GROUP_CAPTURE_ALL_QUEUES=1adopts on the graphics queue too, as before). The walk now also executesSET_*_REG_INDIRECTpackets from their submit-time register lists instead of stopping at them.PrepareForReplacement) and at its teardown, before the journal replay, following the replay'sCancel/Waitpattern.APS5_NO_SUBMIT_LOOKAHEAD=1disables it;APS5_SUBMIT_LOOKAHEAD_THREADSsets the pool's threads (0disables it too);APS5_PROFILE_DRAW=1prints a[lookahead]line for the walk (per queue, with the[groupcap]line) and one for the pool at teardown.Depends on #5875: the stable identities (
RecompileResult::Identity), the find-or-insert stores, the builds outside the store mutexes inCachedPipeline/LinkPipelineFromLibrariesandVulkanDevice::createComputePipeline/ReplayComputePipelineare what the pool builds through. The diff on top of #5875 is 18 files; review the last commit only.Open pull requests near this change, checked before writing it: #4897 (compute pipelines created in
PrepareDispatchbefore the GPU lock, with a futures map that waits on a concurrent build) and #5526 (PrewarmComputePipeline, the same idea) build the compute pipeline on the worker thread at the dispatch's own preparation; #5441 (PrewarmPipeline, draft) builds the graphics pipeline on the worker thread right beforelockForDraw; #5115 (draft) prepares deferred shaders atSET_SH_REG. This pull request implements none of their functions: the builds run on a pool ahead of the packets, from a decode of the whole submission, through the #5875 entry points; the one shared step, deriving the pipeline inputs of a draw from its state and stages, is extracted fromGraphics::Drawitself (pipelineStateOf) and used byDraw. If #5441 lands first, the pool task can call itsPrewarmPipelineinstead. #5747, #5481 and #5477 touchresolveGroupAheadby a few lines each (the indirect-argument stop, NOP skipping, COPY_DATA registers) and will need a trivial rebase on either side.Tested
Linux (Ubuntu 24.04, GCC 13.3), lavapipe (Mesa 25.2.8, LLVM 20), 4 CPU threads, pool of 2 threads. No title or homebrew run: the hardware this is for (NVIDIA, #4897 measured 0.28 to 0.59 pipelines/s with 3 threads; the 5 to 9 s stalls of #5441) is not available here.
New
agc_submit_lookahead(Vulkan mock,agc_driver_graphics_tests --submit-lookahead): the pool builds a draw's pipeline and runs a compute build ahead; the worker's own derivation of the same draw then hits the store and creates nothing; a configuration the worker built first is found present by the pool and not rebuilt; a failed build is counted and the pool goes on; cancelling with 15 builds queued behind a blocking one drops the 15, accepts nothing afterwards, and leaks no Vulkan object (mock.live == 0afterClearCachedPipelines).New
agc_driver_submit_lookaheadandagc_driver_submit_lookahead_off(lavapipe, the same binary withAPS5_NO_SUBMIT_LOOKAHEAD=1): one DCB with aWAIT_REG_MEMon a CPU label, a draw into a linear target (a synchronous draw), a draw into a tiled resident target (a recorded draw) and a dispatch. With the lookahead on, while the worker waits on the label the walk decodes the two draws and the dispatch and the pool builds the three pipelines (two store misses,DispatchPipelineBuildsunchanged); after the label the worker's draws hit the store twice and its dispatch builds nothing; a repeated submission is skipped as seen. With it off, the walk never runs and the worker builds the three pipelines itself. Both variants check the same images (every texel of both targets, in both layouts) and the dispatch's output, so the lookahead changes nothing in what is drawn.agc_driver_submit_lookahead_tests --benchmark N CHAIN(not registered): N new pixel programs (CHAIN multiply-add steps on a user-data word, so the module is not folded) drawn into a tiled resident target and N new compute programs in one DCB, after a warm-up submission that creates the device. Wall time from submit to idle,ANYPS5_NO_SHADER_CACHE=1 APS5_NO_PIPELINE_JOURNAL=1 MESA_SHADER_CACHE_DISABLE=true, two runs each:With pipelines that take time to build, the submission completes 9 to 23 % sooner, with every draw and dispatch finding its pipeline built by the pool (0 built by the worker). With trivial pipelines it is neutral at 24 and 5 % slower at 64: on lavapipe the pool's compile threads share the four cores with the rasterizer threads, which a GPU does not. The walk itself (the decode and recompile of the new configurations, 0.9 to 2.3 s on the worker at 200 steps) is work the worker did at the packets before; it now runs before the first packet.
Validation layer (
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation) on the lavapipe test with the lookahead on and off: the same two VUIDs in both runs, both already in the base (VUID-VkGraphicsPipelineCreateInfo-pLibraries-06681on the fast link of the pipeline libraries, 12 times, andVUID-VkDeviceCreateInfo-pNext-pNextfrom the layer of Vulkan headers 1.3.275 not knowing a newer device feature struct, 6 times); no VUID the lookahead adds.ctest -R '^(agc_|shader_|guest_shader_)': 539 tests, all pass.agc_driver_draw_cache_reuseandagc_driver_draw_cache_reuse_offnow run withAPS5_NO_SUBMIT_LOOKAHEAD=1: they assert the exact draw-cache lookup counts of three draws, which the walk's own decode of the same draws doubles (6 lookups instead of 3), so they keep testing the worker's draws alone.tools/check_conventions.py --base upstream/mainandtools/check_test_registration.py buildpass.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)agc_driver_tessellationsegfaults the same way onmain)