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.62x and c2r 2.63x in fp64. Against torch.fft, which runs a 3D transform as two cuFFT plans and a copy, about 3x.

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 raise TypeError or ValueError.
  • prepare_r2c(x) and prepare_c2r(X) plan for that input buffer and return launch(), which transforms the buffer's current contents into one output tensor that it reuses on every call. Calls of one launch must be ordered (one stream). Keep launch alive as long as a CUDA graph captured from it. fp64 r2c has no two-phase form; its launch() calls r2c(x).
  • Calls on different CUDA streams may run concurrently, including calls of the same shape.
  • The first call for a new shape compiles its kernels (CuTe DSL JIT), typically in 1 to 30 s, up to about 4 minutes for a very long z axis over a tiny y-x plane (such as 4096x8x16). Make that first call outside CUDA graph capture; later calls can be captured.
  • c2r expects a Hermitian-consistent half-spectrum, such as the output of r2c or 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 match torch.fft.irfftn.
  • Tuned on the eight grids below and checked on dozens of others per kernel, including odd and prime extents and up to 1024x1024x512. fp64 r2c raises NotImplementedError when a y or z axis needs a direct DFT longer than 6336 points (prime factors above 13, or smooth lengths above 1536); fp64 c2r raises it for nz from 1280 to 4096, ny from 3072 to 4096 or nx / 2 from 3072 to 4096.

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.

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 gives bit-identical results to the FP64 unit for finite normal or zero operands and results; only the sign of some zero results differs. Outside that domain it is wrong, silently: data below about 1e-300 in magnitude, subnormal values, results that overflow, and Inf or NaN inputs give wrong outputs instead of propagating. Data between about 1e-300 and 1e300 in magnitude is unaffected.

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.89x 2.28x
64x64x64 2.13x 2.00x 2.48x 2.44x
80x80x112 2.22x 1.85x 2.76x 2.77x
96x96x96 2.24x 2.14x 2.37x 2.64x
96x96x144 1.92x 1.77x 2.12x 2.53x
128x128x128 1.80x 1.65x 2.50x 2.99x
192x192x192 1.39x 1.44x 2.72x 2.94x
256x256x256 1.42x 1.59x 2.45x 2.56x
geomean 1.99x 1.85x 2.62x 2.63x
geomean, 128^3 and up 1.53x 1.56x 2.55x 2.82x

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 24.8 / 23.5 / 6.0 18.0 / 16.6 / 7.3
64x64x64 11.6 / 9.4 / 4.4 14.3 / 9.0 / 4.5 32.7 / 31.5 / 12.7 36.3 / 32.6 / 13.4
80x80x112 21.1 / 15.1 / 6.8 20.1 / 13.9 / 7.5 89.5 / 78.9 / 28.5 72.5 / 76.6 / 27.7
96x96x96 23.4 / 16.5 / 7.4 25.6 / 16.7 / 7.8 87.4 / 75.6 / 32.0 83.1 / 76.2 / 28.8
96x96x144 31.4 / 18.5 / 9.6 31.4 / 18.7 / 10.6 117.7 / 98.5 / 46.5 125.9 / 99.0 / 39.1
128x128x128 42.0 / 24.2 / 13.4 56.8 / 23.4 / 14.2 182.8 / 164.0 / 65.5 202.1 / 173.7 / 58.1
192x192x192 123.2 / 66.9 / 48.1 126.4 / 71.2 / 49.3 647.9 / 542.5 / 199.5 590.8 / 538.5 / 182.9
256x256x256 346.8 / 163.0 / 114.5 300.2 / 187.4 / 117.5 1625.0 / 1116.0 / 456.0 1445.3 / 1130.0 / 441.8

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. Speeds differ by a few percent between RTX 5090 cards.

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/, which also holds its tests and the fixes made after the judge's checks. MIT-licensed.

Available functions

  • c2r
  • prepare_c2r
  • prepare_r2c
  • r2c
Downloads last month
-
cuda
cutlass
cute-dsl
fft
fp32
fp64
blackwell
rtx-5090
MIT
Supported hardwares new
CUDA
12.0
DGX Spark
GB10
128GB
GPU
RTX PRO 6000 WS
96GB
GPU
RTX PRO 6000 Max-Q
96GB
GPU
RTX PRO 5000
48GB
GPU
RTX PRO 4500 WS
32GB
GPU
RTX PRO 4000
24GB
GPU
RTX PRO 4000 SFF
24GB
GPU
RTX PRO 2000
16GB
RTX
RTX 5090
32GB
RTX
RTX 5090 D
32GB
RTX
RTX 5090 Mobile
24GB
RTX
RTX 5080
16GB
RTX
RTX 5080 Mobile
16GB
RTX
RTX 5070
12GB
RTX
RTX 5070 Mobile
8GB
RTX
RTX 5070 Ti
16GB
RTX
RTX 5070 Ti Mobile
12GB
RTX
RTX 5060 Ti
16GB
RTX
RTX 5060
8GB
RTX
RTX 5060 Mobile
8GB
RTX
RTX 5050
8GB
RTX
RTX 5050 Mobile
8GB
Kernel Builder
375561f