use fuel_ir::Shape;
use fuel_ir::error::Result;
use fuel_tensor::lazy::Tensor;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct PReLU {
pub weight: Arc<[f32]>,
pub num_parameters: usize,
}
impl PReLU {
pub fn new(weight: Arc<[f32]>, num_parameters: usize) -> Result<Self> {
if weight.len() != num_parameters {
return Err(fuel_ir::error::Error::Msg(format!(
"PReLU::new: weight length {} != num_parameters {}",
weight.len(),
num_parameters,
))
.bt());
}
if num_parameters == 0 {
return Err(fuel_ir::error::Error::Msg(
"PReLU::new: num_parameters must be >= 1".to_string(),
)
.bt());
}
Ok(Self {
weight,
num_parameters,
})
}
pub fn scalar(alpha: f32) -> Self {
let w: Arc<[f32]> = Arc::<[f32]>::from(vec![alpha]);
Self {
weight: w,
num_parameters: 1,
}
}
pub fn per_channel_default(c: usize) -> Result<Self> {
if c == 0 {
return Err(fuel_ir::error::Error::Msg(
"PReLU::per_channel_default: c must be >= 1".to_string(),
)
.bt());
}
let w: Arc<[f32]> = Arc::<[f32]>::from(vec![0.25_f32; c]);
Ok(Self {
weight: w,
num_parameters: c,
})
}
pub fn forward(&self, x: &Tensor) -> Result<Tensor> {
let weight = if self.num_parameters == 1 {
x.const_f32_like(Arc::clone(&self.weight), Shape::from_dims(&[]))?
} else {
let dims = x.shape();
let dims = dims.dims();
if dims.len() < 2 {
return Err(fuel_ir::error::Error::Msg(format!(
"PReLU::forward: per-channel form requires rank >= 2, \
got shape {dims:?}",
))
.bt());
}
let c = dims[1];
if c != self.num_parameters {
return Err(fuel_ir::error::Error::Msg(format!(
"PReLU::forward: channel-axis size {c} != \
num_parameters {} (input shape {dims:?})",
self.num_parameters,
))
.bt());
}
let mut bshape: Vec<usize> = vec![1; dims.len()];
bshape[1] = c;
x.const_f32_like(Arc::clone(&self.weight), Shape::from_dims(&bshape))?
};
let pos = x.relu();
let zero = x.const_f32_like(Arc::<[f32]>::from(vec![0.0_f32]), Shape::from_dims(&[]))?;
let neg = x.minimum(&broadcast_zero_like(&zero, x)?)?;
let weighted_neg = neg.broadcast_mul(&weight)?;
pos.add(&weighted_neg)
}
pub fn load_from_mmapped(
st: &fuel_loaders::safetensors::MmapedSafetensors,
prefix: &str,
num_parameters: Option<usize>,
) -> Result<Self> {
use fuel_tensor::lazy::load_tensor_as_f32;
let n = num_parameters.unwrap_or(1);
let w = load_tensor_as_f32(st, &format!("{prefix}.weight"))?;
if w.len() != n {
fuel_ir::bail!("{prefix}.weight: {} elements, expected {n}", w.len(),);
}
let weight: Arc<[f32]> = Arc::from(w);
Ok(PReLU {
weight,
num_parameters: n,
})
}
}
fn broadcast_zero_like(zero_scalar: &Tensor, like: &Tensor) -> Result<Tensor> {
zero_scalar.broadcast_to(like.shape())
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_tensor::Device;
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() <= eps
}
#[test]
fn prelu_scalar_matches_textbook_formula() {
let device = Device::cpu();
let act = PReLU::scalar(0.1);
let x = Tensor::from_f32(
vec![-2.0_f32, -1.0, 0.0, 1.0, 2.0],
Shape::from_dims(&[5]),
&device,
)
.unwrap();
let y = act.forward(&x).unwrap().realize_f32();
assert_eq!(y.len(), 5);
let expected = [-0.2_f32, -0.1, 0.0, 1.0, 2.0];
for (got, want) in y.iter().zip(expected.iter()) {
assert!(approx_eq(*got, *want, 1e-6), "got {got} expected {want}",);
}
}
#[test]
fn prelu_per_channel_broadcasts_along_dim1() {
let device = Device::cpu();
let weights: Arc<[f32]> = Arc::<[f32]>::from(vec![0.25_f32, 0.5]);
let act = PReLU::new(weights, 2).unwrap();
let x = Tensor::from_f32(
vec![-4.0_f32, -2.0, 0.0, -3.0, 1.0, -1.0],
Shape::from_dims(&[1, 2, 3]),
&device,
)
.unwrap();
let y = act.forward(&x).unwrap().realize_f32();
let expected = [-1.0_f32, -0.5, 0.0, -1.5, 1.0, -0.5];
assert_eq!(y.len(), expected.len());
for (got, want) in y.iter().zip(expected.iter()) {
assert!(approx_eq(*got, *want, 1e-6), "got {got} expected {want}",);
}
}
#[test]
fn prelu_scalar_identity_on_nonnegative_inputs() {
let device = Device::cpu();
let act = PReLU::scalar(0.7);
let x = Tensor::from_f32(
vec![0.0_f32, 0.5, 1.0, 2.5, 100.0],
Shape::from_dims(&[5]),
&device,
)
.unwrap();
let y = act.forward(&x).unwrap().realize_f32();
let expected = [0.0_f32, 0.5, 1.0, 2.5, 100.0];
for (got, want) in y.iter().zip(expected.iter()) {
assert!(approx_eq(*got, *want, 1e-6), "got {got} expected {want}",);
}
}
#[test]
fn prelu_new_rejects_length_mismatch() {
let bad: Arc<[f32]> = Arc::<[f32]>::from(vec![0.25_f32, 0.5]);
let err = PReLU::new(bad, 3).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("weight length") && msg.contains("num_parameters"),
"unexpected error: {msg}",
);
}
#[test]
fn prelu_per_channel_rejects_rank1_input() {
let act = PReLU::per_channel_default(4).unwrap();
let device = Device::cpu();
let x = Tensor::from_f32(
vec![1.0_f32, 2.0, 3.0, 4.0],
Shape::from_dims(&[4]),
&device,
)
.unwrap();
let err = act.forward(&x).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("rank >= 2"), "expected rank-error, got {msg}",);
}
}