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