use candle_core::{DType, Device, IndexOp, Result as CandleResult, Tensor};
use candle_nn::VarBuilder;
use crate::par::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct Cfg {
pub layers: usize,
pub hidden: usize,
pub heads: usize,
pub kv_heads: usize,
pub head_dim: usize,
pub inter: usize,
pub eps: f64,
pub rope_theta: f32,
pub max_pos: usize,
}
struct Block {
ln1: Tensor,
q: Tensor,
k: Tensor,
v: Tensor,
o: Tensor,
ln2: Tensor,
gate: Tensor,
up: Tensor,
down: Tensor,
}
struct KvAppend {
pos: usize,
}
impl candle_core::InplaceOp2 for KvAppend {
fn name(&self) -> &'static str {
"ffai-kv-append"
}
fn cpu_fwd(
&self,
dst: &mut candle_core::CpuStorage,
dl: &candle_core::Layout,
src: &candle_core::CpuStorage,
sl: &candle_core::Layout,
) -> CandleResult<()> {
let candle_core::CpuStorage::F32(dst) = dst else {
candle_core::bail!("ffai-kv-append expects f32")
};
let candle_core::CpuStorage::F32(src) = src else {
candle_core::bail!("ffai-kv-append expects f32")
};
let (Some((dof, _)), Some((sof, sen))) = (dl.contiguous_offsets(), sl.contiguous_offsets())
else {
candle_core::bail!("ffai-kv-append expects contiguous buffers")
};
let (_b, heads, cap, hd) = dl.shape().dims4()?;
let (_sb, sheads, seq, shd) = sl.shape().dims4()?;
if sheads != heads || shd != hd {
candle_core::bail!("ffai-kv-append: src {sheads}x{shd} vs dst {heads}x{hd}");
}
if self.pos + seq > cap {
candle_core::bail!("ffai-kv-append: pos {} + seq {seq} exceeds cap {cap}", self.pos);
}
let src = &src[sof..sen];
for h in 0..heads {
let d0 = dof + h * cap * hd + self.pos * hd;
let s0 = h * seq * hd;
dst[d0..d0 + seq * hd].copy_from_slice(&src[s0..s0 + seq * hd]);
}
crate::cost::copy((heads * seq * hd) as u64);
Ok(())
}
}
struct RmsNorm {
eps: f64,
}
impl candle_core::CustomOp2 for RmsNorm {
fn name(&self) -> &'static str {
"ffai-rms-norm"
}
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, w) = match (s1, s2) {
(candle_core::CpuStorage::F32(a), candle_core::CpuStorage::F32(b)) => (a, b),
_ => candle_core::bail!("ffai-rms-norm expects f32"),
};
let (Some((xo, xe)), Some((wo, we))) = (l1.contiguous_offsets(), l2.contiguous_offsets())
else {
candle_core::bail!("ffai-rms-norm expects contiguous inputs")
};
let (x, w) = (&x[xo..xe], &w[wo..we]);
let h = w.len();
if h == 0 || x.len() % h != 0 {
candle_core::bail!("ffai-rms-norm: {} not divisible by {h}", x.len());
}
let eps = self.eps;
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) };
dst.par_chunks_mut(h).zip(x.par_chunks(h)).for_each(|(o, i)| {
let mut acc = 0f32;
for &v in i {
acc += v * v;
}
let scale = (1.0 / (f64::from(acc) / h as f64 + eps).sqrt()) as f32;
for ((o, &v), &g) in o.iter_mut().zip(i).zip(w) {
*o = v * scale * g;
}
});
crate::cost::elementwise(n as u64, 2, 1);
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), l1.shape().clone()))
}
}
fn rms_norm(xs: &Tensor, w: &Tensor, eps: f64) -> CandleResult<Tensor> {
xs.contiguous()?.apply_op2_no_bwd(&w.contiguous()?, &RmsNorm { eps })
}
struct CausalSoftmax {
offset: usize,
}
impl candle_core::CustomOp1 for CausalSoftmax {
fn name(&self) -> &'static str {
"ffai-causal-softmax"
}
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-causal-softmax expects f32"),
};
let Some((o, end)) = layout.contiguous_offsets() else {
candle_core::bail!("ffai-causal-softmax expects a contiguous input")
};
let src = &src[o..end];
let dims = layout.shape().dims();
let k_len = *dims.last().expect("rank >= 1");
let q_len = dims[dims.len() - 2];
let rows = src.len() / k_len;
let offset = self.offset;
let mut out: Vec<f32> = Vec::with_capacity(src.len());
{
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>(), src.len())
};
dst.par_chunks_mut(k_len)
.zip(src.par_chunks(k_len))
.enumerate()
.for_each(|(r, (o, i))| {
let qpos = r % q_len;
let lim = (qpos + offset + 1).min(k_len);
let row = &i[..lim];
let mut m = f32::NEG_INFINITY;
for &x in row {
if x > m {
m = x;
}
}
let mut sum = 0f32;
for (o, &x) in o[..lim].iter_mut().zip(row) {
let e = ffai_core::fastmath::exp(x - m);
*o = e;
sum += e;
}
let inv = 1.0 / sum;
for o in &mut o[..lim] {
*o *= inv;
}
for o in &mut o[lim..] {
*o = 0.0;
}
});
crate::cost::transcendental_vector((rows * (k_len + 1) / 2) as u64);
crate::cost::elementwise((rows * k_len) as u64, 1, 1);
}
#[allow(unsafe_code)]
unsafe {
out.set_len(src.len());
}
Ok((candle_core::CpuStorage::F32(out), layout.shape().clone()))
}
}
pub struct CausalSoftmaxInplace {
pub offset: usize,
}
impl candle_core::InplaceOp1 for CausalSoftmaxInplace {
fn name(&self) -> &'static str {
"ffai-causal-softmax-inplace"
}
fn cpu_fwd(
&self,
storage: &mut candle_core::CpuStorage,
layout: &candle_core::Layout,
) -> CandleResult<()> {
let dims = layout.shape().dims();
let k_len = *dims.last().expect("rank >= 1");
let q_len = dims[dims.len() - 2];
let Some((start, end)) = layout.contiguous_offsets() else {
candle_core::bail!("ffai-causal-softmax-inplace expects a contiguous input")
};
let candle_core::CpuStorage::F32(buf) = storage else {
candle_core::bail!("ffai-causal-softmax-inplace expects f32")
};
let offset = self.offset;
let rows = (end - start) / k_len;
buf[start..end]
.par_chunks_mut(k_len)
.enumerate()
.for_each(|(r, row)| {
let qpos = r % q_len;
let lim = (qpos + offset + 1).min(k_len);
let mut m = f32::NEG_INFINITY;
for &x in &row[..lim] {
if x > m {
m = x;
}
}
let mut sum = 0f32;
for x in &mut row[..lim] {
let e = ffai_core::fastmath::exp(*x - m);
*x = e;
sum += e;
}
let inv = 1.0 / sum;
for x in &mut row[..lim] {
*x *= inv;
}
for x in &mut row[lim..] {
*x = 0.0;
}
});
crate::cost::transcendental_vector((rows * (k_len + 1) / 2) as u64);
crate::cost::elementwise((rows * k_len) as u64, 1, 1);
Ok(())
}
}
pub(crate) struct AddInplace;
impl candle_core::InplaceOp2 for AddInplace {
fn name(&self) -> &'static str {
"ffai-add-inplace"
}
fn cpu_fwd(
&self,
s1: &mut candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
) -> CandleResult<()> {
let (Some((ao, ae)), Some((bo, be))) = (l1.contiguous_offsets(), l2.contiguous_offsets())
else {
candle_core::bail!("ffai-add-inplace expects contiguous inputs")
};
let candle_core::CpuStorage::F32(rhs) = s2 else {
candle_core::bail!("ffai-add-inplace expects f32")
};
let rhs = &rhs[bo..be];
let candle_core::CpuStorage::F32(lhs) = s1 else {
candle_core::bail!("ffai-add-inplace expects f32")
};
if ae - ao != be - bo {
candle_core::bail!("ffai-add-inplace: length mismatch");
}
let add = |a: &mut [f32], b: &[f32]| {
for (a, &b) in a.iter_mut().zip(b) {
*a += b;
}
};
if crate::par::current_thread_index().is_none() {
lhs[ao..ae]
.par_chunks_mut(8192)
.zip(rhs.par_chunks(8192))
.for_each(|(a, b)| add(a, b));
} else {
lhs[ao..ae]
.chunks_mut(8192)
.zip(rhs.chunks(8192))
.for_each(|(a, b)| add(a, b));
}
crate::cost::elementwise((ae - ao) as u64, 1, 1);
Ok(())
}
}
struct SwiGlu;
impl candle_core::CustomOp2 for SwiGlu {
fn name(&self) -> &'static str {
"ffai-swiglu"
}
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, b) = match (s1, s2) {
(candle_core::CpuStorage::F32(a), candle_core::CpuStorage::F32(b)) => (a, b),
_ => candle_core::bail!("ffai-swiglu expects f32"),
};
let (Some((ao, ae)), Some((bo, be))) = (l1.contiguous_offsets(), l2.contiguous_offsets())
else {
candle_core::bail!("ffai-swiglu expects contiguous inputs")
};
let (a, b) = (&a[ao..ae], &b[bo..be]);
if a.len() != b.len() {
candle_core::bail!("ffai-swiglu: length mismatch");
}
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) };
dst.par_chunks_mut(8192)
.zip(a.par_chunks(8192))
.zip(b.par_chunks(8192))
.for_each(|((o, g), u)| {
for ((o, &g), &u) in o.iter_mut().zip(g).zip(u) {
*o = ffai_core::fastmath::silu(g) * u;
}
});
crate::cost::transcendental_vector(n as u64);
crate::cost::elementwise(n as u64, 2, 1);
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), l1.shape().clone()))
}
}
fn linear(x: &Tensor, w: &Tensor) -> CandleResult<Tensor> {
let (b, s, i) = x.dims3()?;
let o = w.dim(0)?;
x.reshape((b * s, i))?
.matmul(&w.t()?)?
.reshape((b, s, o))
}
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_TEXT_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
}
}
pub use self::CausalSoftmaxInplace as CausalSoftmaxProbe;
pub fn rms_norm_for_probe(xs: &Tensor, w: &Tensor, eps: f64) -> CandleResult<Tensor> {
rms_norm(xs, w, eps)
}
pub fn causal_softmax_for_probe(att: &Tensor, offset: usize) -> CandleResult<Tensor> {
att.apply_op1_no_bwd(&CausalSoftmax { offset })
}
pub fn swiglu_for_probe(gate: &Tensor, up: &Tensor) -> CandleResult<Tensor> {
gate.apply_op2_no_bwd(up, &SwiGlu)
}
struct SwiGluInplace;
impl candle_core::InplaceOp2 for SwiGluInplace {
fn name(&self) -> &'static str {
"ffai-swiglu-inplace"
}
fn cpu_fwd(
&self,
s1: &mut candle_core::CpuStorage,
l1: &candle_core::Layout,
s2: &candle_core::CpuStorage,
l2: &candle_core::Layout,
) -> CandleResult<()> {
let (Some((ao, ae)), Some((bo, be))) = (l1.contiguous_offsets(), l2.contiguous_offsets())
else {
candle_core::bail!("ffai-swiglu-inplace expects contiguous inputs")
};
let candle_core::CpuStorage::F32(up) = s2 else {
candle_core::bail!("ffai-swiglu-inplace expects f32")
};
let up = &up[bo..be];
let candle_core::CpuStorage::F32(gate) = s1 else {
candle_core::bail!("ffai-swiglu-inplace expects f32")
};
if ae - ao != be - bo {
candle_core::bail!("ffai-swiglu-inplace: length mismatch");
}
let n = ae - ao;
let apply = |g: &mut [f32], u: &[f32]| {
for (g, &u) in g.iter_mut().zip(u) {
*g = ffai_core::fastmath::silu(*g) * u;
}
};
if crate::par::current_thread_index().is_none() {
gate[ao..ae]
.par_chunks_mut(8192)
.zip(up.par_chunks(8192))
.for_each(|(g, u)| apply(g, u));
} else {
gate[ao..ae]
.chunks_mut(8192)
.zip(up.chunks(8192))
.for_each(|(g, u)| apply(g, u));
}
crate::cost::transcendental_vector(n as u64);
crate::cost::elementwise(n as u64, 1, 1);
Ok(())
}
}
pub struct TextTower {
embed: Tensor,
blocks: Vec<Block>,
norm: Tensor,
lm_head: Tensor,
cos: Tensor,
sin: Tensor,
cfg: Cfg,
kv: Vec<Option<(Tensor, Tensor)>>,
kv_len: usize,
kv_cap: usize,
}
impl TextTower {
pub fn load(vb: &VarBuilder, cfg: Cfg, device: &Device) -> CandleResult<Self> {
let scale = 1.0 / (cfg.head_dim as f64).sqrt();
let m = vb.pp("model").pp("text_model");
let mut blocks = Vec::with_capacity(cfg.layers);
for i in 0..cfg.layers {
let l = m.pp("layers").pp(i.to_string());
let a = l.pp("self_attn");
let f = l.pp("mlp");
let qh = cfg.heads * cfg.head_dim;
let kh = cfg.kv_heads * cfg.head_dim;
blocks.push(Block {
ln1: l.pp("input_layernorm").get(cfg.hidden, "weight")?,
q: (a.pp("q_proj").get((qh, cfg.hidden), "weight")? * scale)?,
k: a.pp("k_proj").get((kh, cfg.hidden), "weight")?,
v: a.pp("v_proj").get((kh, cfg.hidden), "weight")?,
o: a.pp("o_proj").get((cfg.hidden, qh), "weight")?,
ln2: l.pp("post_attention_layernorm").get(cfg.hidden, "weight")?,
gate: f.pp("gate_proj").get((cfg.inter, cfg.hidden), "weight")?,
up: f.pp("up_proj").get((cfg.inter, cfg.hidden), "weight")?,
down: f.pp("down_proj").get((cfg.hidden, cfg.inter), "weight")?,
});
}
let (cos, sin) = rope_tables(&cfg, device)?;
Ok(Self {
embed: m.pp("embed_tokens").get((49280, cfg.hidden), "weight")?,
blocks,
norm: m.pp("norm").get(cfg.hidden, "weight")?,
lm_head: vb.pp("lm_head").get((49280, cfg.hidden), "weight")?,
cos,
sin,
cfg,
kv: (0..cfg.layers).map(|_| None).collect(),
kv_len: 0,
kv_cap: 0,
})
}
pub fn reset(&mut self) {
for slot in &mut self.kv {
*slot = None;
}
self.kv_len = 0;
self.kv_cap = 0;
}
pub fn forward(&mut self, embeds: &Tensor, index_pos: usize) -> CandleResult<Tensor> {
let (b, seq, _) = embeds.dims3()?;
let c = self.cfg;
let mut x = embeds.clone();
for i in 0..c.layers {
x = self.block(i, &x, index_pos, b, seq)?;
}
let x = x.i((.., seq - 1, ..))?.unsqueeze(1)?.contiguous()?;
let x = rms_norm(&x, &self.norm, c.eps)?.squeeze(1)?;
let logits = x.matmul(&self.lm_head.t()?)?;
crate::cost::matmul(1, 1, c.hidden as u64, 49280);
Ok(logits)
}
pub fn embed(&self, ids: &Tensor) -> CandleResult<Tensor> {
self.embed.index_select(&ids.flatten_all()?, 0)?.reshape((
1,
ids.elem_count(),
self.cfg.hidden,
))
}
fn block(
&mut self,
i: usize,
xs: &Tensor,
index_pos: usize,
b: usize,
seq: usize,
) -> CandleResult<Tensor> {
let c = self.cfg;
let (bu, sq, hd) = (b as u64, seq as u64, c.hidden as u64);
let blk = &self.blocks[i];
let t = crate::clock::Instant::now();
let normed = rms_norm(xs, &blk.ln1, c.eps)?;
prof::add("rms_norm", t);
let t = crate::clock::Instant::now();
let q = linear(&normed, &blk.q)?;
let k = linear(&normed, &blk.k)?;
let v = linear(&normed, &blk.v)?;
prof::add("qkv proj", t);
crate::cost::matmul(1, bu * sq, hd, (c.heads * c.head_dim) as u64);
crate::cost::matmul(2, bu * sq, hd, (c.kv_heads * c.head_dim) as u64);
let t = crate::clock::Instant::now();
let q = q
.reshape((b, seq, c.heads, c.head_dim))?
.transpose(1, 2)?
.contiguous()?;
let k = k
.reshape((b, seq, c.kv_heads, c.head_dim))?
.transpose(1, 2)?
.contiguous()?;
let v = v
.reshape((b, seq, c.kv_heads, c.head_dim))?
.transpose(1, 2)?
.contiguous()?;
crate::cost::copy(bu * sq * hd);
prof::add("qkv reshape+transpose", t);
let t = crate::clock::Instant::now();
let q = self.rope(&q, index_pos)?;
let k = self.rope(&k, index_pos)?;
prof::add("rope", t);
let t = crate::clock::Instant::now();
let (k, v) = {
if self.kv_cap < index_pos + seq {
let want = index_pos + seq + 256;
let shape = (b, c.kv_heads, want, c.head_dim);
for slot in &mut self.kv {
*slot = match slot.take() {
Some((pk, pv)) => {
let (nk, nv) = (
Tensor::zeros(shape, pk.dtype(), pk.device())?,
Tensor::zeros(shape, pv.dtype(), pv.device())?,
);
nk.inplace_op2(&pk.narrow(2, 0, self.kv_len)?, &KvAppend { pos: 0 })?;
nv.inplace_op2(&pv.narrow(2, 0, self.kv_len)?, &KvAppend { pos: 0 })?;
Some((nk, nv))
}
None => Some((
Tensor::zeros(shape, k.dtype(), k.device())?,
Tensor::zeros(shape, v.dtype(), v.device())?,
)),
};
}
self.kv_cap = want;
}
let (bk, bv) = self.kv[i].as_ref().expect("cache allocated above");
bk.inplace_op2(&k, &KvAppend { pos: index_pos })?;
bv.inplace_op2(&v, &KvAppend { pos: index_pos })?;
let used = index_pos + seq;
(bk.narrow(2, 0, used)?, bv.narrow(2, 0, used)?)
};
self.kv_len = index_pos + seq;
prof::add("kv cache", t);
let k_len = k.dim(2)?;
let reps = c.heads / c.kv_heads;
let qg = q.reshape((b, c.kv_heads, reps * seq, c.head_dim))?;
let t = crate::clock::Instant::now();
let att = qg.matmul(&k.t()?)?;
prof::add("q.k^T", t);
crate::cost::matmul(
bu * c.heads as u64,
sq,
c.head_dim as u64,
k_len as u64,
);
let att = att.reshape((b, c.heads, seq, k_len))?;
let t = crate::clock::Instant::now();
att.inplace_op1(&CausalSoftmaxInplace { offset: index_pos })?;
prof::add("causal softmax", t);
let t = crate::clock::Instant::now();
let y = att
.reshape((b, c.kv_heads, reps * seq, k_len))?
.matmul(&v)?
.reshape((b, c.heads, seq, c.head_dim))?;
crate::cost::matmul(
bu * c.heads as u64,
sq,
k_len as u64,
c.head_dim as u64,
);
prof::add("attn.v", t);
let t = crate::clock::Instant::now();
let y = y.transpose(1, 2)?.reshape((b, seq, c.hidden))?;
prof::add("transpose back", t);
crate::cost::copy(bu * sq * hd);
let t = crate::clock::Instant::now();
let y = linear(&y, &blk.o)?;
prof::add("o proj", t);
crate::cost::matmul(1, bu * sq, hd, hd);
let t = crate::clock::Instant::now();
y.inplace_op2(xs, &AddInplace)?;
let xs = y;
prof::add("residual", t);
let t = crate::clock::Instant::now();
let normed = rms_norm(&xs, &blk.ln2, c.eps)?;
prof::add("rms_norm", t);
let t = crate::clock::Instant::now();
let g = linear(&normed, &blk.gate)?;
let u = linear(&normed, &blk.up)?;
prof::add("gate+up proj", t);
crate::cost::matmul(2, bu * sq, hd, c.inter as u64);
let t = crate::clock::Instant::now();
g.inplace_op2(&u, &SwiGluInplace)?;
let h = g;
prof::add("swiglu", t);
let t = crate::clock::Instant::now();
let down = linear(&h, &blk.down)?;
prof::add("down proj", t);
crate::cost::matmul(1, bu * sq, c.inter as u64, hd);
let t = crate::clock::Instant::now();
down.inplace_op2(&xs, &AddInplace)?;
prof::add("residual", t);
Ok(down)
}
fn rope(&self, x: &Tensor, index_pos: usize) -> CandleResult<Tensor> {
let seq = x.dim(2)?;
let cos = self.cos.narrow(0, index_pos, seq)?;
let sin = self.sin.narrow(0, index_pos, seq)?;
candle_nn::rotary_emb::rope(&x.contiguous()?, &cos, &sin)
}
}
fn rope_tables(cfg: &Cfg, device: &Device) -> CandleResult<(Tensor, Tensor)> {
let half = cfg.head_dim / 2;
let theta: Vec<f32> = (0..half)
.map(|i| 1f32 / cfg.rope_theta.powf(2.0 * i as f32 / cfg.head_dim as f32))
.collect();
let theta = Tensor::new(theta.as_slice(), device)?;
let idx = Tensor::arange(0, cfg.max_pos as u32, device)?
.to_dtype(DType::F32)?
.reshape((cfg.max_pos, 1))?;
let f = idx.matmul(&theta.reshape((1, half))?)?;
Ok((f.cos()?, f.sin()?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn causal_softmax_is_bit_identical_to_mask_then_softmax() {
let d = Device::Cpu;
let (h, s) = (3usize, 64usize);
let att = Tensor::rand(-4.0f32, 4.0, (1, h, s, s), &d).expect("att");
let ours = att
.apply_op1_no_bwd(&CausalSoftmax { offset: 0 })
.expect("ours")
.flatten_all()
.expect("f")
.to_vec1::<f32>()
.expect("v");
let mut m = vec![0f32; s * s];
for i in 0..s {
for j in 0..s {
if j > i {
m[i * s + j] = f32::NEG_INFINITY;
}
}
}
let mask = Tensor::from_vec(m, (s, s), &d).expect("mask");
let theirs = candle_nn::ops::softmax_last_dim(
&att.broadcast_add(&mask.reshape((1, 1, s, s)).expect("r")).expect("add"),
)
.expect("softmax")
.flatten_all()
.expect("f")
.to_vec1::<f32>()
.expect("v");
let mut worst = 0f32;
for (a, b) in ours.iter().zip(&theirs) {
worst = worst.max((a - b).abs());
}
assert!(worst < 1e-6, "causal softmax diverged from mask+softmax by {worst:.3e}");
}
#[test]
fn every_causal_row_still_sums_to_one() {
let d = Device::Cpu;
let (h, s) = (2usize, 33usize);
let att = Tensor::rand(-3.0f32, 3.0, (1, h, s, s), &d).expect("att");
let p = att
.apply_op1_no_bwd(&CausalSoftmax { offset: 0 })
.expect("p")
.flatten_all()
.expect("f")
.to_vec1::<f32>()
.expect("v");
for (r, row) in p.chunks(s).enumerate() {
let sum: f32 = row.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "row {r} sums to {sum}");
let lim = (r % s) + 1;
for (j, &x) in row.iter().enumerate().skip(lim) {
assert_eq!(x, 0.0, "row {r} col {j} is {x}, should be masked");
}
}
}
#[test]
fn a_decode_step_attends_to_the_entire_cache() {
let d = Device::Cpu;
let k_len = 40usize;
let att = Tensor::rand(-2.0f32, 2.0, (1, 2, 1, k_len), &d).expect("att");
let p = att
.apply_op1_no_bwd(&CausalSoftmax { offset: k_len - 1 })
.expect("p")
.flatten_all()
.expect("f")
.to_vec1::<f32>()
.expect("v");
for row in p.chunks(k_len) {
assert!(row.iter().all(|&x| x > 0.0), "a decode step masked live keys");
let sum: f32 = row.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "decode row sums to {sum}");
}
}
#[test]
fn swiglu_matches_silu_then_multiply() {
let d = Device::Cpu;
let g = Tensor::rand(-6.0f32, 6.0, (2, 777), &d).expect("g");
let u = Tensor::rand(-6.0f32, 6.0, (2, 777), &d).expect("u");
let ours = g.apply_op2_no_bwd(&u, &SwiGlu).expect("ours");
let theirs = (g.silu().expect("silu") * &u).expect("mul");
let (a, b) = (
ours.flatten_all().expect("f").to_vec1::<f32>().expect("v"),
theirs.flatten_all().expect("f").to_vec1::<f32>().expect("v"),
);
let worst = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0f32, f32::max);
assert!(worst < 1e-5, "swiglu differs from silu*up by {worst:.3e}");
}
#[test]
fn rms_norm_matches_candles() {
let d = Device::Cpu;
let x = Tensor::rand(-2.0f32, 2.0, (1, 37, 576), &d).expect("x");
let w = Tensor::rand(0.5f32, 1.5, 576, &d).expect("w");
let ours = rms_norm(&x, &w, 1e-5).expect("ours");
let theirs = candle_nn::ops::rms_norm(&x, &w, 1e-5).expect("theirs");
let (a, b) = (
ours.flatten_all().expect("f").to_vec1::<f32>().expect("v"),
theirs.flatten_all().expect("f").to_vec1::<f32>().expect("v"),
);
let worst = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0f32, f32::max);
assert!(worst < 1e-5, "rms_norm differs from candle's by {worst:.3e}");
}
}