Skip to main content

tenferro_fft/
backend.rs

1use std::fmt;
2
3use tenferro_runtime::ExtensionCacheStore;
4use tenferro_tensor::{BackendSession, Tensor, TensorRead};
5
6use crate::{FftPlanCache, FftPlanSpec};
7
8#[derive(Clone, Copy, Debug)]
9enum FftCacheOwner {
10    CallerOwned,
11    RuntimeOwned,
12}
13
14/// Execution-cache state supplied to an [`FftBackend`].
15///
16/// Direct repeated calls use a caller-owned [`FftPlanCache`], while traced
17/// execution uses the owning runtime's [`ExtensionCacheStore`]. Both ownership
18/// paths expose the same bounded typed store to a backend, so CPU, Metal, CUDA,
19/// and future implementations can retain private plans or workspaces in their
20/// own cache namespace. Constructors keep the owner representation closed.
21///
22/// # Examples
23///
24/// ```
25/// use tenferro_fft::{FftExecutionCache, FftPlanCache};
26///
27/// let mut plans = FftPlanCache::default();
28/// let cache = FftExecutionCache::caller_owned(&mut plans);
29/// assert!(format!("{cache:?}").contains("CallerOwned"));
30/// ```
31pub struct FftExecutionCache<'a> {
32    owner: FftCacheOwner,
33    store: &'a mut ExtensionCacheStore,
34}
35
36impl fmt::Debug for FftExecutionCache<'_> {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.debug_struct("FftExecutionCache")
39            .field("owner", &self.owner)
40            .field(
41                "stats",
42                &self
43                    .store
44                    .stats(tenferro_runtime::ExtensionCacheSelector::All),
45            )
46            .finish_non_exhaustive()
47    }
48}
49
50impl<'a> FftExecutionCache<'a> {
51    /// Build a context backed by a caller-owned typed FFT execution cache.
52    pub fn caller_owned(cache: &'a mut FftPlanCache) -> Self {
53        Self {
54            owner: FftCacheOwner::CallerOwned,
55            store: cache.store_mut(),
56        }
57    }
58
59    /// Build a context backed by an extension runtime cache store.
60    pub fn runtime_owned(cache: &'a mut ExtensionCacheStore) -> Self {
61        Self {
62            owner: FftCacheOwner::RuntimeOwned,
63            store: cache,
64        }
65    }
66
67    /// Borrow the bounded typed store owned by the caller or extension runtime.
68    ///
69    /// Backend implementations should use a stable family/cache namespace and
70    /// include every plan-identity field in the key discriminator. The store
71    /// owns LRU bounds, typed retrieval, clear behavior, entry counts, and the
72    /// retained-byte estimates supplied at insertion.
73    pub fn store_mut(&mut self) -> &mut ExtensionCacheStore {
74        self.store
75    }
76}
77
78/// Explicit backend capability required by concrete and traced FFT execution.
79///
80/// Implementations must execute on the input's existing placement. Unsupported
81/// dtypes, layouts, placements, or operations return an error; they must never
82/// transfer the tensor or select a different backend.
83///
84/// # Examples
85/// Backend surface required by the FFT extension runtime and the built-in
86/// concrete FFT dispatch.
87///
88/// This is a backend SPI: third-party backend implementers implement this
89/// trait on their execution sessions to provide FFT capability. The concrete
90/// [`TensorFftExt`]/[`TensorReadFftExt`] methods and [`FftExecutor`] take an
91/// erased `&mut dyn BackendSession` and dispatch **internally** to the
92/// built-in CPU/CUDA/WebGPU execution sessions; a session without this
93/// capability returns a typed `Error::Unsupported` instead of being accepted
94/// as an arbitrary third-party backend (issue #1680 Phase 3).
95///
96/// # Examples
97///
98/// ```
99/// use tenferro_cpu::CpuExecSession;
100/// use tenferro_fft::FftBackend;
101///
102/// fn accepts_fft_backend<B: FftBackend>() {}
103///
104/// accepts_fft_backend::<CpuExecSession<'static>>();
105/// ```
106///
107/// A generic tensor backend does not expose the FFT capability at the SPI
108/// level:
109///
110/// ```compile_fail
111/// use tenferro_tensor::{Tensor, TensorBackend};
112/// use tenferro_fft::FftBackend;
113///
114/// fn requires_fft_backend<B: TensorBackend>(backend: &mut B) {
115///     let _: &mut dyn FftBackend = backend;
116/// }
117/// ```
118pub trait FftBackend: BackendSession {
119    /// Validate a borrowed runtime input before the extension read path
120    /// materializes it into a compact tensor.
121    ///
122    /// Backends that can materialize their own placement may keep the default
123    /// no-op. CPU overrides this to report device inputs as unsupported FFT
124    /// placement errors instead of leaking a generic host-materialization error.
125    ///
126    /// # Errors
127    ///
128    /// Returns [`tenferro_tensor::Error::Unsupported`] when this backend cannot
129    /// consume the borrowed input placement without an explicit transfer.
130    fn validate_fft_read_input(
131        &self,
132        _op: &'static str,
133        _input: &TensorRead<'_>,
134    ) -> tenferro_tensor::Result<()> {
135        Ok(())
136    }
137
138    /// Execute a validated FFT from borrowed storage.
139    ///
140    /// The default explicitly canonicalizes view inputs. Backends supporting
141    /// compact borrowed storage override this method to avoid input copies.
142    ///
143    /// # Examples
144    /// The borrowed public surface dispatches through this backend hook:
145    /// ```
146    /// use num_complex::Complex64;
147    /// use tenferro_cpu::CpuBackend;
148    /// use tenferro_fft::{FftNorm, TensorReadFftExt};
149    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
150    /// let input = Tensor::from_vec_col_major([2], vec![Complex64::new(1., 0.); 2])?;
151    /// let mut backend = CpuBackend::with_threads(1)?;
152    /// let output = backend.with_backend_session(|session| {
153    ///     TensorRead::Tensor(&input).fft_read(None, 0, FftNorm::Backward, session)
154    /// })?;
155    /// assert_eq!(output.as_slice::<Complex64>()?, &[Complex64::new(2., 0.), Complex64::new(0., 0.)]);
156    /// # Ok::<(), Box<dyn std::error::Error>>(())
157    /// ```
158    ///
159    /// # Errors
160    /// Returns [`tenferro_tensor::Error::Unsupported`] for unsupported dtype,
161    /// layout, or placement, and [`tenferro_tensor::Error::Validation`] when
162    /// input metadata disagrees with the validated spec. Same-placement
163    /// canonicalization, plan creation, and execution failures preserve the
164    /// selected backend's typed error source; no implicit transfer is performed.
165    fn execute_fft_read(
166        &mut self,
167        input: TensorRead<'_>,
168        spec: &FftPlanSpec,
169        cache: FftExecutionCache<'_>,
170    ) -> tenferro_tensor::Result<Tensor> {
171        match input {
172            TensorRead::Tensor(input) => self.execute_fft(input, spec, cache),
173            input => {
174                let owned = self.to_contiguous_read(input)?;
175                self.execute_fft(&owned, spec, cache)
176            }
177        }
178    }
179
180    /// Execute one validated FFT request on `input`'s existing placement.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`tenferro_tensor::Error::Unsupported`] for an unsupported
185    /// operation, dtype, layout, or placement;
186    /// [`tenferro_tensor::Error::Validation`] for inconsistent input/spec
187    /// metadata or checked shape arithmetic; and a typed backend or runtime
188    /// source when plan creation, cache access, or execution fails.
189    fn execute_fft(
190        &mut self,
191        input: &Tensor,
192        spec: &FftPlanSpec,
193        cache: FftExecutionCache<'_>,
194    ) -> tenferro_tensor::Result<Tensor>;
195}
196
197#[cfg(test)]
198mod tests;