Straight-line codelets for the 8/16/32/64-point base cases of the radix-4 kernel - #135
Straight-line codelets for the 8/16/32/64-point base cases of the radix-4 kernel#135pankgeorg wants to merge 2 commits into
Conversation
Twiddle factors were regenerated on every execution: every kernel seeded Singleton's recurrence with a sincospi call (per output row of the O(n^2) DFT leaf, per j1 in the composite step, per level of the radix-4/3 kernels), and fft_bluestein! allocated three pad-length buffers and recomputed the chirp and its FFT on every call. CallGraph now carries, per node, a twiddle table in the layout its kernel reads sequentially (DFT: w^k; composite: the (j1, k2) block; radix-4/3: per-level interleaved triplets/pairs addressed by a flat offset), a BluesteinScratch (chirp, its pre-scaled transform, work arrays, pow2 tables for the padded length) per Bluestein node, and the direction the tables were built for. All tables derive from one unit_roots table per node that evaluates sincospi on the first octant only when 8 | N. Planned execution is allocation-free for every size. Tables are correctly rounded, so the Float32 error no longer grows with n (~1.5 ulp at 2^22 instead of ~1000); the accuracy test grid is extended to 2^22. The old kernel signatures remain as wrappers that build tables on the fly.
…ix-4 kernel
The power-of-two kernel recursed down to 2- and 4-point base cases, so
about half of its work was call and index overhead. For Complex{Float32}
and Complex{Float64} the recursion now stops at 64 points (or the size
of the transform if smaller) in a fully unrolled radix-2 DIT codelet
emitted by an @generated function keyed on the size and direction, with
folded twiddle constants and one SSA variable per intermediate — the
same structure FFTW's genfft produces offline. Other element types keep
the generic recursion.
Each (size, element type, direction) codelet costs 0.1-0.6 s of LLVM
time on first use, so a PrecompileTools workload compiles them, and the
common plan/execute paths, at package precompile time; time to first
fft(x) in a fresh session drops from ~1.9 s to well under a millisecond.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #135 +/- ##
==========================================
+ Coverage 98.80% 98.90% +0.09%
==========================================
Files 5 6 +1
Lines 585 729 +144
==========================================
+ Hits 578 721 +143
- Misses 7 8 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| out_inds = range(start_out; step=stride_out, length=N) | ||
| copyto!(out, CartesianIndices((out_inds,)), Xk, CartesianIndices((N,))) | ||
| # X_k = conj(b_k) · conv_k | ||
| @inbounds for i in 1:N | ||
| out[start_out + (i-1)*stride_out] = conj(chirp[i]) * conj(a[i]) | ||
| end |
There was a problem hiding this comment.
I suspect copyto! is often more optimized for large N
| # explicit fma (not muladd) so that rounding does not depend on | ||
| # whether LLVM contracts for a particular array type | ||
| push!(stmts, :($t = Complex{$T}(fma($wr, real($o), -$wi * imag($o)), | ||
| fma($wr, imag($o), $wi * real($o))))) |
There was a problem hiding this comment.
examples for when LLVM chooses not to contract for some array types? might have seen something similar before but cannot remember.
in general I think muladd is still the way to go, it accommodates machines with no native fma (emulated fma is slow). IMHO granular fast-math flags would be exactly what's needed here, but the issue for that on the Julia repo is still open...
| # implementation. | ||
|
|
||
| const CODELET_MAX = 64 | ||
| const CodeletEltype = Union{ComplexF32,ComplexF64} |
There was a problem hiding this comment.
maybe better to use traits?
Item C of the plan in #130, stacked on #134 (twiddle tables; the diff includes that commit until it lands).
Before,
fft_pow2_radix4!recursed down to 2- and 4-point base cases, so roughly half of the work on cache-resident powers of two was call and index overhead; FFTW's advantage at these sizes is its library of generated straight-line codelets.Now, for
Complex{Float32}andComplex{Float64}the recursion stops at 64 points (or the whole transform if smaller) in a fully unrolled radix-2 DIT codelet produced by an@generatedfunction keyed on the size and direction — every intermediate in its own SSA variable, twiddles folded to constants, trivial multiplications removed, explicitfmaso rounding does not depend on which array type the codelet is specialised for. This is what FFTW'sgenfftemits offline; here Julia's compiler does it. Other element types (BigFloat,Float16, symbolic) keep the generic recursion, which stays the reference implementation.Compile latency. Each (size, element type, direction) codelet costs 0.1–0.6 s of LLVM time on first use, so the package gets a
PrecompileToolsworkload that compiles the 8/16/32/64-point codelets and the common plan/execute paths at package precompile time (new dependency,PrecompileTools = "1"). Net effect on a fresh session:using FFTA; fft(randn(ComplexF64, 4096))goes from ~1.9 s to well under a millisecond. Standalone, the 8/16/32/64-point codelets run 3.3×/1.6×/2.2×/1.3× faster than the recursion they replace (14.6/47/125/334 ns vs FFTW's 24.8/40.6/80.2/308 ns on this machine).Before/after (aarch64 Neoverse-N1, Julia 1.12.6,
benchmark/suite.jl, planned execution, single thread; "before" is #134,FFTA/FFTWvs FFTW 3.3.11ESTIMATE):Largest slowdowns / speedups (planned execution, FFTA before → after; FFTW for reference):
261 matched cases; geometric-mean speedup 1.22×; 3 cases slower by >5%.
Against
mainthe combination #134 + this PR is a 2.35× geometric-mean speedup over the 495 comparable cases of the suite. Three strided-batched cases are within 10% either way (noise-level on this shared host).Tests: the existing suite (exact-equality checks between strided and contiguous execution included) passes; a 3D-
rfft-unsupported error count in the benchmark is the known limitation, not new.