use fuel_ir::error::Result;
use fuel_ir::{DType, Shape};
use fuel_tensor::lazy::Tensor;
use std::sync::Arc;
#[derive(Copy, Clone, Debug)]
pub struct Dropout {
drop_p: f64,
}
impl Dropout {
pub fn new(drop_p: f64) -> Self {
Self { drop_p }
}
pub fn drop_p(&self) -> f64 {
self.drop_p
}
pub fn forward(&self, x: &Tensor, train: bool) -> Result<Tensor> {
if !train {
return Ok(x.clone());
}
let seed = (x.graph_tensor() as *const _ as usize) as u64;
self.forward_with_seed(x, seed)
}
pub fn forward_with_seed(&self, x: &Tensor, seed: u64) -> Result<Tensor> {
if !(0.0..1.0).contains(&self.drop_p) {
return Err(fuel_ir::error::Error::Msg(format!(
"dropout: drop_p must be in [0, 1), got {}",
self.drop_p,
))
.bt());
}
if self.drop_p == 0.0 {
return Ok(x.clone());
}
if x.dtype() != DType::F32 {
return Err(fuel_ir::error::Error::Msg(format!(
"dropout: v1 only supports F32 inputs, got {:?} (graph-level \
BernoulliMask primitive lands later)",
x.dtype(),
))
.bt());
}
let shape = x.shape();
let n = shape.elem_count();
let mask_shape = Shape::from_dims(shape.dims());
let mask = build_bernoulli_mask(n, self.drop_p, seed);
let mask_t = x.const_f32_like(Arc::<[f32]>::from(mask), mask_shape)?;
x.mul(&mask_t)
}
}
fn build_bernoulli_mask(n: usize, drop_p: f64, seed: u64) -> Vec<f32> {
let keep_p = 1.0 - drop_p;
let scale = (1.0 / keep_p) as f32;
let threshold = drop_p; let mut state = seed.wrapping_add(0x9E3779B97F4A7C15);
let mut out = Vec::with_capacity(n);
for _ in 0..n {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^= z >> 31;
let u = ((z >> 11) as f64) * (1.0 / ((1u64 << 53) as f64));
let m = if u < threshold { 0.0_f32 } else { scale };
out.push(m);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_tensor::Device;
#[test]
fn forward_eval_is_identity() {
let device = Device::cpu();
let data: Vec<f32> = vec![1.0, -2.0, 3.0, -4.0, 5.0, -6.0];
let x = Tensor::from_f32(data.clone(), Shape::from_dims(&[6]), &device).unwrap();
let drop = Dropout::new(0.5);
let y = drop.forward(&x, false).unwrap();
let out = y.realize_f32();
assert_eq!(out, data, "eval-mode dropout must be the identity");
}
#[test]
fn forward_train_zeros_or_scales_each_element() {
let device = Device::cpu();
let data: Vec<f32> = (1..=128).map(|i| i as f32).collect();
let x = Tensor::from_f32(data.clone(), Shape::from_dims(&[128]), &device).unwrap();
let drop = Dropout::new(0.5);
let y = drop.forward_with_seed(&x, 0xDEADBEEF).unwrap();
let out = y.realize_f32();
assert_eq!(out.len(), data.len());
let mut zeros = 0usize;
let mut keeps = 0usize;
for (i, (&o, &d)) in out.iter().zip(data.iter()).enumerate() {
if o == 0.0 {
zeros += 1;
} else {
assert!(
(o - 2.0 * d).abs() < 1e-6,
"elem {i}: expected 0.0 or {} got {o}",
2.0 * d,
);
keeps += 1;
}
}
assert!(
zeros > 0 && keeps > 0,
"drop_p=0.5 over 128 elems should produce a mix \
(zeros={zeros}, keeps={keeps})",
);
}
#[test]
fn forward_train_preserves_expected_value_in_aggregate() {
let device = Device::cpu();
let n = 4096;
let data: Vec<f32> = vec![1.0; n];
let x = Tensor::from_f32(data.clone(), Shape::from_dims(&[n]), &device).unwrap();
let drop = Dropout::new(0.3);
let y = drop.forward_with_seed(&x, 0x5EED_5EED).unwrap();
let out = y.realize_f32();
let in_sum: f32 = data.iter().copied().sum();
let out_sum: f32 = out.iter().copied().sum();
let ratio = out_sum / in_sum;
assert!(
(ratio - 1.0).abs() < 0.05,
"expected E[out]/E[in] ≈ 1.0, got ratio={ratio}",
);
}
#[test]
fn same_seed_gives_same_mask() {
let device = Device::cpu();
let data: Vec<f32> = (0..64).map(|i| (i as f32) * 0.5).collect();
let x = Tensor::from_f32(data.clone(), Shape::from_dims(&[64]), &device).unwrap();
let drop = Dropout::new(0.4);
let a = drop.forward_with_seed(&x, 12345).unwrap().realize_f32();
let b = drop.forward_with_seed(&x, 12345).unwrap().realize_f32();
assert_eq!(a, b, "identical seed must yield identical mask");
}
#[test]
fn drop_p_zero_short_circuits() {
let device = Device::cpu();
let data: Vec<f32> = vec![7.0, -3.5, 0.25, 100.0];
let x = Tensor::from_f32(data.clone(), Shape::from_dims(&[4]), &device).unwrap();
let drop = Dropout::new(0.0);
let y = drop.forward_with_seed(&x, 1).unwrap();
assert_eq!(y.realize_f32(), data);
}
#[test]
fn drop_p_out_of_range_errors() {
let device = Device::cpu();
let x = Tensor::from_f32(vec![1.0_f32, 2.0], Shape::from_dims(&[2]), &device).unwrap();
assert!(Dropout::new(1.0).forward_with_seed(&x, 0).is_err());
assert!(Dropout::new(-0.1).forward_with_seed(&x, 0).is_err());
}
}