Repository navigation
Correct the bpw limits that do not run, measured with the sloppy-carry fix - #92
Conversation
carryFinal declared its temporary as "i32 tmpCarry" regardless of iCARRY. In a CARRY64 build that routes the mid-pair carry through carryStepSignedSloppy(i64 x, i32 *outCarry), which is only exact while |x| < 2^(31 + nBits). That is fine for normal words, but not once the 32-bit sloppy word is enabled (the EXP / NWORDS >= 22 / 23 branches, reachable only for the integer-NTT types with maxbpw >= 33.1). Then every word saturates at ~2^31 whatever bits/word the FFT is configured for, the convolution coefficients grow to ~sqrt(N) * 2^62, and the mid-pair carries to ~sqrt(N) * 2^(62 - nBits), which no longer fits the 32-bit slot below about 22 bits/word. The overflow is exact and silent; the Gerbicz check catches it. This is the mechanism behind the long-standing note on that overload, "the sloppy case fails if BPW is too low ... would require CARRY_LONG to work properly": it did need the 64-bit carry. Declaring tmpCarry as iCARRY is a no-op for the i32 instantiation (WordSize <= 4 types, which never reach the sloppy-32 branch) and gives CARRY64 builds a 64-bit mid-pair carry; the call then resolves to carryStepSignedSloppy(i64, i64*), i.e. plain carryStep, so u.x is normalized and only u.y keeps the sloppy 32-bit value. The now-explained comment is removed. Evidence: with both guards relaxed to >= 18, 1:256:2:256 on a 7900 XTX fails the Gerbicz check at bpw <= 21 and passes at >= 22, and 1:256:4:256 has the same boundary; an exact big-integer simulation of the carry pass reproduces the boundary from the i32 bound and shows the margin drifting down about 2/3 bit per 4x in N, leaving the largest NTT FFTs with roughly 0-1 bit at bpw 22-23 under the shipped constant guard. The >= 22 / 23 guards are left as they are. Not yet run on hardware with this change applied (no NTT-capable GPU available at the time); see the pull request for the data and the remaining checks. Found, analysed and fixed by Claude (Anthropic's Claude Code), working with Mark Rose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y fix 41 of the 201 rows in fftbpw.h advertise bits-per-word limits the FFTs cannot reach. At the shipped value the roundoff pins at 0.500 and the Gerbicz check fails from the first block, so FFTConfig::maxExp() offers exponents that cannot complete. Measured on a Radeon RX 7900 XTX; the failures reproduce on an Intel iGPU and on POCL. Two groups, and the second is the tell: 39 rows in the three FP32 sections, -0.10 to -0.40 bpw (median -0.20 for FFT3261 and FFT323161, -0.30 for FFT3231). 2 of the 8 rows the file marks "// Estimated" -- 1:4K:16:1K at -1.00 and 3:4K:16:1K at -0.30. FFT3161 and FFT61 are otherwise correct in all 32 of their other entries; the only bad row each has is the one nobody measured. Every wrong row in the table is one of those two kinds. The remaining estimated rows were measured too and are left alone, so the marker is now gone from all 8: 51:4K:16:1K, 4:4K:16:1K, 50:4K:16:512 and 50:4K:16:1K all run clean at their shipped values. 50:1K:16:1K did not -- it is the worst row in the file at 11.21 against a measured 9.71. Why this sits on top of the carryFinal fix rather than beside it: MAXBPW is derived from the table entry and the kernels branch on it, SLOPPY_MAXBPW being MAXBPW - 110. Every exponent running more than 1.10 bpw below its FFT's maximum therefore compiles the sloppy carry, where carryFinal truncated an i64 carry to i32. Without that fix the sloppy window fails for many of these shapes and the limits cannot be measured at all; with it, every row here was checked 1.25 bpw below its maximum as well as at it. The two defects are independent -- the fix alone rescues none of these 41 rows -- but the numbers are only valid on top of it. That same feedback is why each candidate was measured against a build whose table carried the candidate value. Measuring against the shipped table tests a kernel that would not ship, and it is not a small effect: on 1:4K:16:1K it indicated -4.50 where the correct protocol gives -1.00. Acceptance required all three of: clean at the top of the range, clean 1.25 bpw below it, and top-of-range ROEmax <= 0.45 -- the level the rows that are already correct reach under the same protocol, not a threshold chosen for this patch. It does real work: 2:1K:16:512 at 29.38 and 4:1K:16:1K at 44.60 both ran clean and were still rejected, at ROEmax 0.474 and 0.455. Verified at 50000 iterations x 3 exponents, except the 33M and 134M-word shapes at 20000 x 1-2. Across all 54 FP32 rows the corrected group's roundoff sits at median 0.428 against 0.445 for the rows left untouched, i.e. slightly more conservative than the entries that were already right. Those untouched rows were verified at the same length rather than assumed: 2:1K:16:512 passed a 4000-iteration screen and failed at 50000, and is corrected here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6b26bf2 to
2de58ae
Compare
|
@gwoltman This is ready for your eyes now. All the values have been tested. |
gwoltman
left a comment
There was a problem hiding this comment.
This commit concerns me. It looks like a performance regression (probably very minor) of doing 64-bit math when 32-bit math would work in most cases. Would a change along the lines of:
#if EXP/NWORDS > 24
i32 tmpCarry
#else
iCARRY tmpCarry;
endif
work? I'm not sure I follow which cases work and don't work to know if something like the above is do-able.
|
Which cases need the 64-bit temporary, precisely (this is the #34 commit; happy to move the discussion there):
So your conditional is the right idea with the condition the other way round: i32 everywhere except when the sloppy 32-bit words are in play. In #if CARRY64 && EXP / NWORDS >= 22 && EXP / NWORDS <= 31 && SLOPPY_MAXBPW >= 3200 && ACTUAL_BPW <= SLOPPY_MAXBPW
iCARRY tmpCarry; // sloppy 32-bit words: the carry into carryFinal can exceed 32 bits
#else
i32 tmpCarry;
#endif(Only the integer NTT types with maxbpw >= 33.1 — FFT3161, FFT323161, FFT6431 and four FFT3261 shapes — ever satisfy the first form; FFT64 never reaches it.) Whether the conditional is worth its complexity depends on whether the i64 temporary costs anything measurable; the extra work is a few 64-bit integer ops per word pair, once per carryFused call. I'm timing master vs #34 vs the conditional above on the XTX now (FFT3161 at high bpw where they differ, plus an FFT64 control for the noise floor) and will post the numbers here. |
|
Timing done on the RX 7900 XTX (gfx1100, ROCm), all on carryFused ISA (registers identical in all builds: 111 VGPRs FFT3161, 92 FFT61, no spills):
The 64-bit temporary adds instructions only inside the sloppy-32 window: +29 on 9164, about 0.3% of the kernel, and that is exactly where it is needed for correctness, so the conditional cannot avoid it. Outside the window the i64 version compiles to the same or fewer instructions than i32 (the compiler folds the widening into the 64-bit add it does anyway). Wall clock, clean-median µs/iteration and paired B/A ratio (median [min..max] over rounds):
The control shape (same code in all three builds) puts the noise floor at about 0.2% on the median; no shape's difference exceeds it. So the i64 temporary costs nothing measurable, and the conditional would only re-select i32 where i32 has no advantage. I'd leave #34 as it is. If you'd still prefer the conditional for clarity I can push it, but it is a five-term guard buying nothing on this hardware. |
|
FYI: I'm enhancing -v to output num registers, shared mem, etc. just like CUDA does. Furthermore, -v 10 will output the assembly code for each kernel. I can test on Radeon VII. Can you test on your AMD cards which are RDNA. I should have the change committed later today. |
|
Happy to. Once it's in I'll run the new -v on the RX 7900 XTX (gfx1100, ROCm 7.2) and on the Ryzen 7800X3D iGPU (gfx1036), and post what it prints for a few kernels next to what we got from the code objects via comgr (AMD_COMGR_SAVE_TEMPS + llvm-readelf --notes / llvm-objdump), so you can see whether the register and LDS numbers agree on RDNA. |
|
Tested 261020f ( As committed it prints nothing on either device. With With the patch, the parser is accurate. Compared against the loaded code objects ( Things worth fixing before this goes wide:
gfx1036 behaves the same as gfx1100 for all of this (its wrong residues are a known ROCm cache problem on that APU, unrelated). Happy to send the two-line ROCm fix plus the before/after directory listing for #1 as a PR, or leave it with you since you're in that code right now. |
|
Fix this to work on your RDNA GPUs as you see fit EXCEPT you cannot use a subdirectory for kernel compiler output files. That was Claude's first instinct on Radeon VII rocm, but no matter what Claude tried, files were output to the working directory. I recall Mihai filing a bug report along these lines 3 years ago. Alas, Radeon VII support is deprecated in rocm, so I cannot upgrade. Help does not need -v 10 documented. For developer's (and Claude) only. Remove -dump. I'm done with this code now, so it's all yours :) |
|
Will do. Understood on the working directory: the plan is to snapshot the directory listing before each compile and only ever touch files that appeared during it, so nothing pre-existing can be deleted or renamed, with the link-option form for the current ROCm kept alongside your build-option form so the Radeon VII path is unchanged. Plus the re-exec path fixes, the used-vs-allocated register numbers, an occupancy label, and |
|
The -v 10 work is up as #123: works on ROCm 7.x (both env forms set, your build-option path kept), a directory snapshot so nothing pre-existing is ever deleted or renamed, re-exec fixed for relative paths and done before the log opens, used vs allocated VGPRs, total SGPRs, occupancy as N/max waves per SIMD, variant-named .s files, and -dump removed. Verified on gfx1100 and gfx1036 against the code objects, plus Intel and POCL for the non-AMD path; the Radeon VII path is unchanged but untested here. |
|
Ugh. I (well really Claude) just fixed a bug in Z calculations. If you were using the Z value in your max bpw changes, then they'll need to be redone. If you were basing it on maxROE or avgROE, then I think your work is OK. |
The acceptance criterion was ROEmax ≤ 0.45 plus clean runs at the top of the range and 1.25 bpw below, and the failures it corrected were roundoff pinned at 0.500 with the Gerbicz check failing from the first block. So we're good there. |
41 of the 201 rows in
fftbpw.hadvertise bits-per-word limits the FFTs cannot reach. At the shipped value the roundoff pins at 0.500 and the Gerbicz check fails from the first block, soFFTConfig::maxExp()hands users exponents that cannot complete. Measured on a Radeon RX 7900 XTX; the failures reproduce on an Intel iGPU and on POCL.Why this is stacked on #34
The values in this PR are only valid with the
carryFinalfix, so the branch sits on top of it rather than beside it.MAXBPWis derived from the table entry, and the kernels branch on it:So every exponent running more than 1.10 bpw below its FFT's maximum compiles the sloppy carry — a much wider window than "small bpw" suggests, and one that
bestFitputs a large share of real assignments into. That is the path wherecarryFinaldeclaredi32 tmpCarrywhileiCARRYisi64, truncating the carry out of the low word. Without #34 the sloppy window fails outright for many of these shapes and a limit cannot be measured at all; with it, every row here was checked 1.25 bpw below its maximum as well as at it, and 51 rows tested in that window came back clean.The two defects are independent — applying #34 alone rescues none of the 41 rows corrected here — but the numbers depend on it.
That same feedback is why each candidate was measured against a build whose table carried that value. Measuring a candidate against the shipped table tests a kernel that would never ship, and it is not a small effect: on
1:4K:16:1Kthe naive protocol indicated −4.50 bpw where the correct one gives −1.00.What was wrong
// Estimated1:4K:16:1K−1.00,3:4K:16:1K−0.30The second group is the tell. FFT3161 and FFT61 are correct in all 32 of their other entries — the only bad row each has is the one nobody measured. Every wrong row in the table is either FP32 or estimated.
The other six estimated rows were measured too, so the marker is gone from all eight:
51:4K:16:1K,4:4K:16:1K,50:4K:16:512and50:4K:16:1Krun clean as shipped and are untouched.50:1K:16:1Kdid not — at 11.21 against a measured 9.71 it is the worst row in the file.Acceptance criterion
All three of: clean at the top of the range, clean 1.25 bpw below it, and top-of-range ROEmax ≤ 0.45 — the level the rows that are already correct reach under the same protocol, rather than a threshold picked for this patch.
It does real work:
2:1K:16:512at 29.38 and4:1K:16:1Kat 44.60 both ran clean and were still rejected, at ROEmax 0.474 and 0.455; the next step down was taken in each case.How far each was tested
*:4K:16:*,*:1K:16:1K): 20 000 iterations × 1–2 exponents — a single run there is 45 minutes and ~10 GB of VRAM.1:4K:16:1K2/2 clean ROEmax 0.092;3:4K:16:1K1/1 clean ROEmax 0.273;51:4K:16:1Kclean ROEmax 0.208.Across all 54 FP32 rows the corrected group's roundoff sits at median 0.428 against 0.445 for the rows left untouched — slightly more conservative than the entries that were already right.
Those untouched rows were verified at the same length rather than assumed, which caught one of mine:
2:1K:16:512passed a 4000-iteration screen and failed at 50 000, so it is corrected here.Caveats
🤖 Generated with Claude Code