include/brotensor/detail/cpu/fft_core.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | // ─── CPU FFT core — shared internals ─────────────────────────────────────── | ||
| 4 | // | ||
| 5 | // Hand-rolled mixed-radix + Bluestein DFT, factored out of src/cpu/fft.cpp so | ||
| 6 | // the STFT / iSTFT ops (and any later spectral op) can reuse the exact same | ||
| 7 | // transform instead of duplicating it. | ||
| 8 | // | ||
| 9 | // This is a CPU-backend-private header (brotensor::detail::cpu). It is | ||
| 10 | // header-only and double-precision internally for accuracy, matching the | ||
| 11 | // fft.cpp original. No external libraries. | ||
| 12 | // | ||
| 13 | // * Cd — tiny double-precision complex helper. | ||
| 14 | // * dft_1d — unscaled length-N DFT, `sign` = -1 forward / +1 | ||
| 15 | // inverse. The "backward" 1/N inverse scaling is | ||
| 16 | // applied by callers that want an inverse. | ||
| 17 | // * load/store_complex_row — interleaved-complex (R, 2*C) tensor row I/O. | ||
| 18 | // | ||
| 19 | // The mixed-radix engine handles sizes whose prime factorisation uses only | ||
| 20 | // the radices 2/3/5/7 (covers Whisper's n_fft = 400 = 2^4 * 5^2); anything | ||
| 21 | // with a large or prime factor falls back to a Bluestein chirp-z transform, | ||
| 22 | // so dft_1d is correct for every length >= 1. | ||
| 23 | // | ||
| 24 | // The twiddle table (mixed-radix path) and chirp table (Bluestein path) are | ||
| 25 | // each a pure function of (N, sign), so they're cached per-thread across | ||
| 26 | // calls (see cached_twiddles / cached_chirp below) — a multi-frame STFT that | ||
| 27 | // calls dft_1d once per frame at a fixed n_fft reuses the same tables | ||
| 28 | // instead of rebuilding them every frame. | ||
| 29 | |||
| 30 | #include <cmath> | ||
| 31 | #include <cstddef> | ||
| 32 | #include <cstdint> | ||
| 33 | #include <unordered_map> | ||
| 34 | #include <vector> | ||
| 35 | |||
| 36 | namespace brotensor::detail::cpu::fftcore { | ||
| 37 | |||
| 38 | constexpr double kPi = 3.14159265358979323846; | ||
| 39 | |||
| 40 | // ─── tiny complex helper (double precision internally for accuracy) ──────── | ||
| 41 | 5590050 | struct Cd { | |
| 42 | 5590050 | double re = 0.0; | |
| 43 | 5590050 | double im = 0.0; | |
| 44 | }; | ||
| 45 | |||
| 46 | 22794620 | inline Cd operator+(Cd a, Cd b) { return {a.re + b.re, a.im + b.im}; } | |
| 47 | inline Cd operator-(Cd a, Cd b) { return {a.re - b.re, a.im - b.im}; } | ||
| 48 | 23055344 | inline Cd operator*(Cd a, Cd b) { | |
| 49 | 23055344 | return {a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re}; | |
| 50 | } | ||
| 51 | |||
| 52 | // ─── twiddle-factor table: w[k] = exp(sign * 2*pi*i * k / N) ─────────────── | ||
| 53 | 108 | inline std::vector<Cd> make_twiddles(int N, int sign) { | |
| 54 | 108 | std::vector<Cd> w(static_cast<std::size_t>(N)); | |
| 55 | 216 | const double s = static_cast<double>(sign) * 2.0 * kPi | |
| 56 | 108 | / static_cast<double>(N); | |
| 57 |
2/2✓ Branch 0 taken 108 times.
✓ Branch 1 taken 10116 times.
|
10224 | for (int k = 0; k < N; ++k) { |
| 58 | 10116 | w[static_cast<std::size_t>(k)] = {std::cos(s * k), std::sin(s * k)}; | |
| 59 | 10116 | } | |
| 60 | 108 | return w; | |
| 61 | 108 | } | |
| 62 | |||
| 63 | // ─── (N, sign) -> precomputed-table cache ────────────────────────────────── | ||
| 64 | // | ||
| 65 | // The mixed-radix recursion and its Bluestein fallback each build a | ||
| 66 | // trigonometric table (the twiddle table / the chirp table) that is a pure | ||
| 67 | // function of (N, sign) — once computed its values never change. A single | ||
| 68 | // STFT/iSTFT call runs `dft_1d` once per frame/row with the *same* n_fft, so | ||
| 69 | // the recursion revisits the exact same set of (N, sign) sub-problems (N's | ||
| 70 | // factorisation tree is deterministic) on every frame. Without caching that | ||
| 71 | // means thousands of redundant heap allocations and sin/cos evaluations per | ||
| 72 | // call. Cache the tables keyed on (N, sign) instead of rebuilding them. | ||
| 73 | // | ||
| 74 | // The cache is `thread_local` rather than behind a mutex: this codebase's | ||
| 75 | // convention (see CLAUDE.md — "no mutexes") is single-owner/thread-local | ||
| 76 | // state, and the CPU backend already uses exactly this | ||
| 77 | // `thread_local static` reusable-scratch pattern elsewhere (mirrored from | ||
| 78 | // src/cuda/resblock.cu and src/metal/resblock.mm, which cache per-thread | ||
| 79 | // scratch tensors across calls). A thread_local table means each thread that | ||
| 80 | // calls into the CPU FFT backend gets its own private cache — no shared | ||
| 81 | // mutable state, so nothing to race on, without needing any locking | ||
| 82 | // machinery that single-threaded CPU-backend use doesn't need. | ||
| 83 | // | ||
| 84 | // No eviction policy: the cache grows to the number of distinct (N, sign) | ||
| 85 | // pairs a thread has ever computed, which in practice is bounded by the | ||
| 86 | // handful of n_fft / signal-length configurations a process actually uses | ||
| 87 | // (plus the O(log N) sub-sizes each one's recursion visits) — not by the | ||
| 88 | // number of frames or calls, so it saturates quickly and stays small. | ||
| 89 | 2121533 | inline std::uint64_t table_cache_key(int N, int sign) { | |
| 90 | 4243066 | return (static_cast<std::uint64_t>(static_cast<std::uint32_t>(N)) << 1) | |
| 91 | 2121533 | | (sign > 0 ? 1u : 0u); | |
| 92 | } | ||
| 93 | |||
| 94 | // Cached twiddle table for fft_recursive's per-level combine step. | ||
| 95 | 2120443 | inline const std::vector<Cd>& cached_twiddles(int N, int sign) { | |
| 96 |
2/2✓ Branch 0 taken 2120439 times.
✓ Branch 1 taken 4 times.
|
2120443 | thread_local std::unordered_map<std::uint64_t, std::vector<Cd>> cache; |
| 97 | 2120443 | const std::uint64_t key = table_cache_key(N, sign); | |
| 98 | 2120443 | auto it = cache.find(key); | |
| 99 |
2/2✓ Branch 0 taken 2120335 times.
✓ Branch 1 taken 108 times.
|
2120443 | if (it != cache.end()) return it->second; |
| 100 |
2/4✓ Branch 0 taken 108 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 108 times.
✗ Branch 3 not taken.
|
108 | return cache.emplace(key, make_twiddles(N, sign)).first->second; |
| 101 | 2120443 | } | |
| 102 | |||
| 103 | // Cached chirp table for Bluestein's chirp-z transform (exp(sign * i*pi*n^2/N)). | ||
| 104 | 1090 | inline const std::vector<Cd>& cached_chirp(int N, int sign) { | |
| 105 |
2/2✓ Branch 0 taken 1088 times.
✓ Branch 1 taken 2 times.
|
1090 | thread_local std::unordered_map<std::uint64_t, std::vector<Cd>> cache; |
| 106 | 1090 | const std::uint64_t key = table_cache_key(N, sign); | |
| 107 | 1090 | auto it = cache.find(key); | |
| 108 |
2/2✓ Branch 0 taken 1084 times.
✓ Branch 1 taken 6 times.
|
1090 | if (it != cache.end()) return it->second; |
| 109 | 6 | std::vector<Cd> chirp(static_cast<std::size_t>(N)); | |
| 110 |
2/2✓ Branch 0 taken 934 times.
✓ Branch 1 taken 6 times.
|
940 | for (int n = 0; n < N; ++n) { |
| 111 | 934 | const long long n2 = (static_cast<long long>(n) * n) % (2LL * N); | |
| 112 | 1868 | const double ang = static_cast<double>(sign) * kPi | |
| 113 | 934 | * static_cast<double>(n2) / static_cast<double>(N); | |
| 114 | 934 | chirp[static_cast<std::size_t>(n)] = {std::cos(ang), std::sin(ang)}; | |
| 115 | 934 | } | |
| 116 |
2/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
|
6 | return cache.emplace(key, std::move(chirp)).first->second; |
| 117 | 1090 | } | |
| 118 | |||
| 119 | // ─── naive O(N^2) DFT — base case + Bluestein small kernels ──────────────── | ||
| 120 | ✗ | inline void dft_naive(const std::vector<Cd>& in, std::vector<Cd>& out, | |
| 121 | int sign) { | ||
| 122 | ✗ | const int N = static_cast<int>(in.size()); | |
| 123 | ✗ | out.assign(static_cast<std::size_t>(N), Cd{}); | |
| 124 | ✗ | const double s = static_cast<double>(sign) * 2.0 * kPi | |
| 125 | ✗ | / static_cast<double>(N); | |
| 126 | ✗ | for (int k = 0; k < N; ++k) { | |
| 127 | ✗ | Cd acc{}; | |
| 128 | ✗ | for (int n = 0; n < N; ++n) { | |
| 129 | ✗ | const double a = s * (static_cast<long long>(k) * n % N); | |
| 130 | ✗ | const Cd tw{std::cos(a), std::sin(a)}; | |
| 131 | ✗ | acc = acc + in[static_cast<std::size_t>(n)] * tw; | |
| 132 | ✗ | } | |
| 133 | ✗ | out[static_cast<std::size_t>(k)] = acc; | |
| 134 | ✗ | } | |
| 135 | ✗ | } | |
| 136 | |||
| 137 | // Smallest supported radix (2,3,5,7) dividing N, or 0 if none does. | ||
| 138 | 2120443 | inline int small_radix(int N) { | |
| 139 |
1/2✓ Branch 0 taken 2207157 times.
✗ Branch 1 not taken.
|
2207157 | for (int r : {2, 3, 5, 7}) { |
| 140 |
2/2✓ Branch 0 taken 2120443 times.
✓ Branch 1 taken 86714 times.
|
2207157 | if (N % r == 0) return r; |
| 141 | } | ||
| 142 | ✗ | return 0; | |
| 143 | 2120443 | } | |
| 144 | |||
| 145 | // True iff N factors entirely into the supported small radices. | ||
| 146 | 1972443 | inline bool is_smooth(int N) { | |
| 147 | 1972443 | int m = N; | |
| 148 |
2/2✓ Branch 0 taken 7889772 times.
✓ Branch 1 taken 1972443 times.
|
9862215 | for (int r : {2, 3, 5, 7}) { |
| 149 |
2/2✓ Branch 0 taken 1108480 times.
✓ Branch 1 taken 7889772 times.
|
8998252 | while (m % r == 0) m /= r; |
| 150 | } | ||
| 151 | 1972443 | return m == 1; | |
| 152 | } | ||
| 153 | |||
| 154 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 138979 times.
|
138979 | inline bool is_power_of_two(int N) { return N > 0 && (N & (N - 1)) == 0; } |
| 155 | |||
| 156 | // Forward declaration: Bluestein needs the recursive (power-of-two) FFT. | ||
| 157 | inline void fft_recursive(const std::vector<Cd>& in, std::vector<Cd>& out, | ||
| 158 | int sign); | ||
| 159 | |||
| 160 | // ─── Bluestein chirp-z transform — arbitrary-length fallback ─────────────── | ||
| 161 | 1090 | inline void bluestein(const std::vector<Cd>& in, std::vector<Cd>& out, | |
| 162 | int sign) { | ||
| 163 | 1090 | const int N = static_cast<int>(in.size()); | |
| 164 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1090 times.
|
1090 | if (N <= 1) { out = in; return; } |
| 165 | |||
| 166 | 1090 | int M = 1; | |
| 167 |
2/2✓ Branch 0 taken 7634 times.
✓ Branch 1 taken 1090 times.
|
8724 | while (M < 2 * N - 1) M <<= 1; |
| 168 | |||
| 169 | 1090 | const std::vector<Cd>& chirp = cached_chirp(N, sign); | |
| 170 | |||
| 171 | 1090 | std::vector<Cd> a(static_cast<std::size_t>(M), Cd{}); | |
| 172 |
2/2✓ Branch 0 taken 59002 times.
✓ Branch 1 taken 1090 times.
|
60092 | for (int n = 0; n < N; ++n) { |
| 173 | 59002 | a[static_cast<std::size_t>(n)] = | |
| 174 | 59002 | in[static_cast<std::size_t>(n)] * chirp[static_cast<std::size_t>(n)]; | |
| 175 | 59002 | } | |
| 176 | |||
| 177 |
1/2✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
|
1090 | std::vector<Cd> b(static_cast<std::size_t>(M), Cd{}); |
| 178 | 1090 | b[0] = {chirp[0].re, -chirp[0].im}; | |
| 179 |
2/2✓ Branch 0 taken 57912 times.
✓ Branch 1 taken 1090 times.
|
59002 | for (int k = 1; k < N; ++k) { |
| 180 | 115824 | const Cd c{chirp[static_cast<std::size_t>(k)].re, | |
| 181 | 57912 | -chirp[static_cast<std::size_t>(k)].im}; | |
| 182 | 57912 | b[static_cast<std::size_t>(k)] = c; | |
| 183 | 57912 | b[static_cast<std::size_t>(M - k)] = c; | |
| 184 | 57912 | } | |
| 185 | |||
| 186 | 1090 | std::vector<Cd> fa, fb; | |
| 187 |
1/2✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
|
1090 | fft_recursive(a, fa, -1); |
| 188 |
1/2✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
|
1090 | fft_recursive(b, fb, -1); |
| 189 |
1/2✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
|
1090 | std::vector<Cd> prod(static_cast<std::size_t>(M)); |
| 190 |
2/2✓ Branch 0 taken 142720 times.
✓ Branch 1 taken 1090 times.
|
143810 | for (int i = 0; i < M; ++i) { |
| 191 | 142720 | prod[static_cast<std::size_t>(i)] = | |
| 192 | 142720 | fa[static_cast<std::size_t>(i)] * fb[static_cast<std::size_t>(i)]; | |
| 193 | 142720 | } | |
| 194 | 1090 | std::vector<Cd> conv; | |
| 195 |
1/2✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
|
1090 | fft_recursive(prod, conv, +1); |
| 196 | 1090 | const double invM = 1.0 / static_cast<double>(M); | |
| 197 | |||
| 198 |
1/2✓ Branch 0 taken 1090 times.
✗ Branch 1 not taken.
|
1090 | out.assign(static_cast<std::size_t>(N), Cd{}); |
| 199 |
2/2✓ Branch 0 taken 59002 times.
✓ Branch 1 taken 1090 times.
|
60092 | for (int k = 0; k < N; ++k) { |
| 200 | 59002 | Cd c = conv[static_cast<std::size_t>(k)]; | |
| 201 | 59002 | c.re *= invM; | |
| 202 | 59002 | c.im *= invM; | |
| 203 | 59002 | out[static_cast<std::size_t>(k)] = | |
| 204 | 59002 | chirp[static_cast<std::size_t>(k)] * c; | |
| 205 | 59002 | } | |
| 206 | 1090 | } | |
| 207 | |||
| 208 | // ─── mixed-radix recursive FFT ───────────────────────────────────────────── | ||
| 209 | 4512009 | inline void fft_recursive(const std::vector<Cd>& in, std::vector<Cd>& out, | |
| 210 | int sign) { | ||
| 211 | 4512009 | const int N = static_cast<int>(in.size()); | |
| 212 |
2/2✓ Branch 0 taken 2391566 times.
✓ Branch 1 taken 2120443 times.
|
4512009 | if (N <= 1) { out = in; return; } |
| 213 | |||
| 214 | 2120443 | const int r = small_radix(N); | |
| 215 |
1/2✓ Branch 0 taken 2120443 times.
✗ Branch 1 not taken.
|
2120443 | if (r == 0) { |
| 216 | ✗ | bluestein(in, out, sign); | |
| 217 | ✗ | return; | |
| 218 | } | ||
| 219 | |||
| 220 | 2120443 | const int m = N / r; | |
| 221 | |||
| 222 |
3/4✓ Branch 0 taken 1970570 times.
✓ Branch 1 taken 149873 times.
✓ Branch 2 taken 1970570 times.
✗ Branch 3 not taken.
|
2120443 | if (N <= 8 && !is_smooth(m)) { |
| 223 | ✗ | dft_naive(in, out, sign); | |
| 224 | ✗ | return; | |
| 225 | } | ||
| 226 | |||
| 227 |
1/2✓ Branch 0 taken 2120443 times.
✗ Branch 1 not taken.
|
2120443 | std::vector<std::vector<Cd>> subs( |
| 228 | 2120443 | static_cast<std::size_t>(r), | |
| 229 | 2120443 | std::vector<Cd>(static_cast<std::size_t>(m))); | |
| 230 |
2/2✓ Branch 0 taken 5320754 times.
✓ Branch 1 taken 2120443 times.
|
7441197 | for (int t = 0; t < m; ++t) { |
| 231 |
2/2✓ Branch 0 taken 10857340 times.
✓ Branch 1 taken 5320754 times.
|
16178094 | for (int j = 0; j < r; ++j) { |
| 232 | 10857340 | subs[static_cast<std::size_t>(j)][static_cast<std::size_t>(t)] = | |
| 233 | 10857340 | in[static_cast<std::size_t>(t * r + j)]; | |
| 234 | 10857340 | } | |
| 235 | 5320754 | } | |
| 236 | |||
| 237 |
1/2✓ Branch 0 taken 2120443 times.
✗ Branch 1 not taken.
|
2120443 | std::vector<std::vector<Cd>> subF(static_cast<std::size_t>(r)); |
| 238 |
2/2✓ Branch 0 taken 2120443 times.
✓ Branch 1 taken 4370850 times.
|
6491293 | for (int j = 0; j < r; ++j) { |
| 239 |
2/4✓ Branch 0 taken 4370850 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4370850 times.
✗ Branch 3 not taken.
|
8741700 | fft_recursive(subs[static_cast<std::size_t>(j)], |
| 240 | 4370850 | subF[static_cast<std::size_t>(j)], sign); | |
| 241 | 4370850 | } | |
| 242 | |||
| 243 |
1/2✓ Branch 0 taken 2120443 times.
✗ Branch 1 not taken.
|
2120443 | const std::vector<Cd>& w = cached_twiddles(N, sign); |
| 244 |
1/2✓ Branch 0 taken 2120443 times.
✗ Branch 1 not taken.
|
2120443 | out.assign(static_cast<std::size_t>(N), Cd{}); |
| 245 |
2/2✓ Branch 0 taken 2120443 times.
✓ Branch 1 taken 10857340 times.
|
12977783 | for (int k = 0; k < N; ++k) { |
| 246 | 10857340 | const int km = k % m; | |
| 247 | 10857340 | Cd acc{}; | |
| 248 |
2/2✓ Branch 0 taken 10857340 times.
✓ Branch 1 taken 22794620 times.
|
33651960 | for (int j = 0; j < r; ++j) { |
| 249 | 22794620 | const int idx = (j * k) % N; | |
| 250 |
3/6✓ Branch 0 taken 22794620 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22794620 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22794620 times.
✗ Branch 5 not taken.
|
45589240 | acc = acc + w[static_cast<std::size_t>(idx)] * |
| 251 | 45589240 | subF[static_cast<std::size_t>(j)] | |
| 252 | 22794620 | [static_cast<std::size_t>(km)]; | |
| 253 | 22794620 | } | |
| 254 | 10857340 | out[static_cast<std::size_t>(k)] = acc; | |
| 255 | 10857340 | } | |
| 256 | 4512009 | } | |
| 257 | |||
| 258 | // ─── public single-signal transform (unscaled) ───────────────────────────── | ||
| 259 | // | ||
| 260 | // dft_1d runs the unscaled transform with the requested sign. The 1/N inverse | ||
| 261 | // scaling for the "backward" convention is applied by inverse callers. | ||
| 262 | 138981 | inline void dft_1d(const std::vector<Cd>& in, std::vector<Cd>& out, int sign) { | |
| 263 | 138981 | const int N = static_cast<int>(in.size()); | |
| 264 |
1/2✓ Branch 0 taken 138981 times.
✗ Branch 1 not taken.
|
138981 | if (N == 0) { out.clear(); return; } |
| 265 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 138979 times.
|
138981 | if (N == 1) { out = in; return; } |
| 266 |
4/4✓ Branch 0 taken 1873 times.
✓ Branch 1 taken 137106 times.
✓ Branch 2 taken 783 times.
✓ Branch 3 taken 1090 times.
|
138979 | if (is_power_of_two(N) || is_smooth(N)) { |
| 267 | 137889 | fft_recursive(in, out, sign); | |
| 268 | 137889 | } else { | |
| 269 | 1090 | bluestein(in, out, sign); | |
| 270 | } | ||
| 271 | 138981 | } | |
| 272 | |||
| 273 | // ─── interleaved-complex tensor row helpers ──────────────────────────────── | ||
| 274 | 364 | inline void load_complex_row(const float* base, int row, int cols2, | |
| 275 | std::vector<Cd>& dst) { | ||
| 276 | 364 | const int C = cols2 / 2; | |
| 277 | 364 | dst.assign(static_cast<std::size_t>(C), Cd{}); | |
| 278 | 364 | const float* p = base + static_cast<std::size_t>(row) * cols2; | |
| 279 |
2/2✓ Branch 0 taken 14104 times.
✓ Branch 1 taken 364 times.
|
14468 | for (int c = 0; c < C; ++c) { |
| 280 | 28208 | dst[static_cast<std::size_t>(c)] = {static_cast<double>(p[2 * c]), | |
| 281 | 14104 | static_cast<double>(p[2 * c + 1])}; | |
| 282 | 14104 | } | |
| 283 | 364 | } | |
| 284 | |||
| 285 | 364 | inline void store_complex_row(float* base, int row, int cols2, | |
| 286 | const std::vector<Cd>& src) { | ||
| 287 | 364 | const int C = cols2 / 2; | |
| 288 | 364 | float* p = base + static_cast<std::size_t>(row) * cols2; | |
| 289 |
2/2✓ Branch 0 taken 14104 times.
✓ Branch 1 taken 364 times.
|
14468 | for (int c = 0; c < C; ++c) { |
| 290 | 14104 | p[2 * c] = static_cast<float>(src[static_cast<std::size_t>(c)].re); | |
| 291 | 14104 | p[2 * c + 1] = static_cast<float>(src[static_cast<std::size_t>(c)].im); | |
| 292 | 14104 | } | |
| 293 | 364 | } | |
| 294 | |||
| 295 | } // namespace brotensor::detail::cpu::fftcore | ||
| 296 |