use onnx_runtime_ep_api::{EpError, Kernel, KernelFactory, Result, TensorMut, TensorView};
use onnx_runtime_ir::{DataType, Node, compute_contiguous_strides};
use super::check_arity;
use crate::dispatch_arith;
#[cfg(feature = "mlas")]
use crate::dtype::to_dense_f32_widen;
use crate::dtype::{ComputeDomain, NumericElem, to_dense, write_dense};
use crate::strided::{next_index, numel};
pub struct AddKernel;
pub struct AddFactory;
impl KernelFactory for AddFactory {
fn create(&self, _node: &Node, _input_shapes: &[Vec<usize>]) -> Result<Box<dyn Kernel>> {
Ok(Box::new(AddKernel))
}
}
pub fn broadcast_apply<T: Copy>(
src: &[T],
src_shape: &[usize],
out_shape: &[usize],
mut f: impl FnMut(usize, T),
) -> Result<()> {
let out_rank = out_shape.len();
let src_strides = compute_contiguous_strides(src_shape);
let mut eff = vec![0i64; out_rank];
for axis in 0..out_rank {
let src_axis = axis as isize - (out_rank as isize - src_shape.len() as isize);
if src_axis < 0 {
continue;
}
let src_axis = src_axis as usize;
let src_dim = src_shape[src_axis];
if src_dim == out_shape[axis] {
eff[axis] = src_strides[src_axis];
} else if src_dim == 1 {
eff[axis] = 0;
} else {
return Err(EpError::Ir(
onnx_runtime_ir::IrError::BroadcastIncompatible {
a: src_shape.to_vec(),
b: out_shape.to_vec(),
},
));
}
}
let n = numel(out_shape);
if n == 0 {
return Ok(());
}
let mut idx = vec![0usize; out_rank];
let mut flat = 0usize;
loop {
let mut src_off = 0i64;
for (e, &i) in eff.iter().zip(&idx) {
src_off += e * i as i64;
}
f(flat, src[src_off as usize]);
flat += 1;
if !next_index(out_shape, &mut idx) {
break;
}
}
Ok(())
}
impl Kernel for AddKernel {
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
check_arity("Add", inputs, outputs, 2, 2, 1)?;
crate::trace::record_kernel_metrics(inputs, outputs, || outputs[0].numel() as u64);
#[cfg(feature = "mlas")]
if add_contiguous_f32(inputs, &mut outputs[0])? {
return Ok(());
}
dispatch_arith!(inputs[0].dtype, "Add", T => add_typed::<T>(inputs, outputs))
}
fn supports_strided_input(&self, _input_idx: usize) -> bool {
true
}
}
#[cfg(feature = "mlas")]
fn add_contiguous_f32(inputs: &[TensorView], output: &mut TensorMut) -> Result<bool> {
if inputs[0].dtype != DataType::Float32
|| inputs[1].dtype != DataType::Float32
|| output.dtype != DataType::Float32
|| inputs[0].shape != inputs[1].shape
|| inputs[0].shape != output.shape
|| !inputs[0].is_contiguous()
|| !inputs[1].is_contiguous()
|| !output.is_contiguous()
{
return Ok(false);
}
let output_start = output.data_ptr_mut::<u8>() as usize;
let output_end = output_start.saturating_add(output.byte_size());
if inputs.iter().any(|input| {
let start = input.data_ptr::<u8>() as usize;
let end = start.saturating_add(input.byte_size());
output_start < end && start < output_end
}) {
return Ok(false);
}
let left = to_dense_f32_widen("Add", &inputs[0])?;
let right = to_dense_f32_widen("Add", &inputs[1])?;
let output_len = output.numel();
let output =
unsafe { std::slice::from_raw_parts_mut(output.data_ptr_mut::<f32>(), output_len) };
mlas_sys::eltwise_add(&left, &right, output);
Ok(true)
}
fn add_typed<T: NumericElem>(inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
require_same_dtype("Add", &inputs[1], T::DTYPE)?;
let a = to_dense::<T>(&inputs[0])?;
let b = to_dense::<T>(&inputs[1])?;
let out_shape = outputs[0].shape.to_vec();
let mut acc = vec![T::Acc::default(); numel(&out_shape)];
broadcast_apply(&a, inputs[0].shape, &out_shape, |i, v| acc[i] = v.to_acc())?;
broadcast_apply(&b, inputs[1].shape, &out_shape, |i, v| {
acc[i] = acc[i].c_add(v.to_acc())
})?;
let out: Vec<T> = acc.into_iter().map(T::from_acc).collect();
write_dense::<T>(&mut outputs[0], &out)
}
pub(crate) fn require_same_dtype(op: &str, view: &TensorView, want: DataType) -> Result<()> {
if view.dtype != want {
return Err(EpError::KernelFailed(format!(
"{op}: all operands must share one dtype (WHAT: got a {:?} operand \
alongside {want:?}). WHY: ONNX elementwise ops are homogeneous โ \
mixed-dtype inputs are undefined. HOW: insert a `Cast` so every \
operand is {want:?}.",
view.dtype
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernels::testutil::Owned;
#[test]
fn add_same_shape() {
let a = Owned::f32(&[2, 2], &[1., 2., 3., 4.]);
let b = Owned::f32(&[2, 2], &[10., 20., 30., 40.]);
let mut out = Owned::zeros_f32(&[2, 2]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
assert_eq!(out.to_f32(), vec![11., 22., 33., 44.]);
}
#[test]
fn add_broadcasts_row_vector() {
let a = Owned::f32(&[2, 3], &[1., 2., 3., 4., 5., 6.]);
let b = Owned::f32(&[3], &[10., 20., 30.]);
let mut out = Owned::zeros_f32(&[2, 3]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
assert_eq!(out.to_f32(), vec![11., 22., 33., 14., 25., 36.]);
}
#[test]
fn add_broadcasts_column_vector() {
let a = Owned::f32(&[2, 3], &[1., 2., 3., 4., 5., 6.]);
let b = Owned::f32(&[2, 1], &[10., 20.]);
let mut out = Owned::zeros_f32(&[2, 3]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
assert_eq!(out.to_f32(), vec![11., 12., 13., 24., 25., 26.]);
}
#[test]
fn add_f16_broadcasts() {
let a = Owned::f16(&[2, 3], &[1., 2., 3., 4., 5., 6.]);
let b = Owned::f16(&[3], &[10., 20., 30.]);
let mut out = Owned::zeros(DataType::Float16, &[2, 3]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
assert_eq!(out.to_f16_as_f32(), vec![11., 22., 33., 14., 25., 36.]);
}
#[test]
fn add_bf16_same_shape() {
let a = Owned::bf16(&[2, 2], &[1., 2., 3., 4.]);
let b = Owned::bf16(&[2, 2], &[10., 20., 30., 40.]);
let mut out = Owned::zeros(DataType::BFloat16, &[2, 2]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
assert_eq!(out.to_bf16_as_f32(), vec![11., 22., 33., 44.]);
}
#[test]
fn add_f16_preserves_nan_and_inf_without_bit_corruption() {
let a = Owned::f16_bits(&[3], &[0x7C00, 0xFF00 , 0x3C00 ]);
let b = Owned::f16(&[3], &[1.0, 1.0, 1.0]);
let mut out = Owned::zeros(DataType::Float16, &[3]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
let bits = out.to_u16_bits();
assert_eq!(bits[0], 0x7C00, "inf + 1 must stay +inf");
assert_eq!(bits[1] & 0x7C00, 0x7C00);
assert_ne!(bits[1] & 0x03FF, 0);
assert_eq!(out.to_f16_as_f32()[2], 2.0);
}
#[test]
fn add_int32_wraps() {
let a = Owned::i32(&[3], &[1, 2, i32::MAX]);
let b = Owned::i32(&[3], &[10, 20, 1]);
let mut out = Owned::zeros(DataType::Int32, &[3]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
assert_eq!(out.to_i32(), vec![11, 22, i32::MIN]);
}
#[test]
fn add_uint8_broadcasts_and_wraps() {
let a = Owned::u8(&[2, 2], &[1, 200, 3, 4]);
let b = Owned::u8(&[1], &[100]);
let mut out = Owned::zeros(DataType::Uint8, &[2, 2]);
AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap();
assert_eq!(out.to_u8(), vec![101, 44, 103, 104]); }
#[test]
fn add_rejects_bool_with_rule1_message() {
let a = Owned::bool_(&[2], &[true, false]);
let b = Owned::bool_(&[2], &[false, true]);
let mut out = Owned::zeros(DataType::Bool, &[2]);
let err = AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("WHAT") && msg.contains("WHY") && msg.contains("HOW"));
}
#[test]
fn add_rejects_mixed_dtype_operands() {
let a = Owned::f16(&[2], &[1., 2.]);
let b = Owned::f32(&[2], &[1., 2.]);
let mut out = Owned::zeros(DataType::Float16, &[2]);
let err = AddKernel
.execute(&[a.view(), b.view()], &mut [out.view_mut()])
.unwrap_err();
assert!(format!("{err}").contains("share one dtype"));
}
}