Skip to main content

onnx_runtime_ep_cpu/
dtype.rs

1//! Reusable dtype-generic machinery for the arithmetic CPU kernels
2//! (`docs/ORT2.md` §4.4; project preference "不同的dtype,是不是可以用 template").
3//!
4//! The Phase-1 kernels were originally f32-only, which turned every ONNX
5//! `float16` / `bfloat16` / integer conformance case into a spurious failure —
6//! the numeric op was correct, the kernel simply refused the element type. This
7//! module supplies the *one* dtype-dispatch mechanism the arithmetic kernels
8//! share so multi-dtype support is written once, not copy-pasted per dtype.
9//!
10//! ## The three moving parts
11//!
12//! * [`ComputeDomain`] — the numeric domain arithmetic is *evaluated* in. Low-
13//!   and medium-precision floats (`f16`/`bf16`/`f32`) compute in `f32`; `f64`
14//!   computes in `f64`; each integer width computes in itself (wrapping,
15//!   C-style). This is where the actual `+`/`-`/`min`/NaN-propagation semantics
16//!   live, once.
17//! * [`NumericElem`] — a *storage* element type (what a tensor view addresses)
18//!   plus how it widens to / narrows from its [`ComputeDomain`]. `f16`/`bf16`
19//!   store as 2-byte LE and round-trip through `f32` for compute (standard),
20//!   never reinterpreted as `f32` bits.
21//! * [`FloatElem`] — the float-only subset used by unary transcendental kernels
22//!   (`Sqrt`, `Tanh`, `Erf`, …) and by the MatMul/Gemm f32-accumulate path.
23//!
24//! ## The dispatch macros
25//!
26//! [`dispatch_arith`] and [`dispatch_float`] map a runtime [`DataType`] to a
27//! monomorphized generic body over the matching Rust element type, and emit a
28//! RULE #1 (`WHAT`/`WHY`/`HOW`) error for any dtype the op is not defined over —
29//! we never fabricate support for a type ONNX does not define the op on.
30//!
31//! New kernels get multi-dtype for free: read with [`to_dense`], compute in the
32//! element's [`NumericElem::Acc`], write with [`write_dense`], and wrap the body
33//! in the appropriate `dispatch_*` macro.
34
35use onnx_runtime_ep_api::{EpError, Result, TensorMut, TensorView};
36use onnx_runtime_ir::DataType;
37
38use crate::strided::{elem_offset, next_index, numel};
39
40/// The numeric domain a kernel evaluates arithmetic in.
41///
42/// Kept separate from the storage element type so several storage widths can
43/// share a single arithmetic implementation (all of `f16`/`bf16`/`f32` fold in
44/// `f32`) and so the delicate semantics — NaN-propagating `min`/`max`, integer
45/// wrapping, integer divide-by-zero → 0 — are written exactly once.
46pub trait ComputeDomain: Copy + Default {
47    fn c_add(self, o: Self) -> Self;
48    fn c_sub(self, o: Self) -> Self;
49    fn c_mul(self, o: Self) -> Self;
50    fn c_div(self, o: Self) -> Self;
51    fn c_pow(self, o: Self) -> Self;
52    fn c_div_usize(self, divisor: usize) -> Self;
53    /// ONNX/numpy `Min`: NaN-propagating for floats, `Ord::min` for integers.
54    fn c_min(self, o: Self) -> Self;
55    /// ONNX/numpy `Max`: NaN-propagating for floats, `Ord::max` for integers.
56    fn c_max(self, o: Self) -> Self;
57}
58
59macro_rules! impl_float_compute {
60    ($($t:ty),*) => {$(
61        impl ComputeDomain for $t {
62            #[inline] fn c_add(self, o: Self) -> Self { self + o }
63            #[inline] fn c_sub(self, o: Self) -> Self { self - o }
64            #[inline] fn c_mul(self, o: Self) -> Self { self * o }
65            #[inline] fn c_div(self, o: Self) -> Self { self / o }
66            #[inline] fn c_pow(self, o: Self) -> Self { self.powf(o) }
67            #[inline] fn c_div_usize(self, divisor: usize) -> Self { self / divisor as $t }
68            // Rust's `min`/`max` SUPPRESS NaN (return the non-NaN operand); ONNX
69            // `Min`/`Max` PROPAGATE it (numpy semantics), so guard explicitly.
70            #[inline] fn c_min(self, o: Self) -> Self {
71                if self.is_nan() || o.is_nan() { <$t>::NAN } else { self.min(o) }
72            }
73            #[inline] fn c_max(self, o: Self) -> Self {
74                if self.is_nan() || o.is_nan() { <$t>::NAN } else { self.max(o) }
75            }
76        }
77    )*};
78}
79impl_float_compute!(f32, f64);
80
81macro_rules! impl_int_compute {
82    ($($t:ty),*) => {$(
83        impl ComputeDomain for $t {
84            // C-style wrapping arithmetic, matching ONNX Runtime's integer ops.
85            #[inline] fn c_add(self, o: Self) -> Self { self.wrapping_add(o) }
86            #[inline] fn c_sub(self, o: Self) -> Self { self.wrapping_sub(o) }
87            #[inline] fn c_mul(self, o: Self) -> Self { self.wrapping_mul(o) }
88            // Integer divide-by-zero is UB in ONNX; return 0 (numpy's result)
89            // rather than panicking. `wrapping_div` also absorbs INT_MIN / -1.
90            #[inline] fn c_div(self, o: Self) -> Self {
91                if o == 0 { 0 } else { self.wrapping_div(o) }
92            }
93            // Integer Pow via f64 (exact for the magnitudes ONNX exercises);
94            // negative exponents (fractional result) truncate toward zero.
95            #[inline] fn c_pow(self, o: Self) -> Self { (self as f64).powf(o as f64) as $t }
96            #[inline] fn c_div_usize(self, divisor: usize) -> Self {
97                ((self as i128) / divisor as i128) as $t
98            }
99            #[inline] fn c_min(self, o: Self) -> Self { core::cmp::min(self, o) }
100            #[inline] fn c_max(self, o: Self) -> Self { core::cmp::max(self, o) }
101        }
102    )*};
103}
104impl_int_compute!(i8, i16, i32, i64, u8, u16, u32, u64);
105
106/// A tensor *storage* element type plus its widen/narrow to a [`ComputeDomain`].
107///
108/// # Safety-adjacent contract
109/// [`DTYPE`](Self::DTYPE) MUST equal the [`DataType`] whose in-memory layout is
110/// exactly `Self` (same `size_of`, native-endian bit pattern). The dispatch
111/// macros bind the generic type to the matched dtype, upholding this so
112/// [`to_dense`]/[`write_dense`] read/write the correct number of bytes.
113pub trait NumericElem: Copy {
114    /// The tensor dtype whose storage layout is exactly `Self`.
115    const DTYPE: DataType;
116    /// The domain this element's arithmetic is evaluated in.
117    type Acc: ComputeDomain;
118    fn to_acc(self) -> Self::Acc;
119    fn from_acc(a: Self::Acc) -> Self;
120}
121
122/// The float-only subset (widens to / narrows from `f32`), used by unary
123/// transcendental kernels and the MatMul/Gemm f32-accumulate path.
124pub trait FloatElem: Copy {
125    const DTYPE: DataType;
126    fn to_f32(self) -> f32;
127    fn from_f32(f: f32) -> Self;
128}
129
130// --- f32 -------------------------------------------------------------------
131impl NumericElem for f32 {
132    const DTYPE: DataType = DataType::Float32;
133    type Acc = f32;
134    #[inline]
135    fn to_acc(self) -> f32 {
136        self
137    }
138    #[inline]
139    fn from_acc(a: f32) -> Self {
140        a
141    }
142}
143impl FloatElem for f32 {
144    const DTYPE: DataType = DataType::Float32;
145    #[inline]
146    fn to_f32(self) -> f32 {
147        self
148    }
149    #[inline]
150    fn from_f32(f: f32) -> Self {
151        f
152    }
153}
154
155// --- f64 -------------------------------------------------------------------
156impl NumericElem for f64 {
157    const DTYPE: DataType = DataType::Float64;
158    type Acc = f64;
159    #[inline]
160    fn to_acc(self) -> f64 {
161        self
162    }
163    #[inline]
164    fn from_acc(a: f64) -> Self {
165        a
166    }
167}
168impl FloatElem for f64 {
169    const DTYPE: DataType = DataType::Float64;
170    #[inline]
171    fn to_f32(self) -> f32 {
172        self as f32
173    }
174    #[inline]
175    fn from_f32(f: f32) -> Self {
176        f as f64
177    }
178}
179
180// --- f16 / bf16 (2-byte LE storage; compute in f32) ------------------------
181impl NumericElem for half::f16 {
182    const DTYPE: DataType = DataType::Float16;
183    type Acc = f32;
184    #[inline]
185    fn to_acc(self) -> f32 {
186        self.to_f32()
187    }
188    #[inline]
189    fn from_acc(a: f32) -> Self {
190        half::f16::from_f32(a)
191    }
192}
193impl FloatElem for half::f16 {
194    const DTYPE: DataType = DataType::Float16;
195    #[inline]
196    fn to_f32(self) -> f32 {
197        half::f16::to_f32(self)
198    }
199    #[inline]
200    fn from_f32(f: f32) -> Self {
201        half::f16::from_f32(f)
202    }
203}
204impl NumericElem for half::bf16 {
205    const DTYPE: DataType = DataType::BFloat16;
206    type Acc = f32;
207    #[inline]
208    fn to_acc(self) -> f32 {
209        self.to_f32()
210    }
211    #[inline]
212    fn from_acc(a: f32) -> Self {
213        half::bf16::from_f32(a)
214    }
215}
216impl FloatElem for half::bf16 {
217    const DTYPE: DataType = DataType::BFloat16;
218    #[inline]
219    fn to_f32(self) -> f32 {
220        half::bf16::to_f32(self)
221    }
222    #[inline]
223    fn from_f32(f: f32) -> Self {
224        half::bf16::from_f32(f)
225    }
226}
227
228// --- integers (compute in themselves) --------------------------------------
229macro_rules! impl_int_elem {
230    ($($t:ty => $dt:expr),* $(,)?) => {$(
231        impl NumericElem for $t {
232            const DTYPE: DataType = $dt;
233            type Acc = $t;
234            #[inline] fn to_acc(self) -> $t { self }
235            #[inline] fn from_acc(a: $t) -> Self { a }
236        }
237    )*};
238}
239impl_int_elem!(
240    i8 => DataType::Int8,
241    i16 => DataType::Int16,
242    i32 => DataType::Int32,
243    i64 => DataType::Int64,
244    u8 => DataType::Uint8,
245    u16 => DataType::Uint16,
246    u32 => DataType::Uint32,
247    u64 => DataType::Uint64,
248);
249
250/// Materialize a strided view of element type `T` into a dense, row-major
251/// `Vec<T>`, applying the view's strides and byte offset.
252///
253/// `T::DTYPE` must match `view.dtype` (the dispatch macros guarantee this); the
254/// debug assertion catches a mis-wired call site before it can read the wrong
255/// element width.
256pub fn to_dense<T: NumericElem>(view: &TensorView) -> Result<Vec<T>> {
257    read_strided::<T>(view, T::DTYPE)
258}
259
260/// [`to_dense`] for the float-only [`FloatElem`] subset.
261pub fn to_dense_float<T: FloatElem>(view: &TensorView) -> Result<Vec<T>> {
262    read_strided::<T>(view, T::DTYPE)
263}
264
265fn read_strided<T: Copy>(view: &TensorView, want: DataType) -> Result<Vec<T>> {
266    view.validate()?;
267    debug_assert_eq!(
268        std::mem::size_of::<T>(),
269        want.byte_size(),
270        "read_strided element width must match dtype byte size"
271    );
272    if view.dtype != want {
273        return Err(EpError::InvalidTensorView {
274            reason: format!("expected {want:?} view, got {:?}", view.dtype),
275        });
276    }
277    let n = numel(view.shape);
278    let mut out = Vec::with_capacity(n);
279    if n == 0 {
280        return Ok(out);
281    }
282    let origin = view.data_ptr::<T>();
283    let mut idx = vec![0usize; view.shape.len()];
284    loop {
285        let off = elem_offset(view.strides, &idx);
286        // SAFETY: `origin` is the element origin of a validated view; `off` is an
287        // in-shape element offset (each component `< shape[d]`), so the address
288        // lies within the extent the view describes — bounds-checked against the
289        // backing allocation by the owning EP (ep-api invariant #1). `T` is a
290        // plain numeric/`half` type with no invalid bit patterns.
291        out.push(unsafe { *origin.offset(off) });
292        if !next_index(view.shape, &mut idx) {
293            break;
294        }
295    }
296    Ok(out)
297}
298
299/// Write a dense, row-major `&[T]` into `out`, applying the output view's
300/// strides and byte offset. `data.len()` must equal the output element count
301/// and `out.dtype` must equal `T::DTYPE`.
302pub fn write_dense<T: NumericElem>(out: &mut TensorMut, data: &[T]) -> Result<()> {
303    write_strided::<T>(out, data, T::DTYPE)
304}
305
306/// [`write_dense`] for the float-only [`FloatElem`] subset.
307pub fn write_dense_float<T: FloatElem>(out: &mut TensorMut, data: &[T]) -> Result<()> {
308    write_strided::<T>(out, data, T::DTYPE)
309}
310
311fn write_strided<T: Copy>(out: &mut TensorMut, data: &[T], want: DataType) -> Result<()> {
312    out.validate()?;
313    if out.dtype != want {
314        return Err(EpError::InvalidTensorView {
315            reason: format!("expected {want:?} output, got {:?}", out.dtype),
316        });
317    }
318    let n = numel(out.shape);
319    if data.len() != n {
320        return Err(EpError::KernelFailed(format!(
321            "output element count {n} does not match produced {}",
322            data.len()
323        )));
324    }
325    if n == 0 {
326        return Ok(());
327    }
328    let origin = out.data_ptr_mut::<T>();
329    let strides = out.strides;
330    let shape = out.shape;
331    let mut idx = vec![0usize; shape.len()];
332    let mut i = 0usize;
333    loop {
334        let off = elem_offset(strides, &idx);
335        // SAFETY: `origin` is the element origin of a validated output view; `off`
336        // is an in-shape offset within the extent the view describes (bounds-
337        // checked by the EP per invariant #1). The row-major walk visits every
338        // logical index exactly once, so each address is written once.
339        unsafe {
340            *origin.offset(off) = data[i];
341        }
342        i += 1;
343        if !next_index(shape, &mut idx) {
344            break;
345        }
346    }
347    Ok(())
348}
349
350/// RULE #1 error for a dtype an op is not defined over: WHAT is unsupported,
351/// WHY, and HOW to proceed.
352pub fn unsupported_dtype(op: &str, dtype: DataType) -> EpError {
353    EpError::KernelFailed(format!(
354        "{op}: unsupported element type {dtype:?} (WHAT: this CPU kernel was asked \
355         to run {op} on a {dtype:?} tensor). WHY: ONNX does not define {op} for \
356         {dtype:?}, or arithmetic on it is not implemented by this execution \
357         provider. HOW: insert a `Cast` to a supported numeric dtype (e.g. \
358         Float32) before {op}, or run the op on an EP that implements {dtype:?}."
359    ))
360}
361
362/// Map a runtime [`DataType`] to a monomorphized body over the matching Rust
363/// element type, across the full ONNX numeric set (floats + signed/unsigned
364/// integers). Binds `$T` via a local `type` alias; unsupported dtypes yield a
365/// RULE #1 error. The body must evaluate to `Result<()>`.
366///
367/// ```ignore
368/// dispatch_arith!(inputs[0].dtype, "Add", T => run::<T>(inputs, outputs))
369/// ```
370#[macro_export]
371macro_rules! dispatch_arith {
372    ($dtype:expr, $op:expr, $T:ident => $body:expr) => {{
373        match $dtype {
374            ::onnx_runtime_ir::DataType::Float32 => {
375                type $T = f32;
376                $body
377            }
378            ::onnx_runtime_ir::DataType::Float16 => {
379                type $T = half::f16;
380                $body
381            }
382            ::onnx_runtime_ir::DataType::BFloat16 => {
383                type $T = half::bf16;
384                $body
385            }
386            ::onnx_runtime_ir::DataType::Float64 => {
387                type $T = f64;
388                $body
389            }
390            ::onnx_runtime_ir::DataType::Int8 => {
391                type $T = i8;
392                $body
393            }
394            ::onnx_runtime_ir::DataType::Int16 => {
395                type $T = i16;
396                $body
397            }
398            ::onnx_runtime_ir::DataType::Int32 => {
399                type $T = i32;
400                $body
401            }
402            ::onnx_runtime_ir::DataType::Int64 => {
403                type $T = i64;
404                $body
405            }
406            ::onnx_runtime_ir::DataType::Uint8 => {
407                type $T = u8;
408                $body
409            }
410            ::onnx_runtime_ir::DataType::Uint16 => {
411                type $T = u16;
412                $body
413            }
414            ::onnx_runtime_ir::DataType::Uint32 => {
415                type $T = u32;
416                $body
417            }
418            ::onnx_runtime_ir::DataType::Uint64 => {
419                type $T = u64;
420                $body
421            }
422            other => Err($crate::dtype::unsupported_dtype($op, other)),
423        }
424    }};
425}
426
427/// Like [`dispatch_arith`] but restricted to the floating-point dtypes ONNX
428/// defines transcendental / accumulate ops over (`f32`, `f16`, `bf16`, `f64`).
429#[macro_export]
430macro_rules! dispatch_float {
431    ($dtype:expr, $op:expr, $T:ident => $body:expr) => {{
432        match $dtype {
433            ::onnx_runtime_ir::DataType::Float32 => {
434                type $T = f32;
435                $body
436            }
437            ::onnx_runtime_ir::DataType::Float16 => {
438                type $T = half::f16;
439                $body
440            }
441            ::onnx_runtime_ir::DataType::BFloat16 => {
442                type $T = half::bf16;
443                $body
444            }
445            ::onnx_runtime_ir::DataType::Float64 => {
446                type $T = f64;
447                $body
448            }
449            other => Err($crate::dtype::unsupported_dtype($op, other)),
450        }
451    }};
452}
453
454/// Widen any supported float view (`f32`/`f16`/`bf16`/`f64`) to a dense
455/// `Vec<f32>` for the MatMul/Gemm f32-accumulate path. Rejects non-float dtypes
456/// with a RULE #1 error.
457pub fn to_dense_f32_widen(op: &str, view: &TensorView) -> Result<Vec<f32>> {
458    dispatch_float!(view.dtype, op, T => {
459        let raw = to_dense_float::<T>(view)?;
460        Ok(raw.into_iter().map(|v| v.to_f32()).collect())
461    })
462}
463
464/// Narrow a dense `Vec<f32>` result into `out`, rounding to `out`'s float dtype
465/// (`f32`/`f16`/`bf16`/`f64`). Counterpart to [`to_dense_f32_widen`].
466pub fn write_dense_f32_narrow(op: &str, out: &mut TensorMut, data: &[f32]) -> Result<()> {
467    dispatch_float!(out.dtype, op, T => {
468        let narrowed: Vec<T> = data.iter().map(|&v| T::from_f32(v)).collect();
469        write_dense_float::<T>(out, &narrowed)
470    })
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn f16_roundtrips_through_f32_without_bit_reinterpret() {
479        // 1.0 in f16 is 0x3C00; reinterpreting those 2 bytes as an f32 would be
480        // a denormal ~1.7e-41, not 1.0 — assert we widen, not bit-cast.
481        let h = half::f16::from_f32(1.0);
482        assert_eq!(h.to_bits(), 0x3C00);
483        assert_eq!(NumericElem::to_acc(h), 1.0f32);
484        assert_eq!(half::f16::from_acc(1.0f32).to_bits(), 0x3C00);
485    }
486
487    #[test]
488    fn int_div_by_zero_is_zero_not_panic() {
489        assert_eq!(5i32.c_div(0), 0);
490        assert_eq!(i32::MIN.c_div(-1), i32::MIN); // no overflow panic
491    }
492
493    #[test]
494    fn float_min_max_propagate_nan() {
495        assert!(f32::NAN.c_min(1.0).is_nan());
496        assert!(1.0f32.c_max(f32::NAN).is_nan());
497        assert_eq!(2.0f32.c_min(3.0), 2.0);
498        assert_eq!(2.0f32.c_max(3.0), 3.0);
499    }
500
501    #[test]
502    fn int_ops_wrap() {
503        assert_eq!(i8::MAX.c_add(1), i8::MIN);
504        assert_eq!(200u8.c_mul(2), 144); // 400 mod 256
505    }
506
507    #[test]
508    fn unsupported_dtype_message_has_what_why_how() {
509        let e = unsupported_dtype("Add", DataType::Bool);
510        let s = format!("{e}");
511        assert!(s.contains("WHAT"));
512        assert!(s.contains("WHY"));
513        assert!(s.contains("HOW"));
514    }
515}