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/// F16C-accelerated bulk `f16`⇆`f32` conversion for **contiguous** tensors.
475///
476/// The KV-cache widen (`f16`→`f32`) and narrow (`f32`→`f16`) in the decode hot
477/// path (GroupQueryAttention, per token, over the whole growing cache) is the
478/// single largest cost in native f16 decode — profiling attributes ~92% of GQA
479/// wall time to these two scalar conversions. The `F16C` instruction set
480/// converts 8 lanes per instruction, so the contiguous case is offloaded to it
481/// when the running CPU advertises `f16c` + `avx2`.
482///
483/// ### Numerical equivalence (RULES.md §4 / cross-EP parity)
484/// * `f16`→`f32` is *exact* for every `f16` value, so `_mm256_cvtph_ps` and
485///   [`half::f16::to_f32`] are bit-identical.
486/// * `f32`→`f16` uses IEEE-754 round-to-nearest-even. `_mm256_cvtps_ph` with
487///   `_MM_FROUND_TO_NEAREST_INT` and [`half::f16::from_f32`] both round to
488///   nearest-even, so results are bit-identical (verified in `tests`).
489///
490/// Non-contiguous or non-x86 callers fall back to the scalar `half` path.
491#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
492mod f16c {
493    #[cfg(target_arch = "x86")]
494    use std::arch::x86::*;
495    #[cfg(target_arch = "x86_64")]
496    use std::arch::x86_64::*;
497
498    #[inline]
499    pub fn available() -> bool {
500        std::arch::is_x86_feature_detected!("f16c") && std::arch::is_x86_feature_detected!("avx2")
501    }
502
503    /// Widen `src.len()` contiguous `f16` bit patterns into `dst` as `f32`.
504    ///
505    /// # Safety
506    /// The running CPU must support `f16c` + `avx2` (see [`available`]);
507    /// `src.len() == dst.len()`.
508    #[target_feature(enable = "f16c,avx2")]
509    pub unsafe fn widen(src: &[u16], dst: &mut [f32]) {
510        debug_assert_eq!(src.len(), dst.len());
511        let n = src.len();
512        let sp = src.as_ptr();
513        let dp = dst.as_mut_ptr();
514        unsafe {
515            let mut i = 0;
516            while i + 8 <= n {
517                let h = _mm_loadu_si128(sp.add(i) as *const __m128i);
518                _mm256_storeu_ps(dp.add(i), _mm256_cvtph_ps(h));
519                i += 8;
520            }
521            // Scalar tail via the same round-trip the SIMD lanes use.
522            while i < n {
523                *dp.add(i) = half::f16::from_bits(*sp.add(i)).to_f32();
524                i += 1;
525            }
526        }
527    }
528
529    /// Narrow `src.len()` contiguous `f32` values into `dst` as `f16` bit
530    /// patterns, rounding to nearest-even.
531    ///
532    /// # Safety
533    /// The running CPU must support `f16c` + `avx2` (see [`available`]);
534    /// `src.len() == dst.len()`.
535    #[target_feature(enable = "f16c,avx2")]
536    pub unsafe fn narrow(src: &[f32], dst: &mut [u16]) {
537        debug_assert_eq!(src.len(), dst.len());
538        let n = src.len();
539        let sp = src.as_ptr();
540        let dp = dst.as_mut_ptr();
541        unsafe {
542            let mut i = 0;
543            while i + 8 <= n {
544                let v = _mm256_loadu_ps(sp.add(i));
545                let h = _mm256_cvtps_ph::<_MM_FROUND_TO_NEAREST_INT>(v);
546                _mm_storeu_si128(dp.add(i) as *mut __m128i, h);
547                i += 8;
548            }
549            while i < n {
550                *dp.add(i) = half::f16::from_f32(*sp.add(i)).to_bits();
551                i += 1;
552            }
553        }
554    }
555}
556
557/// Widen a contiguous `f16` slice (as raw `u16` bit patterns) into an equal-length
558/// `f32` slice, using the [`f16c`] hardware path when available and falling back
559/// to the scalar [`half`] conversion otherwise. `src.len()` must equal `dst.len()`.
560///
561/// Exposed so hot-path kernels (e.g. GroupQueryAttention building its `present`
562/// KV cache) can widen a past-cache head-run *directly into* their destination
563/// buffer, skipping a separate owned widen followed by an `f32`→`f32` copy.
564pub fn widen_f16_slice_into(src: &[u16], dst: &mut [f32]) {
565    debug_assert_eq!(src.len(), dst.len());
566    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
567    if f16c::available() {
568        // SAFETY: `f16c::available()` confirmed `f16c` + `avx2`; lengths match.
569        unsafe { f16c::widen(src, dst) };
570        return;
571    }
572    for (d, &s) in dst.iter_mut().zip(src) {
573        *d = half::f16::from_bits(s).to_f32();
574    }
575}
576
577/// Borrow a contiguous f32 view zero-copy, or materialize/widen any other
578/// supported float view (`f16`/`bf16`/`f64` or strided f32) into dense f32.
579/// Rejects non-float dtypes with a RULE #1 error.
580pub fn to_dense_f32_widen<'a>(op: &str, view: &'a TensorView<'_>) -> Result<Cow<'a, [f32]>> {
581    if view.dtype == DataType::Float32 && view.is_contiguous() {
582        view.validate()?;
583        let len = view.numel();
584        if len == 0 {
585            return Ok(Cow::Borrowed(&[]));
586        }
587        // SAFETY: a validated contiguous Float32 view describes exactly `len`
588        // initialized f32 elements starting at `data_ptr`; the TensorView
589        // contract keeps that storage alive for the duration of this borrow.
590        let data = unsafe { std::slice::from_raw_parts(view.data_ptr::<f32>(), len) };
591        return Ok(Cow::Borrowed(data));
592    }
593    // F16C bulk widen for a contiguous f16 tensor (the KV-cache decode hot path).
594    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
595    if view.dtype == DataType::Float16 && view.is_contiguous() && f16c::available() {
596        view.validate()?;
597        let len = view.numel();
598        if len == 0 {
599            return Ok(Cow::Borrowed(&[]));
600        }
601        // SAFETY: a validated contiguous Float16 view addresses exactly `len`
602        // 2-byte elements; `half::f16` is `repr(transparent)` over `u16`, so the
603        // same storage reads soundly as `u16` bit patterns.
604        let src = unsafe { std::slice::from_raw_parts(view.data_ptr::<u16>(), len) };
605        let mut dst = vec![0.0f32; len];
606        // SAFETY: `f16c::available()` confirmed `f16c` + `avx2`; lengths match.
607        unsafe { f16c::widen(src, &mut dst) };
608        return Ok(Cow::Owned(dst));
609    }
610    dispatch_float!(view.dtype, op, T => {
611        let raw = to_dense_float::<T>(view)?;
612        Ok(Cow::Owned(
613            raw.into_iter().map(|v| v.to_f32()).collect(),
614        ))
615    })
616}
617
618/// Narrow a dense `Vec<f32>` result into `out`, rounding to `out`'s float dtype
619/// (`f32`/`f16`/`bf16`/`f64`). Counterpart to [`to_dense_f32_widen`].
620pub fn write_dense_f32_narrow(op: &str, out: &mut TensorMut, data: &[f32]) -> Result<()> {
621    // F16C bulk narrow for a contiguous f16 output (the KV-cache decode hot path).
622    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
623    if out.dtype == DataType::Float16 && out.is_contiguous() && f16c::available() {
624        out.validate()?;
625        let n = out.numel();
626        if data.len() != n {
627            return Err(EpError::KernelFailed(format!(
628                "output element count {n} does not match produced {}",
629                data.len()
630            )));
631        }
632        if n == 0 {
633            return Ok(());
634        }
635        // SAFETY: a validated contiguous Float16 output addresses exactly `n`
636        // 2-byte elements; `half::f16` is `repr(transparent)` over `u16`, so the
637        // storage is written soundly as `u16` bit patterns.
638        let dst = unsafe { std::slice::from_raw_parts_mut(out.data_ptr_mut::<u16>(), n) };
639        // SAFETY: `f16c::available()` confirmed `f16c` + `avx2`; lengths match.
640        unsafe { f16c::narrow(data, dst) };
641        return Ok(());
642    }
643    dispatch_float!(out.dtype, op, T => {
644        let narrowed: Vec<T> = data.iter().map(|&v| T::from_f32(v)).collect();
645        write_dense_float::<T>(out, &narrowed)
646    })
647}
648
649/// Half-open byte range `[start, end)` spanned by a slice's elements.
650///
651/// The unit of the in-place-aliasing guard ([`output_direct_write_eligible`]):
652/// a widened kernel input (`to_dense_f32_widen`) is either a `Cow::Owned` fresh
653/// heap buffer — whose range never overlaps an executor output — or a
654/// `Cow::Borrowed` view straight into the tensor storage a persistent
655/// `DeviceIoBinding` may share with the output. Pure pointer arithmetic; no
656/// element is dereferenced.
657#[inline]
658pub fn slice_byte_range<T>(slice: &[T]) -> core::ops::Range<usize> {
659    let start = slice.as_ptr() as usize;
660    start..start.saturating_add(std::mem::size_of_val(slice))
661}
662
663/// Whether two byte ranges overlap.
664#[inline]
665pub fn byte_ranges_overlap(a: &core::ops::Range<usize>, b: &core::ops::Range<usize>) -> bool {
666    a.start < b.end && b.start < a.end
667}
668
669/// General in-place-aliasing guard for kernels that write their result straight
670/// into an executor output buffer (the zero-copy "direct write" fast path).
671///
672/// Returns `true` only when it is sound to form a `&mut [f32]` of exactly `len`
673/// elements over `output`'s backing store and write into it while the kernel
674/// still reads the slices covered by `read_ranges`. All of the following must
675/// hold:
676///
677/// * `output` is `Float32`, contiguous row-major, and host-accessible;
678/// * its element count equals `len` (the computed result length); and
679/// * its `len`-element byte range is disjoint from every range in `read_ranges`.
680///
681/// Persistent `DeviceIoBinding`s explicitly permit binding an input buffer onto
682/// an output buffer (`onnx-runtime-session`'s device-binding path), so a kernel
683/// that reads an input *after* it begins writing its output can silently corrupt
684/// that input — or hit copy-`nonoverlapping` UB — when the two alias. Callers
685/// pass the byte ranges of the widened input slices they still read
686/// ([`slice_byte_range`]); on overlap this returns `false` and the caller must
687/// compute into an owned buffer and finish with [`write_dense_f32_narrow`]. The
688/// check is `O(read_ranges)` pointer comparisons with no data movement, so the
689/// common disjoint case keeps the direct-write speed. `len` is used verbatim by
690/// the caller's `unsafe` slice, so the element-count match here is what makes
691/// that slice in-bounds. No hardcoded dimensions — usable by any kernel.
692pub fn output_direct_write_eligible(
693    output: &mut TensorMut,
694    len: usize,
695    read_ranges: &[core::ops::Range<usize>],
696) -> bool {
697    if output.dtype != DataType::Float32
698        || !output.is_contiguous()
699        || !output.device.is_host_accessible()
700        || output.numel() != len
701    {
702        return false;
703    }
704    let start = output.data_ptr_mut::<f32>() as usize;
705    let out_range = start..start.saturating_add(len * std::mem::size_of::<f32>());
706    read_ranges
707        .iter()
708        .all(|r| !byte_ranges_overlap(&out_range, r))
709}
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714
715    #[test]
716    fn f16_roundtrips_through_f32_without_bit_reinterpret() {
717        // 1.0 in f16 is 0x3C00; reinterpreting those 2 bytes as an f32 would be
718        // a denormal ~1.7e-41, not 1.0 — assert we widen, not bit-cast.
719        let h = half::f16::from_f32(1.0);
720        assert_eq!(h.to_bits(), 0x3C00);
721        assert_eq!(NumericElem::to_acc(h), 1.0f32);
722        assert_eq!(half::f16::from_acc(1.0f32).to_bits(), 0x3C00);
723    }
724
725    /// The F16C bulk widen/narrow fast paths must produce bit-identical results
726    /// to the scalar `half` crate reference across the full f16 bit space and a
727    /// representative f32 range (RULES.md §4 parity contract). f16→f32 is exact;
728    /// f32→f16 rounds to nearest-even in both, so equality is exact, not
729    /// approximate.
730    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
731    #[test]
732    fn f16c_widen_narrow_bit_identical_to_scalar() {
733        if !f16c::available() {
734            eprintln!("skipping: host lacks f16c/avx2");
735            return;
736        }
737
738        // Widen: every one of the 65_536 f16 bit patterns, including NaN/inf,
739        // subnormals, and a non-multiple-of-8 tail.
740        let src: Vec<u16> = (0u32..=u16::MAX as u32).map(|b| b as u16).collect();
741        for len in [0usize, 1, 7, 8, 15, 16, 65_533, src.len()] {
742            let s = &src[..len];
743            let mut simd = vec![0.0f32; len];
744            // SAFETY: guarded by f16c::available(); lengths match.
745            unsafe { f16c::widen(s, &mut simd) };
746            for (i, &bits) in s.iter().enumerate() {
747                let want = half::f16::from_bits(bits).to_f32();
748                // NaN compares unequal to itself; compare bit patterns instead.
749                assert_eq!(
750                    simd[i].to_bits(),
751                    want.to_bits(),
752                    "widen mismatch at f16 bits {bits:#06x}"
753                );
754            }
755        }
756
757        // Narrow: a spread of f32 values (exact halves, subnormals, overflow to
758        // inf, negatives, and ties that exercise round-to-nearest-even).
759        let vals: Vec<f32> = vec![
760            0.0,
761            -0.0,
762            1.0,
763            -1.0,
764            0.5,
765            2049.0,    // rounds to nearest-even in f16
766            65_504.0,  // f16::MAX
767            65_520.0,  // overflows to +inf
768            -65_520.0, // overflows to -inf
769            6.1e-5,    // near f16 subnormal boundary
770            1e-8,      // flushes toward zero
771            3.140625,
772            f32::INFINITY,
773            f32::NEG_INFINITY,
774            f32::NAN,
775            std::f32::consts::PI,
776            123456.0,
777        ];
778        for len in [0usize, 1, 7, 8, 15, vals.len()] {
779            let s = &vals[..len.min(vals.len())];
780            let mut simd = vec![0u16; s.len()];
781            // SAFETY: guarded by f16c::available(); lengths match.
782            unsafe { f16c::narrow(s, &mut simd) };
783            for (i, &v) in s.iter().enumerate() {
784                let want = half::f16::from_f32(v).to_bits();
785                let got = simd[i];
786                // Both NaN encodings are acceptable only if both are NaN; assert
787                // exact equality otherwise. f16 NaN canonicalizes identically.
788                if half::f16::from_bits(want).is_nan() {
789                    assert!(
790                        half::f16::from_bits(got).is_nan(),
791                        "narrow NaN mismatch for {v}"
792                    );
793                } else {
794                    assert_eq!(got, want, "narrow mismatch for f32 {v}");
795                }
796            }
797        }
798    }
799
800    #[test]
801    fn int_div_by_zero_is_zero_not_panic() {
802        assert_eq!(5i32.c_div(0), 0);
803        assert_eq!(i32::MIN.c_div(-1), i32::MIN); // no overflow panic
804    }
805
806    #[test]
807    fn float_min_max_propagate_nan() {
808        assert!(f32::NAN.c_min(1.0).is_nan());
809        assert!(1.0f32.c_max(f32::NAN).is_nan());
810        assert_eq!(2.0f32.c_min(3.0), 2.0);
811        assert_eq!(2.0f32.c_max(3.0), 3.0);
812    }
813
814    #[test]
815    fn int_ops_wrap() {
816        assert_eq!(i8::MAX.c_add(1), i8::MIN);
817        assert_eq!(200u8.c_mul(2), 144); // 400 mod 256
818    }
819
820    #[test]
821    fn unsupported_dtype_message_has_what_why_how() {
822        let e = unsupported_dtype("Add", DataType::Bool);
823        let s = format!("{e}");
824        assert!(s.contains("WHAT"));
825        assert!(s.contains("WHY"));
826        assert!(s.contains("HOW"));
827    }
828
829    #[test]
830    fn direct_write_guard_detects_overlap_and_shape() {
831        use onnx_runtime_ep_api::DevicePtrMut;
832        use onnx_runtime_ir::{DeviceId, compute_contiguous_strides};
833
834        let mut buf = vec![0.0f32; 8];
835        let base = buf.as_ptr() as usize;
836        let shape = [2usize, 4];
837        let strides = compute_contiguous_strides(&shape);
838        let mut out = TensorMut::new(
839            DevicePtrMut(buf.as_mut_ptr() as *mut std::ffi::c_void),
840            DataType::Float32,
841            &shape,
842            &strides,
843            DeviceId::cpu(),
844        );
845
846        // Disjoint input range -> eligible for the direct path.
847        let disjoint = (base + 8 * 4)..(base + 8 * 4 + 16);
848        assert!(output_direct_write_eligible(&mut out, 8, &[disjoint]));
849
850        // Overlapping input range -> must reject (fall back to owned buffer).
851        let overlap = (base + 4)..(base + 4 + 16);
852        assert!(!output_direct_write_eligible(&mut out, 8, &[overlap]));
853
854        // Wrong element count -> reject even with no aliasing input.
855        assert!(!output_direct_write_eligible(&mut out, 7, &[]));
856
857        // Range helpers.
858        let s = [0.0f32; 4];
859        let r = slice_byte_range(&s);
860        assert_eq!(r.end - r.start, 16);
861        assert!(byte_ranges_overlap(&(0..10), &(5..15)));
862        assert!(!byte_ranges_overlap(&(0..10), &(10..20)));
863    }
864}