use ruda_kernel::dsl as kernel_dsl;
use ruda_kernel::dsl::prelude::*;
use ruda_kernel::dsl::backtrace::BackTrace;
use ruda_kernel::library::tensor::TensorHandle;
use super::{FftMode, rfft_launch_padded, irfft_launch_padded};
use super::cfft::{CfftBindings, MAX_SHARED_N_FFT, cfft_launch_any_size,
cfft_launch_with_scratch};
mod kernels;
use kernels::*;
fn invalid(reason: impl Into<String>) -> LaunchError {
LaunchError::Unknown { reason: reason.into(), backtrace: BackTrace::capture() }
}
const MAX_CONVOLUTION: usize = MAX_SHARED_N_FFT * MAX_SHARED_N_FFT;
pub fn exact_convolution_len(n: usize) -> Result<Option<usize>, LaunchError> {
if n == 0 { return Err(invalid("FFT length must be positive")); }
if n.is_power_of_two() {
if n > MAX_CONVOLUTION * 2 { return Err(invalid("radix-2 FFT exceeds the implemented four-step limit")); }
return Ok(None);
}
let m = n.checked_mul(2).and_then(|v| v.checked_sub(1))
.and_then(usize::checked_next_power_of_two)
.filter(|&v| v <= MAX_CONVOLUTION)
.ok_or_else(|| invalid("Bluestein convolution exceeds the implemented four-step limit"))?;
Ok(Some(m))
}
fn alloc<R: Runtime>(client: &ComputeClient<R>, rows: usize, cols: usize)
-> Result<TensorHandle<R>, LaunchError>
{
let elements = rows.checked_mul(cols).filter(|&v| v <= u32::MAX as usize)
.ok_or_else(|| invalid("FFT workspace exceeds 32-bit indexing"))?;
let bytes = elements.checked_mul(4).ok_or_else(|| invalid("FFT workspace byte-size overflow"))?;
Ok(TensorHandle::new_contiguous(vec![rows, cols], client.empty(bytes),
f32::as_type_native_unchecked().storage_type()))
}
struct Tables<R: Runtime> {
m: usize,
chirp_re: TensorHandle<R>,
chirp_im: TensorHandle<R>,
spectrum_re: TensorHandle<R>,
spectrum_im: TensorHandle<R>,
}
struct Workspace<R: Runtime> {
rows: usize,
re: TensorHandle<R>,
im: TensorHandle<R>,
four_step: Option<(TensorHandle<R>, TensorHandle<R>)>,
}
pub struct RealFftPlan<R: Runtime> {
client: ComputeClient<R>,
n: usize,
mode: FftMode,
tables: Option<Tables<R>>,
workspace: Option<Workspace<R>>,
}
impl<R: Runtime> RealFftPlan<R> {
pub fn new(client: ComputeClient<R>, n: usize, mode: FftMode) -> Result<Self, LaunchError> {
let m = exact_convolution_len(n)?;
let client = client.fixed_execution_queue();
let tables = if let Some(m) = m {
let chirp_re = alloc(&client, 1, n)?;
let chirp_im = alloc(&client, 1, n)?;
let spectrum_re = alloc(&client, 1, m)?;
let spectrum_im = alloc(&client, 1, m)?;
let block = RudaDim::new_1d(256);
let grid = ruda_kernel::dsl::calculate_ruda_count_elemwise(&client, m, block);
make_chirp::launch::<R>(&client, grid, block,
chirp_re.clone().into_arg(), chirp_im.clone().into_arg(),
spectrum_re.clone().into_arg(), spectrum_im.clone().into_arg(), n, m, mode);
cfft_launch_any_size(&client, CfftBindings {
input_re: spectrum_re.clone().binding(), input_im: spectrum_im.clone().binding(),
output_re: spectrum_re.clone().binding(), output_im: spectrum_im.clone().binding(),
}, 1, f32::as_type_native_unchecked().storage_type(), FftMode::Forward)?;
Some(Tables { m, chirp_re, chirp_im, spectrum_re, spectrum_im })
} else { None };
Ok(Self { client, n, mode, tables, workspace: None })
}
pub fn len(&self) -> usize { self.n }
pub fn is_empty(&self) -> bool { false }
pub fn convolution_len(&self) -> Option<usize> { self.tables.as_ref().map(|t| t.m) }
pub fn retained_bytes(&self) -> usize {
let tables = self.tables.as_ref().map_or(0, |t| 8 * (self.n + t.m));
tables + self.workspace.as_ref().map_or(0, |w| {
let m = self.tables.as_ref().unwrap().m;
w.rows * m * if w.four_step.is_some() { 16 } else { 8 }
})
}
pub fn clear_workspace(&mut self) { self.workspace = None; }
fn prepare_workspace(&mut self, rows: usize) -> Result<(), LaunchError> {
if self.workspace.as_ref().is_some_and(|w| w.rows == rows) { return Ok(()); }
let m = self.tables.as_ref().unwrap().m;
self.workspace = None;
let re = alloc(&self.client, rows, m)?;
let im = alloc(&self.client, rows, m)?;
let four_step = if m > MAX_SHARED_N_FFT {
Some((alloc(&self.client, rows, m)?, alloc(&self.client, rows, m)?))
} else { None };
self.workspace = Some(Workspace { rows, re, im, four_step });
Ok(())
}
fn convolution(&self) -> Result<(), LaunchError> {
let t = self.tables.as_ref().unwrap();
let w = self.workspace.as_ref().unwrap();
let dtype = f32::as_type_native_unchecked().storage_type();
let bindings = || CfftBindings {
input_re: w.re.clone().binding(), input_im: w.im.clone().binding(),
output_re: w.re.clone().binding(), output_im: w.im.clone().binding(),
};
cfft_launch_with_scratch(&self.client, bindings(), 1, dtype, FftMode::Forward,
w.four_step.clone())?;
let total = w.rows * t.m;
let block = RudaDim::new_1d(256);
let grid = ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, total, block);
multiply_spectrum::launch::<R>(&self.client, grid, block,
w.re.clone().into_arg(), w.im.clone().into_arg(),
t.spectrum_re.clone().into_arg(), t.spectrum_im.clone().into_arg(),
total as u32, t.m);
cfft_launch_with_scratch(&self.client, bindings(), 1, dtype, FftMode::Inverse,
w.four_step.clone())
}
pub fn forward(&mut self, signal: TensorBinding<R>, real: TensorBinding<R>,
imag: TensorBinding<R>, dim: usize, used: usize) -> Result<(), LaunchError>
{
if self.mode != FftMode::Forward { return Err(invalid("forward called on inverse FFT plan")); }
let rows = validate(&signal, &real, &imag, dim, self.n, used, false)?;
if rows == 0 { return Ok(()); }
let block = RudaDim::new_1d(256);
if self.n == 1 {
scalar_forward::launch::<R>(&self.client,
ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows, block), block,
signal.into_tensor_arg(), real.into_tensor_arg(), imag.into_tensor_arg(),
rows as u32, used as u32, dim);
return Ok(());
}
if self.tables.is_none() {
return rfft_launch_padded(&self.client, signal, real, imag, dim, used,
f32::as_type_native_unchecked().storage_type());
}
self.prepare_workspace(rows)?;
let t = self.tables.as_ref().unwrap();
let w = self.workspace.as_ref().unwrap();
let total = rows * t.m;
prepare_forward::launch::<R>(&self.client,
ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, total, block), block,
signal.into_tensor_arg(), t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
w.re.clone().into_arg(), w.im.clone().into_arg(), total as u32, used as u32,
self.n, t.m, dim);
self.convolution()?;
let bins = self.n / 2 + 1;
finish_forward::launch::<R>(&self.client,
ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows * bins, block), block,
w.re.clone().into_arg(), w.im.clone().into_arg(),
t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
real.into_tensor_arg(), imag.into_tensor_arg(), (rows * bins) as u32,
self.n, t.m, dim);
Ok(())
}
pub fn inverse(&mut self, real: TensorBinding<R>, imag: TensorBinding<R>,
signal: TensorBinding<R>, dim: usize, used: usize) -> Result<(), LaunchError>
{
if self.mode != FftMode::Inverse { return Err(invalid("inverse called on forward FFT plan")); }
let rows = validate(&signal, &real, &imag, dim, self.n, used, true)?;
if rows == 0 { return Ok(()); }
let block = RudaDim::new_1d(256);
if self.n == 1 {
scalar_inverse::launch::<R>(&self.client,
ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows, block), block,
real.into_tensor_arg(), signal.into_tensor_arg(), rows as u32, used as u32, dim);
return Ok(());
}
if self.tables.is_none() {
return irfft_launch_padded(&self.client, real, imag, signal, dim, used,
f32::as_type_native_unchecked().storage_type());
}
self.prepare_workspace(rows)?;
let t = self.tables.as_ref().unwrap();
let w = self.workspace.as_ref().unwrap();
let total = rows * t.m;
prepare_inverse::launch::<R>(&self.client,
ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, total, block), block,
real.into_tensor_arg(), imag.into_tensor_arg(),
t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
w.re.clone().into_arg(), w.im.clone().into_arg(), total as u32, used as u32,
self.n, t.m, dim);
self.convolution()?;
finish_inverse::launch::<R>(&self.client,
ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows * self.n, block), block,
w.re.clone().into_arg(), w.im.clone().into_arg(),
t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
signal.into_tensor_arg(), (rows * self.n) as u32, self.n, t.m, dim);
Ok(())
}
}
fn check_binding<R: Runtime>(binding: &TensorBinding<R>) -> Result<(), LaunchError> {
if binding.shape.len() != binding.strides.len() || binding.shape.is_empty() {
return Err(invalid("FFT needs non-scalar, well-formed tensor metadata"));
}
if binding.shape.contains(&0) { return Ok(()); }
let last = binding.shape.iter().zip(binding.strides.iter()).try_fold(0usize,
|sum, (&size, &stride)| (size - 1).checked_mul(stride).and_then(|v| sum.checked_add(v)))
.filter(|&v| v < u32::MAX as usize)
.and_then(|v| v.checked_add(1)).and_then(|v| v.checked_mul(4))
.ok_or_else(|| invalid("FFT tensor span overflows F32/U32 addressing"))?;
let available = binding.handle.size.checked_sub(binding.handle.offset_start.unwrap_or(0))
.and_then(|v| v.checked_sub(binding.handle.offset_end.unwrap_or(0)))
.ok_or_else(|| invalid("invalid FFT buffer offsets"))?;
if last as u64 > available { return Err(invalid("FFT tensor strides exceed buffer size")); }
Ok(())
}
fn validate<R: Runtime>(signal: &TensorBinding<R>, real: &TensorBinding<R>,
imag: &TensorBinding<R>, dim: usize, n: usize, used: usize, inverse: bool)
-> Result<usize, LaunchError>
{
for binding in [signal, real, imag] { check_binding(binding)?; }
if dim >= signal.shape.len() || signal.shape.len() != real.shape.len()
|| real.shape != imag.shape {
return Err(invalid("invalid FFT dimension, rank or real/imag shapes"));
}
let bins = n / 2 + 1;
if (!inverse && real.shape[dim] != bins) || (inverse && signal.shape[dim] != n)
|| (!inverse && (used > signal.shape[dim] || used > n))
|| (inverse && (used == 0 || used > real.shape[dim] || used > bins)) {
return Err(invalid("FFT input/output length does not match plan"));
}
let mut rows = 1usize;
for (axis, (&a, &b)) in signal.shape.iter().zip(real.shape.iter()).enumerate() {
if axis != dim {
if a != b { return Err(invalid("FFT batch shapes must match")); }
rows = rows.checked_mul(a).ok_or_else(|| invalid("FFT batch size overflow"))?;
}
}
rows.checked_mul(n.max(bins)).filter(|&v| v <= u32::MAX as usize)
.ok_or_else(|| invalid("FFT output exceeds 32-bit indexing"))?;
for out in if inverse { vec![signal] } else { vec![real, imag] } {
let mut axes: Vec<_> = out.shape.iter().zip(out.strides.iter())
.filter(|(size, _)| **size > 1).map(|(&size, &stride)| (stride, size)).collect();
axes.sort_unstable();
let mut span = 1usize;
for (stride, size) in axes {
if stride < span { return Err(invalid("FFT output layout has overlapping elements")); }
span = (size - 1).checked_mul(stride).and_then(|v| v.checked_add(span))
.ok_or_else(|| invalid("FFT output layout overflow"))?;
}
}
Ok(rows)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_plan_length_limits() {
for n in [1, 2, 4, 1024, 8192] { assert_eq!(exact_convolution_len(n).unwrap(), None); }
for (n, m) in [(3, 8), (6, 16), (7, 16), (1009, 2048), (4097, 16384)] {
assert_eq!(exact_convolution_len(n).unwrap(), Some(m));
}
assert!(exact_convolution_len(0).is_err());
assert!(exact_convolution_len(usize::MAX).is_err());
assert!(exact_convolution_len(MAX_CONVOLUTION).is_ok());
assert!(exact_convolution_len(MAX_CONVOLUTION - 1).is_err());
}
}