perf(vk): mirror the pools the GPU reads most, in place of colder ones #1377
Open
aquinas22 wants to merge 6 commits from
aquinas22/perf/mirrors-by-heat into main
pull from: aquinas22/perf/mirrors-by-heat
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!1377
Loading…
Reference in a new issue
No description provided.
Delete branch "aquinas22/perf/mirrors-by-heat"
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
Mirrors go to the pools the GPU reads most. When the hottest pool waiting for a mirror does not fit in the budget, it takes the mirrors of pools that read a quarter as much or less, coldest first.
Why
A pool refused for the mirrors' budget got a mirror only once another mirror went unbound for 600 submissions (
mirrors_give_back). In play the budget fills with pools that are bound now and then but read little, so they never go idle. A pool bound in nearly every submission then stays in guest memory for the whole session, and the GPU reads it over PCIe.From
hookshot_log.txton an RTX A1000 Laptop (4 GB, mirror budget 495 MB):Here the GPU sat at about 70% use, drawing 15 W of its 45 W, and memory sync took 14–19 ms of each frame.
How
Poolcounts the bytes of the ranges bound from it. This covers every binding, including bind-memo hits (the memo now keeps the pool), and is one relaxedatomic_refadd. The count is halved every 64 submissions (heat).mirrors_swap_for_hot, at a submission's start, only for the hottest wanted pool:last_value <= mem_completed, the same rulemirrors_give_backuses), made 600+ submissions ago, and each under a quarter of the wanted pool's heat;mirror_free: the page table goes back to guest memory, andpools_genchanges, which also invalidates the bind memo.RX_VK_MIRROR_HEAT=0restores the old behaviour. It is documented besideRX_VK_MIRROR_MB.mirror_swap_pick, a pure function covered bytests/test_mirror_heat.py.Testing
Hardware and setup: RTX A1000 Laptop GPU (4 GB, driver 596.58), i7-12800HX, Windows 11. Minimum (4GB) settings: handheld profile, Resolution 50%, frame generation off.
In play: run as
hookshot_vk.dllbuilt from v2.0.11 plus this change. I compiled the same diff onmain, but I have not run that build in game.First version, now fixed: tried every wanted pool and had no heat floor, so 16 MB pools traded mirrors back and forth (256 swaps), kept the mirrors young, and the 160 MB pool never got in. This version tries the hottest pool only, makes it first, and adds the 64 MB floor.
This version: the 160 MB pool took the mirrors of four cold pools on its first swap:
Over a long session there were 16 swaps, each a hot pool taking cold ones, with no back-and-forth and no device lost.
RX_VK_MIRROR_HEAT=0in the same spot on more cards would settle it.python -I -B tests/run_all.py mirror_heatpasses.Thanks, this is a well-argued change and the numbers from the A1000 are convincing.
mirror_swap_pickis clean and the test covers the cases that matter, the heat counting on memo hits is safe (a stale memo can't hit afterpools_genmoves, and retired nodes stay alive), and I don't see a conflict with #1127 or #1376: the hunks don't overlap, victims are only taken fromimported(never the retired pools #1127 inherits from), and the swap runs beforemirror_make, so a remade hot pool still inherits through #1127's page filter.One thing I'd like sorted before merge, about which mirrors can be victims (inline). Freeing a mirror is only lossless if guest memory already has everything the GPU wrote into it. The write-back copies just the bound written ranges. It doesn't copy the bare-table guesses past the first 64 bytes, the pointer table's ranges, or stores through the page table: the comments around
rb_hazard_locked/RX_VK_PROTECT_BAREand the EvilOOT note inretire_pooldescribe exactly that GPU-only data (Kokiri's foliage records). Todaymirrors_give_backfrees a live mirror only after 600 submissions without a sync, so a pool that's bound every frame keeps its mirror. The swap drops that guard. A pool bound every frame through a small binding (64 bytes of a bare entry), with the culling writing past it, has almost no heat, so it's the first victim. Its records go back to stale guest memory, and the remade-larger inheritance can't help because nothing is retired. That's the missing-foliage / trees-as-columns class of bug again, and it would show on small-VRAM cards, which are the ones this targets.Options: skip victims the GPU may have written outside their write-backs (a per-pool flag set from
rb_hazard_locked/ written binds, cleared on mirror free/make), or at least requiremirror_epoch - p.synced_epoch > some idle windowon top ofmade_epoch. Either keeps the A1000 win, since the cold pools there look read-mostly.Smaller things, all nits:
mirror_make(w)can still fail (RX_VK_MIRROR_TIGHT, orcreate_device_bufferfailing), and then the victims were dropped for nothing. You could check the result and log it. It's rare, so fine to leave.heat = heat/2 + windowsettles at about 2x a window's bytes, so the "MB bound a window" in the log and the 64 MB floor are really about 32 MB a window. It's worth wording the log or the floor to match.CI didn't report any checks on this branch. I haven't built it here.
@ -2762,3 +2781,3 @@return remember({p->buffer, off, size, 0});return remember(p, {p->buffer, off, size, 0});}if (p->synced_epoch != mirror_epoch || p->sync_seg > texture_segment()) mirror_sync(*p);Here a victim only needs
last_value <= mem_completedand an old mirror. It can still be in use every frame. If the GPU wrote into it outside the write-back's ranges (bare-entry stores past 64 bytes, page-table stores, seerb_hazard_locked),mirror_freesends the page table back to guest memory that doesn't have those writes, and since such a pool binds little it's the coldest candidate. Could you exclude pools with GPU write hazards since their last sync (or requiremirror_epoch - p.synced_epoch > kIdleEpochs-ish, asmirrors_give_backdoes)?Nit:
mirror_make(*w)below can still fail (RX_VK_MIRROR_TIGHT, or a failedcreate_device_buffer), and then these were freed for nothing. Maybe makewright here and log if it fails?@ -2987,0 +3059,4 @@uint64_t freed = 0, cold_heat = 0;const size_t k = mirror_swap_pick(bytes.data(), heat.data(), cold.size(), mirror_bytes, mirror_budget(), n, w->heat,&freed, &cold_heat);if (!k) return;Nit: with
heat/2 + windowthe steady state is about 2x one window's bytes, so the 64 MB floor is really about 32 MB a window, and the log's "MB bound a window" overstates by about 2x.Thanks for merging main. The only new commit since my last review is that merge (the run_all.py conflict looks resolved correctly), so the earlier points are where they were:
mirrors_swap_for_hot). A victim still only needslast_value <= mem_completedandmade_epoch600+ submissions back. A pool that's bound every frame through a small range, while the GPU writes outside its write-backs (bare-entry stores, page-table stores, seerb_hazard_locked), is still the coldest candidate, and freeing it drops those writes. A per-pool "GPU wrote outside write-backs" flag, or amirror_epoch - p.synced_epochidle window likemirrors_give_backuses, would settle it.mirror_makeafter freeing, the ~2x heat steady state against the 64 MB floor and the log wording, "reads" vs bound bytes) are also unchanged. They're still fine to leave or fix as you like.New, and it's this PR's own: CI's "Python source checks" fails on both Linux and Windows in
tests/test_parallel_rb_refresh.py(ValueError: substring not found). That test, from main, cutsgpu_buffer_bindfrom themirror_sync(*p);line up to the markerreturn remember({p->mirror. This PR changes that line toreturn remember(p, {p->mirror, so the marker no longer exists. It isn't one of main's known failures. Updating the end marker in the test to"return remember(p, {p->mirror"(or just"return remember(") should fix it, since the cut stops before that call anyway. Everything else is green, including the Windows Vulkan build.This rename breaks
tests/test_parallel_rb_refresh.py: it cutsgpu_buffer_bindup to the markerreturn remember({p->mirror, which is now gone (CI:ValueError: substring not foundon Linux and Windows). Updating the marker in the test toreturn remember(p, {p->mirrorshould do it.Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
Thanks for the careful review. Merged main again (the conflicts were
tests/run_all.py, now per-file declarations, sotest_mirror_heat.pydeclares its ownRUN_ALL_ARGS, and the unbound-binding path ingpu_buffer_bind, where main'sshadow_forgetand this PR'sremember(p, ...)both stay). Then:Pool::gpu_onlyis set when the GPU may write a mirror somewhere no write-back copies: every poolmemory_note_gpu_writetouches (a bare entry's guessed range past its 64 bytes, the pointer table's, a wide binding), and a written binding whose write-back main's newRX_VK_WB_MAX_KBskips. It's cleared when the mirror is made or freed, andmirrors_swap_for_hotnever takes agpu_onlymirror. Stores through the page table aren't seen anywhere, so withRX_VK_FOLLOW_STORESset the swap is off entirely.mirrors_give_back's own 600-idle rule is unchanged.test_parallel_rb_refresh.py's end marker is now"return remember(p, {p->mirror".mirror_wanted) and logs when that fails after the victims were freed. The log and comments now say heat is about 2x a window's bytes ("~N MB bound a window" prints heat/2; the 64 MB floor is described as ~32 MB a window). I left the floor's value alone, since that's what was tuned on the A1000. I'll also reword the description from "reads" to "binds".test_mirror_heat.pyalso checks that the swap skipsgpu_onlymirrors and follows theFOLLOW_STORESrule. Built the Windows Vulkan back end locally with the builder's llvm-mingw.Thanks, the points are all handled, and I read the new code.
Fixed:
Pool::gpu_onlyis set frommemory_note_gpu_write(bare-entry guesses, the pointer table, wide bindings) and from written bindings whose write-backRX_VK_WB_MAX_KBskips. It's cleared inmirror_makeandmirror_free, andmirrors_swap_for_hotskips such pools. WithRX_VK_FOLLOW_STORESthe swap is off, which covers the page-table stores nothing sees. That closes the stale-guest-memory case I was worried about. The flag is conservative (a pool staysgpu_onlyuntil its mirror is remade), which is the safe side.test_parallel_rb_refresh.pyis updated and passes.CI: the only red checks are
test_bake_mimo,test_bake_skeleton(Linux) andtest_bake_ds,test_log_batch(Windows), and the last completed main runs fail the same ones with the same counts, so they are not from this PR.test_mirror_heatand the Windows Vulkan build pass. I haven't built or run this here.Good to merge from my side.
Heads-up: this conflicts with main now. Could you rebase onto main and resolve the conflicts? Thanks!
The approval still stands, and the gpu_only change addresses the victim concern. What blocks it now is a single conflict hunk in gpu/vk/vk_memory.cpp, in the not-mirrored path of
gpu_buffer_bind. Main replaced the inlinedirect_write_epochandwb_pages_notelines withnote_direct_write(*p, off, size). The fix is to keep main's call and keep yourreturn remember(p, {p->buffer, off, size, 0});.I checked the other
remember(sites: main and the branch call it in the same five places, so heat counting doesn't miss any binding path added since. Since #2343, GCN cards have no VRAM mirrors by default, so the swap does nothing there. That's fine.After the rebase, please re-run CI so the Python source checks can be compared against current main's results.
The approval and the gpu_only victim rule still stand, but main has moved under them. #2154 (merged) added its own
Pool::gpu_onlyandgpu_writtenin vk_memory.cpp, and they mean something different: on main both flags are sticky, and they keep a mirror from ever being given back in mirrors_give_back. A trial merge of59edbbeinto current main conflicts in four hunks: memory_note_gpu_write, and three remember() returns in gpu_buffer_bind that now call note_in_place_read. The parts that merge silently are the real problem:bool gpu_onlymembers (lines 80 and 86 of the merge), so it won't build.I'd drop your flag, use main's gpu_only and gpu_written as they are (never cleared), and have the swap skip both. The only red in CI on this head is test_env_on on HOS_FPS_LOG, which was main's own failure at the time.