Skip to main content

tenferro_tensor/
capability.rs

1//! Backend operation capability descriptors.
2//!
3//! # Examples
4//!
5//! ```rust
6//! use tenferro_core_ops::PrimitiveOpKind;
7//! use tenferro_tensor::{capability_output_dtype, DType};
8//!
9//! assert_eq!(
10//!     capability_output_dtype(PrimitiveOpKind::Compare, DType::F64),
11//!     Some(DType::Bool)
12//! );
13//! ```
14
15use std::fmt;
16
17use tenferro_core_ops::{descriptor, DTypePolicy, PrimitiveOpKind};
18
19use crate::DType;
20
21/// Stable backend identifier used by capability descriptors.
22///
23/// # Examples
24///
25/// ```rust
26/// use tenferro_tensor::BackendId;
27///
28/// assert_eq!(BackendId::Cuda.as_str(), "cuda");
29/// ```
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
31pub enum BackendId {
32    Cpu,
33    Cuda,
34    WebGpu,
35    Other(&'static str),
36}
37
38impl BackendId {
39    /// Return the stable backend name used in diagnostics and generated docs.
40    ///
41    /// # Examples
42    ///
43    /// ```rust
44    /// use tenferro_tensor::BackendId;
45    ///
46    /// assert_eq!(BackendId::Cpu.as_str(), "cpu");
47    /// ```
48    #[must_use]
49    pub const fn as_str(self) -> &'static str {
50        match self {
51            Self::Cpu => "cpu",
52            Self::Cuda => "cuda",
53            Self::WebGpu => "webgpu",
54            Self::Other(name) => name,
55        }
56    }
57}
58
59impl fmt::Display for BackendId {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str(self.as_str())
62    }
63}
64
65/// Three-valued support level for a backend capability axis.
66///
67/// `FallbackCopy` is intentionally distinct from `Native`: the operation is
68/// accepted, but only by materializing through a default/copy path.
69///
70/// # Examples
71///
72/// ```rust
73/// use tenferro_tensor::SupportLevel;
74///
75/// assert!(SupportLevel::Native > SupportLevel::FallbackCopy);
76/// assert!(SupportLevel::FallbackCopy.is_supported());
77/// assert!(!SupportLevel::Unsupported.is_supported());
78/// ```
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
80pub enum SupportLevel {
81    Unsupported,
82    FallbackCopy,
83    Native,
84}
85
86impl SupportLevel {
87    /// Return whether this level represents any usable implementation.
88    ///
89    /// # Examples
90    ///
91    /// ```rust
92    /// use tenferro_tensor::SupportLevel;
93    ///
94    /// assert!(SupportLevel::Native.is_supported());
95    /// assert!(!SupportLevel::Unsupported.is_supported());
96    /// ```
97    #[must_use]
98    pub const fn is_supported(self) -> bool {
99        !matches!(self, Self::Unsupported)
100    }
101}
102
103/// Axis within an operation capability entry.
104///
105/// # Examples
106///
107/// ```rust
108/// use tenferro_tensor::CapabilityAxis;
109///
110/// let axis = CapabilityAxis::ReadInputs;
111/// assert_eq!(format!("{axis:?}"), "ReadInputs");
112/// ```
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
114pub enum CapabilityAxis {
115    OwnedResult,
116    ReadInputs,
117    WriteOutput,
118    StridedOutput,
119    Accumulation,
120}
121
122/// Query key for a backend capability lookup.
123///
124/// # Examples
125///
126/// ```rust
127/// use tenferro_core_ops::PrimitiveOpKind;
128/// use tenferro_tensor::{CapabilityQuery, DType};
129///
130/// let query = CapabilityQuery::new(PrimitiveOpKind::Add, DType::F32);
131/// assert_eq!(query.dtype, DType::F32);
132/// ```
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
134pub struct CapabilityQuery {
135    pub op: PrimitiveOpKind,
136    pub dtype: DType,
137}
138
139impl CapabilityQuery {
140    /// Build a capability lookup key.
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use tenferro_core_ops::PrimitiveOpKind;
146    /// use tenferro_tensor::{CapabilityQuery, DType};
147    ///
148    /// assert_eq!(
149    ///     CapabilityQuery::new(PrimitiveOpKind::Mul, DType::I64).op,
150    ///     PrimitiveOpKind::Mul
151    /// );
152    /// ```
153    #[must_use]
154    pub const fn new(op: PrimitiveOpKind, dtype: DType) -> Self {
155        Self { op, dtype }
156    }
157}
158
159/// One backend capability entry for a primitive op and input dtype.
160///
161/// # Examples
162///
163/// ```rust
164/// use tenferro_core_ops::PrimitiveOpKind;
165/// use tenferro_tensor::{
166///     BackendId, CapabilityAxis, DType, OperationCapability, SupportLevel,
167/// };
168///
169/// let entry = OperationCapability {
170///     backend: BackendId::Cpu,
171///     op: PrimitiveOpKind::Add,
172///     dtype: DType::F64,
173///     output_dtype: DType::F64,
174///     result: SupportLevel::Native,
175///     read_inputs: SupportLevel::Native,
176///     write_output: SupportLevel::Native,
177///     strided_output: SupportLevel::Native,
178///     accumulation: SupportLevel::Unsupported,
179/// };
180/// assert_eq!(entry.axis(CapabilityAxis::OwnedResult), SupportLevel::Native);
181/// ```
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
183pub struct OperationCapability {
184    pub backend: BackendId,
185    pub op: PrimitiveOpKind,
186    pub dtype: DType,
187    pub output_dtype: DType,
188    pub result: SupportLevel,
189    pub read_inputs: SupportLevel,
190    pub write_output: SupportLevel,
191    pub strided_output: SupportLevel,
192    pub accumulation: SupportLevel,
193}
194
195impl OperationCapability {
196    /// Return the support level for one capability axis.
197    ///
198    /// # Examples
199    ///
200    /// ```rust
201    /// use tenferro_core_ops::PrimitiveOpKind;
202    /// use tenferro_tensor::{
203    ///     BackendId, CapabilityAxis, DType, OperationCapability, SupportLevel,
204    /// };
205    ///
206    /// let entry = OperationCapability {
207    ///     backend: BackendId::Cuda,
208    ///     op: PrimitiveOpKind::ReduceSum,
209    ///     dtype: DType::I32,
210    ///     output_dtype: DType::I32,
211    ///     result: SupportLevel::Native,
212    ///     read_inputs: SupportLevel::FallbackCopy,
213    ///     write_output: SupportLevel::Unsupported,
214    ///     strided_output: SupportLevel::Unsupported,
215    ///     accumulation: SupportLevel::Unsupported,
216    /// };
217    /// assert_eq!(entry.axis(CapabilityAxis::ReadInputs), SupportLevel::FallbackCopy);
218    /// ```
219    #[must_use]
220    pub const fn axis(&self, axis: CapabilityAxis) -> SupportLevel {
221        match axis {
222            CapabilityAxis::OwnedResult => self.result,
223            CapabilityAxis::ReadInputs => self.read_inputs,
224            CapabilityAxis::WriteOutput => self.write_output,
225            CapabilityAxis::StridedOutput => self.strided_output,
226            CapabilityAxis::Accumulation => self.accumulation,
227        }
228    }
229}
230
231/// Backend capability query surface.
232///
233/// # Examples
234///
235/// ```rust
236/// use tenferro_core_ops::PrimitiveOpKind;
237/// use tenferro_tensor::{
238///     BackendId, CapabilityQuery, DType, OperationCapability, SupportLevel,
239///     TensorBackendCapability,
240/// };
241///
242/// struct Backend;
243///
244/// const ENTRIES: &[OperationCapability] = &[OperationCapability {
245///     backend: BackendId::Cpu,
246///     op: PrimitiveOpKind::Add,
247///     dtype: DType::F32,
248///     output_dtype: DType::F32,
249///     result: SupportLevel::Native,
250///     read_inputs: SupportLevel::Native,
251///     write_output: SupportLevel::Native,
252///     strided_output: SupportLevel::Native,
253///     accumulation: SupportLevel::Unsupported,
254/// }];
255///
256/// impl TensorBackendCapability for Backend {
257///     fn backend_id(&self) -> BackendId { BackendId::Cpu }
258///     fn capabilities(&self) -> &'static [OperationCapability] { ENTRIES }
259/// }
260///
261/// assert!(Backend
262///     .capability(CapabilityQuery::new(PrimitiveOpKind::Add, DType::F32))
263///     .is_some());
264/// ```
265pub trait TensorBackendCapability {
266    fn backend_id(&self) -> BackendId;
267    fn capabilities(&self) -> &'static [OperationCapability];
268
269    /// Look up one operation/dtype capability for this backend.
270    ///
271    /// # Examples
272    ///
273    /// ```rust
274    /// use tenferro_core_ops::PrimitiveOpKind;
275    /// use tenferro_tensor::{
276    ///     BackendId, CapabilityQuery, DType, OperationCapability, SupportLevel,
277    ///     TensorBackendCapability,
278    /// };
279    ///
280    /// struct Backend;
281    /// const ENTRIES: &[OperationCapability] = &[OperationCapability {
282    ///     backend: BackendId::Cpu,
283    ///     op: PrimitiveOpKind::Mul,
284    ///     dtype: DType::I64,
285    ///     output_dtype: DType::I64,
286    ///     result: SupportLevel::Native,
287    ///     read_inputs: SupportLevel::Native,
288    ///     write_output: SupportLevel::Unsupported,
289    ///     strided_output: SupportLevel::Unsupported,
290    ///     accumulation: SupportLevel::Unsupported,
291    /// }];
292    /// impl TensorBackendCapability for Backend {
293    ///     fn backend_id(&self) -> BackendId { BackendId::Cpu }
294    ///     fn capabilities(&self) -> &'static [OperationCapability] { ENTRIES }
295    /// }
296    ///
297    /// let entry = Backend
298    ///     .capability(CapabilityQuery::new(PrimitiveOpKind::Mul, DType::I64))
299    ///     .unwrap();
300    /// assert_eq!(entry.result, SupportLevel::Native);
301    /// ```
302    #[must_use]
303    fn capability(&self, query: CapabilityQuery) -> Option<OperationCapability> {
304        self.capabilities().iter().copied().find(|entry| {
305            entry.backend == self.backend_id() && entry.op == query.op && entry.dtype == query.dtype
306        })
307    }
308
309    /// Require support for one operation/dtype/axis, returning a structured
310    /// unsupported error otherwise.
311    ///
312    /// # Examples
313    ///
314    /// ```rust
315    /// use tenferro_core_ops::PrimitiveOpKind;
316    /// use tenferro_tensor::{
317    ///     BackendId, CapabilityAxis, CapabilityQuery, DType, Error, OperationCapability,
318    ///     SupportLevel, TensorBackendCapability,
319    /// };
320    ///
321    /// struct Backend;
322    /// const ENTRIES: &[OperationCapability] = &[OperationCapability {
323    ///     backend: BackendId::Cuda,
324    ///     op: PrimitiveOpKind::Neg,
325    ///     dtype: DType::I32,
326    ///     output_dtype: DType::I32,
327    ///     result: SupportLevel::Unsupported,
328    ///     read_inputs: SupportLevel::Unsupported,
329    ///     write_output: SupportLevel::Unsupported,
330    ///     strided_output: SupportLevel::Unsupported,
331    ///     accumulation: SupportLevel::Unsupported,
332    /// }];
333    /// impl TensorBackendCapability for Backend {
334    ///     fn backend_id(&self) -> BackendId { BackendId::Cuda }
335    ///     fn capabilities(&self) -> &'static [OperationCapability] { ENTRIES }
336    /// }
337    ///
338    /// let err = Backend
339    ///     .require_capability(
340    ///         CapabilityQuery::new(PrimitiveOpKind::Neg, DType::I32),
341    ///         CapabilityAxis::OwnedResult,
342    ///     )
343    ///     .unwrap_err();
344    /// assert!(matches!(err, Error::UnsupportedOpDType { backend: BackendId::Cuda, .. }));
345    /// ```
346    fn require_capability(
347        &self,
348        query: CapabilityQuery,
349        axis: CapabilityAxis,
350    ) -> crate::Result<OperationCapability> {
351        let entry = self.capability(query).ok_or_else(|| {
352            crate::Error::unsupported_op_dtype(
353                descriptor(query.op).name,
354                query.dtype,
355                self.backend_id(),
356            )
357        })?;
358        if entry.axis(axis).is_supported() {
359            Ok(entry)
360        } else {
361            Err(crate::Error::unsupported_op_dtype(
362                descriptor(query.op).name,
363                query.dtype,
364                self.backend_id(),
365            ))
366        }
367    }
368}
369
370/// Return the output dtype allowed by the core op catalog policy for a unary
371/// dtype representative.
372///
373/// `None` means the catalog policy does not admit the queried dtype. Backend
374/// descriptors add implementation support on top of this semantic policy.
375///
376/// # Examples
377///
378/// ```rust
379/// use tenferro_core_ops::PrimitiveOpKind;
380/// use tenferro_tensor::{capability_output_dtype, DType};
381///
382/// assert_eq!(
383///     capability_output_dtype(PrimitiveOpKind::Abs, DType::C64),
384///     Some(DType::F64)
385/// );
386/// assert_eq!(
387///     capability_output_dtype(PrimitiveOpKind::Pow, DType::I32),
388///     Some(DType::I32)
389/// );
390/// ```
391#[must_use]
392pub fn capability_output_dtype(op: PrimitiveOpKind, dtype: DType) -> Option<DType> {
393    let policy = descriptor(op).dtype_policy;
394    match policy {
395        DTypePolicy::SameAny => Some(dtype),
396        DTypePolicy::SameNumeric => numeric_dtype(dtype).then_some(dtype),
397        DTypePolicy::SameFloat => float_dtype(dtype).then_some(dtype),
398        DTypePolicy::AbsToReal => match dtype {
399            DType::F32 => Some(DType::F32),
400            DType::F64 => Some(DType::F64),
401            DType::I32 => Some(DType::I32),
402            DType::I64 => Some(DType::I64),
403            DType::C32 => Some(DType::F32),
404            DType::C64 => Some(DType::F64),
405            DType::Bool => None,
406        },
407        DTypePolicy::SameFloatOrComplex => float_or_complex_dtype(dtype).then_some(dtype),
408        DTypePolicy::CompareToBool => comparable_dtype(dtype).then_some(DType::Bool),
409        DTypePolicy::BoolSelect => Some(dtype),
410        DTypePolicy::Convert | DTypePolicy::Shape | DTypePolicy::Constant => Some(dtype),
411    }
412}
413
414const fn numeric_dtype(dtype: DType) -> bool {
415    matches!(
416        dtype,
417        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::C32 | DType::C64
418    )
419}
420
421const fn float_dtype(dtype: DType) -> bool {
422    matches!(dtype, DType::F32 | DType::F64)
423}
424
425const fn float_or_complex_dtype(dtype: DType) -> bool {
426    matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64)
427}
428
429const fn comparable_dtype(dtype: DType) -> bool {
430    matches!(
431        dtype,
432        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
433    )
434}