KDA 3D FFTs on Packed Hermitian Half-Spectra
CuTe-DSL forward (real-to-complex) and inverse (complex-to-real) unnormalized 3D
FFTs of one real grid, in fp32 and fp64, for the NVIDIA RTX 5090. The spectrum
is the packed half-spectrum that structured-grid spectral solvers use: x
innermost, interleaved (re, im), an x-row stride of exactly nx // 2 + 1.
The results are those of torch.fft.rfftn and
torch.fft.irfftn(..., norm="forward").
Against cuFFT's own 3D plans, geometric mean over eight grids from 32^3 to
256^3: r2c 1.99x and c2r 1.85x in fp32, r2c 2.63x and c2r 2.58x in fp64.
Against torch.fft, which runs a 3D transform as two cuFFT plans and a copy,
about 3x. Any grid runs FFT kernels: on random grids up to 256 per axis, 1.5x to
1.9x over cuFFT (geomean), and no sampled grid slower than cuFFT.
Requirements
- NVIDIA GPU with compute capability 12.0; tuned and tested on the RTX 5090 only
- CUDA 13 compatible driver
- PyTorch 2.12.1 with CUDA 13.0 (tested)
nvidia-cutlass-dsl[cu13]==4.6.0(tested)
Usage
import torch
from kernels import get_kernel
fft3d = get_kernel("humanfia-lab/fft3d", version=1, trust_remote_code=True)
u = torch.randn(96, 96, 144, device="cuda") # [nz, ny, nx], fp32 or fp64
spectrum = fft3d.r2c(u) # [96, 96, 73, 2] == torch.view_as_real(torch.fft.rfftn(u))
back = fft3d.c2r(spectrum) # [96, 96, 144], unnormalized: 96 * 96 * 144 * u
launch = fft3d.prepare_r2c(u) # plan once for this buffer...
u.copy_(torch.randn_like(u))
spectrum = launch() # ...then transform its current contents
r2c(x)takes a contiguous[nz, ny, nx]CUDA tensor, float32 or float64.c2r(X)takes a[nz, ny, nf, 2]half-spectrum of the same dtypes and returns[nz, ny, 2 * (nf - 1)]. Each call returns a new tensor and leaves its input unchanged. Other dtypes, devices and shapes raiseTypeErrororValueError. The work runs on the input's device (which need not be the current one), on its current CUDA stream.prepare_r2c(x)andprepare_c2r(X)plan for that input buffer and returnlaunch(), which transforms the buffer's current contents into one output tensor that it reuses on every call. Calls of onelaunchmust be ordered (one stream). Keeplaunchalive as long as a CUDA graph captured from it. fp64 r2c has no two-phase form; itslaunch()callsr2c(x). Alaunch()on another stream than the oneprepare_*ran on waits forprepare_*'s work and keeps its output and workspaces reserved for that stream's work, even if the launch object is dropped before the work ran.- Calls on different CUDA streams may run concurrently, including calls of the same shape. Calls may also come from several threads: CuTe DSL is not thread-safe while it compiles or makes the first call of a compiled function, so a new shape's plan is built, and makes its first call, under a lock that all kda kernels share (later calls take no lock).
- A call costs about 13 to 17 us of host time (TVM-FFI launches on the current
stream). Timed per call as the requests' benchmark does (flashinfer-bench:
synchronize, event, call, event), every kernel is faster than
torch.ffton every grid tried, the smallest included (see Performance). - The first call for a new shape compiles its kernels (CuTe DSL JIT), typically in 1 to 15 s, up to about 4 minutes for a very long z axis over a tiny y-x plane (such as 4096x8x16). That first call may itself be inside CUDA graph capture.
c2rexpects a Hermitian-consistent half-spectrum, such as the output ofr2cor Fourier-space data that keeps its symmetry. On other input the result depends on the order of the 1D passes, as with cuFFT; these kernels then matchtorch.fft.irfftn.- Tuned on the eight grids below; any grid with nz, ny >= 8 and nx >= 16 is
accepted. Within 256 per axis every grid of every kernel runs FFT kernels,
odd and prime extents included, checked on RTX 5090s by sweeps over every
plan signature, and on grids up to 1024x1024x512. Beyond 256 per axis, axis
lengths with two prime factors above 16 (fp32 c2r) or a squared prime factor
above 13 (fp64, such as 289) still take slow generic kernels. fp64 r2c raises
NotImplementedErrorwhen a y or z axis needs a direct DFT longer than 6336 points; fp64 c2r raises it for nz from 1280 to 4096, ny from 3072 to 4096 or nx / 2 from 3072 to 4096. The fp64 kernels address with 32-bit offsets: fp64 r2c raisesValueErrorpast 2^30 complex outputs and fp64 c2r on 2^31 or more half-spectrum elements.
Accuracy
fp32 kernels compute in fp32 from twiddle tables computed in float64 on the host: no reduced precision, no tensor cores, no approximate math. Against the float64 result the relative L2 error is 1.4e-7 to 1.8e-7 on the grids below, where cuFFT's is 2.0e-7 to 2.8e-7. Every element is within 1e-2, or within 1e-3 of its own magnitude. On other grids within 256 per axis the relative L2 error is 1e-7 to 3.4e-7, every element passes that rule on random input, and large prime extents stay below cuFFT's error (c2r of a random 251x251x254 half-spectrum: max 0.006, cuFFT 0.016).
fp64 kernels are IEEE float64 throughout, within 3.5e-16 to 5.9e-16 relative L2
of torch.fft's float64 result. The RTX 5090 runs FP64 at 1/64 of the FP32
rate, so both fp64 kernels compute part of their float64 additions with a
software IEEE add (round to nearest even) on the integer units, which run
alongside the FP64 unit. It returns exactly what the FP64 unit returns for every
input, subnormal operands and results, overflow, signed zeros, Inf and NaN
included (2.5e10 operand pairs per routine compared with the native
instructions). Until 2026-10-24 it was exact only for finite normal operands and
results, and data near the bottom of the normal range came out wrong (see
Changes). Data with a large mean keeps cuFFT-level accuracy or
better in every kernel.
Repeated calls give bitwise identical results.
Performance
Speedup over cuFFT's 3D plan:
| grid (nz x ny x nx) | r2c fp32 | c2r fp32 | r2c fp64 | c2r fp64 |
|---|---|---|---|---|
| 32x32x32 | 3.33x | 2.60x | 3.93x | 2.22x |
| 64x64x64 | 2.13x | 2.00x | 2.48x | 2.40x |
| 80x80x112 | 2.22x | 1.85x | 2.63x | 2.67x |
| 96x96x96 | 2.24x | 2.14x | 2.43x | 2.52x |
| 96x96x144 | 1.92x | 1.77x | 2.13x | 2.39x |
| 128x128x128 | 1.80x | 1.65x | 2.51x | 2.91x |
| 192x192x192 | 1.39x | 1.44x | 2.75x | 2.95x |
| 256x256x256 | 1.42x | 1.59x | 2.47x | 2.65x |
| geomean | 1.99x | 1.85x | 2.63x | 2.58x |
| geomean, 128^3 and up | 1.53x | 1.56x | 2.58x | 2.83x |
Times in microseconds (torch.fft / cuFFT 3D plan / kernel):
| grid | r2c fp32 | c2r fp32 | r2c fp64 | c2r fp64 |
|---|---|---|---|---|
| 32x32x32 | 10.4 / 8.6 / 2.6 | 9.3 / 7.0 / 2.7 | 26.4 / 26.9 / 6.8 | 20.5 / 19.1 / 8.6 |
| 64x64x64 | 11.6 / 9.4 / 4.4 | 14.3 / 9.0 / 4.5 | 35.3 / 37.4 / 15.1 | 41.5 / 37.5 / 15.6 |
| 80x80x112 | 21.1 / 15.1 / 6.8 | 20.1 / 13.9 / 7.5 | 98.7 / 92.7 / 35.2 | 83.6 / 89.7 / 33.6 |
| 96x96x96 | 23.4 / 16.5 / 7.4 | 25.6 / 16.7 / 7.8 | 96.4 / 88.8 / 36.5 | 96.2 / 89.4 / 35.5 |
| 96x96x144 | 31.4 / 18.5 / 9.6 | 31.4 / 18.7 / 10.6 | 128.1 / 115.8 / 54.5 | 146.0 / 116.1 / 48.6 |
| 128x128x128 | 42.0 / 24.2 / 13.4 | 56.8 / 23.4 / 14.2 | 201.4 / 193.9 / 77.1 | 233.7 / 205.0 / 70.4 |
| 192x192x192 | 123.2 / 66.9 / 48.1 | 126.4 / 71.2 / 49.3 | 701.7 / 642.8 / 233.5 | 682.8 / 634.7 / 215.0 |
| 256x256x256 | 346.8 / 163.0 / 114.5 | 300.2 / 187.4 / 117.5 | 1688.8 / 1323.1 / 536.0 | 1671.9 / 1337.3 / 505.1 |
On small grids cuFFT's several kernels are launch-bound, and one or two fused kernels win most. On large fp32 grids the kernels make fewer passes over memory. In fp64 the FP64 unit is the limit, and the integer-unit adder adds throughput beside it.
Measurements use one RTX 5090 (driver 580.82.07) with CUPTI cold-L2 kernel time
over CUDA graph replay, 3 warmup and 30 measured iterations, median of 3 trials,
each trial timing cuFFT before and after the kernel. cuFFT is the cuFFT 12.0
that PyTorch 2.12.1+cu130 ships, as one cufftPlan3d per grid made before
timing and executed out of place; its C2R overwrites its input and is given a
copy. torch.fft is rfftn / irfftn on the same library. The fp64 columns
were measured again after the IEEE-complete adders (see Changes), on
an RTX 5090 that runs both cuFFT and the kernels 10-20% slower in absolute time;
the speedups moved by at most 2%. Speeds differ by a few percent between RTX
5090 cards.
Beyond the eight grids, against cuFFT's 3D plans on fixed seeded samples of the requests' range (nz, ny 8..256, nx 16..256, even nx for c2r; random input), geomean with the slowest grid in brackets:
| sample | r2c fp32 | c2r fp32 | r2c fp64 | c2r fp64 |
|---|---|---|---|---|
| 60 uniform | 1.81x (1.15x) | 1.63x (1.11x) | 1.93x (1.15x) | 1.67x (1.18x) |
| 60 with 7-smooth axes | 1.49x (1.06x) | 1.81x (1.28x) | 1.83x (1.29x) | 1.76x (1.17x) |
| 40 with a prime factor >= 17 | 1.72x (1.22x) | 1.62x (1.15x) | 1.88x (1.20x) | 1.71x (1.16x) |
Every sampled grid is faster than cuFFT's 3D plan and than torch.fft. Large
prime extents gain most: 251x251x254 takes 0.25 ms in fp32 r2c (cuFFT 0.59 ms)
and 2.6 ms in fp64 c2r (cuFFT 4.9 ms).
Timed per call the way the requests' benchmark (flashinfer-bench 0.1.2) does it,
on fresh copies of the input with a cleared L2 and the host's launch work
included, against the requests' baseline (torch.fft):
| eight grids (geomean) | 8x8x16 to 48x40x36 (slowest) | |
|---|---|---|
| r2c fp32 | 1.91x | 1.54x |
| c2r fp32 | 1.72x | 1.11x |
| r2c fp64 | 2.20x | 1.29x |
| c2r fp64 | 2.23x | 1.12x |
Changes
Updated on 2026-10-24 with fixes found by sweeping every planner signature over nz, ny in 8..256 and nx in 16..256 (the requests' range), on a B300 with the RTX 5090's plans:
- fp32 r2c returned garbage, often NaN, on 11,765 grids of that range, all with nx >= 84 and ny >= 84 (such as 8x126x140, 8x252x156, 70x104x224): their plane or y-split plan needs a whole plane or sub-plane as one row block, and the x pass halved the rows that did not fit in shared memory. Those grids now take another plan; the plans of the eight grids in the tables above are unchanged.
- The fp64 software adders were exact only for finite normal operands and results: fp64 r2c of a 96x96x144 grid with a quarter of its z planes at 1e-308 to 1e-300 returned NaN everywhere, and data scaled by 1e-300 or 1e-310, subnormal bands of a spectrum and single NaN or Inf inputs failed on many grids. They now handle every IEEE case, and fp64 c2r no longer routes 2x - y through software on five tuned grids. On an RTX 5090 the eight grids' speedups moved by at most 2% (the fp64 columns above).
- fp64 inputs on a CUDA device other than the current one raised
(
ValueError,BufferError); every call now runs under the input's device.
Updated again on 2026-10-24 after a sweep on RTX 5090s over every plan signature of the four kernels within 256 per axis (about 45,000 grids, random and Hermitian c2r input, spectrum-shaped and offset data, prepare, graph and two-stream checks):
- fp32 c2r missed the 1e-2 / 1e-3 tolerance on about 6,900 grids of 6.5M points and more with a large prime extent (such as 251x251x254) on random half-spectra, the requests' own input: up to 69 elements per grid off by up to 0.03, where cuFFT's fp32 plan has none. Now at most 0.006.
- fp32 r2c returned garbage on grids of 2^24 points and more with an odd ny (such as 256x297x256, beyond 256 per axis).
- Most grids ran slow fallbacks: fp32 r2c ran every grid whose nx is not a
multiple of 4 or that has a prime factor above 13 (96.8% of the range) as
O(n^2) direct DFTs, 17 to 180 times slower than
torch.fft(96x96x98: 0.97 ms against cuFFT's 0.020 ms); fp32 c2r ran 77% of the range on a global-memory generic path, and fp64 ran axes with a prime factor above 13 as direct DFTs (fp64 c2r: the whole grid, about 90% of the range), 3 to 10 times slower than cuFFT. They now run general FFT kernels (two real rows per complex FFT for odd x, Rader's or Bluestein's algorithm for large primes), faster than cuFFT on every sampled grid (Performance). Sweeps over every new kernel signature found no wrong result. - fp64 r2c on data with a large mean (randn + 1e3) missed atol 1e-6 on 21 of 100 sample grids of 1M points and more, where cuFFT passed; now at most 1.07x cuFFT's error.
- Per-call host time: the fp64 kernels spent about 40 to 50 us of host time per
call (0.48x to 0.69x of
torch.ffton small grids when timed per call, as the requests' benchmark does). All kernels now launch through TVM-FFI, at about 13 to 17 us; outputs are bitwise unchanged. fp64 c2r now transforms an input that is not 16-byte aligned from an aligned copy instead of raising.
The eight grids keep their plans and their outputs are bitwise unchanged.
Updated on 2026-10-27 with the fixes of a second audit on RTX 5090s and B300s (threads, streams, CUDA graph capture, 32-bit limits; no wrong result was found within 256 per axis):
- Threads making first calls at once failed: CuTe DSL is not thread-safe while it compiles or makes the first call of a TVM-FFI function. Concurrent plan builds raised MLIR errors or ended the process, and two first calls of one shape at once left a spin lock held, so that the next first call of any kernel in the process hung for good. Plans are now built, and make their first call, under one process-wide lock; later calls take none.
- The first call of a shape inside CUDA graph capture raised; its tables are now copied on a stream of the package's own, outside the graph.
- A
launch()right afterprepare_*on another stream could run before work still queued onprepare_*'s stream, which then overwrote its output, and a launch object dropped before its kernels ran handed its buffers to new tensors onprepare_*'s stream. Such a launch now waits forprepare_*'s stream and keeps its buffers until its work is done; same-stream launches are unchanged. - fp32 r2c transforms a contiguous input that is not 16-byte aligned from an aligned copy instead of raising.
- fp64 r2c past 2^30 complex outputs (an illegal memory access) and fp64 c2r
on 2^31 or more half-spectrum elements (a wrong result) now raise
ValueError, beyond 256 per axis. - fp64 axes of 1529 points and more whose big-prime tile does not fit in shared memory failed to compile (such as 8x6329x16); they keep the direct DFT.
The eight grids keep their plans; their outputs and speeds are unchanged.
Source and licensing
Written by Claude Opus 5.5 in four 12-hour runs of KDA (kernel design agents),
one per KDA task (fft3d_r2c, fft3d_c2r, fft3d_r2c_f64, fft3d_c2r_f64),
built from the requests in NVlabs/kda#7,
and accepted by the KDA judge. Published from commit e539498 of
humanfia/kda-kernels-internal, directory rtx5090/fft3d/ (now
sm120/fft3d/), which also holds its tests and the fixes made after the
judge's checks; updated from commits b0811f8 and 2238a9e (see
Changes).
MIT-licensed.
Available functions
c2rprepare_c2rprepare_r2cr2c
- Downloads last month
- -
- Kernel Builder
- 375561f


