Skip to content

Oversample instead of Bluestein for non-nice sizes - #29

Open
oscardssmith wants to merge 7 commits into
mainfrom
os/resample
Open

oscardssmith wants to merge 7 commits into
mainfrom
os/resample

Conversation

@oscardssmith

@oscardssmith oscardssmith commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Cells are main → PR (PR / main). Timing from AMD EPYC 9354 (32 cores) pinned to 1 numa node.

The rows with a short prime part (37 or 127: 74x259x127 at 1 to 8 threads, fft 37, 127x127 and the small composite table) are from ea3b7f5 against main at d63b21b. All other cells are from 18582fa against main at 103b9b6; that includes the 16- and 32-thread cells of 74x259x127, which are not re-measured.

Thread scaling

fft, 1D, Float64

Size 1 thread 2 threads 4 threads 8 threads 16 threads 32 threads
100003 2.21 → 1.67 ms (0.75) 1.84 → 1.14 ms (0.62) 2.28 → 1.61 ms (0.71) 1.62 → 0.9 ms (0.55) 1.57 → 0.559 ms (0.36) 819 → 460 µs (0.56)
1000003 50.2 → 30.3 ms (0.60) 33 → 17.8 ms (0.54) 22.2 → 17.2 ms (0.77) 15.1 → 10.3 ms (0.68) 15.9 → 5.57 ms (0.35) 14.3 → 3.88 ms (0.27)
10000019 565 → 335 ms (0.59) 371 → 209 ms (0.56) 196 → 121 ms (0.62) 115 → 61.8 ms (0.54) 74.9 → 35.5 ms (0.47) 69.7 → 30.9 ms (0.44)
1519500 29.7 → 31.5 ms (1.06) 25.4 → 22.5 ms (0.89) 16.7 → 11.6 ms (0.69) 8.59 → 6.6 ms (0.77) 6.46 → 3.75 ms (0.58) 6.01 → 3.07 ms (0.51)
591159 17.1 → 15.8 ms (0.92) 11.4 → 11 ms (0.96) 6.78 → 6.12 ms (0.90) 4.29 → 4.42 ms (1.03) 2.84 → 2.56 ms (0.90) 2.58 → 2.28 ms (0.88)
2389312 71.3 → 54.3 ms (0.76) 41.7 → 32.1 ms (0.77) 22.9 → 19.8 ms (0.86) 13.9 → 13.3 ms (0.96) 8.97 → 8.9 ms (0.99) 9.17 → 9.63 ms (1.05)

fft, multidimensional, Float64

Size 1 thread 2 threads 4 threads 8 threads 16 threads 32 threads
1009x1024 13 → 9.72 ms (0.75) 6.9 → 4.64 ms (0.67) 3.38 → 2.43 ms (0.72) 1.73 → 1.34 ms (0.78) 963 → 654 µs (0.68) 678 → 445 µs (0.66)
1024x1009 24 → 17.3 ms (0.72) 12.2 → 8.58 ms (0.71) 6 → 4.83 ms (0.81) 3.26 → 2.83 ms (0.87) 1.86 → 1.31 ms (0.71) 1.31 → 0.793 ms (0.61)
1009x1009 27.1 → 19.1 ms (0.70) 13.9 → 9.77 ms (0.70) 7.24 → 5.47 ms (0.76) 4.42 → 3.12 ms (0.70) 2.55 → 1.53 ms (0.60) 1.96 → 0.989 ms (0.50)
74x259x127 80.9 → 73.5 ms (0.91) 46.7 → 40.2 ms (0.86) 24.8 → 27.4 ms (1.10) 15.2 → 15.4 ms (1.01) 11.6 → 13.3 ms (1.15) 7.84 → 8.22 ms (1.05)

rfft, Float64

Size 1 thread 2 threads 4 threads 8 threads 16 threads 32 threads
200006 2.65 → 1.85 ms (0.70) 1.98 → 1.16 ms (0.58) 2.51 → 1.33 ms (0.53) 1.73 → 0.901 ms (0.52) 1.16 → 0.571 ms (0.49) 839 → 507 µs (0.60)
2000006 46.7 → 27.9 ms (0.60) 35.8 → 18.1 ms (0.51) 22.9 → 17.3 ms (0.76) 15 → 11.4 ms (0.76) 12.8 → 5.81 ms (0.45) 12.1 → 4.17 ms (0.34)
1000003 47.8 → 30.6 ms (0.64) 36.2 → 22.3 ms (0.62) 23.2 → 21.3 ms (0.92) 16.6 → 14.2 ms (0.85) 15.9 → 10.6 ms (0.67) 11.2 → 8.57 ms (0.76)

fft, 1D, Float32

Size 1 thread 2 threads 4 threads 8 threads 16 threads 32 threads
100003 1.66 → 1.04 ms (0.63) 1.34 → 0.863 ms (0.64) 1.59 → 0.92 ms (0.58) 1.23 → 0.563 ms (0.46) 646 → 384 µs (0.60) 1.43 → 0.332 ms (0.23)
1000003 29 → 16 ms (0.55) 22.8 → 9.15 ms (0.40) 12.9 → 9.14 ms (0.71) 7.59 → 4.53 ms (0.60) 6.4 → 2.51 ms (0.39) 6.19 → 2.06 ms (0.33)
10000019 318 → 179 ms (0.56) 209 → 122 ms (0.58) 116 → 78.9 ms (0.68) 71.4 → 41.9 ms (0.59) 56.2 → 25.2 ms (0.45) 56.1 → 20.8 ms (0.37)
1519500 19.2 → 12.6 ms (0.66) 11.8 → 6.56 ms (0.55) 11 → 7.06 ms (0.64) 8.21 → 3.82 ms (0.47) 5.69 → 2.25 ms (0.39) 6.1 → 1.6 ms (0.26)
591159 9.7 → 7.66 ms (0.79) 6.29 → 5.14 ms (0.82) 8.19 → 6.25 ms (0.76) 5.11 → 3.48 ms (0.68) 3.23 → 1.67 ms (0.52) 4.53 → 1.12 ms (0.25)
2389312 40.1 → 31.6 ms (0.79) 28.3 → 19.7 ms (0.70) 16.8 → 15.4 ms (0.91) 10.9 → 12 ms (1.10) 8.28 → 6.97 ms (0.84) 7.91 → 7.82 ms (0.99)

fft, multidimensional, Float32

Size 1 thread 2 threads 4 threads 8 threads 16 threads 32 threads
1009x1024 8.48 → 4.03 ms (0.48) 4.59 → 2.09 ms (0.46) 2.24 → 1.24 ms (0.55) 1.16 → 0.66 ms (0.57) 605 → 340 µs (0.56) 396 → 229 µs (0.58)
1024x1009 18.2 → 12.4 ms (0.69) 8.91 → 6.29 ms (0.71) 4.55 → 3.31 ms (0.73) 2.36 → 2.7 ms (1.14) 1.27 → 0.897 ms (0.71) 982 → 611 µs (0.62)
1009x1009 18.6 → 8.9 ms (0.48) 10.2 → 4.51 ms (0.44) 5.14 → 4.09 ms (0.80) 2.85 → 2.12 ms (0.75) 1.61 → 0.935 ms (0.58) 1.65 → 1.08 ms (0.65)
74x259x127 60.5 → 40.1 ms (0.66) 34.3 → 24.3 ms (0.71) 24.5 → 18.6 ms (0.76) 12.4 → 10.1 ms (0.81) 11.1 → 6.46 ms (0.58) 8.08 → 4.17 ms (0.52)

rfft, Float32

Size 1 thread 2 threads 4 threads 8 threads 16 threads 32 threads
200006 1.58 → 1.03 ms (0.65) 1.41 → 0.91 ms (0.65) 1.73 → 1.03 ms (0.60) 1.28 → 0.603 ms (0.47) 1.11 → 0.441 ms (0.40) 695 → 409 µs (0.59)
2000006 26.9 → 15.7 ms (0.58) 16 → 12.6 ms (0.79) 13.7 → 10.5 ms (0.77) 8.05 → 6.05 ms (0.75) 5.93 → 2.63 ms (0.44) 4.72 → 1.9 ms (0.40)
1000003 27.1 → 15.1 ms (0.56) 17.9 → 13.9 ms (0.78) 13.5 → 9.42 ms (0.70) 8.39 → 7.66 ms (0.91) 6.55 → 5.31 ms (0.81) 6.82 → 4.74 ms (0.70)

Small lengths and other paths, 1 thread

Float64

Transform Size Time
fft 1024 1.51 → 1.54 µs (1.02)
fft 37 0.369 → 0.3 µs (0.81)
fft 1009 11 → 8.37 µs (0.76)
fft 10007 167 → 150 µs (0.90)
fft 37888 660 → 423 µs (0.64)
fft 64576 880 → 784 µs (0.89)
fft 4736 58.1 → 39.5 µs (0.68)
fft along dims 1009x64, dims 1 731 → 546 µs (0.75)
fft along dims 64x1009, dims 2 810 → 622 µs (0.77)
fft along dims 127x127, dims 12 344 → 273 µs (0.79)
fft, run-time plan 1009 11.1 → 8.38 µs (0.75)
fft, run-time plan 100003 2.21 → 1.8 ms (0.81)
rfft 2018 11.7 → 8.91 µs (0.76)
rfft 20014 158 → 154 µs (0.97)
rfft 100003 2.64 → 1.83 ms (0.69)

Float32

Transform Size Time
fft 1024 0.87 → 0.869 µs (1.00)
fft 37 0.329 → 0.189 µs (0.57)
fft 1009 7.93 → 3.78 µs (0.48)
fft 10007 94 → 46.7 µs (0.50)
fft 37888 509 → 272 µs (0.54)
fft 64576 570 → 382 µs (0.67)
fft 4736 45.2 → 25.9 µs (0.57)
fft along dims 1009x64, dims 1 512 → 251 µs (0.49)
fft along dims 64x1009, dims 2 552 → 300 µs (0.54)
fft along dims 127x127, dims 12 253 → 118 µs (0.47)
fft, run-time plan 1009 8 → 4.01 µs (0.50)
fft, run-time plan 100003 1.51 → 0.978 ms (0.65)
rfft 2018 8.62 → 4.55 µs (0.53)
rfft 20014 99.7 → 51.9 µs (0.52)
rfft 100003 1.55 → 1.01 ms (0.65)

Small composite lengths (fft, Float64, 1 thread)

Size, dimension main kernel PR kernel Time
74x259x127 d1 Composite Composite 27 → 26.6 ms (0.99)
74x259x127 d2 Composite Composite 25.8 → 23.4 ms (0.91)
74x259x127 d3 Bluestein Resample 29.6 → 23.9 ms (0.81)
74x4096 d1 Composite Composite 3.03 → 2.91 ms (0.96)
4096x74 d2 Composite Composite 4.06 → 3.28 ms (0.81)
259x4096 d1 Composite Composite 12.1 → 11.3 ms (0.94)
4096x259 d2 Composite Composite 17.9 → 16.2 ms (0.90)
127x4096 d1 Bluestein Resample 5.67 → 4.33 ms (0.76)
4096x127 d2 Bluestein Resample 9.12 → 6.68 ms (0.73)
74 d1 Composite Composite 0.74 → 0.69 µs (0.93)
259 d1 Composite Composite 2.52 → 2.39 µs (0.94)
127 d1 Bluestein Resample 1.35 → 1.02 µs (0.76)

@tknopp

tknopp commented Nov 12, 2026

Copy link
Copy Markdown
Member

Benchmark of head 18582fa8 against main (103b9b6) on an Apple M1 Max and an AMD EPYC 7702. Pinned cores, interleaved rounds, minimum per case. Control runs of main against itself show ±5 % noise.

Clear gains (both machines):

  • c2c 37888 and r2c 37888: about 25–35 % faster at 1 thread, 20–30 % faster at 8 threads.
  • c2c 92935 and r2c 92935: 20–30 % faster with 8 threads on the M1, similar on the EPYC.
  • c2c 1000003: about 25–40 % faster with threads.
  • c2c 1009²: about 16 % faster on the EPYC at 1 thread, neutral on the M1.

Regressions:

  • c2c 101³: +41 % at 1 thread on the M1, +32 % at 8 threads. Neutral on the EPYC.
  • c2c 101² at 8 threads on the M1: +44 %.
  • r2c 101³ at 8 threads on the M1: +47 %.
  • r2c 92935 at 1 thread on the M1: +56 %; on the EPYC only +6 %.
  • c2c 64576 on the EPYC at 1 thread: +13 %, while the M1 shows −22 %. Not explained yet.

Questions:

  1. Is the slowdown of the 3-D case (101³) expected? Our guess is the resampling overhead for length-101 lines that do not fit the batched pass.
  2. Do you have numbers for c2c 64576 on your machine?

oscardssmith and others added 7 commits November 12, 2026 21:12
Lengths with a prime factor above MAX_RADIX_PRIME take a type-2 NUFFT at the
points k·m/n (FFTJ's resampler): deconvolve, zero-pad to a smooth m ≥ 2n, one
transform, interpolate with a w-tap exponential-of-semicircle kernel. One
transform of length 2n where Bluestein takes two. FFTJ's settings: error
10 eps relative to ‖x‖ (w = 16 for F64, 8 for F32), σ = 2, γ = 0.97, weight
table up to a size (stored twice, for the re and im lanes of the interleaved
grid) and polynomial weights above it; the table limit is at measured parity
(F64 1 MiB, F32 4 MiB). Blocked interpolation: one load of the w interleaved
grid values per output, FMA folds, one tree reduction per vector of outputs.

- whole-length kernel (Resample), run-time plans (Resample over RChain; w
  from the element type and the polynomial degree a field, so RResample is a
  union of two concrete types), one long line on threads (ResampleMT: four-step
  grid transform, deconvolution and output ranges over the tasks)
- composite kernel: the length-q DFTs of all s lanes in one batched pass
  (RsBatch, as FFTJ's composite): lane-fastest grid rows in cache-sized chunks,
  one batched chain, output rows as w FMAs of grid rows with broadcast weights
- own_buffers covers Resample (its buffers are per kernel)

Tiger Lake, resample / Bluestein (alternating rounds, min):
  serial    F64 1009 0.60, 10007 0.82, 100003 0.54
            F32 1009 0.51, 10007 0.65, 100003 0.58
  run-time  F64 0.64, 0.94, 0.62    F32 0.50, 0.55, 0.63
  4 threads F64 100003 0.60, 1000003 0.53   F32 0.63, 0.48
  composite F64 1024·37 0.90, 64·1009 0.76, 128·37 0.79
            F32 1024·37 0.64, 64·1009 0.81
Error vs a 128-bit reference: F64 2.5–4.7e-15 (Bluestein ~5e-16), F32
1.2–2.2e-7 (Bluestein 2.0–2.7e-7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CompositeMT resampled lane by lane while the serial Composite runs the batched
pass, so the two no longer agreed bitwise. The threaded kernel now runs the
same pass, its tasks taking chunks of lanes (rs_chunk!) with copies of the
RsBatch, at least one chunk per task where the lanes allow.

The chain's vector width follows the lane count of a chunk, and vectors of
one complex value round differently from wider ones, so a chunk runs its
multiple of the vector's complex count and its remainder as separate groups;
chunk starts are multiples of that count. Only the last s % CU lanes then take
the narrow path, whatever the chunk size, and serial and threaded plans agree
bitwise (all 46 cases of the threaded testset, 2–4 threads; F32 3·193·1021 on
4 threads is planned as a whole-length ResampleMT, as it was with Bluestein).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference was a direct O(n²) sum; the composite cases of 130462 points
took about 11 minutes per call and four calls in the quick run, past a
30-minute timeout. FFTW.jl (already a test dependency) is an independent
implementation at every size. The test of the AbstractFFTs generics without
FFTW moves ahead of loading it; FFTW is imported, not used, so its exports do
not meet FFTL's in the files included before fftw_compat.jl.

Quick run: 9m15s (Tiger Lake), all passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resampling has replaced it everywhere on the Float32/Float64 path (whole
lengths, composite leftovers, run-time plans, threads), so Bluestein,
BluesteinMT, bluestein_tables and the per-lane composite leftovers go. The
helpers the resampler keeps are renamed after the grid: grid_length,
grid_cost, GRID_RADIX, GRID_W, GRID_FOURSTEP_BYTES; use_resample_mt,
resample_mt_dim and TUNING.resample_mt_min (same value); the fields
Composite.rs, CompositeMT.rss and RPlan.rs. The composite split now sizes the
leftover's grid as the resampler does (grid_length(T, 2q), was 2q - 1).

Generic float types keep their exact Bluestein (generic.jl), and the
KernelAbstractions extension its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The algorithms page describes the resampled DFT in place of Bluestein's
algorithm (method, widths, weight tables, the measured comparison) and the
batched pass of the composite kernel; the platforms table says per column which
method a path uses (generic types and GPUs keep Bluestein). The Zen 2 timings
of the composite kernel and the Rader paragraph, both measured or argued
against Bluestein, are dropped. README and the other pages follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The s resampled transforms of length q of a composite kernel (n = s·q) run as
one batched pass (RsBatch) only from 32 lanes on. Below that every lane is one
resampled transform (Resample), vectorised along the line.

The batched pass vectorises across the lanes, so few lanes, or a lane count
that is not a multiple of the vector width, leave most of the vector unused.
Batched / lane by lane on an EPYC 9354 (AVX-512, one thread, q = 37, 1009,
10007), Float64 (Float32):

  s = 2    1.7–3.8   (2.1–7.3)
  s = 7    1.1–1.6   (1.6–2.9)
  s = 16   0.73–1.04 (0.84–1.31)
  s = 32   0.56–1.04 (0.87–0.96)
  s = 128  0.50–0.97 (0.66–1.13)

The rule (RS_BATCH_LANES, leftover_kernel) depends on the length only, not on
the number of threads, so Composite and CompositeMT take the same path and stay
bitwise equal. The run-time pass plans take the same rule; RComposite is a
union of four concrete types.

Float64, one thread, same machine: 74 = 2·37 takes 0.26 of the time of the
batched pass, 259 = 7·37 0.65, 3·193·1021 0.65; 1024·37, 64·1009 and 128·37
keep the batched pass and are unchanged. Not measured on AVX2 or aarch64.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The grid of a single resampled transform (Resample, ResampleMT) lies
between two margins. After the grid transform the margins hold copies of
the other end of the grid, so the window of every output is one
contiguous load and all outputs take the vector interpolation path.

The outputs whose window wraps around the grid ends (7 per Float64
transform, 3 per Float32) otherwise need a scalar loop at about ten
times the cost of the others: 250 of 559 ns at n = 37 and 290 of 1104 ns
at n = 101. Time with margins over time without, Float64, AMD EPYC 9354,
one thread, minima of interleaved runs (the same code against itself
within 6 %): n = 37: 0.55, 101: 0.75, 127: 0.79, 1009: 0.96,
100003: 0.98, 101^3: 0.78. Against main: 0.81, 0.81, 0.77, 0.64, 0.80,
0.83, and 0.85 for 101^3 on 8 threads (1.17 without margins).

The margin is a whole page, so the grid keeps the page offset of the
work buffers: with a 128-byte margin the prime length 100003 took
1.03-1.05 of the time without margins. Measured on x86 only (512-bit
and 256-bit vectors), not on aarch64.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@oscardssmith

Copy link
Copy Markdown
Member Author

timings for short primes improved.

@tknopp

tknopp commented Nov 13, 2026

Copy link
Copy Markdown
Member

Follow-up on head ea3b7f5. We re-measured against main (103b9b6) on an Apple M1 Max and an AMD EPYC 7702.

Regressions that reproduce on the M1 (second, quieter run):

  • r2c 92935, 1 thread: +47 % (minimum and median agree)
  • c2c 101², 8 threads: +12 %
  • c2c 101³, 1 thread: +11 %

Please disregard the 8-thread 37888 and 64576 differences from our first M1 run. They did not reproduce; they were load noise.

Open on the EPYC:

  • c2c 64576, 1 thread: +12 %. The M1 shows −24 % for the same case, and we have no explanation yet.
  • c2c 2^20, 16 threads: +20 %. Not measured on the M1 with 16 threads.

How we measure (so you can reproduce on x64):

  • Julia 1.13.3. Each variant is its own environment with the package developed from that branch and FFTW.jl as reference.
  • The single-case A/B scripts are in bench/ab/ (merged with Benchmark harness, figures and panel data in bench/ #32): bench_pr.jl for one thread, bench_mt.jl for N threads. Single-thread runs are pinned to one core (taskset -c N julia -t 1 …), multi-thread runs to a fixed core set with the matching -t k, one process per configuration.
  • Variants are interleaved over several rounds (main, PR, main, PR, …), so drift affects both equally.
  • Per case: five blocks of 10 to 30 calls, minimum over the blocks. We report the minimum over rounds and the median as a check.
  • Noise floor: main against an identical copy of main differs by up to ±5 %. Smaller differences are not meaningful. We check the load before starting and drop runs under load (we discarded runs above 10 on the M1).
  • Cases: c2c and r2c at 37888, 64576, 92935, 1000003, 2^20, 1009², 101², 101³, 1024², 1009×16, and small 1-D lengths.

Quick check on x64: c2c 101³ and r2c 92935 single-thread, and c2c 64576 single-thread, against main, five interleaved rounds each.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants