Repository navigation
fix(shader): correctly rounded f32 reciprocal on every device - #7671
tibzejoker wants to merge 2 commits into
Conversation
v_rcp_f32 and v_rcp_iflag_f32 were a plain OpFDiv, whose precision Vulkan leaves to the device: an RTX 4090 and an Intel UHD 770 return reciprocals one ulp apart, and each differs from the correctly rounded value for some inputs, so the same shader produced different bytes on the two vendors (seen on a GTA V compute pass). Two fused Newton steps from the division estimate now give the correctly rounded reciprocal whenever the input and the estimate are normal; zero, infinities, NaN and denormals keep the division result. Integer division sequences built on v_rcp_iflag_f32 also get the same value on every device. Test: agc_driver_rcp_correctly_rounded compares v_rcp_f32 of 8192 normal inputs over the whole exponent range with the correctly rounded reciprocal; on main it fails on both GPUs, with this change it passes on both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P
|
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.
|
|
Tested on AMD on Linux, which this PR had not covered.
Full
|
|
Thanks @DavidAmsellem, that covers the vendor I could not test. If you still have the build, it would be useful to know whether AI-assisted (Claude Code). |
|
@tibzejoker RADV is not correctly rounded either, so this is not NVIDIA only. I rebuilt RADV's result is one ULP below the correctly rounded one, which matches what |
|
Thanks, that settles it: all three vendors are one ulp off on some inputs with AI-assisted (Claude Code). |
|
Native RDNA 2 datapoint for the reference used by head GPU: Ryzen 9 9950X iGPU, gfx1036, PCI v_rcp_f32 v10, v4
v_rcp_iflag_f32 v11, v4Inputs are the exact 8192 I repeated the complete corpus in wave32 and wave64, each with Reference/control checks:
This measures gfx1036, not PS5 gfx1013, and does not establish the console's exact result or the GTA V pass's expected bytes. It does show that cross-device correctly rounded division is a different target from the measured gfx1036 instructions. Could the intended semantic target be clarified or measured on gfx1013 before treating the new correctly rounded oracle as console equivalence? No Vulkan-PR runtime or title result is claimed here. |
|
RDNA 3 datapoint, same method as @simonbbby: gfx1100's native GPU: RX 7900 XT, gfx1100, RADV Mesa 25.2.8, Linux. The two VALU instructions straight through the v_rcp_f32 v10, v4
v_rcp_iflag_f32 v11, v4Inputs are the 8192
So the one ULP shortfall is not vendor specific and not RDNA 2 specific: RDNA 1 (earlier report), This measures gfx1100, not the console's gfx1013, so like the gfx1036 report it does not establish AI-assisted (Claude Code). |
|
Thank you both, that is exactly the data I was missing. You're right about the target. This PR makes the result identical on every host GPU: today NVIDIA, Intel and RADV each return their own value. It does not reproduce the console. Your measurements show that RDNA 1, 2 and 3 agree with each other bit for bit (889/8192, same first input), so gfx1013 most likely returns the same values. That makes the hardware result a better target than correct rounding: it would be both deterministic on every host and faithful to the console. Reproducing it needs the hardware algorithm, not a table. For a normal input the mantissa of Until then I'd keep this PR as it is (one value on every device, at most one ulp from RDNA on about 11 % of inputs) rather than leaving each driver to round differently. Maintainers, if you'd rather wait for the hardware-exact version, I can mark it as a draft. AI-assisted (Claude Code). |
|
AMD run, since the description covers an RTX 4090. Radeon 860M (RDNA 3.5, driver 32.0.13058.8001), Windows 11 26300, WinLibs GCC 15.2.0 r7, Release,
AI-assisted |
|
Tested at A wider probe ran 283,697 inputs per GPU, covering denormals, ±0, inf, NaN, exponents 1/2/253/254, powers of two and all-ones significands.
One limit of the two-step refinement: when the estimate is 1 ulp off, one input per exponent still comes out wrong, the one whose significand is all ones. An exhaustive CPU model shows this ( Both GPUs here return exact estimates for all 508 such inputs, so nothing fails, but Side note, pre-existing and unchanged: both GPUs flush denormals despite float mode AI-assisted: yes (Claude Code); results above were run on this machine. |
Two Newton steps from an estimate one ulp off still miss the correctly rounded reciprocal of an all-ones significand (0x3fffffff: estimates 0x3f000000 and 0x3f000002 both end at 0x3f000000). One fused Newton step then a choice between the result and its neighbour in the direction of the exact residual fma(-x, y, 1), keeping the smaller residual, is correctly rounded for every significand and estimates up to six ulps off (exhaustive model with hardware FMA), and uses two fewer FMAs. The test adds the all-ones significand of every exponent it covers, with both signs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P
|
@wachi35676 thanks, good catch on the all-ones significand. Pushed c402b92, which changes the last step: one fused Newton step, then the closer of the result and its neighbour in the direction of the exact residual An exhaustive CPU model with hardware FMA, over all 2^23 significands with estimates from -6 to +6 ulps around the correct result:
It also costs two fewer FMAs. The test now adds AI-assisted (Claude Code). |
|
Re-tested at
AI-assisted: yes (Claude Code); results above were run on this machine. |
jotitadev
left a comment
There was a problem hiding this comment.
Apple GPU / MoltenVK run. Metal's division is already correctly rounded on this test's inputs, so on Apple the change keeps the same values, and it also holds when MoltenVK's fast math is forced on.
Machine: Apple M2 Max, macOS 27.2, x86-64 build under Rosetta. Head c402b92, PR diff applied cleanly. Drivers: MoltenVK 1.4.2 (Vulkan SDK 1.4.363.0) and MoltenVK main + SPIRV-Cross #2704.
Tested: the tree is main a373e85 + mugurc's macOS series (ci/macos-series-2) + the PRs needed to build on macOS (#6667, #6668, #7524, #6969, and two files of #7332), not plain main (it doesn't build on macOS yet). #7254 was applied in the same build (disjoint files). Base on that tree: 40 ^agc failures with MoltenVK 1.4.2 and 26 with MoltenVK main + #2704, the same known failures in every run below.
agc_driver_rcp_correctly_rounded,agc_driver_vop1_float_unaryandagc_driver_divisionpass 3 of 3 on both drivers.- Full
^agcsuite with the PR: 40 of 729 fail on MoltenVK 1.4.2 and 26 of 729 on MoltenVK main + #2704, the same tests as the base on each driver. - The test alone on the base tree (
SpirvAluEmitterMath.cppat the base) also passes on both drivers, 3 of 3, withANYPS5_NO_SHADER_CACHE=1. The module is a plainOpFDiv %float_1and declaresSignedZeroInfNanPreserve 32, so MoltenVK's default fast-math mode ("on demand") builds it without fast math, and Metal's precise1.0 / xgives the correctly rounded value on all inputs the test generates, all-ones significands included. - With
MVK_CONFIG_FAST_MATH_ENABLED=1(forced on, not the default) the base fails:v_rcp_f32 of 0xb5b0e85a is 0xc9393a04, expected the correctly rounded 0xc9393a03. With this PR it passes on both drivers with fast math forced on.
So Apple is a fourth vendor here, and so far the only one already correctly rounded on these inputs, which means this test can't catch a regression on a default Apple setup. If the target moves to RDNA's hardware values, as discussed above, Apple will need the emulation as well.
GPUs tested: Apple M2 Max (MoltenVK 1.4.2; MoltenVK main + SPIRV-Cross #2704)
Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.
|
Thanks @jotitadev, a fourth vendor is very useful. So on Apple the default build was already right, and only forcing fast math on breaks the base ( You're right that the test cannot catch a regression on a default Apple setup. It still catches one on NVIDIA and Intel (both fail on main, see the description) and on MoltenVK with fast math forced on, so I'll leave it as is. If the target moves to RDNA's hardware values later, the expected values change for every vendor, Apple included, and the test will cover all four. Written with AI assistance (Claude Code). |
|
@tibzejoker here is the sweep you asked for: native Data:
Desktop RDNA 2, not the console's gfx1013. |
|
Thank you @thuanquang, this is exactly the data I asked for. I checked it here: the words file agrees with your delta file and with an integer correctly rounded reference (761,701 one ulp low, 135,974 one ulp high), and What the table shows about the hardware:
So matching AI-assisted: yes (Claude Code) |
adriadam10
left a comment
There was a problem hiding this comment.
Thanks for the thorough testing and the exhaustive model. The blocker is the target, not the implementation:
- Real RDNA
v_rcp_f32/v_rcp_iflag_f32are not correctly rounded: the oracle runs in this thread (gfx1036, gfx1100, and @thuanquang's gfx1031 sweep) show 889/8192 inputs of this PR's own fixture one ulp off the rounded reference (0x0f5b5acdis0x6f956235on HW, the test expects...36). - On AMD hosts RADV lowers
1/xto the nativev_rcp_f32, somainis already bit-identical to the console there. This PR makes AMD wrong on ~11% of inputs, andRcpCorrectlyRounded.cpp:101pins that wrong value as the expectation (Rounded()atRcpCorrectlyRounded.cpp:59). - "Same on every vendor" is only worth it if it is the hardware value; a deterministic result that differs from the console is not better for the GTA V compute pass than the current one, and costs ~10 SPIR-V instructions per rcp (
SpirvAluEmitterMath.cpp:919).
What would fix it: emulate the hardware result (the quadratic-interpolation model you derived from @thuanquang's table), with the test expectations taken from the HW sweep (gfx1013 is expected to match gfx1031/1036/1100). Please include the mismatch count of the emulation against the full 2^23 sweep (0 expected) and the added instruction count. If the exact algorithm cannot be recovered, keep main and say so rather than pinning correct rounding.
|
Thanks @adriadam10, agreed: a deterministic value that is not the console's is not worth ~10 instructions per rcp, and pinning correct rounding in the test would make RADV wrong where I'll make one bounded attempt to recover the datapath from @thuanquang's full sweep. If it reaches 0 mismatches over the 2^23 inputs, I'll rewrite this PR around it, with the expectations taken from the sweep and the instruction count. If it doesn't, I'll close this PR and leave AI-assisted: yes (Claude Code) |
|
Closing, as said above: the bounded attempt did not reach 0 mismatches, so What the gfx1031 sweep did show, for whoever picks this up later:
The next step would be an exhaustive search of the integer C1 per segment under truncated array-multiplier rules. Even then, an exact emulation would cost a 32-entry table and a cubic per rcp. Thanks @thuanquang for the sweep and everyone who ran the oracle. AI-assisted: yes (Claude Code) |
|
Follow-up to the closing note: the datapath is now known exactly. AMD's rocjitsu emulator (ROCm/rocm-systems,
I wrote that formula independently in Python and checked it against @thuanquang's gfx1031 sweep (all 2^23 significands). Result: 0 mismatches out of 8,388,608. The cubic alone misses exactly those 2,761 inputs. So RDNA 2 (gfx1031), RDNA 3 (gfx1100) and RDNA 4 (gfx1201) compute the same reciprocal, which very likely means the PS5 GPU does too. That makes a bit-exact
My plan would be:
@adriadam10, would you take that as a new PR, or do you see a better place for it? |
|
Follow-up: the bit-exact reciprocal is now #9305, opt-in behind AI-assisted: yes (Claude Code) |
What
v_rcp_f32andv_rcp_iflag_f32are translated to a plainOpFDiv(1, x), and Vulkan leaves the precision of a division to the device. In practice an RTX 4090 and an Intel UHD 770 return reciprocals one ulp apart. Each of them also differs from the correctly rounded value on some inputs. As a result, the same shader stores different bytes on the two vendors: I saw this on a GTA V compute pass (PPSA04264, compute 0x143978f00, one f32 one ulp apart).This change adds two fused Newton steps (
e = fma(-x, y, 1),y = fma(y, e, y)) after the division estimate. They give the correctly rounded reciprocal whenever the input and the estimate are normal. Zero, infinities, NaN and denormals keep the division result, so the special-value handling stays as it is.It also matters for integer division: RDNA compilers implement it with
v_rcp_iflag_f32followed by corrections, so it now gives the same value on every device.Cost: about ten SPIR-V instructions per reciprocal.
Tested
Windows 11, MinGW GCC 15.2:
agc_driver_rcp_correctly_rounded. It checksv_rcp_f32of 8192 normal inputs across the whole exponent range against the correctly rounded reciprocal.mainit fails on both GPUs:v_rcp_f32 of 0x858a127e is 0xf96d5337, expected the correctly rounded 0xf96d5336v_rcp_f32 of 0x1646f91a is 0x68a4af84, expected ... 0x68a4af85mainthe test fails (v_rcp_f32 of 0x0f5b5acd is 0x6f956235, expected ... 0x6f956236, one ulp below), and it passes with this change.ctest -R ^agc_on the RTX 4090: 727/728, includingagc_driver_vop1_float_unary. The failure isagc_driver_bda_device, which fails onmaintoo.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)🤖 Generated with Claude Code
https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P