use crate::{DType, Shape};
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FftNorm {
#[default]
Backward,
Forward,
Ortho,
}
impl FftNorm {
pub fn output_scale(self, n: usize, inverse: bool) -> f64 {
let n = n as f64;
match self {
FftNorm::Backward => 1.0,
FftNorm::Forward => {
if inverse {
1.0 / n
} else {
1.0
}
}
FftNorm::Ortho => 1.0 / n.sqrt(),
}
}
pub fn tag(self) -> u32 {
match self {
FftNorm::Backward => 0,
FftNorm::Forward => 1,
FftNorm::Ortho => 2,
}
}
pub fn from_tag(tag: u32) -> Self {
match tag {
0 => FftNorm::Backward,
1 => FftNorm::Forward,
2 => FftNorm::Ortho,
other => panic!("fft: unknown FftNorm tag {other}"),
}
}
}
pub fn next_pow2(n: usize) -> usize {
if n <= 1 {
return 1;
}
1_usize << ((n - 1).ilog2() + 1)
}
pub fn bit_reverse(mut x: usize, bits: u32) -> usize {
x = x.reverse_bits() >> (usize::BITS - bits);
x
}
pub const FFT_TILE_SIZE: usize = 1024;
pub const FFT_TILE_BITS: usize = 10;
pub const FFT_WG_SIZE: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FftGpuPlan {
pub n: usize,
pub log2n: usize,
pub inner_stages: usize,
pub outer_rad4_q: Vec<usize>,
pub outer_r2_hs: Option<usize>,
}
impl FftGpuPlan {
pub fn new(n: usize) -> Option<Self> {
if n < 2 || !n.is_power_of_two() {
return None;
}
let log2n = n.trailing_zeros() as usize;
let inner_stages = log2n.min(FFT_TILE_BITS);
let mut outer_rad4_q = Vec::new();
let mut rem = log2n.saturating_sub(inner_stages);
let mut s = inner_stages;
while rem >= 2 {
outer_rad4_q.push(1_usize << s);
s += 2;
rem -= 2;
}
let outer_r2_hs = if rem >= 1 { Some(1_usize << s) } else { None };
Some(FftGpuPlan {
n,
log2n,
inner_stages,
outer_rad4_q,
outer_r2_hs,
})
}
pub fn single_inner_only(&self) -> bool {
self.outer_rad4_q.is_empty() && self.outer_r2_hs.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FftMeta {
pub outer: usize,
pub n_complex: usize,
pub axis_extent: usize,
}
impl FftMeta {
pub fn row_bytes(&self, dtype: DType) -> usize {
match dtype {
DType::F32 => self.axis_extent * 4,
DType::F64 => self.axis_extent * 8,
DType::C64 => self.axis_extent * 8,
other => panic!("fft: unsupported dtype {other:?}"),
}
}
}
pub fn fft_meta(shape: &Shape) -> FftMeta {
let rank = shape.rank();
assert!(rank >= 1, "fft: tensor must have at least 1 axis");
let axis_extent = shape.dim(rank - 1).unwrap_static();
let n_complex = match shape.dtype() {
DType::C64 => axis_extent,
DType::F32 | DType::F64 | DType::I32 => {
assert!(
axis_extent.is_multiple_of(2),
"fft: last axis size {axis_extent} must be even (2N real-block layout)"
);
axis_extent / 2
}
other => panic!("fft: requires F32, F64, C64, or I32, got {other:?}"),
};
let total = shape.num_elements().unwrap_or(0);
assert!(
axis_extent > 0 && total.is_multiple_of(axis_extent),
"fft: shape {shape:?} is not divisible by last-axis extent {axis_extent}"
);
FftMeta {
outer: total / axis_extent,
n_complex,
axis_extent,
}
}
pub fn fftn_axes_all(rank: usize) -> Vec<usize> {
(0..rank).collect()
}
pub fn gpu_fft_native_eligible(dtype: DType, n_complex: usize) -> bool {
matches!(dtype, DType::F32) && n_complex.is_power_of_two() && n_complex >= 2
}
pub fn prime_factors(mut n: usize) -> Vec<usize> {
if n < 2 {
return Vec::new();
}
let mut out = Vec::new();
let mut d = 2usize;
while d * d <= n {
while n.is_multiple_of(d) {
out.push(d);
n /= d;
}
d += 1;
}
if n > 1 {
out.push(n);
}
out
}
pub fn fft_arena_byte_span(
src_byte_off: usize,
dst_byte_off: usize,
row_bytes: usize,
outer: usize,
) -> (usize, usize) {
let len = outer * row_bytes;
let start = src_byte_off.min(dst_byte_off);
let end = src_byte_off.max(dst_byte_off) + len;
(start, end - start)
}
pub fn fftfreq(n: usize) -> Vec<f64> {
assert!(n > 0, "fftfreq: n must be positive");
(0..n)
.map(|k| {
let f = if k <= n / 2 {
k as f64
} else {
k as f64 - n as f64
};
f / n as f64
})
.collect()
}
pub fn rfftfreq(n: usize) -> Vec<f64> {
assert!(n > 0, "rfftfreq: n must be positive");
let half = n / 2 + 1;
(0..half).map(|k| k as f64 / n as f64).collect()
}
pub fn normalize_fftn_axes(rank: usize, axes: &[usize]) -> Vec<usize> {
let mut out: Vec<usize> = axes.to_vec();
out.sort_unstable();
out.dedup();
for &ax in &out {
assert!(
ax < rank,
"fftn: axis {ax} out of range for rank-{rank} tensor"
);
}
out
}
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FftQScale {
#[default]
None,
Saturating,
EveryOther,
PerStage,
}
impl FftQScale {
pub fn bits_lost(self, n: usize) -> u32 {
let stages = n.trailing_zeros();
match self {
Self::None | Self::Saturating => 0,
Self::EveryOther => stages.div_ceil(2),
Self::PerStage => stages,
}
}
pub fn headroom_needed(self, n: usize) -> u32 {
n.trailing_zeros() - self.bits_lost(n)
}
pub fn tag(self) -> u32 {
match self {
Self::None => 0,
Self::PerStage => 1,
Self::Saturating => 2,
Self::EveryOther => 3,
}
}
pub fn from_tag(t: u32) -> Self {
match t {
1 => Self::PerStage,
2 => Self::Saturating,
3 => Self::EveryOther,
_ => Self::None,
}
}
}
pub const FFT_Q_TWIDDLE_FRAC: u32 = 30;
fn twiddles_q30(n: usize, inverse: bool) -> (Vec<i32>, Vec<i32>) {
let scale = (1i64 << FFT_Q_TWIDDLE_FRAC) as f64;
let sign = if inverse { 1.0 } else { -1.0 };
let mut re = Vec::with_capacity(n / 2);
let mut im = Vec::with_capacity(n / 2);
for k in 0..n / 2 {
let a = sign * 2.0 * core::f64::consts::PI * k as f64 / n as f64;
re.push((a.cos() * scale).round() as i32);
im.push((a.sin() * scale).round() as i32);
}
(re, im)
}
#[inline]
fn rshift_round(v: i64, s: u32) -> i32 {
if s == 0 {
return v as i32;
}
((v + (1 << (s - 1))) >> s) as i32
}
pub fn fft1d_q32_block(
data: &mut [i32],
outer: usize,
n: usize,
inverse: bool,
norm: FftNorm,
scale: FftQScale,
) -> Result<(), String> {
if !n.is_power_of_two() {
return Err(format!(
"fixed-point FFT length {n} is not a power of two; only radix-2 is supported"
));
}
if data.len() != outer * 2 * n {
return Err(format!(
"fixed-point FFT expects {} elements ({outer} x 2 x {n}), got {}",
outer * 2 * n,
data.len()
));
}
if n < 2 {
return Ok(());
}
let plan = FftQ32Plan::new(n, inverse, norm, scale)?;
for row in data.chunks_exact_mut(2 * n) {
plan.run_row(row);
}
Ok(())
}
pub struct FftQ32Plan {
tw_re: Vec<i32>,
tw_im: Vec<i32>,
n: usize,
bits: u32,
inverse: bool,
norm: FftNorm,
scale: FftQScale,
}
impl FftQ32Plan {
pub fn new(n: usize, inverse: bool, norm: FftNorm, scale: FftQScale) -> Result<Self, String> {
if !n.is_power_of_two() {
return Err(format!(
"fixed-point FFT length {n} is not a power of two; only radix-2 is supported"
));
}
let (tw_re, tw_im) = twiddles_q30(n, inverse);
Ok(Self {
tw_re,
tw_im,
n,
bits: n.trailing_zeros(),
inverse,
norm,
scale,
})
}
pub fn row_elems(&self) -> usize {
2 * self.n
}
pub fn run_row(&self, row: &mut [i32]) {
assert_eq!(
row.len(),
2 * self.n,
"FftQ32Plan::run_row expects a {}-element row",
2 * self.n
);
let n = self.n;
for i in 0..n {
let j = ((i as u32).reverse_bits() >> (32 - self.bits)) as usize;
if j > i {
row.swap(i, j);
row.swap(n + i, n + j);
}
}
let mut len = 2;
let mut stage = 0u32;
while len <= n {
let step = n / len;
for base in (0..n).step_by(len) {
for k in 0..len / 2 {
let (wr, wi) = (
i64::from(self.tw_re[k * step]),
i64::from(self.tw_im[k * step]),
);
let (a, b) = (base + k, base + k + len / 2);
let (br, bi) = (i64::from(row[b]), i64::from(row[n + b]));
let tr = rshift_round(wr * br - wi * bi, FFT_Q_TWIDDLE_FRAC);
let ti = rshift_round(wr * bi + wi * br, FFT_Q_TWIDDLE_FRAC);
let (ar, ai) = (row[a], row[n + a]);
if self.scale == FftQScale::Saturating {
row[b] = ar.saturating_sub(tr);
row[n + b] = ai.saturating_sub(ti);
row[a] = ar.saturating_add(tr);
row[n + a] = ai.saturating_add(ti);
} else {
row[b] = ar.wrapping_sub(tr);
row[n + b] = ai.wrapping_sub(ti);
row[a] = ar.wrapping_add(tr);
row[n + a] = ai.wrapping_add(ti);
}
}
}
let halve = match self.scale {
FftQScale::PerStage => true,
FftQScale::EveryOther => stage.is_multiple_of(2),
FftQScale::None | FftQScale::Saturating => false,
};
if halve {
for v in &mut row[..2 * n] {
*v = rshift_round(i64::from(*v), 1);
}
}
stage += 1;
len <<= 1;
}
match self.norm {
FftNorm::Backward => {}
FftNorm::Forward => {
if !self.inverse {
for v in &mut row[..2 * n] {
*v = rshift_round(i64::from(*v), self.bits);
}
} else {
}
}
FftNorm::Ortho => {
let s = ((1i64 << FFT_Q_TWIDDLE_FRAC) as f64 / (n as f64).sqrt()).round() as i64;
for v in &mut row[..2 * n] {
*v = rshift_round(i64::from(*v) * s, FFT_Q_TWIDDLE_FRAC);
}
}
}
}
}
#[cfg(test)]
mod q_tests {
use super::*;
fn dft(re: &[f64], im: &[f64], inverse: bool) -> (Vec<f64>, Vec<f64>) {
let n = re.len();
let sign = if inverse { 1.0 } else { -1.0 };
let mut or = vec![0.0; n];
let mut oi = vec![0.0; n];
for k in 0..n {
for t in 0..n {
let a = sign * 2.0 * core::f64::consts::PI * (k * t) as f64 / n as f64;
or[k] += re[t] * a.cos() - im[t] * a.sin();
oi[k] += re[t] * a.sin() + im[t] * a.cos();
}
}
(or, oi)
}
fn block(re: &[i32], im: &[i32]) -> Vec<i32> {
let mut v = re.to_vec();
v.extend_from_slice(im);
v
}
#[test]
fn matches_a_direct_dft() {
for n in [8usize, 64, 256, 1024] {
let re: Vec<i32> = (0..n)
.map(|t| ((t as f64 * 0.37).sin() * 20000.0) as i32)
.collect();
let im = vec![0i32; n];
let mut data = block(&re, &im);
fft1d_q32_block(&mut data, 1, n, false, FftNorm::Backward, FftQScale::None).unwrap();
let (wr, wi) = dft(
&re.iter().map(|&v| f64::from(v)).collect::<Vec<_>>(),
&vec![0.0; n],
false,
);
let peak = wr
.iter()
.zip(&wi)
.fold(0.0f64, |m, (a, b)| m.max((a * a + b * b).sqrt()));
let mut worst = 0.0f64;
for k in 0..n {
let dr = f64::from(data[k]) - wr[k];
let di = f64::from(data[n + k]) - wi[k];
worst = worst.max((dr * dr + di * di).sqrt() / peak);
}
assert!(worst < 3e-5, "n={n}: worst bin {worst:.2e} of peak");
}
}
#[test]
fn round_trips_to_n_times_the_input() {
let n = 256usize;
let re: Vec<i32> = (0..n)
.map(|t| (((t * 37 % 211) as i32) - 105) * 150)
.collect();
let mut data = block(&re, &vec![0; n]);
fft1d_q32_block(&mut data, 1, n, false, FftNorm::Backward, FftQScale::None).unwrap();
fft1d_q32_block(&mut data, 1, n, true, FftNorm::Backward, FftQScale::None).unwrap();
let peak = re.iter().map(|v| v.abs()).max().unwrap() as f64 * n as f64;
for t in 0..n {
let want = f64::from(re[t]) * n as f64;
let rel = (f64::from(data[t]) - want).abs() / peak;
assert!(rel < 1e-3, "sample {t}: {} vs {want} ({rel:.2e})", data[t]);
}
}
#[test]
fn per_stage_scaling_costs_a_bit_per_stage() {
let n = 1024usize;
let re: Vec<i32> = (0..n)
.map(|t| ((t as f64 * 0.11).cos() * 20000.0) as i32)
.collect();
let (wr, wi) = dft(
&re.iter().map(|&v| f64::from(v)).collect::<Vec<_>>(),
&vec![0.0; n],
false,
);
let peak = wr
.iter()
.zip(&wi)
.fold(0.0f64, |m, (a, b)| m.max((a * a + b * b).sqrt()));
let err = |scale: FftQScale, gain: f64| {
let mut d = block(&re, &vec![0; n]);
fft1d_q32_block(&mut d, 1, n, false, FftNorm::Backward, scale).unwrap();
let mut worst = 0.0f64;
for k in 0..n {
let dr = f64::from(d[k]) * gain - wr[k];
let di = f64::from(d[n + k]) * gain - wi[k];
worst = worst.max((dr * dr + di * di).sqrt() / peak);
}
worst
};
let none = err(FftQScale::None, 1.0);
let staged = err(FftQScale::PerStage, n as f64);
assert!(none < 1e-4, "unscaled error {none:.2e}");
assert!(
staged > none * 50.0,
"per-stage {staged:.2e} should be far worse than unscaled {none:.2e}"
);
}
#[test]
fn rejects_lengths_it_cannot_do_well() {
let mut d = vec![0i32; 2 * 12];
let e = fft1d_q32_block(&mut d, 1, 12, false, FftNorm::Backward, FftQScale::None);
assert!(e.unwrap_err().contains("power of two"));
}
}
#[cfg(test)]
mod q_scale_tests {
use super::*;
fn tone(n: usize, amp: f64) -> Vec<i32> {
(0..n)
.map(|t| ((t as f64 * 0.19).sin() * amp) as i32)
.collect()
}
fn run(re: &[i32], n: usize, scale: FftQScale) -> Vec<i32> {
let mut d = re.to_vec();
d.extend(core::iter::repeat_n(0, n));
fft1d_q32_block(&mut d, 1, n, false, FftNorm::Backward, scale).unwrap();
d
}
#[test]
fn scaling_policies_trade_precision_for_headroom() {
let n = 1024usize;
let re = tone(n, 20000.0);
let mut base = re.clone();
base.extend(core::iter::repeat_n(0, n));
let exact = run(&re, n, FftQScale::None);
let peak = (0..n)
.map(|k| {
let (a, b) = (f64::from(exact[k]), f64::from(exact[n + k]));
(a * a + b * b).sqrt()
})
.fold(0.0f64, f64::max);
let err_of = |scale: FftQScale| {
let got = run(&re, n, scale);
let gain = f64::from(1u32 << scale.bits_lost(n));
(0..n)
.map(|k| {
let dr = f64::from(got[k]) * gain - f64::from(exact[k]);
let di = f64::from(got[n + k]) * gain - f64::from(exact[n + k]);
(dr * dr + di * di).sqrt() / peak
})
.fold(0.0f64, f64::max)
};
let sat = err_of(FftQScale::Saturating);
let every = err_of(FftQScale::EveryOther);
let per = err_of(FftQScale::PerStage);
assert!(
sat < 1e-9,
"saturating should match unscaled exactly: {sat:.2e}"
);
assert!(
every < per,
"every-other {every:.2e} should beat per-stage {per:.2e}"
);
assert_eq!(FftQScale::None.bits_lost(n), 0);
assert_eq!(FftQScale::EveryOther.bits_lost(n), 5);
assert_eq!(FftQScale::PerStage.bits_lost(n), 10);
assert_eq!(FftQScale::PerStage.headroom_needed(n), 0);
assert_eq!(FftQScale::None.headroom_needed(n), 10);
}
#[test]
fn saturating_clips_where_none_wraps() {
let n = 64usize;
let re: Vec<i32> = (0..n).map(|_| i32::MAX / 4).collect();
let wrapped = run(&re, n, FftQScale::None);
let clipped = run(&re, n, FftQScale::Saturating);
assert!(
wrapped[0] < 0,
"expected the unscaled path to wrap: {}",
wrapped[0]
);
assert_eq!(clipped[0], i32::MAX, "saturating should clip at the top");
}
#[test]
fn per_stage_needs_no_headroom() {
let n = 64usize;
let re: Vec<i32> = (0..n).map(|_| i32::MAX / 4).collect();
let out = run(&re, n, FftQScale::PerStage);
let want = i32::MAX / 4;
let rel = (f64::from(out[0]) - f64::from(want)).abs() / f64::from(want);
assert!(rel < 1e-6, "DC {} vs {want} ({rel:.2e})", out[0]);
}
}