use onnx_runtime_ep_api::{EpError, Kernel, KernelFactory, Result, TensorMut, TensorView};
use onnx_runtime_ir::Node;
use super::{check_arity, to_dense_f32, write_dense_f32};
use crate::strided::numel;
pub struct SoftmaxKernel {
axis: i64,
coerce_2d: bool,
}
pub struct SoftmaxFactory;
pub struct SoftmaxLegacyFactory;
impl KernelFactory for SoftmaxFactory {
fn create(&self, node: &Node, _shapes: &[Vec<usize>]) -> Result<Box<dyn Kernel>> {
let axis = node.attr("axis").and_then(|a| a.as_int()).unwrap_or(1);
Ok(Box::new(SoftmaxKernel {
axis,
coerce_2d: false,
}))
}
}
impl KernelFactory for SoftmaxLegacyFactory {
fn create(&self, node: &Node, _shapes: &[Vec<usize>]) -> Result<Box<dyn Kernel>> {
let axis = node.attr("axis").and_then(|a| a.as_int()).unwrap_or(1);
Ok(Box::new(SoftmaxKernel {
axis,
coerce_2d: true,
}))
}
}
pub(crate) fn softmax_slices(
x: &[f32],
out: &mut [f32],
outer: usize,
axis_dim: usize,
inner: usize,
) {
for o in 0..outer {
for i in 0..inner {
let base = o * axis_dim * inner + i;
let mut max = f32::NEG_INFINITY;
for a in 0..axis_dim {
let v = x[base + a * inner];
if v > max {
max = v;
}
}
let mut sum = 0.0f32;
for a in 0..axis_dim {
let e = (x[base + a * inner] - max).exp();
out[base + a * inner] = e;
sum += e;
}
let inv = 1.0 / sum;
for a in 0..axis_dim {
out[base + a * inner] *= inv;
}
}
}
}
impl Kernel for SoftmaxKernel {
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
check_arity("Softmax", inputs, outputs, 1, 1, 1)?;
let x = to_dense_f32(&inputs[0])?;
let shape = inputs[0].shape;
let rank = shape.len();
if rank == 0 {
return Err(EpError::KernelFailed(
"Softmax: input 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!(
"Softmax: axis {} out of range for rank {rank}",
self.axis
)));
}
let axis = axis as usize;
let mut out = vec![0.0f32; numel(shape)];
if self.coerce_2d {
let rows: usize = shape[..axis].iter().product();
let cols: usize = shape[axis..].iter().product();
softmax_slices(&x, &mut out, rows, cols, 1);
} else {
let axis_dim = shape[axis];
let outer: usize = shape[..axis].iter().product();
let inner: usize = shape[axis + 1..].iter().product();
softmax_slices(&x, &mut out, outer, axis_dim, inner);
}
write_dense_f32(&mut outputs[0], &out)
}
fn supports_strided_input(&self, _input_idx: usize) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernels::testutil::Owned;
fn run(axis: i64, x: &Owned, out: &mut Owned) {
SoftmaxKernel {
axis,
coerce_2d: false,
}
.execute(&[x.view()], &mut [out.view_mut()])
.unwrap();
}
fn run_legacy(axis: i64, x: &Owned, out: &mut Owned) {
SoftmaxKernel {
axis,
coerce_2d: true,
}
.execute(&[x.view()], &mut [out.view_mut()])
.unwrap();
}
fn approx(a: &[f32], b: &[f32]) {
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b) {
assert!((x - y).abs() < 1e-6, "{a:?} vs {b:?}");
}
}
#[test]
fn softmax_last_axis_2d() {
let x = Owned::f32(&[2, 3], &[1., 2., 3., 1., 2., 3.]);
let mut out = Owned::zeros_f32(&[2, 3]);
run(1, &x, &mut out);
let e = [0.090_030_57, 0.244_728_47, 0.665_240_96];
let mut want = e.to_vec();
want.extend_from_slice(&e);
approx(&out.to_f32(), &want);
let r = out.to_f32();
assert!((r[0] + r[1] + r[2] - 1.0).abs() < 1e-6);
}
#[test]
fn softmax_axis0() {
let x = Owned::f32(&[2, 2], &[1., 2., 1., 2.]);
let mut out = Owned::zeros_f32(&[2, 2]);
run(0, &x, &mut out);
approx(&out.to_f32(), &[0.5, 0.5, 0.5, 0.5]);
}
#[test]
fn softmax_negative_axis() {
let x = Owned::f32(&[1, 3], &[0., 0., 0.]);
let mut out = Owned::zeros_f32(&[1, 3]);
run(-1, &x, &mut out);
approx(&out.to_f32(), &[1. / 3., 1. / 3., 1. / 3.]);
}
#[test]
fn softmax_numerically_stable_large_values() {
let x = Owned::f32(&[1, 3], &[1000.0, 1001.0, 1002.0]);
let mut out = Owned::zeros_f32(&[1, 3]);
run(1, &x, &mut out);
let r = out.to_f32();
assert!(r.iter().all(|v| v.is_finite()));
assert!((r.iter().sum::<f32>() - 1.0).abs() < 1e-6);
approx(&r, &[0.090_030_57, 0.244_728_47, 0.665_240_96]);
}
#[test]
fn softmax_batched_last_axis_4d() {
let x = Owned::f32(&[1, 1, 2, 2], &[1., 2., 3., 4.]);
let mut out = Owned::zeros_f32(&[1, 1, 2, 2]);
run(-1, &x, &mut out);
let r = out.to_f32();
approx(&r, &[0.268_941_43, 0.731_058_6, 0.268_941_43, 0.731_058_6]);
}
#[test]
fn softmax_opset12_axis0_coerces_to_single_row() {
let x = Owned::f32(&[2, 2], &[1., 2., 3., 4.]);
let mut out = Owned::zeros_f32(&[2, 2]);
run_legacy(0, &x, &mut out);
let r = out.to_f32();
approx(&r, &[0.032_058_6, 0.087_144_32, 0.236_882_82, 0.643_914_2]);
assert!((r.iter().sum::<f32>() - 1.0).abs() < 1e-6);
}
#[test]
fn softmax_opset12_differs_from_opset13_when_axis_not_last() {
let x = Owned::f32(&[2, 2], &[1., 2., 3., 4.]);
let mut per_axis = Owned::zeros_f32(&[2, 2]);
let mut legacy = Owned::zeros_f32(&[2, 2]);
run(0, &x, &mut per_axis);
run_legacy(0, &x, &mut legacy);
approx(
&per_axis.to_f32(),
&[0.119_202_92, 0.119_202_92, 0.880_797_1, 0.880_797_1],
);
let (a, b) = (per_axis.to_f32(), legacy.to_f32());
assert!(a.iter().zip(&b).any(|(x, y)| (x - y).abs() > 1e-3));
}
#[test]
fn softmax_opset12_matches_opset13_on_last_axis() {
let x = Owned::f32(&[2, 3], &[1., 2., 3., 4., 5., 6.]);
let mut per_axis = Owned::zeros_f32(&[2, 3]);
let mut legacy = Owned::zeros_f32(&[2, 3]);
run(-1, &x, &mut per_axis);
run_legacy(-1, &x, &mut legacy);
approx(&per_axis.to_f32(), &legacy.to_f32());
}
}