use std::ffi::c_void;
use std::sync::Arc;
use cudarc::driver::PushKernelArg;
use onnx_runtime_ep_api::{EpError, Kernel, KernelFactory, Result, TensorMut, TensorView};
use onnx_runtime_ir::{DataType, Node};
use crate::cudnn::{CudnnBufferPair, CudnnSoftmaxMode, TensorDescriptorSpec};
use crate::error::{driver_err, not_implemented};
use crate::runtime::{CudaRuntime, cuptr};
const SOFTMAX_SRC: &str = r#"
extern "C" __global__ void softmax_f32(
const float* x,
float* y,
const int outer,
const int axis_dim,
const int inner)
{
const float NEG_INF = __int_as_float(0xff800000);
const int group = blockIdx.x; // in [0, outer*inner)
const int total = outer * inner;
if (group >= total) return;
const int o = group / inner;
const int i = group % inner;
const size_t base = (size_t)o * axis_dim * inner + i;
extern __shared__ float red[];
const int tid = threadIdx.x;
const int nt = blockDim.x;
// Pass 1: row max (stable-softmax shift).
float local_max = NEG_INF;
for (int a = tid; a < axis_dim; a += nt)
local_max = fmaxf(local_max, x[base + (size_t)a * inner]);
red[tid] = local_max;
__syncthreads();
for (int off = nt >> 1; off > 0; off >>= 1) {
if (tid < off) red[tid] = fmaxf(red[tid], red[tid + off]);
__syncthreads();
}
const float row_max = red[0];
__syncthreads();
// Pass 2: exponentiate (stable) and accumulate the row sum.
float local_sum = 0.0f;
for (int a = tid; a < axis_dim; a += nt) {
const float e = expf(x[base + (size_t)a * inner] - row_max);
y[base + (size_t)a * inner] = e;
local_sum += e;
}
red[tid] = local_sum;
__syncthreads();
for (int off = nt >> 1; off > 0; off >>= 1) {
if (tid < off) red[tid] += red[tid + off];
__syncthreads();
}
const float row_sum = red[0];
__syncthreads();
// Pass 3: normalize (guard a degenerate all-equal / empty row).
const float invs = (row_sum > 0.0f) ? (1.0f / row_sum) : 0.0f;
for (int a = tid; a < axis_dim; a += nt)
y[base + (size_t)a * inner] *= invs;
}
"#;
const SOFTMAX_MODULE: &str = "softmax_f32";
const SOFTMAX_ENTRY: &str = "softmax_f32";
const SOFTMAX_BLOCK: u32 = 256;
pub struct SoftmaxFactory {
pub runtime: Arc<CudaRuntime>,
}
pub struct SoftmaxLegacyFactory {
pub runtime: Arc<CudaRuntime>,
}
impl KernelFactory for SoftmaxFactory {
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(1);
Ok(Box::new(SoftmaxKernel {
axis,
coerce_2d: false,
runtime: self.runtime.clone(),
}))
}
}
impl KernelFactory for SoftmaxLegacyFactory {
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(1);
Ok(Box::new(SoftmaxKernel {
axis,
coerce_2d: true,
runtime: self.runtime.clone(),
}))
}
}
#[derive(Debug)]
pub struct SoftmaxKernel {
axis: i64,
coerce_2d: bool,
runtime: Arc<CudaRuntime>,
}
pub(crate) fn softmax_view(shape: &[usize], axis: usize, coerce_2d: bool) -> (usize, usize, usize) {
if coerce_2d {
let outer: usize = shape[..axis].iter().product();
let axis_dim: usize = shape[axis..].iter().product();
(outer, axis_dim, 1)
} else {
let outer: usize = shape[..axis].iter().product();
let axis_dim = shape[axis];
let inner: usize = shape[axis + 1..].iter().product();
(outer, axis_dim, inner)
}
}
pub(crate) fn cudnn_softmax_spec(
dtype: DataType,
outer: usize,
axis_dim: usize,
inner: usize,
coerce_2d: bool,
) -> Result<(TensorDescriptorSpec, CudnnSoftmaxMode)> {
let dims = [outer, axis_dim, inner, 1];
let strides = [axis_dim * inner, inner, 1, 1];
let mode = if coerce_2d {
CudnnSoftmaxMode::Instance
} else {
CudnnSoftmaxMode::Channel
};
Ok((TensorDescriptorSpec::new(dtype, &dims, &strides)?, mode))
}
pub(crate) fn resolve_axis(op: &str, axis: i64, rank: usize) -> Result<usize> {
let a = if axis < 0 { axis + rank as i64 } else { axis };
if a < 0 || a as usize >= rank {
return Err(EpError::KernelFailed(format!(
"cuda_ep {op}: axis {axis} is out of range for a rank-{rank} input; \
axis must lie in [-{rank}, {rank})"
)));
}
Ok(a as usize)
}
impl SoftmaxKernel {
fn run(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
if inputs.len() != 1 || outputs.len() != 1 {
return Err(EpError::KernelFailed(format!(
"cuda_ep Softmax: expected 1 input and 1 output, got {} and {}",
inputs.len(),
outputs.len()
)));
}
let x = &inputs[0];
if !matches!(
x.dtype,
DataType::Float32 | DataType::Float16 | DataType::BFloat16
) {
return Err(not_implemented(format!(
"Softmax with input dtype {:?} (cuDNN supports f32/f16/bf16)",
x.dtype
)));
}
if outputs[0].dtype != x.dtype {
return Err(EpError::KernelFailed(format!(
"cuda_ep Softmax: output dtype {:?} must equal input dtype {:?}",
outputs[0].dtype, x.dtype
)));
}
if !x.is_contiguous() || !outputs[0].is_contiguous() {
return Err(not_implemented(
"Softmax with a non-contiguous (strided) input/output; \
insert an explicit copy to materialise it before the op",
));
}
let rank = x.shape.len();
if rank == 0 {
return Err(EpError::KernelFailed(
"cuda_ep Softmax: input must have rank >= 1".into(),
));
}
if outputs[0].shape != x.shape {
return Err(EpError::KernelFailed(format!(
"cuda_ep Softmax: output shape {:?} must equal input shape {:?}",
outputs[0].shape, x.shape
)));
}
let axis = resolve_axis("Softmax", self.axis, rank)?;
let (outer, axis_dim, inner) = softmax_view(x.shape, axis, self.coerce_2d);
let groups = outer * inner;
if groups == 0 || axis_dim == 0 {
return Ok(());
}
if self.runtime.cudnn().is_available() {
let (spec, mode) = cudnn_softmax_spec(x.dtype, outer, axis_dim, inner, self.coerce_2d)?;
let x_ptr = cuptr(x.data_ptr::<u8>() as *const c_void);
let y_ptr = cuptr(outputs[0].data_ptr_mut::<u8>() as *const c_void);
self.runtime.cudnn().with_handle(|handle| {
handle.softmax(
&spec,
mode,
CudnnBufferPair {
input: x_ptr,
output: y_ptr,
input_numel: x.numel(),
output_numel: outputs[0].numel(),
},
)
})?;
return self.runtime.synchronize();
}
if x.dtype != DataType::Float32 {
return self.runtime.cudnn().with_handle(|_| Ok(()));
}
self.run_nvrtc_f32(x, outputs, outer, axis_dim, inner, groups)
}
fn run_nvrtc_f32(
&self,
x: &TensorView,
outputs: &mut [TensorMut],
outer: usize,
axis_dim: usize,
inner: usize,
groups: usize,
) -> Result<()> {
let (outer_i, axis_i, inner_i) = (
i32::try_from(outer).map_err(|_| dim_overflow("outer", outer))?,
i32::try_from(axis_dim).map_err(|_| dim_overflow("axis_dim", axis_dim))?,
i32::try_from(inner).map_err(|_| dim_overflow("inner", inner))?,
);
let groups_u = u32::try_from(groups).map_err(|_| dim_overflow("groups", groups))?;
let x_ptr = cuptr(x.data_ptr::<u8>() as *const c_void);
let y_ptr = cuptr(outputs[0].data_ptr_mut::<u8>() as *const c_void);
let func = self
.runtime
.nvrtc_function(SOFTMAX_MODULE, SOFTMAX_SRC, SOFTMAX_ENTRY)?;
let cfg = self.runtime.reduction_launch_config(
&func,
groups_u,
SOFTMAX_BLOCK,
std::mem::size_of::<f32>() as u32,
)?;
let stream = self.runtime.stream();
let mut builder = stream.launch_builder(&func);
builder
.arg(&x_ptr)
.arg(&y_ptr)
.arg(&outer_i)
.arg(&axis_i)
.arg(&inner_i);
unsafe { builder.launch(cfg) }.map_err(|e| driver_err("launch softmax_f32", e))?;
self.runtime.synchronize()
}
}
fn dim_overflow(name: &str, v: usize) -> EpError {
EpError::KernelFailed(format!(
"cuda_ep Softmax: {name} ({v}) exceeds the i32 kernel bound"
))
}
impl Kernel for SoftmaxKernel {
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
self.run(inputs, outputs)
}
fn supports_strided_input(&self, _idx: usize) -> bool {
false
}
fn capture_support(&self) -> onnx_runtime_ep_api::CaptureSupport {
onnx_runtime_ep_api::CaptureSupport::Supported
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_point_present_in_source() {
assert!(SOFTMAX_SRC.contains(SOFTMAX_ENTRY));
}
#[test]
fn v13_view_is_single_axis() {
assert_eq!(softmax_view(&[2, 3, 4], 1, false), (2, 3, 4));
assert_eq!(softmax_view(&[2, 3, 4], 2, false), (6, 4, 1));
}
#[test]
fn legacy_view_coerces_to_2d() {
assert_eq!(softmax_view(&[2, 3, 4], 1, true), (2, 12, 1));
assert_eq!(
softmax_view(&[2, 3, 4], 2, true),
softmax_view(&[2, 3, 4], 2, false)
);
}
#[test]
fn cudnn_layout_selects_onnx_semantics() {
let (v13, mode) = cudnn_softmax_spec(DataType::Float16, 2, 3, 4, false).unwrap();
assert_eq!(mode, CudnnSoftmaxMode::Channel);
assert_eq!(v13.dims(), &[2, 3, 4, 1]);
assert_eq!(v13.strides(), &[12, 4, 1, 1]);
let (legacy, mode) = cudnn_softmax_spec(DataType::BFloat16, 2, 12, 1, true).unwrap();
assert_eq!(mode, CudnnSoftmaxMode::Instance);
assert_eq!(legacy.dims(), &[2, 12, 1, 1]);
assert_eq!(legacy.strides(), &[12, 1, 1, 1]);
}
#[test]
fn resolve_axis_handles_negatives_and_rejects_out_of_range() {
assert_eq!(resolve_axis("Softmax", -1, 3).unwrap(), 2);
assert_eq!(resolve_axis("Softmax", 0, 3).unwrap(), 0);
let e = resolve_axis("Softmax", 3, 3).unwrap_err();
let msg = format!("{e}");
assert!(msg.contains("out of range"), "{msg}");
assert!(msg.contains("axis 3"), "{msg}");
}
}