use std::ffi::c_void;
use std::sync::{Arc, Mutex};
use cudarc::driver::{LaunchConfig, PushKernelArg};
use onnx_runtime_ep_api::{
DeviceGraphResource, EpError, Kernel, KernelFactory, Result, TensorMut, TensorView,
};
use onnx_runtime_ir::{DataType, Node};
use super::elementwise::{
BroadcastMetadataCache, BroadcastMetadataKey, capture_shape_eligible,
require_matching_capture_signature,
};
use crate::error::{driver_err, not_implemented};
use crate::runtime::{CudaRuntime, cuptr};
const BLOCK: u32 = 256;
fn grid_for(n: usize) -> u32 {
const MAX_BLOCKS: usize = 65_535;
n.div_ceil(BLOCK as usize).clamp(1, MAX_BLOCKS) as u32
}
fn float_suffix(op: &str, dtype: DataType) -> Result<&'static str> {
match dtype {
DataType::Float32 => Ok("f32"),
DataType::Float16 => Ok("f16"),
DataType::BFloat16 => Ok("bf16"),
other => Err(not_implemented(format!(
"{op} with dtype {other:?} (supported: Float32, Float16, BFloat16)"
))),
}
}
const PRELU_SRC: &str = r#"
#if __has_include(<cuda_fp16.h>) && __has_include(<cuda_bf16.h>)
#define NXRT_HAS_CUDA_HALF_HEADERS 1
#include <cuda_fp16.h>
#include <cuda_bf16.h>
#endif
template <typename T> __device__ float load_float(T value);
template <> __device__ float load_float<float>(float value) { return value; }
#ifdef NXRT_HAS_CUDA_HALF_HEADERS
template <> __device__ float load_float<__half>(__half value) { return __half2float(value); }
template <> __device__ float load_float<__nv_bfloat16>(__nv_bfloat16 value) { return __bfloat162float(value); }
#endif
template <typename T> __device__ T store_float(float value);
template <> __device__ float store_float<float>(float value) { return value; }
#ifdef NXRT_HAS_CUDA_HALF_HEADERS
template <> __device__ __half store_float<__half>(float value) { return __float2half_rn(value); }
template <> __device__ __nv_bfloat16 store_float<__nv_bfloat16>(float value) { return __float2bfloat16_rn(value); }
#endif
__device__ __forceinline__ void broadcast_indices(unsigned long long out, const unsigned long long* m, int rank, unsigned long long* xi, unsigned long long* si) {
*xi = 0; *si = 0;
for (int axis = rank - 1; axis >= 0; --axis) {
unsigned long long coord = out % m[axis]; out /= m[axis];
*xi += coord * m[rank + axis]; *si += coord * m[2 * rank + axis];
}
}
#define DEFINE_PRELU(TYPE, SUFFIX) \
extern "C" __global__ void prelu_##SUFFIX(const TYPE* x, const TYPE* slope, TYPE* y, const unsigned long long* m, int rank, const unsigned long long n) { \
for (unsigned long long i = blockIdx.x*blockDim.x + threadIdx.x; i < n; i += (unsigned long long)gridDim.x * blockDim.x) { \
unsigned long long xi, si; broadcast_indices(i, m, rank, &xi, &si); \
float xv = load_float<TYPE>(x[xi]); \
float sv = load_float<TYPE>(slope[si]); \
float r = (xv >= 0.0f) ? xv : (xv * sv); \
y[i] = store_float<TYPE>(r); \
} \
}
DEFINE_PRELU(float, f32)
#ifdef NXRT_HAS_CUDA_HALF_HEADERS
DEFINE_PRELU(__half, f16)
DEFINE_PRELU(__nv_bfloat16, bf16)
#endif
"#;
const PRELU_MODULE: &str = "prelu_float_v1";
#[derive(Clone, Debug, PartialEq, Eq)]
struct PReluCaptureSignature {
dtype: DataType,
shapes: BroadcastMetadataKey,
}
pub struct PReluFactory {
pub runtime: Arc<CudaRuntime>,
}
impl KernelFactory for PReluFactory {
fn create(&self, _node: &Node, _input_shapes: &[Vec<usize>]) -> Result<Box<dyn Kernel>> {
Ok(Box::new(PReluKernel {
runtime: self.runtime.clone(),
metadata: Mutex::new(BroadcastMetadataCache::new(self.runtime.clone())),
last_capture_safe_signature: Mutex::new(None),
capture_seq_independent: false,
}))
}
}
#[derive(Debug)]
pub struct PReluKernel {
runtime: Arc<CudaRuntime>,
metadata: Mutex<BroadcastMetadataCache>,
last_capture_safe_signature: Mutex<Option<PReluCaptureSignature>>,
capture_seq_independent: bool,
}
impl PReluKernel {
fn run(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
const OP: &str = "PRelu";
let mut last_signature = self.last_capture_safe_signature.lock().map_err(|_| {
EpError::KernelFailed("cuda_ep PRelu capture signature lock was poisoned".into())
})?;
let warmed_signature = last_signature.clone();
if inputs.len() != 2 || outputs.len() != 1 {
return Err(EpError::KernelFailed(format!(
"cuda_ep {OP}: expected 2 inputs and 1 output, got {} and {}",
inputs.len(),
outputs.len()
)));
}
let x = &inputs[0];
let slope = &inputs[1];
let suffix = float_suffix(OP, x.dtype)?;
if x.dtype != DataType::Float32 {
self.runtime.require_nvrtc_half_headers(OP)?;
}
if slope.dtype != x.dtype || outputs[0].dtype != x.dtype {
return Err(EpError::KernelFailed(format!(
"cuda_ep {OP}: X/slope/output dtypes must match, got {:?}/{:?}/{:?}",
x.dtype, slope.dtype, outputs[0].dtype
)));
}
if !x.is_contiguous() || !slope.is_contiguous() || !outputs[0].is_contiguous() {
return Err(not_implemented(format!(
"{OP} with a non-contiguous (strided) tensor; \
insert an explicit copy to materialise it before the op"
)));
}
if slope.shape.len() > x.shape.len() {
return Err(EpError::KernelFailed(format!(
"cuda_ep {OP}: slope rank {} exceeds X rank {}",
slope.shape.len(),
x.shape.len()
)));
}
let broadcast =
onnx_runtime_ir::broadcast_shapes(x.shape, slope.shape).map_err(EpError::Ir)?;
if broadcast != x.shape {
return Err(EpError::KernelFailed(format!(
"cuda_ep {OP}: slope shape {:?} is not unidirectionally broadcastable to X shape {:?}",
slope.shape, x.shape
)));
}
if outputs[0].shape != x.shape {
return Err(EpError::KernelFailed(format!(
"cuda_ep {OP}: output shape {:?} must equal X shape {:?}",
outputs[0].shape, x.shape
)));
}
let out_shape = x.shape.to_vec();
let n = outputs[0].numel();
let n_u64 = u64::try_from(n)
.map_err(|_| EpError::KernelFailed(format!("cuda_ep {OP}: {n} elements exceed u64")))?;
let rank = i32::try_from(out_shape.len())
.map_err(|_| EpError::KernelFailed(format!("cuda_ep {OP}: rank exceeds i32")))?;
let current_signature = capture_shape_eligible(self.capture_seq_independent, &out_shape)
.then(|| PReluCaptureSignature {
dtype: x.dtype,
shapes: BroadcastMetadataKey {
a_shape: x.shape.to_vec(),
b_shape: slope.shape.to_vec(),
out_shape: out_shape.clone(),
},
});
require_matching_capture_signature(
&self.runtime,
OP,
warmed_signature.as_ref(),
current_signature.as_ref(),
)?;
let entry = format!("prelu_{suffix}");
let func = self
.runtime
.nvrtc_function(PRELU_MODULE, PRELU_SRC, &entry)?;
let mut metadata = self.metadata.lock().map_err(|_| {
EpError::KernelFailed("cuda_ep PRelu metadata lock was poisoned".into())
})?;
let mut metadata_candidate = metadata.clone();
let metadata_ptr = metadata_candidate.prepare(x.shape, slope.shape, &out_shape)?;
let x_ptr = cuptr(x.data_ptr::<u8>() as *const c_void);
let slope_ptr = cuptr(slope.data_ptr::<u8>() as *const c_void);
let y_ptr = cuptr(outputs[0].data_ptr_mut::<u8>() as *const c_void);
let cfg = LaunchConfig {
grid_dim: (grid_for(n), 1, 1),
block_dim: (BLOCK, 1, 1),
shared_mem_bytes: 0,
};
let stream = self.runtime.stream();
let mut builder = stream.launch_builder(&func);
builder
.arg(&x_ptr)
.arg(&slope_ptr)
.arg(&y_ptr)
.arg(&metadata_ptr)
.arg(&rank)
.arg(&n_u64);
unsafe { builder.launch(cfg) }.map_err(|e| driver_err(&format!("launch {entry}"), e))?;
if !self.runtime.is_capturing()? {
*metadata = metadata_candidate;
}
*last_signature = current_signature;
Ok(())
}
}
impl Kernel for PReluKernel {
fn execute(&self, inputs: &[TensorView], outputs: &mut [TensorMut]) -> Result<()> {
self.run(inputs, outputs)
}
fn supports_strided_input(&self, _idx: usize) -> bool {
false
}
fn device_graph_resources(&self) -> Vec<DeviceGraphResource> {
self.metadata
.lock()
.ok()
.and_then(|metadata| metadata.device_graph_resource())
.into_iter()
.collect()
}
fn capture_support(&self) -> onnx_runtime_ep_api::CaptureSupport {
match self.last_capture_safe_signature.lock() {
Ok(signature) if signature.is_some() => onnx_runtime_ep_api::CaptureSupport::Supported,
Ok(_) => onnx_runtime_ep_api::CaptureSupport::unsupported(
"PRelu broadcast shape/dtype signature does not match the warmed capture signature",
),
Err(_) => onnx_runtime_ep_api::CaptureSupport::unsupported(
"PRelu capture signature is unavailable because its state lock was poisoned",
),
}
}
fn set_capture_seq_independent(&mut self, seq_independent: bool) {
self.capture_seq_independent = seq_independent;
}
}
pub(crate) fn unsupported_reason(node: &Node, input_dtypes: &[DataType]) -> Option<String> {
if node.inputs.len() != 2
|| node.outputs.len() != 1
|| node.inputs.iter().any(Option::is_none)
|| input_dtypes.len() != 2
{
return Some(format!(
"PRelu: requires 2 present inputs and 1 output, got {} inputs and {} outputs",
node.inputs.len(),
node.outputs.len()
));
}
if !matches!(
input_dtypes[0],
DataType::Float32 | DataType::Float16 | DataType::BFloat16
) {
return Some(format!(
"PRelu: X dtype {:?} unsupported on CUDA EP; expected Float32, Float16, or BFloat16",
input_dtypes[0]
));
}
if input_dtypes[1] != input_dtypes[0] {
return Some(format!(
"PRelu: slope dtype {:?} must match X dtype {:?} on CUDA EP",
input_dtypes[1], input_dtypes[0]
));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prelu_entry_points_are_present_in_source() {
for expansion in [
"DEFINE_PRELU(float, f32)",
"DEFINE_PRELU(__half, f16)",
"DEFINE_PRELU(__nv_bfloat16, bf16)",
] {
assert!(
PRELU_SRC.contains(expansion),
"missing NVRTC generator {expansion}"
);
}
}
#[test]
fn claim_rejects_mismatched_slope_dtype() {
let node = Node::new(
onnx_runtime_ir::NodeId(0),
"PRelu",
vec![
Some(onnx_runtime_ir::ValueId(0)),
Some(onnx_runtime_ir::ValueId(1)),
],
vec![onnx_runtime_ir::ValueId(2)],
);
let reason = unsupported_reason(&node, &[DataType::Float16, DataType::Float32]);
assert!(reason.is_some());
}
#[test]
fn claim_accepts_matching_float_dtypes() {
let node = Node::new(
onnx_runtime_ir::NodeId(0),
"PRelu",
vec![
Some(onnx_runtime_ir::ValueId(0)),
Some(onnx_runtime_ir::ValueId(1)),
],
vec![onnx_runtime_ir::ValueId(2)],
);
assert!(unsupported_reason(&node, &[DataType::BFloat16, DataType::BFloat16]).is_none());
}
}