Repository navigation
perf(agc): spin before blocking on the GPU mutex and the draw committer hand-offs - #6632
Conversation
|
The performance report captures for this PR and #6631 are in #6631 (comment) (one comment for both PRs, since the captures ran with both levers together and with neither). AI-assisted: yes |
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
|
Second-machine numbers for this PR and #6631 are on #6631. They were measured by @parl0v on an i5-14400F with an RTX 4060 8 GB, at the Minecraft Dungeons II title over three alternating rounds. This PR alone gives +3.2 % against |
…er hand-offs At the Minecraft Dungeons II title the queue-0 worker, the draw committer and the compute worker hand work to each other 49,000 times per 10 s through the GPU mutex, the committer's condition variable and DrawPipeline::Drain, and each hand-off that has to wait costs a kernel wake-up. The three workers are busy 21-34 % of the time: the GPU idles because the chain of hand-offs is slow, not because a thread runs out of CPU. GpuMutexType::lock retries try_lock for a window before it blocks, the committer polls its item count for the window before it waits on its condition variable, and Drain polls for twice the window before it waits for the committer. A hand-off that completes within the window costs no wake-up. The window is 150 us on machines with at least 8 logical processors and 0 below that; APS5_HANDOFF_SPIN_US sets it (0 turns it off). Ordering and the lock's semantics are unchanged. The test agc_handoff_spin covers the window policy, the spin helpers and contended locks, and docs/dev/ARCHITECTURE.md describes the spin.
d98b21f to
e74484e
Compare
|
Rebased onto Fork CI, Linux with lavapipe, every AI-assisted: yes |
…ff spin guard The automatic window needed 8 logical processors, which a 4-core CPU with SMT passes while the waited-for thread may run on a sibling thread, and ignored the process affinity. It now needs more than 8 processors in the affinity mask (CpuTopology), falling back to hardware_concurrency.
|
Data point from #5867 (now closed in favour of this), which polled only in the committer's wait, for the core-count policy here. Hades II (PPSA36082), level UI/menu, Linux, 7800X3D (8 cores / 16 threads), RTX 4090, pipelined draws at the default depth. One build that switched a 50 us poll off and on every 15 s in one sitting, 4 windows each, MangoHud fps per window:
With cores to spare the poll won every pair; on 4 cores it gave nothing and lengthened the tail, which is what a window of 0 at 8 or fewer logical processors avoids. LLM assistance used. |
|
Data point from a CPU with two L3 domains, where the spin costs Stray (PPSA02100) about 10 %. The committer's poll accounts for about half of that. Setup. Linux (CachyOS, kernel 7.2.10), Ryzen 9 9950X (16 cores in two CCDs, each with its own L3: CPUs 0-7/16-23 and 8-15/24-31), RTX 3090 (NVIDIA 615.78.08), GCC 16.2.1 Release, 3840x2160, the scx_lavd scheduler (sched_ext, lavd 1.1.3 with On and off, frame p50 and presents in the 20 s window:
One more Per site. These runs use a local build of f1af6c5 that reads one window per site from the environment. That build is not proposed. Three alternating rounds:
Where the cost comes from. I sampled per-thread CPU time and the CPU each thread last ran on from
My guess is that a hand-off partner that blocks less often looks less latency-critical to lavd, which then moves it off the best-ranked cores, and those are all on CCD0 here. I have not checked that in lavd's code. On one CCD the spin helps. I ran the same build with the whole process on CCD0 (
So the spin itself is fine here. The cost comes from the scheduler spreading the hand-off partners over both L3 domains, and keeping the process in one domain would remove it without turning the spin off. EEVDF does this by itself. With sched_ext off, the kernel's scheduler keeps every busy thread on CCD0 (0 of 90-97 samples on CCD1). On Not checked: Windows on this machine, other titles. AI-assisted: yes (analysis and text with Claude Code; the runs are from my machine). |
What
Companion: #6631 (independent of this one; the two together give more than the sum, see Tested).
At the Minecraft Dungeons II (PPSA16064) title screen (3840x2160, about 8 FPS) the GPU idles for 44 ms of every 126 ms frame. A trace of the plateau shows that for 32 % of the window the GPU waited for a batch that had not been submitted yet, and that the three threads which submit it are not out of CPU: the queue 0 worker is 34 % busy, the draw committer 22 %, the compute worker 21 %, and every other thread under 2 %. They hand work to each other 49,000 times per 10 s (GPU mutex, the committer's condition variable,
DrawPipeline::Drain), and each hand-off that has to wait costs a kernel wake-up (its latency was not timed on its own; the effect is what the rows below measure). The queue 0 worker spends 39 % of its time waiting for the committer inDrain, the committer is idle for half of the window, and the GPU mutex is held 79 % of the time.GpuMutexType::lockretriestry_lockfor a window before it blocks; the committer polls its item count for the window before it waits on its condition variable;DrawPipeline::Drainpolls the item count for twice the window before it waits for the committer. A hand-off that completes within the window costs no wake-up.HandoffSpin.hppholds the policy (HandoffSpinWindowFor) and the two helpers (SpinUntil,LockSpinning).CpuTopology;hardware_concurrency()where the mask is unknown) and 0 otherwise, where spinning threads would compete with the thread they wait for, which may be a sibling hyperthread of the spinner.APS5_HANDOFF_SPIN_US=<n>sets it (0 turns it off, values above 10000 are limited to 10000).docs/dev/ARCHITECTURE.md: one paragraph.Tested
Windows 11, MinGW-w64 GCC 15.2, NVIDIA GeForce RTX 5070 Laptop GPU (driver 617.42), 20 logical processors (Ryzen AI 9 365) for the title rows; ubuntu-latest (4 logical processors, lavapipe) for the tests. The title rows ran on this change on top of
44df21632; the branch is squashed onto5d9dcc42d(the 59 commits in between merge without a conflict and the changed lines are the same).agc_handoff_spin(new, no GPU): the window policy for usable processor counts (9 spin, 8 and 4 do not) and the processor count itself, andAPS5_HANDOFF_SPIN_USvalues (unset, 0, number, negative, text, above the limit),SpinUntil(ready at once, zero window, ready on the fifth evaluation, never ready for a window, ready from another thread), four threads contending onstd::mutexandstd::recursive_mutexwith windows of 0, 50 us, 1 ms and 150 us (never two holders, no lost increment), the fallback to the blocking lock when the holder outlasts the window, recursion. Fork CI (Linux, lavapipe, ubuntu-latest; this squashed head onorigin/main5d9dcc42d): https://github.com/AlxcBTW/AnyPS5/actions/runs/38817879745,agc_handoff_spinpasses in 0.05 s. After the guard change (head23a804e39, onorigin/maineb901bcf4): https://github.com/AlxcBTW/AnyPS5/actions/runs/38943017617,agc_handoff_spinpasses in 0.04 s and the driver and recorder tests pass withAPS5_HANDOFF_SPIN_US=150; the one failure isagc_driver_graphics, which also fails without this change.APS5_HANDOFF_SPIN_US=150; the 455 tests of^(agc_handoff_spin|agc_driver_|agc_recorder)pass, including the pipelined draw tests that drain the committer (same run).libsandrelinkercompile on the fork CI (MinGW-w64 GCC): https://github.com/AlxcBTW/AnyPS5/actions/runs/38817908816 (this squashed head).tools/check_conventions.py --base origin/mainpasses.APS5_TEXTURE_CACHE_MIB=3072,APS5_PROFILE_DRAW=1,APS5_PROFILE_GPU=1,APS5_NO_PAD_INPUT=1; themainarm is the binary with both switches off, which an earlier session measured against a build of origin/main: 7.80 against 7.93 FPS):main:APS5_NO_DCC_PRESYNC=1 APS5_HANDOFF_SPIN_US=0APS5_HANDOFF_SPIN_US=0APS5_NO_DCC_PRESYNC=1Rows run in the order both, companion, this PR, main, main, this PR, companion, both. The warm-up row and the first
bothrow, the first two after the relink, found no title windows (probably cold caches), sobothhas one row of eight windows. Rows of identical code differ by up to 0.8 FPS (the twomainrows read 7.08 and 7.62, the two rows of this PR alone 7.16 and 7.97), about 10 %, so a single row cannot resolve an effect of that size; the pooled arms and the repetition in other sessions can. The same scene on the same machine in two earlier sessions: origin/main against the first commit of the companion with this PR, alternating rows, 7.93 against 8.76 FPS (+10.5 %); an earlier lab build, levers off against both levers on, 7.94 against 8.76 (+10.2 %), with the companion's lever alone at +2.6 % and this PR's at +2.9 %. The baseline therefore lands between 7.35 and 7.94 FPS from session to session, and the changed build between 8.55 and 8.76 FPS: +10 % to +16 %, +12 % on average of the three sessions. Alone, this PR gives +2.9 % and +3.0 %, the companion +2.6 % and +4.0 %; together they give more than the sum, and the GPU busy time stays at about 80 ms while its idle time per frame falls from 54.7 to 36.3 ms.Spin window, measured with the DCC pre-sync of the companion PR on (two rows of eight windows each, anchor without both 7.88 FPS): 50 us 8.28 FPS, 150 us 8.62, 400 us 8.59; the compute worker's label-wait spin (1500 us) and a mutex priority for the queue 0 worker and the committer added nothing on top (8.53 and 8.56) and are not in this PR.
APS5_TRACE_SECONDS=60:10is set besidesAPS5_TEXTURE_CACHE_MIB=3072,APS5_NO_PAD_INPUT=1and the trace directory; same machine, scene and session, one capture per build, no event dropped): origin/main, 81 presents in 10.0 s = 8.09 FPS, frame time p50 120.7 / p90 138.8 / p99 152.7 ms, GPU busy 79.2 ms; both PRs, 80 presents in 10.0 s = 7.97 FPS, p50 120.1 / p90 153.0 / p99 176.4 ms, GPU busy 78.5 ms. These two captures do not show the difference: a capture is 80 frames of one row, and rows of identical code differ by more than the effect (see above). The captures of the earlier session (20 s,APS5_TRACE_SECONDS=60:20, the queue 0 worker's event buffer full after 12 s, so they are flaggedtrace_dropped) read 8.27 FPS (p99 151.9 ms) for origin/main and 8.99 FPS (p99 130.6 ms) for the first commit of the companion with this PR (+8.7 %). The summary lines of the four captures follow as a comment; the traces are kept and available on request.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)