use std::borrow::Cow;
use onnx_runtime_ep_api::{EpError, Result, TensorMut, TensorView};
use onnx_runtime_ir::DataType;
use crate::strided::{elem_offset, next_index, numel};
pub trait ComputeDomain: Copy + Default {
fn c_add(self, o: Self) -> Self;
fn c_sub(self, o: Self) -> Self;
fn c_mul(self, o: Self) -> Self;
fn c_div(self, o: Self) -> Self;
fn c_pow(self, o: Self) -> Self;
fn c_div_usize(self, divisor: usize) -> Self;
fn c_min(self, o: Self) -> Self;
fn c_max(self, o: Self) -> Self;
}
macro_rules! impl_float_compute {
($($t:ty),*) => {$(
impl ComputeDomain for $t {
#[inline] fn c_add(self, o: Self) -> Self { self + o }
#[inline] fn c_sub(self, o: Self) -> Self { self - o }
#[inline] fn c_mul(self, o: Self) -> Self { self * o }
#[inline] fn c_div(self, o: Self) -> Self { self / o }
#[inline] fn c_pow(self, o: Self) -> Self { self.powf(o) }
#[inline] fn c_div_usize(self, divisor: usize) -> Self { self / divisor as $t }
#[inline] fn c_min(self, o: Self) -> Self {
if self.is_nan() || o.is_nan() { <$t>::NAN } else { self.min(o) }
}
#[inline] fn c_max(self, o: Self) -> Self {
if self.is_nan() || o.is_nan() { <$t>::NAN } else { self.max(o) }
}
}
)*};
}
impl_float_compute!(f32, f64);
macro_rules! impl_int_compute {
($($t:ty),*) => {$(
impl ComputeDomain for $t {
#[inline] fn c_add(self, o: Self) -> Self { self.wrapping_add(o) }
#[inline] fn c_sub(self, o: Self) -> Self { self.wrapping_sub(o) }
#[inline] fn c_mul(self, o: Self) -> Self { self.wrapping_mul(o) }
#[inline] fn c_div(self, o: Self) -> Self {
if o == 0 { 0 } else { self.wrapping_div(o) }
}
#[inline] fn c_pow(self, o: Self) -> Self { (self as f64).powf(o as f64) as $t }
#[inline] fn c_div_usize(self, divisor: usize) -> Self {
((self as i128) / divisor as i128) as $t
}
#[inline] fn c_min(self, o: Self) -> Self { core::cmp::min(self, o) }
#[inline] fn c_max(self, o: Self) -> Self { core::cmp::max(self, o) }
}
)*};
}
impl_int_compute!(i8, i16, i32, i64, u8, u16, u32, u64);
pub trait NumericElem: Copy {
const DTYPE: DataType;
type Acc: ComputeDomain;
fn to_acc(self) -> Self::Acc;
fn from_acc(a: Self::Acc) -> Self;
fn from_f32_scalar(f: f32) -> Self;
}
pub trait FloatElem: Copy {
const DTYPE: DataType;
fn to_f32(self) -> f32;
fn from_f32(f: f32) -> Self;
}
impl NumericElem for f32 {
const DTYPE: DataType = DataType::Float32;
type Acc = f32;
#[inline]
fn to_acc(self) -> f32 {
self
}
#[inline]
fn from_acc(a: f32) -> Self {
a
}
#[inline]
fn from_f32_scalar(f: f32) -> Self {
f
}
}
impl FloatElem for f32 {
const DTYPE: DataType = DataType::Float32;
#[inline]
fn to_f32(self) -> f32 {
self
}
#[inline]
fn from_f32(f: f32) -> Self {
f
}
}
impl NumericElem for f64 {
const DTYPE: DataType = DataType::Float64;
type Acc = f64;
#[inline]
fn to_acc(self) -> f64 {
self
}
#[inline]
fn from_acc(a: f64) -> Self {
a
}
#[inline]
fn from_f32_scalar(f: f32) -> Self {
f as f64
}
}
impl FloatElem for f64 {
const DTYPE: DataType = DataType::Float64;
#[inline]
fn to_f32(self) -> f32 {
self as f32
}
#[inline]
fn from_f32(f: f32) -> Self {
f as f64
}
}
impl NumericElem for half::f16 {
const DTYPE: DataType = DataType::Float16;
type Acc = f32;
#[inline]
fn to_acc(self) -> f32 {
self.to_f32()
}
#[inline]
fn from_acc(a: f32) -> Self {
half::f16::from_f32(a)
}
#[inline]
fn from_f32_scalar(f: f32) -> Self {
half::f16::from_f32(f)
}
}
impl FloatElem for half::f16 {
const DTYPE: DataType = DataType::Float16;
#[inline]
fn to_f32(self) -> f32 {
half::f16::to_f32(self)
}
#[inline]
fn from_f32(f: f32) -> Self {
half::f16::from_f32(f)
}
}
impl NumericElem for half::bf16 {
const DTYPE: DataType = DataType::BFloat16;
type Acc = f32;
#[inline]
fn to_acc(self) -> f32 {
self.to_f32()
}
#[inline]
fn from_acc(a: f32) -> Self {
half::bf16::from_f32(a)
}
#[inline]
fn from_f32_scalar(f: f32) -> Self {
half::bf16::from_f32(f)
}
}
impl FloatElem for half::bf16 {
const DTYPE: DataType = DataType::BFloat16;
#[inline]
fn to_f32(self) -> f32 {
half::bf16::to_f32(self)
}
#[inline]
fn from_f32(f: f32) -> Self {
half::bf16::from_f32(f)
}
}
macro_rules! impl_int_elem {
($($t:ty => $dt:expr),* $(,)?) => {$(
impl NumericElem for $t {
const DTYPE: DataType = $dt;
type Acc = $t;
#[inline] fn to_acc(self) -> $t { self }
#[inline] fn from_acc(a: $t) -> Self { a }
#[inline] fn from_f32_scalar(f: f32) -> Self { f as $t }
}
)*};
}
impl_int_elem!(
i8 => DataType::Int8,
i16 => DataType::Int16,
i32 => DataType::Int32,
i64 => DataType::Int64,
u8 => DataType::Uint8,
u16 => DataType::Uint16,
u32 => DataType::Uint32,
u64 => DataType::Uint64,
);
pub fn to_dense<T: NumericElem>(view: &TensorView) -> Result<Vec<T>> {
read_strided::<T>(view, T::DTYPE)
}
pub fn to_dense_float<T: FloatElem>(view: &TensorView) -> Result<Vec<T>> {
read_strided::<T>(view, T::DTYPE)
}
fn read_strided<T: Copy>(view: &TensorView, want: DataType) -> Result<Vec<T>> {
view.validate()?;
debug_assert_eq!(
std::mem::size_of::<T>(),
want.byte_size(),
"read_strided element width must match dtype byte size"
);
if view.dtype != want {
return Err(EpError::InvalidTensorView {
reason: format!("expected {want:?} view, got {:?}", view.dtype),
});
}
let n = numel(view.shape);
let mut out = Vec::with_capacity(n);
if n == 0 {
return Ok(out);
}
let origin = view.data_ptr::<T>();
let mut idx = vec![0usize; view.shape.len()];
loop {
let off = elem_offset(view.strides, &idx);
out.push(unsafe { *origin.offset(off) });
if !next_index(view.shape, &mut idx) {
break;
}
}
Ok(out)
}
pub fn write_dense<T: NumericElem>(out: &mut TensorMut, data: &[T]) -> Result<()> {
write_strided::<T>(out, data, T::DTYPE)
}
pub fn write_dense_float<T: FloatElem>(out: &mut TensorMut, data: &[T]) -> Result<()> {
write_strided::<T>(out, data, T::DTYPE)
}
fn write_strided<T: Copy>(out: &mut TensorMut, data: &[T], want: DataType) -> Result<()> {
out.validate()?;
if out.dtype != want {
return Err(EpError::InvalidTensorView {
reason: format!("expected {want:?} output, got {:?}", out.dtype),
});
}
let n = numel(out.shape);
if data.len() != n {
return Err(EpError::KernelFailed(format!(
"output element count {n} does not match produced {}",
data.len()
)));
}
if n == 0 {
return Ok(());
}
let origin = out.data_ptr_mut::<T>();
let strides = out.strides;
let shape = out.shape;
let mut idx = vec![0usize; shape.len()];
let mut i = 0usize;
loop {
let off = elem_offset(strides, &idx);
unsafe {
*origin.offset(off) = data[i];
}
i += 1;
if !next_index(shape, &mut idx) {
break;
}
}
Ok(())
}
pub fn unsupported_dtype(op: &str, dtype: DataType) -> EpError {
EpError::KernelFailed(format!(
"{op}: unsupported element type {dtype:?} (WHAT: this CPU kernel was asked \
to run {op} on a {dtype:?} tensor). WHY: ONNX does not define {op} for \
{dtype:?}, or arithmetic on it is not implemented by this execution \
provider. HOW: insert a `Cast` to a supported numeric dtype (e.g. \
Float32) before {op}, or run the op on an EP that implements {dtype:?}."
))
}
#[macro_export]
macro_rules! dispatch_arith {
($dtype:expr, $op:expr, $T:ident => $body:expr) => {{
match $dtype {
::onnx_runtime_ir::DataType::Float32 => {
type $T = f32;
$body
}
::onnx_runtime_ir::DataType::Float16 => {
type $T = half::f16;
$body
}
::onnx_runtime_ir::DataType::BFloat16 => {
type $T = half::bf16;
$body
}
::onnx_runtime_ir::DataType::Float64 => {
type $T = f64;
$body
}
::onnx_runtime_ir::DataType::Int8 => {
type $T = i8;
$body
}
::onnx_runtime_ir::DataType::Int16 => {
type $T = i16;
$body
}
::onnx_runtime_ir::DataType::Int32 => {
type $T = i32;
$body
}
::onnx_runtime_ir::DataType::Int64 => {
type $T = i64;
$body
}
::onnx_runtime_ir::DataType::Uint8 => {
type $T = u8;
$body
}
::onnx_runtime_ir::DataType::Uint16 => {
type $T = u16;
$body
}
::onnx_runtime_ir::DataType::Uint32 => {
type $T = u32;
$body
}
::onnx_runtime_ir::DataType::Uint64 => {
type $T = u64;
$body
}
other => Err($crate::dtype::unsupported_dtype($op, other)),
}
}};
}
#[macro_export]
macro_rules! dispatch_float {
($dtype:expr, $op:expr, $T:ident => $body:expr) => {{
match $dtype {
::onnx_runtime_ir::DataType::Float32 => {
type $T = f32;
$body
}
::onnx_runtime_ir::DataType::Float16 => {
type $T = half::f16;
$body
}
::onnx_runtime_ir::DataType::BFloat16 => {
type $T = half::bf16;
$body
}
::onnx_runtime_ir::DataType::Float64 => {
type $T = f64;
$body
}
other => Err($crate::dtype::unsupported_dtype($op, other)),
}
}};
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod f16c {
#[cfg(target_arch = "x86")]
use std::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
#[inline]
pub fn available() -> bool {
std::arch::is_x86_feature_detected!("f16c") && std::arch::is_x86_feature_detected!("avx2")
}
#[target_feature(enable = "f16c,avx2")]
pub unsafe fn widen(src: &[u16], dst: &mut [f32]) {
debug_assert_eq!(src.len(), dst.len());
let n = src.len();
let sp = src.as_ptr();
let dp = dst.as_mut_ptr();
unsafe {
let mut i = 0;
while i + 8 <= n {
let h = _mm_loadu_si128(sp.add(i) as *const __m128i);
_mm256_storeu_ps(dp.add(i), _mm256_cvtph_ps(h));
i += 8;
}
while i < n {
*dp.add(i) = half::f16::from_bits(*sp.add(i)).to_f32();
i += 1;
}
}
}
#[target_feature(enable = "f16c,avx2")]
pub unsafe fn narrow(src: &[f32], dst: &mut [u16]) {
debug_assert_eq!(src.len(), dst.len());
let n = src.len();
let sp = src.as_ptr();
let dp = dst.as_mut_ptr();
unsafe {
let mut i = 0;
while i + 8 <= n {
let v = _mm256_loadu_ps(sp.add(i));
let h = _mm256_cvtps_ph::<_MM_FROUND_TO_NEAREST_INT>(v);
_mm_storeu_si128(dp.add(i) as *mut __m128i, h);
i += 8;
}
while i < n {
*dp.add(i) = half::f16::from_f32(*sp.add(i)).to_bits();
i += 1;
}
}
}
}
pub fn widen_f16_slice_into(src: &[u16], dst: &mut [f32]) {
debug_assert_eq!(src.len(), dst.len());
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if f16c::available() {
unsafe { f16c::widen(src, dst) };
return;
}
for (d, &s) in dst.iter_mut().zip(src) {
*d = half::f16::from_bits(s).to_f32();
}
}
pub fn to_dense_f32_widen<'a>(op: &str, view: &'a TensorView<'_>) -> Result<Cow<'a, [f32]>> {
if view.dtype == DataType::Float32 && view.is_contiguous() {
view.validate()?;
let len = view.numel();
if len == 0 {
return Ok(Cow::Borrowed(&[]));
}
let data = unsafe { std::slice::from_raw_parts(view.data_ptr::<f32>(), len) };
return Ok(Cow::Borrowed(data));
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if view.dtype == DataType::Float16 && view.is_contiguous() && f16c::available() {
view.validate()?;
let len = view.numel();
if len == 0 {
return Ok(Cow::Borrowed(&[]));
}
let src = unsafe { std::slice::from_raw_parts(view.data_ptr::<u16>(), len) };
let mut dst = vec![0.0f32; len];
unsafe { f16c::widen(src, &mut dst) };
return Ok(Cow::Owned(dst));
}
dispatch_float!(view.dtype, op, T => {
let raw = to_dense_float::<T>(view)?;
Ok(Cow::Owned(
raw.into_iter().map(|v| v.to_f32()).collect(),
))
})
}
pub fn write_dense_f32_narrow(op: &str, out: &mut TensorMut, data: &[f32]) -> Result<()> {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
if out.dtype == DataType::Float16 && out.is_contiguous() && f16c::available() {
out.validate()?;
let n = out.numel();
if data.len() != n {
return Err(EpError::KernelFailed(format!(
"output element count {n} does not match produced {}",
data.len()
)));
}
if n == 0 {
return Ok(());
}
let dst = unsafe { std::slice::from_raw_parts_mut(out.data_ptr_mut::<u16>(), n) };
unsafe { f16c::narrow(data, dst) };
return Ok(());
}
dispatch_float!(out.dtype, op, T => {
let narrowed: Vec<T> = data.iter().map(|&v| T::from_f32(v)).collect();
write_dense_float::<T>(out, &narrowed)
})
}
#[inline]
pub fn slice_byte_range<T>(slice: &[T]) -> core::ops::Range<usize> {
let start = slice.as_ptr() as usize;
start..start.saturating_add(std::mem::size_of_val(slice))
}
#[inline]
pub fn byte_ranges_overlap(a: &core::ops::Range<usize>, b: &core::ops::Range<usize>) -> bool {
a.start < b.end && b.start < a.end
}
pub fn output_direct_write_eligible(
output: &mut TensorMut,
len: usize,
read_ranges: &[core::ops::Range<usize>],
) -> bool {
if output.dtype != DataType::Float32
|| !output.is_contiguous()
|| !output.device.is_host_accessible()
|| output.numel() != len
{
return false;
}
let start = output.data_ptr_mut::<f32>() as usize;
let out_range = start..start.saturating_add(len * std::mem::size_of::<f32>());
read_ranges
.iter()
.all(|r| !byte_ranges_overlap(&out_range, r))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn f16_roundtrips_through_f32_without_bit_reinterpret() {
let h = half::f16::from_f32(1.0);
assert_eq!(h.to_bits(), 0x3C00);
assert_eq!(NumericElem::to_acc(h), 1.0f32);
assert_eq!(half::f16::from_acc(1.0f32).to_bits(), 0x3C00);
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[test]
fn f16c_widen_narrow_bit_identical_to_scalar() {
if !f16c::available() {
eprintln!("skipping: host lacks f16c/avx2");
return;
}
let src: Vec<u16> = (0u32..=u16::MAX as u32).map(|b| b as u16).collect();
for len in [0usize, 1, 7, 8, 15, 16, 65_533, src.len()] {
let s = &src[..len];
let mut simd = vec![0.0f32; len];
unsafe { f16c::widen(s, &mut simd) };
for (i, &bits) in s.iter().enumerate() {
let want = half::f16::from_bits(bits).to_f32();
assert_eq!(
simd[i].to_bits(),
want.to_bits(),
"widen mismatch at f16 bits {bits:#06x}"
);
}
}
let vals: Vec<f32> = vec![
0.0,
-0.0,
1.0,
-1.0,
0.5,
2049.0, 65_504.0, 65_520.0, -65_520.0, 6.1e-5, 1e-8, 3.140625,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NAN,
std::f32::consts::PI,
123456.0,
];
for len in [0usize, 1, 7, 8, 15, vals.len()] {
let s = &vals[..len.min(vals.len())];
let mut simd = vec![0u16; s.len()];
unsafe { f16c::narrow(s, &mut simd) };
for (i, &v) in s.iter().enumerate() {
let want = half::f16::from_f32(v).to_bits();
let got = simd[i];
if half::f16::from_bits(want).is_nan() {
assert!(
half::f16::from_bits(got).is_nan(),
"narrow NaN mismatch for {v}"
);
} else {
assert_eq!(got, want, "narrow mismatch for f32 {v}");
}
}
}
}
#[test]
fn int_div_by_zero_is_zero_not_panic() {
assert_eq!(5i32.c_div(0), 0);
assert_eq!(i32::MIN.c_div(-1), i32::MIN); }
#[test]
fn float_min_max_propagate_nan() {
assert!(f32::NAN.c_min(1.0).is_nan());
assert!(1.0f32.c_max(f32::NAN).is_nan());
assert_eq!(2.0f32.c_min(3.0), 2.0);
assert_eq!(2.0f32.c_max(3.0), 3.0);
}
#[test]
fn int_ops_wrap() {
assert_eq!(i8::MAX.c_add(1), i8::MIN);
assert_eq!(200u8.c_mul(2), 144); }
#[test]
fn unsupported_dtype_message_has_what_why_how() {
let e = unsupported_dtype("Add", DataType::Bool);
let s = format!("{e}");
assert!(s.contains("WHAT"));
assert!(s.contains("WHY"));
assert!(s.contains("HOW"));
}
#[test]
fn direct_write_guard_detects_overlap_and_shape() {
use onnx_runtime_ep_api::DevicePtrMut;
use onnx_runtime_ir::{DeviceId, compute_contiguous_strides};
let mut buf = vec![0.0f32; 8];
let base = buf.as_ptr() as usize;
let shape = [2usize, 4];
let strides = compute_contiguous_strides(&shape);
let mut out = TensorMut::new(
DevicePtrMut(buf.as_mut_ptr() as *mut std::ffi::c_void),
DataType::Float32,
&shape,
&strides,
DeviceId::cpu(),
);
let disjoint = (base + 8 * 4)..(base + 8 * 4 + 16);
assert!(output_direct_write_eligible(&mut out, 8, &[disjoint]));
let overlap = (base + 4)..(base + 4 + 16);
assert!(!output_direct_write_eligible(&mut out, 8, &[overlap]));
assert!(!output_direct_write_eligible(&mut out, 7, &[]));
let s = [0.0f32; 4];
let r = slice_byte_range(&s);
assert_eq!(r.end - r.start, 16);
assert!(byte_ranges_overlap(&(0..10), &(5..15)));
assert!(!byte_ranges_overlap(&(0..10), &(10..20)));
}
}