Repository navigation
perf(vk): the 90% rule and the driver's limit evict their excess, not an eighth of the texture cache - #2826
Conversation
… an eighth of the texture cache rule_target cut to 7/8 of the rules' budget once the textures held more than it. That is right for a budget that is a size. The 90% rule's and the driver-limit rule's budgets are "what the textures hold less the excess over a line", as the hold's is, and are crossed again whenever anything grows: Castle Town Market entered from Hyrule Field on an 8 GB card sat 62 MB over the 90% line and lost ~400 MB of textures unused for a second, all in view again within the camera's turn, made again, over the line again. For those two rules what goes is the excess and 64 MB more; the budget's size still asks for its 7/8. RX_VK_EVICT_EXCESS=0: as before. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
A correction to the measurements above, found an hour after opening this: this PC's DLSS model row was set to Transformer 2 (L), not the default (K), for every run in the description. L costs ~4 ms more GPU time a frame on this card (#2836) and, judging by the count, more VRAM too, so it is the harder case. The same test with the default model (
|
hckrman101
left a comment
There was a problem hiding this comment.
Thanks, the diagnosis reads right to me, and the change is small and contained. The 90% rule's budget is "total less the excess", not a size, so a 7/8 cut of it is ~an eighth of the cache for a few MB over the line. The hold was already fixed the same way. I traced it through texture_frame and found no correctness problems:
- Units are consistent.
total_bytescounts allocation size (bytes + pad), andhold_metcounts the chosen textures the same way. The excess comes from the driver's count less the textures' pool free space, and an evicted texture's allocation goes straight into that free space. So a pass that takes excess + 64 MB really does bring the 90% rule's pressure 64 MB under the line by the next budget pass, ten presents later, oncedeadhas been freed. - No new ratchet or thrash.
rule_tois only used in the hold's pass, for textures unused 1-10 s. Older ones still go to the 85/80 lines. Each pass is LRU and bounded, and the next one needs 64 MB of new pressure. Where the scene's working set is really bigger than the room under the line, you'll see more small passes instead of fewer big ones. That's an improvement: re-uploads can only go down when less goes per pass. One thing it doesn't fix: an evicted texture whose memory isn't in the textures' pool doesn't lower the count, so the next pass sees the same excess. That happened with the 7/8 cut too, only bigger, and #2711 handles that case for the 97% rule. - Compaction off is unchanged, as the description says (that pass still breaks at
budget / 8 * 7).
One thing the description gets wrong, and it matters on small cards. "Not tested" says the 97% rule is unchanged and still takes its share. With the compaction on, the 97% rule's cut now always goes through rule_target as excess + 64 MB too. It only fires while the blocks hold less than an eighth of the allowance free, so the reclaim is always under the 17 points that would put its budget below the reserve's. That makes evict_to non-zero and the pass is the hold's. The title says this, but the reasoning about 4-6 GB cards doesn't. At 97% the driver is about to page VRAM out, which costs far more than re-uploading a texture, so the eighth of headroom was doing something there. I'd either keep the 7/8 cut for the driver's limit and take only the 90% rule's excess (track the 90% rule's budget apart from the 97% one), or show it holds on a smaller allowance. RX_VK_TEST_ALLOWED_MB=5632 on the 3060 Ti gives a 6 GB card's allowance, and a few minutes of the field and the Market with the 97% line's log lines would do.
On the numbers (thanks for posting the correction quickly): with the default DLSS model (K), the only measurement is #2825 + #2826 against the main stack, two runs each, and that's within noise. Since this one changes the default eviction for every card, could you add the pair that isolates it with K: #2825 alone vs #2825 + this PR, same Market-from-the-field route? Please include the count the description already uses: uploads a frame and the textures retired sawtooth. That shows the mechanism much more clearly than fps does. A base without #2731's old default-on state would also match what will land.
CI is green and the branch merges cleanly. It will need a small rebase after #2711, which edits the 97% rule's condition three lines above your hunk; the two are complementary. #2834 touches vram_compact.h and its tests in other places.
| inline constexpr uint64_t kExcessSlack = 64ull << 20; | ||
| inline uint64_t rule_target(uint64_t total, uint64_t size_budget, uint64_t rule_budget) { | ||
| uint64_t to = rule_target(total, size_budget); | ||
| if (rule_budget < size_budget && total > rule_budget) to = std::min(to, rule_budget - std::min(rule_budget, kExcessSlack)); |
There was a problem hiding this comment.
rule_budget here is the lower of the 90% rule's and the 97% rule's budgets, so the driver's limit gets the 64 MB slack too (see the review body). To keep the deeper cut at the limit, texture_frame could pass the 90% rule's budget here and keep the old two-argument form for the 97% part, taking the lower of the two.
| @@ -4303,7 +4304,11 @@ void texture_frame(uint64_t completed) { | |||
| budget = std::min(budget, total_bytes > over ? total_bytes - over : 0); | |||
| rule_budget = std::min(rule_budget, total_bytes > over ? total_bytes - over : 0); | |||
There was a problem hiding this comment.
This is the 97% rule's contribution to rule_budget, which now also goes through the excess-only target. #2711 changes the condition above, so expect a small rebase.
|
Data from a Steam Deck, where the budget's size is what evicts: the same sawtooth, and the same remedy seems to work there This PR is not in my renderer, but reading it explained what I see on a Steam Deck. Here are the numbers, in case they help for the case the PR leaves at 7/8: a budget that is a size. Setup. Steam Deck OLED (Van Gogh Which rule evicts here. The driver's count stays under the 90% line ( As it is, in play (13 minutes): 170 passes for the budget, 16,485 textures let go (1,250 a minute); 51 With the excess and 64 MB instead (7 lines, below:
Limits. One Deck, one session each, two different routes. A system memory setting also changed between the two sessions ( So where the budget is a size well under what the driver allows, cutting to 7/8 of it seems to cost what this PR removes for the 90% rule. Whether that belongs here, in a follow-up or behind a switch is yours to judge. I am glad to run a build on this Deck and report. The test patch (gpu/vk/vk_texture.cpp, on main c544828 with the six PRs above)diff --git a/gpu/vk/vk_texture.cpp b/gpu/vk/vk_texture.cpp
index 7dc58baf..7ce7c1f4 100644
--- a/gpu/vk/vk_texture.cpp
+++ b/gpu/vk/vk_texture.cpp
@@ -4527,10 +4527,16 @@ void texture_frame(uint64_t completed) {
if (frame_no - last < need_unused && !shifted) continue;
if (shifted || drawn_over(tex)) hold_dead += tex->bytes + tex->pad;
}
+ // (deck test) RX_VK_EVICT_SLACK_MB=<MB>: a pass over the budget's size lets go of the excess and that much more, not an
+ // eighth of the budget. A Steam Deck at RX_VK_VRAM_MB=2000: 19,351 textures went for the budget in 14 minutes of play
+ // (203 passes, the cache cut from 2,000 to 1,750 MB each time), what was on screen seconds before among them
+ static const int64_t evict_slack_mb = getenv("RX_VK_EVICT_SLACK_MB") ? atoll(getenv("RX_VK_EVICT_SLACK_MB")) : -1;
+ const uint64_t cut_to = evict_slack_mb < 0 ? budget / 8 * 7
+ : std::max(budget / 8 * 7, budget - std::min(budget, (uint64_t)evict_slack_mb << 20));
for (auto& [last, tex] : order) {
// (total_bytes can have drifted below the textures' sum (destroy_later clamps it): the chosen ones' bytes past it mean the
// budget is met, not a wrapped difference that never is)
- if ((!sweep && !by_hold && (pending_bytes >= total_bytes || total_bytes - pending_bytes <= budget / 8 * 7)) ||
+ if ((!sweep && !by_hold && (pending_bytes >= total_bytes || total_bytes - pending_bytes <= cut_to)) ||
frame_no - last < need_dead)
break;
const bool shifted = drawn_over_shifted(tex); |
hckrman101
left a comment
There was a problem hiding this comment.
I accept this as is, including the 97% path now taking the excess plus 64 MB too. Approved as the maintainer.
Not a blocker, but please keep an eye on 4-6 GB cards. If you can, post a run with RX_VK_TEST_ALLOWED_MB=5632 (the field and the Market, with the 97% line's log lines). This one goes together with #2825. #2711 edits the 97% condition three lines above your hunk, so whichever lands second may need a small rebase.
… its live parts (#2825) **Why.** On an 8 GB card, Castle Town Market runs at ~54 fps with almost no long frames when it is loaded from a save, and at ~45 fps with a long frame every second or two when the player walks in from Hyrule Field, for as long as they stay. Part of the difference is VRAM the previous scene's pools still hold: imports never shrink. - An import only grows: a binding across its end makes it again, larger, and a pool the game binds whole is imported whole. A pool's VRAM mirror, and its write-back shadow, are its import's whole range. - When the game frees a scene's large pools and makes the next scene's smaller ones at the same addresses, the import keeps the old range. It is bound every frame through the small live pool inside it, so no idle rule ever lets its mirror go. - Measured (the two largest; from a local diagnostic that lists the live mirrors, not in this PR): the field's 213 MB pool at `0x108c711000` leaves a **224 MB** mirror where the Market loaded from a save has **48 MB**; the culling lists' pool is **152 MB** where it has **104 MB**. With their shadows that is ~260 MB the scene does not bind, on a card where the Market's textures sit right at the 90% line. This is the launch-to-launch "fast or slow Market" from #995 and my note on #2685: it was not the launch, it was whether the save was in the Market or in the field. ## What `gpu/vk/mirror_trim.h` (rules only) and `mirrors_trim()` in `vk_memory.cpp`, called from `memory_begin_submission` every 64 submissions: - **Which import.** One with a mirror whose game pools (the host's table, each out to its 4 MB chunks, parts closer than 8 MB joined) leave at least 24 MB and a quarter of it outside every pool, in 1 to 4 live parts, and that has stayed so for 1800 submissions (~5 s). Only while VRAM is over 80% of the allowance: with room to spare nothing is gained. - **How.** Its live parts are imported first (a failed import leaves everything as it was). Then it is retired with `retire_pool`, as a pool made again larger is, and each part gets its page table entries and a mirror **in the same submission's uploads**, before anything is recorded. Each new mirror takes what the GPU wrote from the old one through `mirror_make`'s existing inherit (pages the CPU stamped since come from guest memory, as there). The old mirror goes once the GPU is done with it. - **Room.** Nothing is trimmed unless the new mirrors fit the mirror budget beside the old one. - **No thrash.** A range the game binds across again right after a trim is made larger again by the existing path; it then waits 4x and 16x longer, and after three trims in ~2 minutes it is left alone. The count starts over after 36,000 submissions without a trim, so a scene change every few minutes over a session is still trimmed. - **Not touched:** an import with no game pool in it at all (nothing binds it: the idle rules, and #2759's retire of finalized pools, cover that case; this PR is the partly-live one, which #2759 leaves as "alive"), parallel replay segments, cards without mirrors. - `RX_VK_MIRROR_TRIM=0`: off. One log line a trim. What is given up: what the GPU wrote into the **dead** part of the old mirror and never wrote back. No game pool covers that memory any more, so nothing can bind it; a pool the game makes there later gets a mirror from guest memory, as any new pool does. ## Measured **Note:** every run below had this PC's DLSS model row on Transformer 2 (L), not the default K: the harder case (more GPU time and VRAM, #2836). With K the gain is smaller; those numbers are in the comment below. Windows 11, RTX 3060 Ti 8 GB (driver 32.0.16.2114), Ryzen 5 5600X, 2560x1440 DLSS. `main` ba005ca with #2711, #2731 (at bce3d17, where it was still on by default), #2685 and #2709 merged locally (the VRAM picture on this card without #2711 is the #2690 regression). Load the save, teleport to Hyrule Field, 10 s there, teleport to the Market, settle, then **80 s** of the camera turning. `RX_VK_FRAMEGEN=0`, `HOS_FPS_MAX=240`. Three runs an arm, arms alternated. The fourth arm adds #2826 (the 90% rule evicting its excess instead of an eighth of the cache), which is the other half of the same stutter. | arm | fps mean | frames over 50 ms in 80 s | after the first 20 s: fps | after the first 20 s: frames over 50 ms per 20 s | |---|---|---|---|---| | main stack | 44.4 / 45.0 / 45.0 | 100 / 69 / 60 | 46.5 | 12.3 | | **this PR** | 45.7 / 47.2 / 47.4 | 66 / 37 / 28 | 48.5 | 4.7 | | #2826 alone | 43.3 / 46.6 / 46.2 | 75 / 25 / 60 | 47.0 | 7.0 | | this PR + #2826 | 49.7 / 48.8 / 49.3 | 10 / 15 / 22 | 50.6 | 0.3 | - The first 20 s are the arrival itself (the trim only starts ~6 s after the old pools die), and are about the same in every arm. - Both trims happen in every run: `pool 0x108c000000..0x109a400000 (228 MB) trimmed to its 1 live part(s), 48 MB` and `0x1178400000..0x1181c00000 (152 MB) ... 104 MB`. Mirrors at the end of a run: 1,584 MB against 1,820 MB. The new imports took under 2 ms each (no `imported in` line, which is logged from 2 ms). - Loaded from a save in the Market, no import qualifies and no trim runs (0 in two 30 s runs, 47.1 / 47.9 fps; with this and #2826 off 46.5 / 46.0). Those are lower than the 53-55 fps the same load gave in the morning: the driver's allowance was ~100 MB lower by then (6,944 against 7,043 MB), which is enough to put the direct load over the 90% line too. **A session:** two rounds of 12 scenes by teleport (Hyrule Field, Kokiri Forest, Market, Kakariko, Lost Woods, Goron City, Zora's Domain, Graveyard, Death Mountain Trail, Zora's River, Sacred Forest Meadow, Gerudo Valley), 20 s of camera turning in each, with the flash detector on (`RX_VK_FLASH=1`, `RX_VK_SHOT_EVERY=1`): 24 of 24 scenes reached, **13 trims (600 MB given back in all), no flash detected, no device loss, no failed import**. The mirrors moved between 1.2 and 2.0 GB and ended at 1,656 MB; the same tour before this sat at the 2,000 MB budget. Screenshots after the camera turn in Kokiri Forest (foliage, grass), Hyrule Field and the Market look right; I did not watch the runs live. ## Not tested - AMD, Intel, Linux. On Windows the retired import goes back by the existing rules; AMD and Intel hold retired imports for a grace time there (`retired_give_back.h`), which this does not change but does feed. - `RX_VK_PARALLEL` (the trim does not run inside its segments, and was not run with it on). - A card with more VRAM than it needs: the 80% gate means it should never run there. - Long play (hours). ## Tests - New `tests/test_mirror_trim.py` compiles and runs `tests/test_mirror_trim.cpp` (the Market's case, imports their pools fill, a small dead share, no live pool, parts joined and kept apart, too many parts, the waits and the count that starts over, the VRAM gate) and checks `vk_memory.cpp` keeps the order: parts imported before the retire, mirrors made before the old one stops waiting to be inherited, room in the budget first. - `nx2/test_import_windows.py`'s model of `Pool` gets the two new fields `pool_lookup` carries over; `tests/test_texel_buffer_ownership.py` counts the trim's rollback among the callers of `destroy_import_buffer` (10). - `python -I -B tests/run_all.py` on Windows: 460 passed, 2 failed, 34 skipped; the two (`nx2/test_nup_paths.py`, `tests/test_input_background.py`) fail on unchanged `main` here too. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com> Co-authored-by: hckrman101 <239247847+hckrman101@users.noreply.github.com>
… its 7/8 Review of NotRexed#2826 (hckrman101): rule_budget was the lower of the 90% rule's and the 97% rule's budgets, so the driver's limit got the 64 MB slack too. Past 97% the driver pages VRAM out, which costs more than a texture made again, so that rule keeps the eighth of headroom: texture_frame now passes the 90% rule's budget to the excess-only target and the limit's to the two-argument form, and takes the lower. RX_VK_EVICT_EXCESS is read by the switches' rule. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Thanks for the review and the merge. Two notes for the record:
@hclaria thank you for the Deck numbers: 1,250 -> 663 textures a minute and 5.9 -> 1.0 s of upload stalls is the same sawtooth one rule over. That case (a budget that is a size, well under what the driver allows) is not touched by this PR, and your seven-line patch is the natural follow-up. It is yours to open if you want it; I only have this one NVIDIA card, so I could not measure it where it matters. |
Why. On an 8 GB card the texture cache sits right at the 90% line in the larger scenes. When it is a few MB over, the pass lets go of an eighth of the cache, textures that were on screen a second ago, and they are made again within the camera's turn: a loop of evictions, uploads and compaction runs that shows as a long frame every second or two.
rule_targetcuts to 7/8 of the rules' budget once the textures hold more than it. That is right for a budget that is a size (the card's or a preset's).2,873 MBof textures, 62 MB over the 90% line. The pass's target was7/8 x 2,811 = 2,460 MB: ~400 MB of textures unused for 60 presents go, all in view again a few seconds later. The log shows it as a sawtooth,textures retired ... budget 386 / 362 / 295and the cache going 2.95 -> 2.5 -> 2.95 GB, with 8-9 uploads a frame, where the same scene loaded from a save (just under the line) has 0.What
vram_compact::rule_target(total, size_budget, rule_budget): the budget's size still asks for its 7/8. A rule budget below the size (the 90% rule, the driver's limit) asks for its excess andkExcessSlack(64 MB) more, so that not every pass is one. The lower of the two when both apply.texture_framekeeps the budget as it was before the 90% rule (size_budget) and passes both.RX_VK_EVICT_EXCESS=0: to 7/8 of their budget, as before.RX_VK_COMPACT, the default) goes throughrule_target; with the compaction off nothing changes.Measured
Note: every run below had this PC's DLSS model row on Transformer 2 (L), not the default K: the harder case (more GPU time and VRAM, #2836). With K the gain is smaller; those numbers are in the comment below.
Windows 11, RTX 3060 Ti 8 GB (driver 32.0.16.2114), Ryzen 5 5600X, 2560x1440 DLSS.
mainba005ca with #2711, #2731 (at bce3d17, where it was still on by default), #2685 and #2709 merged locally. Load the save, teleport to Hyrule Field, 10 s there, teleport to the Market, settle, then 80 s of the camera turning.RX_VK_FRAMEGEN=0,HOS_FPS_MAX=240. Three runs an arm, arms alternated. The other half of the same stutter is #2825 (the previous scene's oversized mirrors), so both are shown alone and together.Not tested
Tests
tests/test_vram_compact.cpp: the three-argumentrule_target(no rule, the excess alone with the Market's numbers, the size's 7/8, both, a budget under the slack), next to the existing cases, which are unchanged.tests/test_vram_compact.pycheckstexture_framekeepssize_budgetand calls it, with the switch.python -I -B tests/run_all.py vram_compact texture_squeeze vram_pressure vram_budget preset_texture target_image(the checks that read this code): 6 passed. The full suite was run on perf(vk): an import mostly outside every game pool is trimmed back to its live parts #2825's branch (460 passed; the 2 that fail there fail onmainhere too), not again on this one.🤖 Generated with Claude Code