use std::collections::HashMap;
use std::ffi::c_void;
use onnx_runtime_ep_api::{DevicePtr, DevicePtrMut, EpError, TensorMut, TensorView};
use onnx_runtime_ir::{compute_contiguous_strides, Attribute, DataType, Node, NodeId, ValueId};
use onnx_runtime_shape_inference::{DimExpr, MergePolicy, NodeIo, SymbolInterner, TypeInfo};
use crate::cache::KernelCacheKey;
use crate::error::{EagerError, Result};
use crate::tensor::Tensor;
use crate::EagerContext;
fn ephemeral_node(
op_type: &str,
domain: &str,
attrs: &HashMap<String, Attribute>,
num_inputs: usize,
num_outputs: usize,
) -> Node {
let inputs: Vec<Option<ValueId>> = (0..num_inputs).map(|i| Some(ValueId(i as u32))).collect();
let outputs: Vec<ValueId> = (0..num_outputs)
.map(|i| ValueId((num_inputs + i) as u32))
.collect();
let mut node = Node::new(NodeId(0), op_type, inputs, outputs);
node.domain = domain.to_string();
node.attributes = attrs.clone();
node
}
impl EagerContext {
pub fn dispatch(
&self,
op_type: &str,
domain: &str,
inputs: &[&Tensor],
attrs: &HashMap<String, Attribute>,
explicit_opset: Option<u64>,
) -> Result<Vec<Tensor>> {
let opset = self
.domains
.read()
.expect("domain registry lock poisoned")
.resolve_opset(domain, explicit_opset);
let device = self.resolve_device(inputs)?;
let ep = self.ep_for_device(device)?;
let input_shapes: Vec<Vec<usize>> = inputs.iter().map(|t| t.shape().to_vec()).collect();
let node = ephemeral_node(op_type, domain, attrs, inputs.len(), 1);
let cache_key = KernelCacheKey {
op_type: op_type.to_string(),
domain: domain.to_string(),
opset,
input_shapes: input_shapes.clone(),
input_dtypes: inputs.iter().map(|t| t.dtype()).collect(),
device,
};
let kernel = self
.cache
.lock()
.expect("kernel cache lock poisoned")
.get_or_create(cache_key, || -> Result<Box<dyn onnx_runtime_ep_api::Kernel>> {
ep.get_kernel(&node, &input_shapes, opset).map_err(|e| match e {
EpError::NoEpForOp { .. } => EagerError::NoKernel {
op_type: op_type.to_string(),
domain: domain.to_string(),
device,
},
other => EagerError::Kernel(other),
})
})?;
let output_meta = self.infer_output_meta(&node, op_type, domain, opset, inputs)?;
let mut outputs: Vec<Tensor> = Vec::with_capacity(output_meta.len());
for (dtype, shape) in &output_meta {
outputs.push(Tensor::zeros_in(ep.clone(), *dtype, shape.clone())?);
}
let in_strides: Vec<Vec<i64>> = input_shapes
.iter()
.map(|s| compute_contiguous_strides(s))
.collect();
let out_shapes: Vec<Vec<usize>> = output_meta.iter().map(|(_, s)| s.clone()).collect();
let out_strides: Vec<Vec<i64>> = out_shapes
.iter()
.map(|s| compute_contiguous_strides(s))
.collect();
let in_ptrs: Vec<*const c_void> = inputs.iter().map(|t| t.device_ptr()).collect();
let out_ptrs: Vec<*mut c_void> =
outputs.iter_mut().map(|t| t.device_ptr_mut()).collect();
let input_views: Vec<TensorView> = (0..inputs.len())
.map(|i| {
TensorView::new(
DevicePtr(in_ptrs[i]),
inputs[i].dtype(),
&input_shapes[i],
&in_strides[i],
device,
)
})
.collect();
let mut output_views: Vec<TensorMut> = (0..outputs.len())
.map(|i| {
TensorMut::new(
DevicePtrMut(out_ptrs[i]),
output_meta[i].0,
&out_shapes[i],
&out_strides[i],
device,
)
})
.collect();
{
let guard = kernel.lock().expect("cached kernel mutex poisoned");
guard.execute(&input_views, &mut output_views)?;
}
drop(output_views);
drop(input_views);
Ok(outputs)
}
fn infer_output_meta(
&self,
node: &Node,
op_type: &str,
domain: &str,
opset: u64,
inputs: &[&Tensor],
) -> Result<Vec<(DataType, Vec<usize>)>> {
let input_ios: Vec<NodeIo> = inputs
.iter()
.map(|t| {
let shape: Vec<DimExpr> =
t.shape().iter().map(|&d| DimExpr::constant(d as i64)).collect();
NodeIo::typed(TypeInfo::new(t.dtype(), shape))
})
.collect();
let mut opset_imports: HashMap<String, u64> = HashMap::new();
opset_imports.insert(domain.to_string(), opset);
let mut interner = SymbolInterner::new(0);
let out_ios = self.inference.infer_node(
node,
&opset_imports,
input_ios,
MergePolicy::Permissive,
&mut interner,
)?;
let mut meta = Vec::with_capacity(out_ios.len());
for (i, io) in out_ios.iter().enumerate() {
let type_info = io.type_info.as_ref().ok_or_else(|| EagerError::ShapeInference {
op_type: op_type.to_string(),
domain: domain.to_string(),
reason: format!(
"no shape-inference rule resolved output {i} \
(kernel-provided fallback is DEFERRED, EAGER.md §9.2)"
),
})?;
let mut dims = Vec::with_capacity(type_info.shape.len());
for (axis, d) in type_info.shape.iter().enumerate() {
let c = d.as_const().ok_or_else(|| EagerError::ShapeInference {
op_type: op_type.to_string(),
domain: domain.to_string(),
reason: format!("output {i} axis {axis} is symbolic, not allocatable"),
})?;
if c < 0 {
return Err(EagerError::ShapeInference {
op_type: op_type.to_string(),
domain: domain.to_string(),
reason: format!("output {i} axis {axis} has a negative extent {c}"),
});
}
dims.push(c as usize);
}
meta.push((type_info.dtype, dims));
}
Ok(meta)
}
}