Repository navigation
One looped kernel for the prime radices 17 to 73 - #31
Conversation
A stage of a prime radix from 17 on runs prime_stage! (src/primestage.jl): one kernel for all such radices, with the radix and a table of cosines and sines as arguments, compiled once per vector type. It is the symmetric form of the odd-prime codelet as loops, with up to eight output pairs per pass over the inputs, and covers the stages vectorised over the lanes, the last stage, lane blocks and the grouped first stage. The radices up to 16 keep their straight-line codelets. A straight-line codelet of a prime r has about r^2/2 statements and needed 82 kernel methods per radix on AVX-512, which is why the codelets of 17-31 were compiled at first use. That machinery (callable tables, the lazy_radices preference) is removed: a prime now costs a table. MAX_RADIX_PRIME is 73, the largest prime for which the kernel was faster than Bluestein's algorithm in every measured case, the line of the prime alone included. The KernelAbstractions kernels and the generic number types keep their limit of 31 (factorize takes the limit as an argument). The stage kernels have no @generated twins any more: every combination the chain can select has a plain method. One table (STAGE_KERNELS) lists the kernels and their arguments; the methods and the dispatch switches are made from it. stage! and stage_range! share one body builder, and the scalar tail of the grouped first stage is the grouped kernel on a vector of one complex value. The generated bodies of the radices 2-16 are unchanged. The calls of the prime kernel are behind a function of their own, outside the dispatch switches: they take more arguments than fit in registers and cost every case of a switch about 1 ns otherwise. AMD EPYC 9354 (AVX-512), one thread, plan_fft and mul!, rounds in alternating processes, median time over that of the previous code: - lengths with a factor 17-31 (40 cases): geometric mean 0.82, one case above 1.05 (1.06); - long lengths with such a factor (36 cases): 0.94, one above 1.05 (1.08); - the primes 37-73 against Bluestein's algorithm (100 cases): 0.29; - 88 general cases without such factors: 1.00, three above 1.05 (Float64 lines of 32, 48, 60: 1.07-1.15; 1.04, 0.83, 1.05 in a second run of 6 rounds on short lines, whose stage kernels compile to the same code). Error against a 200-bit reference at 841, 6256, 6859 (Float64): 1.4-1.8 eps instead of 3.5-4.6 eps. libfftl.so 26.8 MB instead of 49.5 MB. The package image is 66 MB instead of 59 MB, because the kernel is in the image and the lazy codelets were not. Not verified on aarch64. Tests that used 37, 41 and 43 as examples of primes without a radix use 79, 83 and 89. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Checked on aarch64 (Apple M1 Max, same setup as in #30, time over
The primes 37–73 confirm your numbers. The one case against the x86 measurement is The image growth to 66 MB is a fair price for no compilation on first use and half the |
|
I think I may have more image size improvements. (right now we compile all eagerly and precompile maximally and neither likely makes sense) |
Stacked on #30.
What
prime_stage!(src/primestage.jl): the radix and a table of cosines and sines are arguments, and the kernel is compiled once per vector type. It covers the stages vectorised over the lanes, the last stage, lane blocks and the grouped first stage. The radices up to 16 keep their straight-line codelets.MAX_RADIX_PRIMEis 73 (was 31). The KernelAbstractions kernels and the generic number types keep 31;factorizetakes the limit as an argument.lazy_radicespreference.@generatedstage kernels. Every combination the chain can select has a plain method. One table (STAGE_KERNELS) lists the kernels and their arguments; the methods and the dispatch switches are made from it.stage!andstage_range!share one body builder, and the scalar tail of the grouped first stage is the grouped kernel on a vector of one complex value. The generated bodies of the radices 2–16 are unchanged (compared for every kind, element type, sign, vector type and radix).Why
A straight-line codelet of a prime r has about r²/2 statements with as many constants (525 statements for 31) and needed 82 kernel methods per radix on AVX-512, which is why 17–31 were compiled at first use. With the loop a prime costs a table.
Measurements
AMD EPYC 9354 (AVX-512), Julia 1.13.3, one thread,
plan_fftandmul!, rounds in alternating processes, median time over the median time on #30:Float64100 × 31: 1.06)Float6419·2¹⁷: 1.08)Float64lines of 32, 48, 60: 1.07–1.15)Float64), 0.48–0.90 (Float32); as a factor of a longer length: 0.13–0.53.Float64, lengths 841, 6256, 6859: 1.4–1.8 ε, with the codelets 3.5–4.6 ε.Sizes on the same machine:
libfftl.so(x86-64-v3,--trim=safe)libfftl.soThe package image grows because the kernel and its tables are in the image and the lazy codelets were not; 1.4 MB of that is typed code of an inlined helper (see below).
The limit
Measured with the limit at 127: as a factor of a longer length a prime from 79 on is still 1.3–4× faster than Bluestein, but the line of the prime alone is slower from 79 on for
Float32(1.05; 1.5 at 127) and from 97 on forFloat64. 73 is the largest prime for which every measured case is faster.What made the loop as fast as unrolled code
Tests
libfftlbuilds;fftw3_ctest.shpasses (3940 comparisons, 0 failed).Not done
Float64) and 1.05–1.06 (Float32) of the codelets; not measured with the single kernel.Decision and evidence:
docs/src/explanation/design-decisions.md("The prime radices from 17 to 73 share one looped kernel").AGENTS.mdno longer says that@generatedmethods stay as a fallback.🤖 Generated with Claude Code