use candle_core::{CpuStorage, CustomOp1, Layout, Result, Shape, Tensor};
use rayon::prelude::*;
const CHUNK: usize = 8192;
struct ElemOp {
name: &'static str,
f: fn(f32) -> f32,
}
impl CustomOp1 for ElemOp {
fn name(&self) -> &'static str {
self.name
}
fn cpu_fwd(&self, storage: &CpuStorage, layout: &Layout) -> Result<(CpuStorage, Shape)> {
let CpuStorage::F32(src) = storage else {
candle_core::bail!("{}: expects f32", self.name)
};
let Some((start, end)) = layout.contiguous_offsets() else {
candle_core::bail!("{}: expects a contiguous input", self.name)
};
let src = &src[start..end];
let n = src.len();
let mut out: Vec<f32> = Vec::with_capacity(n);
{
let spare = out.spare_capacity_mut();
#[allow(unsafe_code)]
let dst: &mut [f32] =
unsafe { std::slice::from_raw_parts_mut(spare.as_mut_ptr().cast::<f32>(), n) };
let f = self.f;
dst.par_chunks_mut(CHUNK)
.zip(src.par_chunks(CHUNK))
.for_each(|(d, s)| {
for (o, &i) in d.iter_mut().zip(s) {
*o = f(i);
}
});
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((CpuStorage::F32(out), layout.shape().clone()))
}
}
macro_rules! tensor_op {
($fn_name:ident, $kernel:path, $tag:literal, $doc:literal) => {
#[doc = $doc]
pub fn $fn_name(xs: &Tensor) -> Result<Tensor> {
xs.contiguous()?.apply_op1_no_bwd(&ElemOp {
name: $tag,
f: $kernel,
})
}
};
}
tensor_op!(
gelu_erf,
crate::fastmath::gelu_erf,
"ffai-gelu-erf",
"`0.5x(1 + erf(x/sqrt 2))` — the drop-in for candle's `.gelu_erf()`."
);
tensor_op!(
gelu_tanh,
crate::fastmath::gelu_tanh,
"ffai-gelu-tanh",
"`gelu_pytorch_tanh` — the drop-in for candle's `.gelu()`."
);
tensor_op!(
tanh,
crate::fastmath::tanh,
"ffai-tanh",
"`tanh(x)` — the drop-in for candle's `.tanh()`."
);
tensor_op!(
silu,
crate::fastmath::silu,
"ffai-silu",
"`x * sigmoid(x)` — the drop-in for candle's `.silu()`."
);
tensor_op!(
erf,
crate::fastmath::erf,
"ffai-erf",
"`erf(x)` — the drop-in for candle's `.erf()`."
);
#[cfg(test)]
mod tests {
use super::*;
use candle_core::{DType, Device};
#[test]
fn every_op_tracks_candles() {
let d = Device::Cpu;
let xs = Tensor::rand(-8.0f32, 8.0, (3, 4099), &d).expect("xs");
assert_ne!(4099 % CHUNK, 0, "the tail must not be a whole chunk");
let cases: Vec<(&str, Tensor, Tensor)> = vec![
(
"gelu_erf",
gelu_erf(&xs).expect("ours"),
xs.gelu_erf().expect("candle"),
),
(
"gelu_tanh",
gelu_tanh(&xs).expect("ours"),
xs.gelu().expect("candle"),
),
("tanh", tanh(&xs).expect("ours"), xs.tanh().expect("candle")),
("silu", silu(&xs).expect("ours"), xs.silu().expect("candle")),
("erf", erf(&xs).expect("ours"), xs.erf().expect("candle")),
];
for (name, a, b) in cases {
assert_eq!(a.dims(), b.dims(), "{name}: shape");
let (av, bv) = (
a.flatten_all().expect("a").to_vec1::<f32>().expect("av"),
b.flatten_all().expect("b").to_vec1::<f32>().expect("bv"),
);
let worst = av
.iter()
.zip(&bv)
.map(|(x, y)| (x - y).abs())
.fold(0.0f32, f32::max);
eprintln!(" {name:<10} worst abs {worst:.3e}");
assert!(worst < 1e-4, "{name} differs from candle by {worst:.3e}");
}
}
#[test]
fn the_two_gelus_stay_different() {
let d = Device::Cpu;
let xs = Tensor::rand(-4.0f32, 4.0, 2048, &d).expect("xs");
let a = gelu_erf(&xs)
.expect("erf")
.flatten_all()
.expect("f")
.to_vec1::<f32>()
.expect("v");
let b = gelu_tanh(&xs)
.expect("tanh")
.flatten_all()
.expect("f")
.to_vec1::<f32>()
.expect("v");
let worst = a
.iter()
.zip(&b)
.map(|(x, y)| (x - y).abs())
.fold(0.0f32, f32::max);
assert!(
worst > 1e-4,
"gelu_erf and gelu_tanh differ by only {worst:.3e} — one has been aliased to the other"
);
}
#[test]
fn a_strided_view_is_handled_not_misread() {
let d = Device::Cpu;
let xs = Tensor::rand(-3.0f32, 3.0, (7, 5), &d).expect("xs");
let strided = xs.t().expect("transpose");
assert!(!strided.is_contiguous(), "the test needs a strided view");
let ours = tanh(&strided).expect("strided must be handled");
let theirs = strided.tanh().expect("candle");
assert_eq!(ours.dims(), theirs.dims());
let (a, b) = (
ours.flatten_all().expect("a").to_vec1::<f32>().expect("av"),
theirs
.flatten_all()
.expect("b")
.to_vec1::<f32>()
.expect("bv"),
);
let worst = a
.iter()
.zip(&b)
.map(|(x, y)| (x - y).abs())
.fold(0.0f32, f32::max);
assert!(
worst < 1e-5,
"strided result differs by {worst:.3e} — misread as dense?"
);
}
#[test]
fn an_empty_tensor_does_not_panic() {
let d = Device::Cpu;
let xs = Tensor::zeros((0, 8), DType::F32, &d).expect("xs");
assert_eq!(gelu_erf(&xs).expect("gelu").elem_count(), 0);
assert_eq!(tanh(&xs).expect("tanh").elem_count(), 0);
}
}