use onnx_runtime_ep_api::{EpError, Kernel, KernelFactory, Result, TensorMut, TensorView};
use onnx_runtime_ir::{Node, is_contiguous};
use super::{check_arity, elem_size, to_dense_bytes, to_dense_i64, write_dense_bytes};
use crate::strided::numel;
fn checked_product(shape: &[usize], what: &str) -> Result<usize> {
shape.iter().try_fold(1usize, |count, &dim| {
count.checked_mul(dim).ok_or_else(|| {
EpError::KernelFailed(format!("Gather: {what} overflowed for shape {shape:?}"))
})
})
}
fn normalize_index(raw: i64, axis_dim: usize) -> Result<usize> {
let normalized = if raw < 0 {
raw as i128 + axis_dim as i128
} else {
raw as i128
};
if normalized < 0 || normalized >= axis_dim as i128 {
return Err(EpError::KernelFailed(format!(
"Gather: index {raw} out of range for axis dim {axis_dim}"
)));
}
Ok(normalized as usize)
}
pub struct GatherKernel {
axis: i64,
}
pub struct GatherFactory;
impl KernelFactory for GatherFactory {
fn create(&self, node: &Node, _input_shapes: &[Vec<usize>]) -> Result<Box<dyn Kernel>> {
let axis = node.attr("axis").and_then(|a| a.as_int()).unwrap_or(0);
Ok(Box::new(GatherKernel { axis }))
}
}
impl Kernel for GatherKernel {
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
check_arity("Gather", inputs, outputs, 2, 2, 1)?;
let data = &inputs[0];
if outputs[0].dtype != data.dtype {
return Err(EpError::KernelFailed(format!(
"Gather: output dtype {:?} must match data dtype {:?}",
outputs[0].dtype, data.dtype
)));
}
let esize = elem_size(data.dtype)?;
let indices = to_dense_i64(&inputs[1])?;
let data_shape = inputs[0].shape;
let idx_shape = inputs[1].shape;
let rank = data_shape.len();
if rank == 0 {
return Err(EpError::KernelFailed(
"Gather: data must have rank >= 1".into(),
));
}
let axis = if self.axis < 0 {
self.axis + rank as i64
} else {
self.axis
};
if axis < 0 || axis as usize >= rank {
return Err(EpError::KernelFailed(format!(
"Gather: axis {} out of range for rank {rank}",
self.axis
)));
}
let axis = axis as usize;
let axis_dim = data_shape[axis];
let outer: usize = data_shape[..axis].iter().product();
let inner: usize = data_shape[axis + 1..].iter().product();
let num_idx = numel(idx_shape);
if axis == 0
&& is_contiguous(data.shape, data.strides)
&& is_contiguous(outputs[0].shape, outputs[0].strides)
{
data.validate()?;
outputs[0].validate()?;
let row_elements = checked_product(&data_shape[1..], "axis-0 row element count")?;
let expected_output_elements =
indices.len().checked_mul(row_elements).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 output element count overflowed".into())
})?;
let output_elements = checked_product(outputs[0].shape, "destination element count")?;
if output_elements != expected_output_elements {
return Err(EpError::KernelFailed(format!(
"Gather: axis-0 destination has {output_elements} elements, expected \
{expected_output_elements} for {} indices and {row_elements} elements per row",
indices.len()
)));
}
let row_bytes = row_elements.checked_mul(esize).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 row byte count overflowed".into())
})?;
let source_elements = axis_dim.checked_mul(row_elements).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 source element count overflowed".into())
})?;
let source_bytes = source_elements.checked_mul(esize).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 source byte count overflowed".into())
})?;
let expected_output_bytes =
expected_output_elements.checked_mul(esize).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 output byte count overflowed".into())
})?;
for &raw in &indices {
normalize_index(raw, axis_dim)?;
}
let src = data.data_ptr::<u8>();
let dst = outputs[0].data_ptr_mut::<u8>();
for (output_row, &raw) in indices.iter().enumerate() {
let idx = normalize_index(raw, axis_dim)?;
let src_offset = idx.checked_mul(row_bytes).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 source byte offset overflowed".into())
})?;
let dst_offset = output_row.checked_mul(row_bytes).ok_or_else(|| {
EpError::KernelFailed(
"Gather: axis-0 destination byte offset overflowed".into(),
)
})?;
let src_end = src_offset.checked_add(row_bytes).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 source row end overflowed".into())
})?;
let dst_end = dst_offset.checked_add(row_bytes).ok_or_else(|| {
EpError::KernelFailed("Gather: axis-0 destination row end overflowed".into())
})?;
if src_end > source_bytes || dst_end > expected_output_bytes {
return Err(EpError::KernelFailed(
"Gather: axis-0 row copy exceeds validated tensor bounds".into(),
));
}
unsafe {
std::ptr::copy_nonoverlapping(
src.add(src_offset),
dst.add(dst_offset),
row_bytes,
);
}
}
return Ok(());
}
let data = to_dense_bytes(data)?;
let mut out = vec![0u8; outer * num_idx * inner * esize];
let mut w = 0usize;
for o in 0..outer {
for &raw in &indices {
let idx = if raw < 0 { raw + axis_dim as i64 } else { raw };
if idx < 0 || idx as usize >= axis_dim {
return Err(EpError::KernelFailed(format!(
"Gather: index {raw} out of range for axis dim {axis_dim}"
)));
}
let base = (o * axis_dim + idx as usize) * inner;
let len = inner * esize;
let base = base * esize;
out[w..w + len].copy_from_slice(&data[base..base + len]);
w += len;
}
}
write_dense_bytes(&mut outputs[0], &out)
}
fn supports_strided_input(&self, _input_idx: usize) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use crate::CpuExecutionProvider;
use crate::kernels::testutil::Owned;
use onnx_runtime_ep_api::ExecutionProvider;
use onnx_runtime_ir::{
Attribute, DataType, Graph, Node, NodeId, compute_contiguous_strides, static_shape,
};
use onnx_runtime_loader::Model;
fn run_result(
axis: i64,
data: &Owned,
idx: &Owned,
out: &mut Owned,
) -> onnx_runtime_ep_api::Result<()> {
let mut graph = Graph::new();
graph.opset_imports.insert(String::new(), 13);
let data_value =
graph.create_named_value("data", data.dtype, static_shape(data.shape.iter().copied()));
let indices_value = graph.create_named_value(
"indices",
idx.dtype,
static_shape(idx.shape.iter().copied()),
);
let output =
graph.create_named_value("output", out.dtype, static_shape(out.shape.iter().copied()));
graph.add_input(data_value);
graph.add_input(indices_value);
let mut node = Node::new(
NodeId(0),
"Gather",
vec![Some(data_value), Some(indices_value)],
vec![output],
);
node.attributes.insert("axis".into(), Attribute::Int(axis));
let node = graph.insert_node(node);
graph.add_output(output);
let model = Model::new(&graph);
CpuExecutionProvider::new()
.get_kernel(model.graph.node(node), &[], 13)
.unwrap()
.execute(&[data.view(), idx.view()], &mut [out.view_mut()])
}
fn run(axis: i64, data: &Owned, idx: &Owned, out: &mut Owned) {
run_result(axis, data, idx, out).unwrap();
}
fn take_rows(data: &[f32], columns: usize, indices: &[i64]) -> Vec<f32> {
let rows = data.len() / columns;
let mut expected = Vec::with_capacity(indices.len() * columns);
for &raw in indices {
let row = if raw < 0 {
(rows as i64 + raw) as usize
} else {
raw as usize
};
expected.extend_from_slice(&data[row * columns..(row + 1) * columns]);
}
expected
}
#[test]
fn gather_axis0_decode_single_row() {
let values = [1., 2., 3., 4., 5., 6.];
let indices = [2];
let data = Owned::f32(&[3, 2], &values);
let idx = Owned::i64(&[1], &indices);
let mut out = Owned::zeros_f32(&[1, 2]);
run(0, &data, &idx, &mut out);
assert_eq!(out.to_f32(), take_rows(&values, 2, &indices));
}
#[test]
fn gather_axis0_multiple_rows() {
let values = [1., 2., 3., 4., 5., 6.];
let indices = [2, 0];
let data = Owned::f32(&[3, 2], &values);
let idx = Owned::i64(&[2], &indices);
let mut out = Owned::zeros_f32(&[2, 2]);
run(0, &data, &idx, &mut out);
assert_eq!(out.to_f32(), take_rows(&values, 2, &indices));
}
#[test]
fn gather_columns_axis1() {
let data = Owned::f32(&[2, 3], &[1., 2., 3., 4., 5., 6.]);
let idx = Owned::i64(&[2], &[0, 2]);
let mut out = Owned::zeros_f32(&[2, 2]);
run(1, &data, &idx, &mut out);
assert_eq!(out.to_f32(), vec![1., 3., 4., 6.]);
}
#[test]
fn gather_negative_index() {
let values = [1., 2., 3., 4., 5., 6.];
let indices = [-1, -3];
let data = Owned::f32(&[3, 2], &values);
let idx = Owned::i64(&[2], &indices);
let mut out = Owned::zeros_f32(&[2, 2]);
run(0, &data, &idx, &mut out);
assert_eq!(out.to_f32(), take_rows(&values, 2, &indices));
}
#[test]
fn gather_axis0_rejects_out_of_range_before_writing() {
let data = Owned::f32(&[3, 2], &[1., 2., 3., 4., 5., 6.]);
let idx = Owned::i64(&[2], &[0, 3]);
let mut out = Owned::f32(&[2, 2], &[9., 9., 9., 9.]);
let err = run_result(0, &data, &idx, &mut out).unwrap_err();
assert!(err.to_string().contains("index 3 out of range"));
assert_eq!(out.to_f32(), vec![9., 9., 9., 9.]);
}
#[test]
fn gather_axis0_rejects_mismatched_output_before_writing() {
let data = Owned::f32(&[3, 2], &[1., 2., 3., 4., 5., 6.]);
let idx = Owned::i64(&[2], &[2, 0]);
let mut out = Owned::f32(&[4, 2], &[9.; 8]);
out.shape = vec![1, 2];
out.strides = compute_contiguous_strides(&out.shape);
let err = run_result(0, &data, &idx, &mut out).unwrap_err();
assert!(
err.to_string()
.contains("destination has 2 elements, expected 4")
);
assert_eq!(out.to_f32(), vec![9.; 8]);
}
#[test]
fn gather_noncontiguous_axis1_uses_general_path() {
let mut data = Owned::f32(&[2, 3], &[1., 4., 2., 5., 3., 6.]);
data.strides = vec![1, 2];
let idx = Owned::i64(&[2], &[2, 0]);
let mut out = Owned::zeros_f32(&[2, 2]);
run(1, &data, &idx, &mut out);
let logical = [[1., 2., 3.], [4., 5., 6.]];
let expected: Vec<f32> = logical.iter().flat_map(|row| [row[2], row[0]]).collect();
assert_eq!(out.to_f32(), expected);
}
#[test]
fn gather_2d_indices_embedding() {
let data = Owned::f32(&[4, 2], &[0., 1., 2., 3., 4., 5., 6., 7.]);
let idx = Owned::i64(&[1, 3], &[0, 2, 3]);
let mut out = Owned::zeros_f32(&[1, 3, 2]);
run(0, &data, &idx, &mut out);
assert_eq!(out.to_f32(), vec![0., 1., 4., 5., 6., 7.]);
}
#[test]
fn gather_int64_shape_dimension_with_int32_indices() {
let data = Owned::i64(&[4], &[8, 16, 32, 64]);
let idx = Owned::i32(&[1], &[2]);
let mut out = Owned::zeros(DataType::Int64, &[1]);
run(0, &data, &idx, &mut out);
assert_eq!(out.to_i64(), vec![32]);
}
#[test]
fn gather_int64_multidim_indices_negative_axis() {
let data = Owned::i64(&[2, 3], &[10, 20, 30, 40, 50, 60]);
let idx = Owned::i64(&[2, 2], &[2, 0, 1, 2]);
let mut out = Owned::zeros(DataType::Int64, &[2, 2, 2]);
run(-1, &data, &idx, &mut out);
assert_eq!(out.to_i64(), vec![30, 10, 20, 30, 60, 40, 50, 60]);
}
#[test]
fn gather_int64_negative_index_wraps() {
let data = Owned::i64(&[3], &[11, 22, 33]);
let idx = Owned::i64(&[1], &[-1]);
let mut out = Owned::zeros(DataType::Int64, &[1]);
run(0, &data, &idx, &mut out);
assert_eq!(out.to_i64(), vec![33]);
}
}