1use ruda_kernel::dsl as kernel_dsl;
8use ruda_kernel::dsl::prelude::*;
9use ruda_kernel::dsl::backtrace::BackTrace;
10use ruda_kernel::library::tensor::TensorHandle;
11use super::{FftMode, rfft_launch_padded, irfft_launch_padded};
12use super::cfft::{CfftBindings, MAX_SHARED_N_FFT, cfft_launch_any_size,
13 cfft_launch_with_scratch, cfft_inverse_product_with_scratch};
14
15mod kernels;
16use kernels::*;
17
18fn invalid(reason: impl Into<String>) -> LaunchError {
19 LaunchError::Unknown { reason: reason.into(), backtrace: BackTrace::capture() }
20}
21
22const MAX_CONVOLUTION: usize = MAX_SHARED_N_FFT * MAX_SHARED_N_FFT;
24
25pub fn exact_convolution_len(n: usize) -> Result<Option<usize>, LaunchError> {
28 if n == 0 { return Err(invalid("FFT length must be positive")); }
29 if n.is_power_of_two() {
30 if n > MAX_CONVOLUTION * 2 { return Err(invalid("radix-2 FFT exceeds the implemented four-step limit")); }
31 return Ok(None);
32 }
33 let m = n.checked_mul(2).and_then(|v| v.checked_sub(1))
34 .and_then(usize::checked_next_power_of_two)
35 .filter(|&v| v <= MAX_CONVOLUTION)
36 .ok_or_else(|| invalid("Bluestein convolution exceeds the implemented four-step limit"))?;
37 Ok(Some(m))
38}
39
40fn alloc<R: Runtime>(client: &ComputeClient<R>, rows: usize, cols: usize)
41 -> Result<TensorHandle<R>, LaunchError>
42{
43 let elements = rows.checked_mul(cols).filter(|&v| v <= u32::MAX as usize)
44 .ok_or_else(|| invalid("FFT workspace exceeds 32-bit indexing"))?;
45 let bytes = elements.checked_mul(4).ok_or_else(|| invalid("FFT workspace byte-size overflow"))?;
46 Ok(TensorHandle::new_contiguous(vec![rows, cols], client.empty(bytes),
47 f32::as_type_native_unchecked().storage_type()))
48}
49
50struct Tables<R: Runtime> {
51 m: usize,
52 chirp_re: TensorHandle<R>,
53 chirp_im: TensorHandle<R>,
54 spectrum_re: TensorHandle<R>,
55 spectrum_im: TensorHandle<R>,
56}
57struct Workspace<R: Runtime> {
58 rows: usize,
59 re: TensorHandle<R>,
60 im: TensorHandle<R>,
61 four_step: Option<(TensorHandle<R>, TensorHandle<R>)>,
62}
63
64pub struct RealFftPlan<R: Runtime> {
69 client: ComputeClient<R>,
70 n: usize,
71 mode: FftMode,
72 tables: Option<Tables<R>>,
73 workspace: Option<Workspace<R>>,
74 fuse_spectrum: bool,
75}
76impl<R: Runtime> RealFftPlan<R> {
77 pub fn new(client: ComputeClient<R>, n: usize, mode: FftMode) -> Result<Self, LaunchError> {
78 let m = exact_convolution_len(n)?;
79 let client = client.fixed_execution_queue();
82 let tables = if let Some(m) = m {
83 let chirp_re = alloc(&client, 1, n)?;
84 let chirp_im = alloc(&client, 1, n)?;
85 let spectrum_re = alloc(&client, 1, m)?;
86 let spectrum_im = alloc(&client, 1, m)?;
87 let block = RudaDim::new_1d(256);
88 let grid = ruda_kernel::dsl::calculate_ruda_count_elemwise(&client, m, block);
89 make_chirp::launch::<R>(&client, grid, block,
90 chirp_re.clone().into_arg(), chirp_im.clone().into_arg(),
91 spectrum_re.clone().into_arg(), spectrum_im.clone().into_arg(), n, m, mode);
92 cfft_launch_any_size(&client, CfftBindings {
93 input_re: spectrum_re.clone().binding(), input_im: spectrum_im.clone().binding(),
94 output_re: spectrum_re.clone().binding(), output_im: spectrum_im.clone().binding(),
95 }, 1, f32::as_type_native_unchecked().storage_type(), FftMode::Forward)?;
96 Some(Tables { m, chirp_re, chirp_im, spectrum_re, spectrum_im })
97 } else { None };
98 Ok(Self { client, n, mode, tables, workspace: None, fuse_spectrum: false })
99 }
100
101 pub fn set_spectrum_fusion(&mut self, enabled: bool) { self.fuse_spectrum = enabled; }
106
107 pub fn spectrum_fusion_enabled(&self) -> bool {
109 self.fuse_spectrum && self.tables.is_some()
110 }
111
112 pub fn len(&self) -> usize { self.n }
113 pub fn is_empty(&self) -> bool { false }
114 pub fn convolution_len(&self) -> Option<usize> { self.tables.as_ref().map(|t| t.m) }
115 pub fn retained_bytes(&self) -> usize {
118 let tables = self.tables.as_ref().map_or(0, |t| 8 * (self.n + t.m));
119 tables + self.workspace.as_ref().map_or(0, |w| {
120 let m = self.tables.as_ref().unwrap().m;
121 w.rows * m * if w.four_step.is_some() { 16 } else { 8 }
122 })
123 }
124 pub fn clear_workspace(&mut self) { self.workspace = None; }
127
128 fn prepare_workspace(&mut self, rows: usize) -> Result<(), LaunchError> {
129 if self.workspace.as_ref().is_some_and(|w| w.rows == rows) { return Ok(()); }
130 let m = self.tables.as_ref().unwrap().m;
131 self.workspace = None;
134 let re = alloc(&self.client, rows, m)?;
135 let im = alloc(&self.client, rows, m)?;
136 let four_step = if m > MAX_SHARED_N_FFT {
137 Some((alloc(&self.client, rows, m)?, alloc(&self.client, rows, m)?))
138 } else { None };
139 self.workspace = Some(Workspace { rows, re, im, four_step });
140 Ok(())
141 }
142
143 fn convolution(&self) -> Result<(), LaunchError> {
144 let t = self.tables.as_ref().unwrap();
145 let w = self.workspace.as_ref().unwrap();
146 let dtype = f32::as_type_native_unchecked().storage_type();
147 let bindings = || CfftBindings {
148 input_re: w.re.clone().binding(), input_im: w.im.clone().binding(),
149 output_re: w.re.clone().binding(), output_im: w.im.clone().binding(),
150 };
151 cfft_launch_with_scratch(&self.client, bindings(), 1, dtype, FftMode::Forward,
152 w.four_step.clone())?;
153 if self.fuse_spectrum {
154 return cfft_inverse_product_with_scratch(&self.client, bindings(), 1, dtype,
155 (t.spectrum_re.clone().binding(), t.spectrum_im.clone().binding()),
156 w.four_step.clone());
157 }
158 let total = w.rows * t.m;
159 let block = RudaDim::new_1d(256);
160 let grid = ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, total, block);
161 multiply_spectrum::launch::<R>(&self.client, grid, block,
162 w.re.clone().into_arg(), w.im.clone().into_arg(),
163 t.spectrum_re.clone().into_arg(), t.spectrum_im.clone().into_arg(),
164 total as u32, t.m);
165 cfft_launch_with_scratch(&self.client, bindings(), 1, dtype, FftMode::Inverse,
166 w.four_step.clone())
167 }
168
169 pub fn forward(&mut self, signal: TensorBinding<R>, real: TensorBinding<R>,
172 imag: TensorBinding<R>, dim: usize, used: usize) -> Result<(), LaunchError>
173 {
174 if self.mode != FftMode::Forward { return Err(invalid("forward called on inverse FFT plan")); }
175 let rows = validate(&signal, &real, &imag, dim, self.n, used, false)?;
176 if rows == 0 { return Ok(()); }
177 let block = RudaDim::new_1d(256);
178 if self.n == 1 {
179 scalar_forward::launch::<R>(&self.client,
180 ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows, block), block,
181 signal.into_tensor_arg(), real.into_tensor_arg(), imag.into_tensor_arg(),
182 rows as u32, used as u32, dim);
183 return Ok(());
184 }
185 if self.tables.is_none() {
186 return rfft_launch_padded(&self.client, signal, real, imag, dim, used,
187 f32::as_type_native_unchecked().storage_type());
188 }
189 self.prepare_workspace(rows)?;
190 let t = self.tables.as_ref().unwrap();
191 let w = self.workspace.as_ref().unwrap();
192 let total = rows * t.m;
193 prepare_forward::launch::<R>(&self.client,
194 ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, total, block), block,
195 signal.into_tensor_arg(), t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
196 w.re.clone().into_arg(), w.im.clone().into_arg(), total as u32, used as u32,
197 self.n, t.m, dim);
198 self.convolution()?;
199 let bins = self.n / 2 + 1;
200 finish_forward::launch::<R>(&self.client,
201 ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows * bins, block), block,
202 w.re.clone().into_arg(), w.im.clone().into_arg(),
203 t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
204 real.into_tensor_arg(), imag.into_tensor_arg(), (rows * bins) as u32,
205 self.n, t.m, dim);
206 Ok(())
207 }
208
209 pub fn inverse(&mut self, real: TensorBinding<R>, imag: TensorBinding<R>,
212 signal: TensorBinding<R>, dim: usize, used: usize) -> Result<(), LaunchError>
213 {
214 if self.mode != FftMode::Inverse { return Err(invalid("inverse called on forward FFT plan")); }
215 let rows = validate(&signal, &real, &imag, dim, self.n, used, true)?;
216 if rows == 0 { return Ok(()); }
217 let block = RudaDim::new_1d(256);
218 if self.n == 1 {
219 scalar_inverse::launch::<R>(&self.client,
220 ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows, block), block,
221 real.into_tensor_arg(), signal.into_tensor_arg(), rows as u32, used as u32, dim);
222 return Ok(());
223 }
224 if self.tables.is_none() {
225 return irfft_launch_padded(&self.client, real, imag, signal, dim, used,
226 f32::as_type_native_unchecked().storage_type());
227 }
228 self.prepare_workspace(rows)?;
229 let t = self.tables.as_ref().unwrap();
230 let w = self.workspace.as_ref().unwrap();
231 let total = rows * t.m;
232 prepare_inverse::launch::<R>(&self.client,
233 ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, total, block), block,
234 real.into_tensor_arg(), imag.into_tensor_arg(),
235 t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
236 w.re.clone().into_arg(), w.im.clone().into_arg(), total as u32, used as u32,
237 self.n, t.m, dim);
238 self.convolution()?;
239 finish_inverse::launch::<R>(&self.client,
240 ruda_kernel::dsl::calculate_ruda_count_elemwise(&self.client, rows * self.n, block), block,
241 w.re.clone().into_arg(), w.im.clone().into_arg(),
242 t.chirp_re.clone().into_arg(), t.chirp_im.clone().into_arg(),
243 signal.into_tensor_arg(), (rows * self.n) as u32, self.n, t.m, dim);
244 Ok(())
245 }
246}
247
248fn check_binding<R: Runtime>(binding: &TensorBinding<R>) -> Result<(), LaunchError> {
249 if binding.shape.len() != binding.strides.len() || binding.shape.is_empty() {
250 return Err(invalid("FFT needs non-scalar, well-formed tensor metadata"));
251 }
252 if binding.shape.contains(&0) { return Ok(()); }
253 let last = binding.shape.iter().zip(binding.strides.iter()).try_fold(0usize,
254 |sum, (&size, &stride)| (size - 1).checked_mul(stride).and_then(|v| sum.checked_add(v)))
255 .filter(|&v| v < u32::MAX as usize)
256 .and_then(|v| v.checked_add(1)).and_then(|v| v.checked_mul(4))
257 .ok_or_else(|| invalid("FFT tensor span overflows F32/U32 addressing"))?;
258 let available = binding.handle.size.checked_sub(binding.handle.offset_start.unwrap_or(0))
259 .and_then(|v| v.checked_sub(binding.handle.offset_end.unwrap_or(0)))
260 .ok_or_else(|| invalid("invalid FFT buffer offsets"))?;
261 if last as u64 > available { return Err(invalid("FFT tensor strides exceed buffer size")); }
262 Ok(())
263}
264
265fn validate<R: Runtime>(signal: &TensorBinding<R>, real: &TensorBinding<R>,
266 imag: &TensorBinding<R>, dim: usize, n: usize, used: usize, inverse: bool)
267 -> Result<usize, LaunchError>
268{
269 for binding in [signal, real, imag] { check_binding(binding)?; }
270 if dim >= signal.shape.len() || signal.shape.len() != real.shape.len()
271 || real.shape != imag.shape {
272 return Err(invalid("invalid FFT dimension, rank or real/imag shapes"));
273 }
274 let bins = n / 2 + 1;
275 if (!inverse && real.shape[dim] != bins) || (inverse && signal.shape[dim] != n)
276 || (!inverse && (used > signal.shape[dim] || used > n))
277 || (inverse && (used == 0 || used > real.shape[dim] || used > bins)) {
278 return Err(invalid("FFT input/output length does not match plan"));
279 }
280 let mut rows = 1usize;
281 for (axis, (&a, &b)) in signal.shape.iter().zip(real.shape.iter()).enumerate() {
282 if axis != dim {
283 if a != b { return Err(invalid("FFT batch shapes must match")); }
284 rows = rows.checked_mul(a).ok_or_else(|| invalid("FFT batch size overflow"))?;
285 }
286 }
287 rows.checked_mul(n.max(bins)).filter(|&v| v <= u32::MAX as usize)
288 .ok_or_else(|| invalid("FFT output exceeds 32-bit indexing"))?;
289 for out in if inverse { vec![signal] } else { vec![real, imag] } {
292 let mut axes: Vec<_> = out.shape.iter().zip(out.strides.iter())
293 .filter(|(size, _)| **size > 1).map(|(&size, &stride)| (stride, size)).collect();
294 axes.sort_unstable();
295 let mut span = 1usize;
296 for (stride, size) in axes {
297 if stride < span { return Err(invalid("FFT output layout has overlapping elements")); }
298 span = (size - 1).checked_mul(stride).and_then(|v| v.checked_add(span))
299 .ok_or_else(|| invalid("FFT output layout overflow"))?;
300 }
301 }
302 Ok(rows)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 #[test]
309 fn exact_plan_length_limits() {
310 for n in [1, 2, 4, 1024, 8192] { assert_eq!(exact_convolution_len(n).unwrap(), None); }
311 for (n, m) in [(3, 8), (6, 16), (7, 16), (1009, 2048), (4097, 16384)] {
312 assert_eq!(exact_convolution_len(n).unwrap(), Some(m));
313 }
314 assert!(exact_convolution_len(0).is_err());
315 assert!(exact_convolution_len(usize::MAX).is_err());
316 assert!(exact_convolution_len(MAX_CONVOLUTION).is_ok());
317 assert!(exact_convolution_len(MAX_CONVOLUTION - 1).is_err());
318 }
319}