Repository navigation
Conversation
…compute modules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(shader): keep each BDA page-table walk to one inlined copy per read RADV inlines every SPIR-V function, DontInline included, so every get_bda_pointer call site becomes its own copy of the page-table search, and NIR's copy propagation of variables grows with each copy. The lookup now keeps its search bounds in phis and records its header and range faults from one call site each, picking the first failing check; a byte read and the byte fallback of a span read loop over one lookup instead of unrolling one per byte or dword. (cherry picked from commit 2d3a970) Ported from upstream PR boykopovar#3655 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(shader): decode a GPU-selected formatted load from a format table A formatted load whose V# is selected on the GPU emitted all 77 format cases in its shared function, and drivers that inline every function, like RADV, copied all of them into each call site. The load now reads the format's component count, widths, byte and bit offsets and type from a constant table and runs one path for its four components, with the existing conversion of each (type, width) pair selected at run time. Stores and loads through a prepared V# keep the format switch. (cherry picked from commit 369928d) Ported from upstream PR boykopovar#3656 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(shader): pin GPU-selected formatted loads for every buffer format agc_driver_gpu_buffer_format loads the V#'s records and word3 from memory and moves them into SGPRs with v_readfirstlane, so the recompiler sees a GPU-selected descriptor and the load goes through the format table. It checks buffer_load_format x/xy/xyz/xyzw and d16_xyzw on two lanes for formats 1-77, selectors 0/1/4-7 on 1-, 2- and 4-component formats, loads with a component past NUM_RECORDS, and that formats 0 and 78 stop the invocation before its stores. The expected values were captured on the format switch (2d3a970) and are identical with the table. (cherry picked from commit 6aaae03) Ported from upstream PR boykopovar#3656 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(shader): try the last resolved range first in BDA lookups Every access by guest address calls a lookup that binary-searches the BDA range table: about 11 dependent steps over ~1200 ranges, 34 table loads per access. Since boykopovar#639 every buffer access goes through it. The lookups (read, probe, write and atomic) now keep the last range a search of the invocation resolved in four private variables: guest begin and end, device base and permissions. An access inside that range with the wanted permissions returns base + (address - begin) at once. Anything else (another range, a missing permission, an unmapped, zero-byte or wrapping access) takes the full search with every check and fault it records today, and a successful search refills the cache. The variables only hold what the table says, and no shader writes the table, so the result is the search's own answer. On one title's busiest scene (Windows, discrete GPU), GPU time of the draws 59.5 -> 35 ms per frame. APS5_NO_BDA_LOOKUP_CACHE=1 emits the lookups without the cache. The switch is part of the shader disk cache key. (cherry picked from commit 443583a) Ported from upstream PR boykopovar#2837 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(shader): read adjacent BDA dword loads of one block through one span read A scalar load of several dwords (s_load_dwordx8) becomes one LoadAddressU32 per dword, and each took its own span read: a probe of the page table and a fallback that looks every byte up. Loads in one block with equal address operands and offsets within 16 bytes of the first now share its span read. Instructions that write memory end a group; the address operands are compared by content, since each load carries its own GetAddressResource and SelectU32 values. A title's main-menu pixel shader goes from 136 span reads to 34. AMD's Windows compiler inlines the read helpers at every call, and did not finish that shader before. (cherry picked from commit 4577633) Ported from upstream PR boykopovar#4931 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(shader): read GPU-selected V#s of s_buffer_load as SMEM does An s_buffer_load_dword* whose V# the recompiler reads at run time (a GPU-selected V#, for example one built with v_readfirstlane) went through the vector buffer descriptor checks. A nonzero TYPE in word 3 made it return 0 and record an invalid-descriptor fault, and the base was used with its two low bits, so a base off a dword boundary read shifted bytes. SMEM uses only the base, stride and num_records of the V#, and the two low bits of the base and of the final address are ignored (AMD RDNA2 ISA guide, 30 November 2020, section 7.1 Table 27 SBASE and section 7.2.1 "Reads using Buffer Constant"). The capture-time evaluator already follows both rules, and the bound path already drops the two low bits. For scalar buffer loads the runtime path now masks the base and skips the TYPE check; vector, formatted and atomic accesses keep it. agc_driver_scalar_buffer_gpu_descriptor runs s_buffer_load_dword and s_buffer_load_dwordx2 through such a V# with the base 0 to 3 bytes off a dword and TYPE 0 to 3, in wave32 and wave64. (cherry picked from commit 86fdb52) Ported from upstream PR boykopovar#5161 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(recompiler): read the buffer stride and presence at run time in compute modules (cherry picked from commit 573ba3b) Ported from upstream PR boykopovar#5353 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: DotDebian <quentin.waguet@hotmail.fr> Co-authored-by: poonasor <rickypoon89@icloud.com> Co-authored-by: Niko <nikobilger2000@gmail.com> Co-authored-by: Alex Collini <pipodi93@gmail.com>
CnRJay
left a comment
There was a problem hiding this comment.
Well-documented change, and the compile-time measurement is careful. One side of the trade-off is missing, though: what the run-time form costs when the pipeline executes.
Specialization constants are there so the driver compiler can fold the stride into every address computation and drop the code of an absent buffer entirely. Reading them from the V# turns that into per-access ALU work, an extra predicate per load/store, and, for 1.1–1.5M-word programs, possibly higher register pressure. If occupancy drops, every dispatch of these programs gets slower for the rest of the session. The replay table shows the compile doesn't get slower, but not whether the dispatch does.
The two costs are also paid at different times:
- The 103 s saved is a cold-boot, cold-driver-cache cost. Once the driver cache holds the absent variants, a second boot doesn't build them again.
- Any GPU-time cost of the run-time form is paid every frame, on every boot.
Before this goes in for all compute stages, I'd like to see GPU time for these dispatches in both forms: the trace's GPU ranges for the Yōtei programs, or agc_compute_pipeline_replay timing a dispatch rather than the compile. If one form costs measurably more on large programs, a narrower option is to read stride and presence at run time only where a variant with a different stride or presence was actually seen for the program. That covers the absent-then-present pattern here without changing every compute module.
The code changes look consistent with the registration-form path (descriptor() reuses its run-time presence expression, Prepare drops exactly the two constants), and the new sameModule checks are a good guard.
|
CnRJay's question is the one that decides this: compute modules now read the stride and presence at run time, which may cost GPU time on every dispatch, and the 103 s saved is a cold-cache cost only. |
|
I measured on PPSA26344 what @CnRJay and @oneandonlydean asked for: the GPU time per dispatch, and the template's summary lines after a warm-up. In short, I found no measurable GPU cost. The run-time form does add a cold-boot cost that the PR description doesn't mention. Builds. Both builds are this PR's base 18c438b plus the same additions.
None of these change how buffer accesses are emitted. Machine: RTX 4090, NVIDIA 617.42, Windows 10.0.26200, 3840x2160. Both builds ran with the same env: Scene. The loading phase, presents 700 to 760 ( Runs. For each build, I ran warm-up boots until the shader disk cache was complete. Then I restarted it with only the trace switch set. Every capture had 0 disk cache misses. I captured A twice, before and after B, to see how much boots differ. Summary lines (presents 700 to 760):
The two A captures differ by 6.4% in GPU busy time, and B is 3.6% below their mean, inside that spread. Frame times in this window are set by CPU shader preparation (8 prepare threads about 90% busy, Full summary linesGPU time per dispatch (
The programs that take the most GPU time are within 5% either way. Programs under 1 ms vary by up to 20% in both directions. In presents 700 to 760, Cold boot, not in the description. With empty caches, B shows its first frame at about 462 s, against 141 s for A. The absent-buffer variants were cheap to build: A builds So: in this title I found no GPU cost per dispatch or per present. The trade is a cold-only one: fewer variants in total, at the price of a longer wait before the first frame. Celegans12 reported on #675 that specializing buffer slots that are null when first bound took these programs from 37 variants to 6. That keeps the cheap builds, so it may be worth comparing with this PR or with the narrower option. |
|
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.
|
CnRJay
left a comment
There was a problem hiding this comment.
Thanks @Pipodi, this answers the question I asked: no GPU cost per dispatch or per present, with the top programs within 5% in both directions and the A/A spread (6.4%) larger than the A/B difference.
That leaves the cold boot as the real price of this change: first frame at about 462 s against 141 s, because the stride and presence split no longer lets the large programs build a cheap variant first. That is a regression for every first launch with an empty cache, and the description should state it. The shipped trade is "fewer variants in total, 3x longer wait before the first frame", and a user who has never run the title sees only the second part.
Two things to settle before this goes further:
- Compare against the narrower option from #675 (specialize slots that are null when first bound, 37 variants down to 6). If it keeps the cheap builds and gets a similar steady-state result, the run-time form is the wrong approach for these programs.
- Builds hold the GPU mutex on main, so the longer builds block the first present. If the run-time form stays, the 86.5 s build of
0x8000289300should not run under that lock.
|
Agreed with @CnRJay's review. The cold-boot cost (first frame at about 462 s against 141 s with empty caches) should be in the description, since it's what every first launch pays. Before this goes further I'd also like the comparison with the narrower option from #675 (specializing slots that are null when first bound), and, if the run-time form stays, the 86.5 s build of |
|
@CnRJay @oneandonlydean I measured all three on current Setup. PPSA26344, RTX 4090, NVIDIA 617.42, Windows 11 Pro 26200. Three builds on
Each arm ran one cold boot for 1200 s, in the order P, Q, R, with default settings. Each started with an empty shader cache, pipeline cache and NVIDIA driver cache. The NVIDIA cache was moved aside before each run; our earlier cold runs had silently hit it. Each run was checked to be cold: 1.08M words of
Builds of the ≥ 1M-word programs, per arm
What this shows, with one run per arm:
So this PR shouldn't merge, and the narrow option isn't worth a PR on this evidence. I'm closing it. Thanks both for pushing for the cold measurement. AI-assisted: yes (Claude Code) |
|
Thanks for measuring all three arms with cold caches. A 120 s cold-boot cost with no saving in total on current main is a clear answer, and closing makes sense. |
What
A specialized compute module bakes four words of each buffer V#: the stride (
dwords[1] & 0xffff0000), the format word (dwords[3]), whether the buffer is present, and the base misalignment. When a program is dispatched with a different stride, or once with an absent buffer and later with a real one, the module differs only in those constants. The driver still compiles each one as a new pipeline, from scratch.With this change compute modules read the stride and the presence from the V# at run time, the way registration-form modules (#4152) already do:
descriptor()takesword1andpresentfrom the V# operands for compute stages. The record count was already a run-time value there.DescriptorBindingBuilder::Prepareno longer adds those two constants for compute stages, so modules that differ only in them get the same specialization and the same pipeline.What stays baked: the format word, because a run-time format makes the driver compile every format decoder (#4152 item 13), and the misalignment, because the unaligned store loops it would otherwise keep caused hangs (#4152 item 15). Graphics stages are unchanged.
Why
Ghost of Yōtei (PPSA26344) dispatches its largest compute programs (1.1–1.5M SPIR-V words) first with one buffer absent and later with it present. The two modules are structurally identical: diffing them shows only 44–132 constants changed. These are the absent buffer's stride, its presence flag, and the folded results (
8 → 192,false → true). A cold boot builds both.Measured offline:
agc_compute_pipeline_replay, RTX 4090, driver 617.42, fresh driver cache per run, idle machine. Modules from captured pipelines; for the run-time form, the constants that differ between a pair are replaced with push-constant loads.0x800022d100present, specialized0x800022d100present, stride and presence at run time0x8000261500present, specialized0x8000261500present, stride and presence at run timeReading the two values at run time does not slow the driver's compile down, unlike the run-time format and component selectors measured in #4152. A cold boot of this title then compiles 13 fewer giant pipelines, about 103 s on this machine. That's 18% of those programs' compile time.
Tested
Windows 11 Pro 26200, RTX 4090, driver 617.42.
agc_driver_buffer_descriptor_runtime: newstride 16case. In specialized form thealigned,records 37,stride 16andabsentcases must give byte-identical SPIR-V and the same pipeline variant, and every case still checks the values it reads. Onmainthe new check fails ("the stride, record count or presence of the V# changed the compute module").ctest -j 16): 1096/1097.agc_driver_pixel_ldsfailed once under the parallel run; on its own it passes 4 times in 4. It's a pixel-shader test this change doesn't touch, and perf(agc): build shader pipeline libraries when shaders are registered #4152 reports the same test as unstable on an RTX 4090.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt (none).mdfiles or images added to the repository🤖 Generated with Claude Code