use candle_core::{IndexOp, Module, Result as CandleResult, Tensor};
use candle_nn::{Conv2dConfig, LayerNorm, Linear, VarBuilder};
use candle_transformers::models::siglip::VisionConfig;
use crate::par::prelude::*;
static KERNELS_PARALLEL: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
pub fn set_kernels_parallel(on: bool) -> bool {
KERNELS_PARALLEL.swap(on, std::sync::atomic::Ordering::Relaxed)
}
fn kernels_parallel() -> bool {
KERNELS_PARALLEL.load(std::sync::atomic::Ordering::Relaxed)
}
struct Embeddings {
w_flat: Tensor,
bias: Option<Tensor>,
channels: usize,
position: Tensor,
patch_size: usize,
}
impl Embeddings {
fn new(cfg: &VisionConfig, vb: VarBuilder) -> CandleResult<Self> {
let patch = candle_nn::conv2d(
cfg.num_channels,
cfg.hidden_size,
cfg.patch_size,
Conv2dConfig {
stride: cfg.patch_size,
..Default::default()
},
vb.pp("patch_embedding"),
)?;
let side = cfg.image_size / cfg.patch_size;
let position =
candle_nn::embedding(side * side, cfg.hidden_size, vb.pp("position_embedding"))?
.embeddings()
.clone();
let w_flat = patch
.weight()
.reshape((cfg.hidden_size, cfg.num_channels * cfg.patch_size * cfg.patch_size))?
.t()?
.contiguous()?;
let bias = patch.bias().cloned();
Ok(Self {
w_flat,
bias,
position,
patch_size: cfg.patch_size,
channels: cfg.num_channels,
})
}
fn forward(&self, pixel_values: &Tensor) -> CandleResult<Tensor> {
let (_b, _c, h, w) = pixel_values.dims4()?;
if h % self.patch_size != 0 || w % self.patch_size != 0 {
candle_core::bail!(
"image {h}x{w} is not a multiple of patch size {}",
self.patch_size
);
}
let (b, c, gh, gw) = (
pixel_values.dim(0)?,
self.channels,
h / self.patch_size,
w / self.patch_size,
);
let p = self.patch_size;
let seq = gh * gw;
let k = c * p * p;
let cols = pixel_values
.reshape((b, c, gh, p, gw, p))?
.permute((0, 2, 4, 1, 3, 5))?
.contiguous()?
.reshape((b * seq, k))?;
crate::cost::copy((b * seq * k) as u64);
let xs = cols.matmul(&self.w_flat)?;
let c_out = self.w_flat.dim(1)?;
crate::cost::matmul(1, (b * seq) as u64, k as u64, c_out as u64);
let out = match self.bias.as_ref() {
Some(bias) => xs.apply_op3_no_bwd(bias, &self.position, &EmbedAddOp)?,
None => xs.reshape((b, seq, c_out))?.broadcast_add(&self.position)?,
};
crate::cost::elementwise((c_out * seq) as u64, 2, 1);
Ok(out)
}
}
pub mod prof {
use std::sync::Mutex;
use crate::clock::Instant;
static ACC: Mutex<Vec<(&'static str, f64)>> = Mutex::new(Vec::new());
pub(crate) fn on() -> bool {
use std::sync::atomic::{AtomicU8, Ordering};
static C: AtomicU8 = AtomicU8::new(u8::MAX);
match C.load(Ordering::Relaxed) {
u8::MAX => {
let v = std::env::var("FFAI_VIS_PROFILE").is_ok_and(|x| x == "1");
C.store(u8::from(v), Ordering::Relaxed);
v
}
v => v == 1,
}
}
pub(crate) fn add(name: &'static str, t: Instant) {
if !on() {
return;
}
let ms = t.elapsed().as_secs_f64() * 1e3;
if let Ok(mut v) = ACC.lock() {
match v.iter_mut().find(|(n, _)| *n == name) {
Some(e) => e.1 += ms,
None => v.push((name, ms)),
}
}
}
#[must_use]
pub fn take() -> Vec<(&'static str, f64)> {
let mut v = ACC.lock().map(|mut g| std::mem::take(&mut *g)).unwrap_or_default();
v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
v
}
}
struct Layer {
ln1: LayerNorm,
qkv: Linear,
qkv_bias: Option<Tensor>,
out_proj: Linear,
out_bias: Option<Tensor>,
ln2: LayerNorm,
fc1: Linear,
fc1_bias: Option<Tensor>,
fc2: Linear,
fc2_bias: Option<Tensor>,
heads: usize,
head_dim: usize,
}
impl Layer {
fn load(cfg: &VisionConfig, vb: &VarBuilder) -> CandleResult<Self> {
let hidden = cfg.hidden_size;
let heads = cfg.num_attention_heads;
let head_dim = hidden / heads;
let attn = vb.pp("self_attn");
let scale = (head_dim as f64).powf(-0.5);
let wq = (attn.get((hidden, hidden), "q_proj.weight")? * scale)?;
let wk = attn.get((hidden, hidden), "k_proj.weight")?;
let wv = attn.get((hidden, hidden), "v_proj.weight")?;
let bq = (attn.get(hidden, "q_proj.bias")? * scale)?;
let bk = attn.get(hidden, "k_proj.bias")?;
let bv = attn.get(hidden, "v_proj.bias")?;
let qkv = Linear::new(
Tensor::cat(&[&wq, &wk, &wv], 0)?.contiguous()?,
Some(Tensor::cat(&[&bq, &bk, &bv], 0)?.contiguous()?),
);
let (qkv, qkv_bias) = split_bias(qkv);
let out = split_bias(candle_nn::linear(hidden, hidden, attn.pp("out_proj"))?);
let fc1 = split_bias(candle_nn::linear(hidden, cfg.intermediate_size, vb.pp("mlp.fc1"))?);
let fc2 = split_bias(candle_nn::linear(cfg.intermediate_size, hidden, vb.pp("mlp.fc2"))?);
Ok(Self {
ln1: candle_nn::layer_norm(hidden, cfg.layer_norm_eps, vb.pp("layer_norm1"))?,
qkv,
qkv_bias,
out_proj: out.0,
out_bias: out.1,
ln2: candle_nn::layer_norm(hidden, cfg.layer_norm_eps, vb.pp("layer_norm2"))?,
fc1: fc1.0,
fc1_bias: fc1.1,
fc2: fc2.0,
fc2_bias: fc2.1,
heads,
head_dim,
})
}
fn forward(&self, xs: &Tensor) -> CandleResult<Tensor> {
let (b, seq, hidden) = xs.dims3()?;
let residual = xs;
let (bu, sq, hd) = (b as u64, seq as u64, hidden as u64);
let heads = self.heads as u64;
let hdim = self.head_dim as u64;
let t = crate::clock::Instant::now();
let normed = layer_norm(&self.ln1, xs)?;
prof::add("ln1+ln2", t);
crate::cost::elementwise(bu * sq * hd, 2, 1);
let t = crate::clock::Instant::now();
let qkv = self.qkv.forward(&normed)?;
prof::add("qkv linear", t);
crate::cost::matmul(1, bu * sq, hd, 3 * hd);
let t = crate::clock::Instant::now();
let packed = match (self.qkv_bias.as_ref(), fuse_bias()) {
(Some(bias), true) => qkv.apply_op2_no_bwd(
bias,
&PackedQkvOp { heads: self.heads, head_dim: self.head_dim },
)?,
(bias, _) => {
let qkv = match bias {
Some(bias) => qkv.broadcast_add(bias)?,
None => qkv,
};
qkv.reshape((b, seq, 3, self.heads, self.head_dim))?
.permute((2, 0, 3, 1, 4))?
.contiguous()?
}
};
crate::cost::copy(3 * bu * sq * hd);
prof::add("packed permute+copy", t);
let q = packed.i(0)?;
let k = packed.i(1)?;
let v = packed.i(2)?;
if head_attn() {
let mut per_head = Vec::with_capacity(self.heads);
for h in 0..self.heads {
let t = crate::clock::Instant::now();
let qh = q.i((.., h))?;
let kh = k.i((.., h))?;
let scores = qh.matmul(&kh.t()?)?;
prof::add("q.k^T", t);
crate::cost::matmul(bu, sq, hdim, sq);
let t = crate::clock::Instant::now();
scores.inplace_op1(&SoftmaxInplace)?;
let probs = scores;
prof::add("softmax", t);
crate::cost::elementwise(bu * sq * sq, 2, 1);
crate::cost::transcendental_vector(bu * sq * sq);
let t = crate::clock::Instant::now();
per_head.push(probs.matmul(&v.i((.., h))?)?);
crate::cost::matmul(bu, sq, sq, hdim);
prof::add("attn.v", t);
}
let t = crate::clock::Instant::now();
let attn = Tensor::stack(&per_head, 1)?
.transpose(1, 2)?
.reshape((b, seq, hidden))?;
crate::cost::copy(bu * sq * hd);
prof::add("attn transpose", t);
return self.finish_attention(residual, &attn, bu, sq, hd);
}
let blk = attn_block();
if late_normalize() && blk > 0 && blk < seq {
let kt = k.t()?;
let mut outs: Vec<Tensor> = Vec::with_capacity(seq.div_ceil(blk));
let mut sums_parts: Vec<Tensor> = Vec::with_capacity(outs.capacity());
let mut start = 0usize;
while start < seq {
let len = blk.min(seq - start);
let t = crate::clock::Instant::now();
let q_blk = q.narrow(2, start, len)?.contiguous()?;
let scores = q_blk.matmul(&kt)?;
crate::cost::matmul(bu * heads, len as u64, hdim, sq);
prof::add("q.k^T", t);
let t = crate::clock::Instant::now();
let rows = (bu * heads) as usize * len;
let mut sums = vec![0f32; rows];
scores.inplace_op1(&SoftmaxExpInplace {
sums: RowSums(sums.as_mut_ptr()),
rows,
})?;
crate::cost::transcendental_vector(bu * heads * len as u64 * sq);
prof::add("softmax", t);
sums_parts.push(Tensor::from_vec(sums, (b, self.heads, len), scores.device())?);
let t = crate::clock::Instant::now();
outs.push(scores.matmul(&v)?);
crate::cost::matmul(bu * heads, len as u64, sq, hdim);
prof::add("attn.v", t);
start += len;
}
let t = crate::clock::Instant::now();
let attn = Tensor::cat(&outs, 2)?;
let sums = Tensor::cat(&sums_parts, 2)?;
let attn = attn.apply_op2_no_bwd(
&sums,
&AttnMergeOp { heads: self.heads, head_dim: self.head_dim },
)?;
crate::cost::copy(bu * sq * hd);
prof::add("attn transpose", t);
return self.finish_attention(residual, &attn, bu, sq, hd);
}
let t = crate::clock::Instant::now();
let scores = q.matmul(&k.t()?)?;
prof::add("q.k^T", t);
crate::cost::matmul(bu * heads, sq, hdim, sq);
let t = crate::clock::Instant::now();
if late_normalize() {
let rows = (bu * heads * sq) as usize;
let mut sums = vec![0f32; rows];
scores.inplace_op1(&SoftmaxExpInplace {
sums: RowSums(sums.as_mut_ptr()),
rows,
})?;
prof::add("softmax", t);
let sums = Tensor::from_vec(sums, (b, self.heads, seq), scores.device())?;
let t = crate::clock::Instant::now();
let attn = scores.matmul(&v)?;
crate::cost::matmul(bu * heads, sq, sq, hdim);
prof::add("attn.v", t);
let t = crate::clock::Instant::now();
let attn = attn.apply_op2_no_bwd(
&sums,
&AttnMergeOp { heads: self.heads, head_dim: self.head_dim },
)?;
crate::cost::copy(bu * sq * hd);
prof::add("attn transpose", t);
return self.finish_attention(residual, &attn, bu, sq, hd);
}
let probs = if inplace_softmax() {
scores.inplace_op1(&SoftmaxInplace)?;
scores
} else {
candle_nn::ops::softmax_last_dim(&scores)?
};
prof::add("softmax", t);
crate::cost::elementwise(bu * heads * sq * sq, 2, 1);
crate::cost::transcendental_vector(bu * heads * sq * sq);
let t = crate::clock::Instant::now();
let attn = probs.matmul(&v)?;
crate::cost::matmul(bu * heads, sq, sq, hdim);
prof::add("attn.v", t);
let t = crate::clock::Instant::now();
let attn = attn.transpose(1, 2)?.reshape((b, seq, hidden))?;
crate::cost::copy(bu * sq * hd);
prof::add("attn transpose", t);
self.finish_attention(residual, &attn, bu, sq, hd)
}
fn finish_attention(
&self,
residual: &Tensor,
attn: &Tensor,
bu: u64,
sq: u64,
hd: u64,
) -> CandleResult<Tensor> {
let (b, seq, hidden) = attn.dims3()?;
let _ = (b, seq, hidden);
let t = crate::clock::Instant::now();
let out = self.out_proj.forward(attn)?;
prof::add("out_proj", t);
crate::cost::matmul(1, bu * sq, hd, hd);
let t = crate::clock::Instant::now();
let xs = match (self.out_bias.as_ref(), fuse_bias()) {
(Some(bias), true) => residual.apply_op3_no_bwd(&out, bias, &AddBiasOp)?,
(Some(bias), false) => (residual + out.broadcast_add(bias)?)?,
(None, _) => (residual + out)?,
};
prof::add("residual+bias", t);
crate::cost::elementwise(bu * sq * hd, 2, 1);
let residual = &xs;
let t = crate::clock::Instant::now();
let normed = layer_norm(&self.ln2, &xs)?;
prof::add("ln1+ln2", t);
crate::cost::elementwise(bu * sq * hd, 2, 1);
let inter = self.fc1.weight().dims()[0] as u64;
let t = crate::clock::Instant::now();
let h = self.fc1.forward(&normed)?;
prof::add("fc1", t);
crate::cost::matmul(1, bu * sq, hd, inter);
let t = crate::clock::Instant::now();
let h = match (self.fc1_bias.as_ref(), fuse_bias()) {
(Some(b), true) => h.apply_op2_no_bwd(b, &GeluBiasOp)?,
(Some(b), false) => gelu_tanh_par(&h.broadcast_add(b)?)?,
(None, _) => gelu_tanh_par(&h)?,
};
prof::add("gelu+bias", t);
let t = crate::clock::Instant::now();
let down = self.fc2.forward(&h)?;
prof::add("fc2", t);
crate::cost::matmul(1, bu * sq, inter, hd);
let t = crate::clock::Instant::now();
let out = match (self.fc2_bias.as_ref(), fuse_bias()) {
(Some(bias), true) => residual.apply_op3_no_bwd(&down, bias, &AddBiasOp)?,
(Some(bias), false) => (residual + down.broadcast_add(bias)?)?,
(None, _) => (residual + down)?,
};
prof::add("residual+bias", t);
crate::cost::elementwise(bu * sq * hd, 2, 1);
Ok(out)
}
}
#[inline(always)]
fn gelu_one(v: f32) -> f32 {
ffai_core::fastmath::gelu_tanh(v)
}
#[inline]
fn gelu_chunk_scalar(chunk: &mut [f32]) {
for x in chunk {
*x = gelu_one(*x);
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn gelu_chunk_avx2(chunk: &mut [f32]) {
for x in chunk {
*x = gelu_one(*x);
}
}
#[inline]
fn softmax_row_scalar(dst: &mut [f32], row: &[f32]) {
let mut max = f32::NEG_INFINITY;
for &x in row {
if x > max {
max = x;
}
}
let mut acc = [0.0f32; 8];
let n = row.len();
let tail = n % 8;
let mut i = 0;
while i + 8 <= n {
for l in 0..8 {
let e = ffai_core::fastmath::exp(row[i + l] - max);
dst[i + l] = e;
acc[l] += e;
}
i += 8;
}
let mut sum = ((acc[0] + acc[1]) + (acc[2] + acc[3])) + ((acc[4] + acc[5]) + (acc[6] + acc[7]));
for k in 0..tail {
let e = ffai_core::fastmath::exp(row[i + k] - max);
dst[i + k] = e;
sum += e;
}
let inv = 1.0 / sum;
for d in dst.iter_mut() {
*d *= inv;
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2,fma")]
unsafe fn softmax_row_avx2(dst: &mut [f32], row: &[f32]) {
softmax_row_scalar(dst, row);
}
#[cfg(target_arch = "x86_64")]
fn have_avx2_cached() -> bool {
use std::sync::atomic::{AtomicU8, Ordering};
static CACHE: AtomicU8 = AtomicU8::new(0); match CACHE.load(Ordering::Relaxed) {
1 => false,
2 => true,
_ => {
let yes = std::arch::is_x86_feature_detected!("avx2")
&& std::arch::is_x86_feature_detected!("fma");
CACHE.store(u8::from(yes) + 1, Ordering::Relaxed);
yes
}
}
}
#[must_use]
pub fn gelu_kernel_name() -> &'static str {
#[cfg(target_arch = "x86_64")]
{
if have_avx2_cached() {
return "avx2+fma";
}
"scalar (no avx2)"
}
#[cfg(not(target_arch = "x86_64"))]
"scalar (non-x86_64)"
}
pub fn gelu_scalar_for_probe(chunk: &mut [f32]) {
gelu_chunk_scalar(chunk);
}
#[cfg(target_arch = "x86_64")]
pub fn gelu_avx2_for_probe(chunk: &mut [f32]) {
assert!(have_avx2_cached(), "AVX2+FMA not available on this CPU");
unsafe { gelu_chunk_avx2(chunk) };
}
#[cfg(not(target_arch = "x86_64"))]
const fn have_avx2_cached() -> bool {
false
}
struct LayerNormOp {
eps: f64,
}
impl candle_core::CustomOp3 for LayerNormOp {
fn name(&self) -> &'static str {
"ffai-layer-norm"
}
fn cpu_fwd(
&self,
s1: &candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
s3: &candle_core::CpuStorage,
l3: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let (x, w, b) = match (s1, s2, s3) {
(
candle_core::CpuStorage::F32(x),
candle_core::CpuStorage::F32(w),
candle_core::CpuStorage::F32(b),
) => (x, w, b),
_ => candle_core::bail!("ffai-layer-norm expects f32"),
};
let (Some((xo, xe)), Some((wo, we)), Some((bo, be))) = (
l1.contiguous_offsets(),
l2.contiguous_offsets(),
l3.contiguous_offsets(),
) else {
candle_core::bail!("ffai-layer-norm expects contiguous inputs")
};
let (x, w, b) = (&x[xo..xe], &w[wo..we], &b[bo..be]);
let width = w.len();
if width == 0 || b.len() != width || x.len() % width != 0 {
candle_core::bail!("ffai-layer-norm: {} not divisible by {width}", x.len());
}
let n = x.len();
let inv_n = 1.0f64 / width as f64;
let eps = self.eps;
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) };
crate::cost::elementwise(n as u64, 2, 1);
let row = |(o, r): (&mut [f32], &[f32])| {
const L: usize = 8;
let (mut sum, mut sq) = ([0.0f32; L], [0.0f32; L]);
let mut it = r.chunks_exact(L);
for c in &mut it {
for i in 0..L {
sum[i] += c[i];
sq[i] += c[i] * c[i];
}
}
for &v in it.remainder() {
sum[0] += v;
sq[0] += v * v;
}
let (sum, sq) = (
f64::from(sum.iter().sum::<f32>()),
f64::from(sq.iter().sum::<f32>()),
);
let mean = sum * inv_n;
#[allow(clippy::suspicious_operation_groupings)]
let var = (sq * inv_n - mean * mean).max(0.0);
let inv_std = 1.0 / (var + eps).sqrt();
let (mean, inv_std) = (mean as f32, inv_std as f32);
for (((o, &v), &w), &b) in o.iter_mut().zip(r).zip(w).zip(b) {
*o = (v - mean) * inv_std * w + b;
}
};
if kernels_parallel() {
dst.par_chunks_mut(width).zip(x.par_chunks(width)).for_each(row);
} else {
dst.chunks_mut(width).zip(x.chunks(width)).for_each(row);
}
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), l1.shape().clone()))
}
}
fn fused_ln() -> bool {
FUSED_LN.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_fused_ln(on: bool) -> bool {
FUSED_LN.swap(on, std::sync::atomic::Ordering::Relaxed)
}
static FUSED_LN: std::sync::LazyLock<std::sync::atomic::AtomicBool> =
std::sync::LazyLock::new(|| {
std::sync::atomic::AtomicBool::new(arm_flag("FFAI_ARGUS_FUSED_LN", false))
});
fn layer_norm(ln: &LayerNorm, x: &Tensor) -> CandleResult<Tensor> {
match (fused_ln(), ln.bias()) {
(true, Some(bias)) => x.apply_op3_no_bwd(ln.weight(), bias, &LayerNormOp { eps: ln.eps() }),
_ => ln.forward(x),
}
}
pub fn layer_norm_for_probe(ln: &LayerNorm, x: &Tensor) -> CandleResult<Tensor> {
layer_norm(ln, x)
}
fn arm_flag(key: &str, default: bool) -> bool {
match std::env::var(key).ok().as_deref() {
Some("1") => true,
Some("0") => false,
_ => default,
}
}
fn attn_block() -> usize {
ATTN_BLOCK.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_attn_block(n: usize) -> usize {
ATTN_BLOCK.swap(n, std::sync::atomic::Ordering::Relaxed)
}
static ATTN_BLOCK: std::sync::LazyLock<std::sync::atomic::AtomicUsize> =
std::sync::LazyLock::new(|| {
std::sync::atomic::AtomicUsize::new(
std::env::var("FFAI_ARGUS_ATTN_BLOCK")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0),
)
});
pub(crate) fn kernels_parallel_for_probe() -> bool {
kernels_parallel()
}
struct EmbedAddOp;
impl candle_core::CustomOp3 for EmbedAddOp {
fn name(&self) -> &'static str {
"ffai-embed-add"
}
fn cpu_fwd(
&self,
s1: &candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
s3: &candle_core::CpuStorage,
l3: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let (x, bias, pos) = match (s1, s2, s3) {
(
candle_core::CpuStorage::F32(x),
candle_core::CpuStorage::F32(b),
candle_core::CpuStorage::F32(p),
) => (x, b, p),
_ => candle_core::bail!("ffai-embed-add expects f32"),
};
let (Some((xo, xe)), Some((bo, be)), Some((po, pe))) = (
l1.contiguous_offsets(),
l2.contiguous_offsets(),
l3.contiguous_offsets(),
) else {
candle_core::bail!("ffai-embed-add expects contiguous inputs")
};
let (x, bias, pos) = (&x[xo..xe], &bias[bo..be], &pos[po..pe]);
let hidden = bias.len();
if hidden == 0 || x.len() % hidden != 0 || pos.len() % hidden != 0 {
candle_core::bail!("ffai-embed-add: {} elems, hidden {hidden}", x.len());
}
let seq = pos.len() / hidden;
let rows = x.len() / hidden;
if seq == 0 || rows % seq != 0 {
candle_core::bail!("ffai-embed-add: {rows} rows not a multiple of seq {seq}");
}
let b = rows / seq;
let n = x.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) };
crate::cost::elementwise(n as u64, 2, 1);
let row = |(i, (o, xr)): (usize, (&mut [f32], &[f32]))| {
let pr = &pos[(i % seq) * hidden..(i % seq + 1) * hidden];
for (((o, &v), &bz), &pz) in o.iter_mut().zip(xr).zip(bias).zip(pr) {
*o = (v + bz) + pz;
}
};
if kernels_parallel() {
dst.par_chunks_mut(hidden).zip(x.par_chunks(hidden)).enumerate().for_each(row);
} else {
dst.chunks_mut(hidden).zip(x.chunks(hidden)).enumerate().for_each(row);
}
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), (b, seq, hidden).into()))
}
}
struct RowSums(*mut f32);
#[allow(unsafe_code)]
unsafe impl Sync for RowSums {}
#[allow(unsafe_code)]
unsafe impl Send for RowSums {}
struct SoftmaxExpInplace {
sums: RowSums,
rows: usize,
}
impl candle_core::InplaceOp1 for SoftmaxExpInplace {
fn name(&self) -> &'static str {
"ffai-softmax-exp-inplace"
}
fn cpu_fwd(
&self,
storage: &mut candle_core::CpuStorage,
layout: &candle_core::Layout,
) -> CandleResult<()> {
let candle_core::CpuStorage::F32(x) = storage else {
candle_core::bail!("ffai-softmax-exp-inplace expects f32")
};
let Some((o, e)) = layout.contiguous_offsets() else {
candle_core::bail!("ffai-softmax-exp-inplace expects a contiguous input")
};
let width = *layout.shape().dims().last().expect("rank >= 1");
if width == 0 || (e - o) != self.rows * width {
candle_core::bail!("ffai-softmax-exp-inplace: {} vs {}x{width}", e - o, self.rows);
}
crate::cost::elementwise((e - o) as u64, 1, 1);
crate::cost::transcendental_vector((e - o) as u64);
let sums = &self.sums;
x[o..e].par_chunks_mut(width).enumerate().for_each(|(r, row)| {
let max = ffai_core::fastmath::max_f32(row);
let sum = ffai_core::fastmath::exp_sub_sum_inplace(row, max);
#[allow(unsafe_code)]
unsafe {
*sums.0.add(r) = sum;
}
});
Ok(())
}
}
struct AttnMergeOp {
heads: usize,
head_dim: usize,
}
impl candle_core::CustomOp2 for AttnMergeOp {
fn name(&self) -> &'static str {
"ffai-attn-merge"
}
fn cpu_fwd(
&self,
s1: &candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let (a, sums) = match (s1, s2) {
(candle_core::CpuStorage::F32(a), candle_core::CpuStorage::F32(b)) => (a, b),
_ => candle_core::bail!("ffai-attn-merge expects f32"),
};
let (Some((ao, ae)), Some((so, se))) = (l1.contiguous_offsets(), l2.contiguous_offsets())
else {
candle_core::bail!("ffai-attn-merge expects contiguous inputs")
};
let (a, sums) = (&a[ao..ae], &sums[so..se]);
let (heads, hd) = (self.heads, self.head_dim);
let hidden = heads * hd;
let n = ae - ao;
if hidden == 0 || n % hidden != 0 || sums.len() * hd != n {
candle_core::bail!("ffai-attn-merge: {n} elems, {} sums", sums.len());
}
let dims = l1.shape().dims4()?;
let (b, seq) = (dims.0, dims.2);
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) };
crate::cost::elementwise(n as u64, 1, 1);
let row = |(i, o): (usize, &mut [f32])| {
let (bb, s) = (i / seq, i % seq);
for h in 0..heads {
let inv = 1.0 / sums[(bb * heads + h) * seq + s];
let src = (bb * heads + h) * seq * hd + s * hd;
for (o, &v) in o[h * hd..(h + 1) * hd].iter_mut().zip(&a[src..src + hd]) {
*o = v * inv;
}
}
};
if kernels_parallel() {
dst.par_chunks_mut(hidden).enumerate().for_each(row);
} else {
dst.chunks_mut(hidden).enumerate().for_each(row);
}
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), (b, seq, hidden).into()))
}
}
fn late_normalize() -> bool {
LATE_NORM.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_late_normalize(on: bool) -> bool {
LATE_NORM.swap(on, std::sync::atomic::Ordering::Relaxed)
}
static LATE_NORM: std::sync::LazyLock<std::sync::atomic::AtomicBool> =
std::sync::LazyLock::new(|| {
std::sync::atomic::AtomicBool::new(arm_flag("FFAI_ARGUS_LATE_NORM", true))
});
struct SoftmaxInplace;
impl candle_core::InplaceOp1 for SoftmaxInplace {
fn name(&self) -> &'static str {
"ffai-softmax-inplace"
}
fn cpu_fwd(
&self,
storage: &mut candle_core::CpuStorage,
layout: &candle_core::Layout,
) -> CandleResult<()> {
let candle_core::CpuStorage::F32(x) = storage else {
candle_core::bail!("ffai-softmax-inplace expects f32")
};
let Some((o, e)) = layout.contiguous_offsets() else {
candle_core::bail!("ffai-softmax-inplace expects a contiguous input")
};
let width = *layout.shape().dims().last().expect("rank >= 1");
if width == 0 {
return Ok(());
}
let rows = &mut x[o..e];
crate::cost::elementwise((e - o) as u64, 2, 1);
crate::cost::transcendental_vector((e - o) as u64);
let row = |r: &mut [f32]| {
let max = ffai_core::fastmath::max_f32(r);
let sum = ffai_core::fastmath::exp_sub_sum_inplace(r, max);
let inv = 1.0 / sum;
for v in r.iter_mut() {
*v *= inv;
}
};
rows.par_chunks_mut(width).for_each(row);
Ok(())
}
}
fn inplace_softmax() -> bool {
INPLACE_SOFTMAX.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_inplace_softmax(on: bool) -> bool {
INPLACE_SOFTMAX.swap(on, std::sync::atomic::Ordering::Relaxed)
}
static INPLACE_SOFTMAX: std::sync::LazyLock<std::sync::atomic::AtomicBool> =
std::sync::LazyLock::new(|| {
std::sync::atomic::AtomicBool::new(arm_flag("FFAI_ARGUS_INPLACE_SOFTMAX", true))
});
fn head_attn() -> bool {
HEAD_ATTN.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_head_attn(on: bool) -> bool {
HEAD_ATTN.swap(on, std::sync::atomic::Ordering::Relaxed)
}
static HEAD_ATTN: std::sync::LazyLock<std::sync::atomic::AtomicBool> =
std::sync::LazyLock::new(|| {
std::sync::atomic::AtomicBool::new(arm_flag("FFAI_ARGUS_HEAD_ATTN", false))
});
fn fuse_bias() -> bool {
FUSE_BIAS.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_fuse_bias(on: bool) -> bool {
FUSE_BIAS.swap(on, std::sync::atomic::Ordering::Relaxed)
}
static FUSE_BIAS: std::sync::LazyLock<std::sync::atomic::AtomicBool> =
std::sync::LazyLock::new(|| {
std::sync::atomic::AtomicBool::new(arm_flag("FFAI_ARGUS_FUSE_BIAS", true))
});
struct PackedQkvOp {
heads: usize,
head_dim: usize,
}
impl candle_core::CustomOp2 for PackedQkvOp {
fn name(&self) -> &'static str {
"ffai-packed-qkv"
}
fn cpu_fwd(
&self,
s1: &candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let (x, bias) = match (s1, s2) {
(candle_core::CpuStorage::F32(a), candle_core::CpuStorage::F32(b)) => (a, b),
_ => candle_core::bail!("ffai-packed-qkv expects f32"),
};
let (Some((xo, xe)), Some((bo, be))) = (l1.contiguous_offsets(), l2.contiguous_offsets())
else {
candle_core::bail!("ffai-packed-qkv expects contiguous inputs")
};
let (x, bias) = (&x[xo..xe], &bias[bo..be]);
let (b, seq, wide) = l1.shape().dims3()?;
let (heads, hd) = (self.heads, self.head_dim);
let hidden = heads * hd;
if wide != 3 * hidden || bias.len() != wide {
candle_core::bail!("ffai-packed-qkv: {wide} != 3*{hidden}, bias {}", bias.len());
}
let n = 3 * b * hidden * seq;
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) };
crate::cost::elementwise(n as u64, 1, 1);
let chunk = |(c, o): (usize, &mut [f32])| {
let (g, rem) = (c / (b * heads), c % (b * heads));
let (bb, h) = (rem / heads, rem % heads);
let col = g * hidden + h * hd;
let bias = &bias[col..col + hd];
let row0 = bb * seq * wide + col;
for (s, o) in o.chunks_mut(hd).enumerate() {
let src = &x[row0 + s * wide..row0 + s * wide + hd];
for ((o, &v), &c) in o.iter_mut().zip(src).zip(bias) {
*o = v + c;
}
}
};
if kernels_parallel() {
dst.par_chunks_mut(seq * hd).enumerate().for_each(chunk);
} else {
dst.chunks_mut(seq * hd).enumerate().for_each(chunk);
}
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), (3, b, heads, seq, hd).into()))
}
}
fn split_bias(l: Linear) -> (Linear, Option<Tensor>) {
let bias = l.bias().cloned();
(Linear::new(l.weight().clone(), None), bias)
}
struct AddBiasOp;
impl candle_core::CustomOp3 for AddBiasOp {
fn name(&self) -> &'static str {
"ffai-add-bias"
}
fn cpu_fwd(
&self,
s1: &candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
s3: &candle_core::CpuStorage,
l3: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let (a, b, bias) = match (s1, s2, s3) {
(
candle_core::CpuStorage::F32(a),
candle_core::CpuStorage::F32(b),
candle_core::CpuStorage::F32(c),
) => (a, b, c),
_ => candle_core::bail!("ffai-add-bias expects f32"),
};
let (Some((ao, ae)), Some((bo, be)), Some((co, ce))) = (
l1.contiguous_offsets(),
l2.contiguous_offsets(),
l3.contiguous_offsets(),
) else {
candle_core::bail!("ffai-add-bias expects contiguous inputs")
};
let (a, b, bias) = (&a[ao..ae], &b[bo..be], &bias[co..ce]);
let width = bias.len();
if width == 0 || a.len() != b.len() || a.len() % width != 0 {
candle_core::bail!("ffai-add-bias: {} vs {} width {width}", a.len(), b.len());
}
let n = a.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) };
crate::cost::elementwise(n as u64, 2, 1);
let row = |((o, x), y): ((&mut [f32], &[f32]), &[f32])| {
for (((o, &x), &y), &c) in o.iter_mut().zip(x).zip(y).zip(bias) {
*o = x + (y + c);
}
};
if kernels_parallel() {
dst.par_chunks_mut(width)
.zip(a.par_chunks(width))
.zip(b.par_chunks(width))
.for_each(row);
} else {
dst.chunks_mut(width).zip(a.chunks(width)).zip(b.chunks(width)).for_each(row);
}
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), l1.shape().clone()))
}
}
struct GeluBiasOp;
impl candle_core::CustomOp2 for GeluBiasOp {
fn name(&self) -> &'static str {
"ffai-gelu-bias"
}
fn cpu_fwd(
&self,
s1: &candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let (x, bias) = match (s1, s2) {
(candle_core::CpuStorage::F32(a), candle_core::CpuStorage::F32(b)) => (a, b),
_ => candle_core::bail!("ffai-gelu-bias expects f32"),
};
let (Some((xo, xe)), Some((bo, be))) = (l1.contiguous_offsets(), l2.contiguous_offsets())
else {
candle_core::bail!("ffai-gelu-bias expects contiguous inputs")
};
let (x, bias) = (&x[xo..xe], &bias[bo..be]);
let width = bias.len();
if width == 0 || x.len() % width != 0 {
candle_core::bail!("ffai-gelu-bias: {} not divisible by {width}", x.len());
}
let n = x.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) };
crate::cost::elementwise(n as u64, 1, 1);
let row = |(o, i): (&mut [f32], &[f32])| {
for ((o, &v), &b) in o.iter_mut().zip(i).zip(bias) {
*o = v + b;
}
ffai_core::fastmath::gelu_tanh_inplace(o);
};
if kernels_parallel() {
dst.par_chunks_mut(width).zip(x.par_chunks(width)).for_each(row);
} else {
dst.chunks_mut(width).zip(x.chunks(width)).for_each(row);
}
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), l1.shape().clone()))
}
}
struct GeluOp;
impl candle_core::CustomOp1 for GeluOp {
fn name(&self) -> &'static str {
"ffai-gelu-tanh"
}
fn cpu_fwd(
&self,
storage: &candle_core::CpuStorage,
layout: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let src = match storage {
candle_core::CpuStorage::F32(v) => v,
_ => candle_core::bail!("ffai-gelu expects f32"),
};
let Some((o, end)) = layout.contiguous_offsets() else {
candle_core::bail!("ffai-gelu expects a contiguous input")
};
let src = &src[o..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) };
crate::cost::elementwise(n as u64, 1, 1);
fill_gelu(dst, src, kernels_parallel());
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), layout.shape().clone()))
}
}
fn fill_gelu(dst: &mut [f32], src: &[f32], parallel: bool) {
debug_assert_eq!(dst.len(), src.len());
let kernel = |(o, i): (&mut [f32], &[f32])| {
o.copy_from_slice(i);
ffai_core::fastmath::gelu_tanh_inplace(o);
};
if parallel {
dst.par_chunks_mut(8192).zip(src.par_chunks(8192)).for_each(kernel);
} else {
dst.chunks_mut(8192).zip(src.chunks(8192)).for_each(kernel);
}
}
pub fn gelu_tanh_par(xs: &Tensor) -> CandleResult<Tensor> {
xs.apply_op1_no_bwd(&GeluOp)
}
struct SoftmaxLastDimOp {
last: usize,
}
impl candle_core::CustomOp1 for SoftmaxLastDimOp {
fn name(&self) -> &'static str {
"ffai-softmax-last-dim"
}
fn cpu_fwd(
&self,
storage: &candle_core::CpuStorage,
layout: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let src = match storage {
candle_core::CpuStorage::F32(v) => v,
_ => candle_core::bail!("ffai-softmax expects f32"),
};
let Some((o, end)) = layout.contiguous_offsets() else {
candle_core::bail!("ffai-softmax expects a contiguous input")
};
let src = &src[o..end];
let mut out = vec![0.0f32; src.len()];
let n = self.last;
crate::cost::elementwise(out.len() as u64, 2, 1);
let avx2 = cfg!(target_arch = "x86_64") && have_avx2_cached();
let kernel = move |(dst, row): (&mut [f32], &[f32])| {
#[cfg(target_arch = "x86_64")]
if avx2 {
unsafe { softmax_row_avx2(dst, row) };
return;
}
softmax_row_scalar(dst, row);
};
if kernels_parallel() {
out.par_chunks_mut(n).zip(src.par_chunks(n)).for_each(kernel);
} else {
out.chunks_mut(n).zip(src.chunks(n)).for_each(kernel);
}
Ok((candle_core::CpuStorage::F32(out), layout.shape().clone()))
}
}
pub fn softmax_last_dim_ours(xs: &Tensor) -> CandleResult<Tensor> {
let last = xs.dims()[xs.rank() - 1];
xs.apply_op1_no_bwd(&SoftmaxLastDimOp { last })
}
pub struct VisionTower {
embeddings: Embeddings,
layers: Vec<Layer>,
post_ln: LayerNorm,
}
impl VisionTower {
pub fn new(cfg: &VisionConfig, vb: VarBuilder) -> CandleResult<Self> {
let layers = (0..cfg.num_hidden_layers)
.map(|i| Layer::load(cfg, &vb.pp(format!("encoder.layers.{i}"))))
.collect::<CandleResult<Vec<_>>>()?;
Ok(Self {
embeddings: Embeddings::new(cfg, vb.pp("embeddings"))?,
layers,
post_ln: candle_nn::layer_norm(
cfg.hidden_size,
cfg.layer_norm_eps,
vb.pp("post_layernorm"),
)?,
})
}
pub fn forward(&self, pixel_values: &Tensor) -> CandleResult<Tensor> {
let t = crate::clock::Instant::now();
let mut xs = self.embeddings.forward(pixel_values)?;
prof::add("patch+pos embed", t);
for layer in &self.layers {
let t = crate::clock::Instant::now();
let next = layer.forward(&xs)?;
prof::add("LAYER TOTAL", t);
xs = next;
}
let t = crate::clock::Instant::now();
let out = layer_norm(&self.post_ln, &xs);
prof::add("post_ln", t);
out
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_non_overlapping_conv_is_a_matmul_over_permuted_patches() {
use candle_core::{Device, Module, Tensor};
let d = Device::Cpu;
let (b, c, p, gh, gw, out) = (2usize, 3usize, 4usize, 6usize, 8usize, 5usize);
let px = Tensor::rand(-1.0f32, 1.0, (b, c, gh * p, gw * p), &d).expect("px");
let w = Tensor::rand(-1.0f32, 1.0, (out, c, p, p), &d).expect("w");
let bias = Tensor::rand(-1.0f32, 1.0, out, &d).expect("bias");
let conv = candle_nn::Conv2d::new(
w.clone(),
Some(bias.clone()),
candle_nn::Conv2dConfig { stride: p, ..Default::default() },
);
let want = conv
.forward(&px)
.expect("conv")
.flatten_from(2)
.expect("flat")
.transpose(1, 2)
.expect("t")
.contiguous()
.expect("c");
let w_flat = w
.reshape((out, c * p * p)).expect("wr")
.t().expect("wt")
.contiguous().expect("wc");
let got = px
.reshape((b, c, gh, p, gw, p)).expect("r")
.permute((0, 2, 4, 1, 3, 5)).expect("perm")
.contiguous().expect("c")
.reshape((b * gh * gw, c * p * p)).expect("r2")
.matmul(&w_flat).expect("mm")
.broadcast_add(&bias).expect("bias")
.reshape((b, gh * gw, out)).expect("r3");
assert_eq!(want.dims(), got.dims(), "shape");
let (a, e) = (
want.flatten_all().expect("f").to_vec1::<f32>().expect("v"),
got.flatten_all().expect("f").to_vec1::<f32>().expect("v"),
);
let worst = a.iter().zip(&e).map(|(x, y)| (x - y).abs()).fold(0f32, f32::max);
assert!(worst < 1e-4, "conv and matmul disagree by {worst:.3e}");
}
use super::*;
mod fused_bias {
use super::*;
fn rnd(shape: (usize, usize, usize)) -> Tensor {
Tensor::rand(-2.0f32, 2.0, shape, &Device::Cpu).expect("rand")
}
fn max_abs_diff(a: &Tensor, b: &Tensor) -> f32 {
(a - b)
.expect("sub")
.abs()
.expect("abs")
.max_all()
.expect("max")
.to_scalar::<f32>()
.expect("scalar")
}
#[test]
fn embed_add_matches_the_two_broadcast_adds() {
let d = Device::Cpu;
for (b, seq, hidden) in [(1usize, 16usize, 8usize), (2, 9, 5), (1, 4, 3)] {
let xs = Tensor::rand(-2.0f32, 2.0, (b * seq, hidden), &d).expect("rand");
let bias = Tensor::rand(-1.0f32, 1.0, hidden, &d).expect("rand");
let pos = Tensor::rand(-1.0f32, 1.0, (seq, hidden), &d).expect("rand");
let fused = xs.apply_op3_no_bwd(&bias, &pos, &EmbedAddOp).expect("fused");
let want = xs
.broadcast_add(&bias)
.expect("bias")
.reshape((b, seq, hidden))
.expect("reshape")
.broadcast_add(&pos)
.expect("pos");
assert_eq!(fused.dims(), want.dims(), "shape at b{b} seq{seq}");
let got = fused.flatten_all().expect("f").to_vec1::<f32>().expect("v");
let want = want.flatten_all().expect("f").to_vec1::<f32>().expect("v");
assert_eq!(got, want, "embed add at b{b} seq{seq} hidden{hidden}");
}
}
#[test]
fn gelu_bias_matches_gelu_of_broadcast_add() {
let (x, bias) = (rnd((1, 64, 128)), rnd((1, 1, 128)).flatten_all().expect("flat"));
let fused = x.apply_op2_no_bwd(&bias, &GeluBiasOp).expect("fused");
let plain =
gelu_tanh_par(&x.broadcast_add(&bias).expect("add")).expect("gelu");
let d = max_abs_diff(&fused, &plain);
assert!(d < 1e-6, "gelu(x+b) fused vs two-op differ by {d:e}");
}
#[test]
fn add_bias_matches_residual_plus_broadcast_add() {
let (r, y) = (rnd((1, 64, 128)), rnd((1, 64, 128)));
let bias = rnd((1, 1, 128)).flatten_all().expect("flat");
let fused = r.apply_op3_no_bwd(&y, &bias, &AddBiasOp).expect("fused");
let plain = (&r + y.broadcast_add(&bias).expect("add")).expect("sum");
let d = max_abs_diff(&fused, &plain);
assert!(d < 1e-5, "r+(y+b) fused vs two-op differ by {d:e}");
}
#[test]
fn packed_qkv_matches_the_permute_it_replaces() {
let (heads, hd, seq, b) = (4usize, 8usize, 16usize, 2usize);
let hidden = heads * hd;
let x = Tensor::rand(-2.0f32, 2.0, (b, seq, 3 * hidden), &Device::Cpu).expect("rand");
let bias = Tensor::rand(-1.0f32, 1.0, 3 * hidden, &Device::Cpu).expect("rand");
let fused = x
.apply_op2_no_bwd(&bias, &PackedQkvOp { heads, head_dim: hd })
.expect("fused");
let plain = x
.broadcast_add(&bias)
.expect("add")
.reshape((b, seq, 3, heads, hd))
.expect("reshape")
.permute((2, 0, 3, 1, 4))
.expect("permute")
.contiguous()
.expect("contig");
assert_eq!(fused.dims(), plain.dims(), "packed shape");
let d = max_abs_diff(&fused, &plain);
assert_eq!(d, 0.0, "packed copy is pure movement; differs by {d:e}");
}
#[test]
fn deferred_normalisation_matches_softmax_then_matmul() {
let (heads, hd, seq, b) = (3usize, 8usize, 32usize, 2usize);
let d = Device::Cpu;
let scores = Tensor::rand(-4.0f32, 4.0, (b, heads, seq, seq), &d).expect("rand");
let v = Tensor::rand(-1.0f32, 1.0, (b, heads, seq, hd), &d).expect("rand");
let want = candle_nn::ops::softmax_last_dim(&scores)
.expect("softmax")
.matmul(&v)
.expect("av")
.transpose(1, 2)
.expect("t")
.reshape((b, seq, heads * hd))
.expect("reshape");
let exps = scores.copy().expect("copy");
let rows = b * heads * seq;
let mut sums = vec![0f32; rows];
exps.inplace_op1(&SoftmaxExpInplace { sums: RowSums(sums.as_mut_ptr()), rows })
.expect("exp");
let sums = Tensor::from_vec(sums, (b, heads, seq), &d).expect("sums");
let got = exps
.matmul(&v)
.expect("av")
.apply_op2_no_bwd(&sums, &AttnMergeOp { heads, head_dim: hd })
.expect("merge");
assert_eq!(got.dims(), want.dims(), "merged shape");
let e = max_abs_diff(&got, &want);
assert!(e < 1e-6, "deferred normalisation differs by {e:e}");
}
#[test]
fn row_sums_are_never_below_one() {
let d = Device::Cpu;
let scores = Tensor::rand(-90.0f32, -80.0, (1, 2, 8, 8), &d).expect("rand");
let rows = 1 * 2 * 8;
let mut sums = vec![0f32; rows];
scores
.inplace_op1(&SoftmaxExpInplace { sums: RowSums(sums.as_mut_ptr()), rows })
.expect("exp");
for (i, &s) in sums.iter().enumerate() {
assert!(s >= 1.0, "row {i} total {s} < 1 — max was not subtracted");
assert!(s.is_finite(), "row {i} total {s} is not finite");
}
}
}
use candle_core::{DType, Device};
fn dev() -> Device {
Device::Cpu
}
#[test]
fn parallel_softmax_matches_candles() {
let d = dev();
let xs = Tensor::rand(-30.0f32, 30.0, (2, 3, 7, 65), &d).expect("xs");
let ours = softmax_last_dim_ours(&xs).expect("ours");
let theirs = candle_nn::ops::softmax_last_dim(&xs).expect("candle");
let worst = (&ours - &theirs)
.expect("sub")
.abs()
.expect("abs")
.max_all()
.expect("max")
.to_scalar::<f32>()
.expect("scalar");
assert!(worst < 1e-6, "parallel softmax differs by {worst:.3e}");
let sums = ours
.sum(candle_core::D::Minus1)
.expect("sum")
.flatten_all()
.expect("f");
for s in sums.to_vec1::<f32>().expect("v") {
assert!((s - 1.0).abs() < 1e-5, "row sums to {s}");
}
}
#[test]
fn parallel_gelu_matches_candles() {
let d = dev();
let xs = Tensor::rand(-8.0f32, 8.0, (4, 1000), &d).expect("xs");
let ours = gelu_tanh_par(&xs).expect("ours");
let theirs = xs.gelu().expect("candle");
let worst = (&ours - &theirs)
.expect("sub")
.abs()
.expect("abs")
.max_all()
.expect("max")
.to_scalar::<f32>()
.expect("scalar");
assert!(worst < 1e-5, "parallel gelu differs by {worst:.3e}");
}
#[test]
#[cfg(target_arch = "x86_64")]
fn gelu_avx2_matches_scalar() {
if !super::have_avx2_cached() {
eprintln!("SKIP: no AVX2 on this CPU — the scalar path is the only one");
return;
}
let mut src: Vec<f32> = Vec::new();
for i in 0..4099 {
src.push((i as f32 - 2049.0) / 97.0); }
src.extend_from_slice(&[0.0, -0.0, 1e-30, -1e-30, 88.0, -88.0, 120.0, -120.0]);
assert_ne!(src.len() % 8, 0, "the tail must not be a whole vector");
let (mut a, mut b) = (src.clone(), src.clone());
super::gelu_chunk_scalar(&mut a);
unsafe { super::gelu_chunk_avx2(&mut b) };
for (i, (x, y)) in a.iter().zip(&b).enumerate() {
assert_eq!(
x.to_bits(),
y.to_bits(),
"lane {i} (input {}) diverged: scalar {x} vs avx2 {y}",
src[i]
);
}
}
#[test]
fn gelu_is_still_gelu_at_the_landmarks() {
let d = dev();
let xs = Tensor::from_vec(vec![-10.0f32, -2.0, -0.75, 0.0, 0.5, 2.0, 10.0], 7, &d)
.expect("xs");
let g = gelu_tanh_par(&xs).expect("g").to_vec1::<f32>().expect("v");
assert!(g[0].abs() < 1e-6, "gelu(-10) should vanish, got {}", g[0]);
assert!(g[3].abs() < 1e-9, "gelu(0) must be exactly 0, got {}", g[3]);
assert!((g[6] - 10.0).abs() < 1e-4, "gelu(10) ~= 10, got {}", g[6]);
assert!(
(-0.18..-0.15).contains(&g[2]),
"gelu(-0.75) should sit near the -0.17 minimum, got {}",
g[2]
);
assert!(g[1] < 0.0, "gelu(-2) should be negative, got {}", g[1]);
for w in g[2..].windows(2) {
assert!(w[1] >= w[0] - 1e-6, "not monotone above the dip: {w:?}");
}
}
#[test]
fn a_zero_length_tensor_does_not_panic() {
let d = dev();
let xs = Tensor::zeros((0, 8), DType::F32, &d).expect("xs");
assert_eq!(gelu_tanh_par(&xs).expect("gelu").elem_count(), 0);
assert_eq!(
softmax_last_dim_ours(&xs).expect("sm").elem_count(),
0
);
}
}