use crate::error::{ForgeError, Result};
use crate::ops::{self, MatmulSpec};
use crate::tensor::Tensor;
#[derive(Clone)]
pub struct TVar {
pub id: usize,
pub t: Tensor,
}
type BackwardFn = Box<dyn Fn(&Tensor) -> Result<Vec<(usize, Tensor)>>>;
struct Node {
backward: Option<BackwardFn>,
}
#[derive(Default)]
pub struct Tape {
nodes: Vec<Node>,
}
impl Tape {
pub fn new() -> Tape {
Tape::default()
}
pub fn leaf(&mut self, t: Tensor) -> TVar {
let id = self.nodes.len();
self.nodes.push(Node { backward: None });
TVar { id, t }
}
fn record(&mut self, t: Tensor, f: BackwardFn) -> TVar {
let id = self.nodes.len();
self.nodes.push(Node { backward: Some(f) });
TVar { id, t }
}
pub fn add(&mut self, a: &TVar, b: &TVar) -> Result<TVar> {
let out = ops::add(&a.t, &b.t)?;
let (ia, ib) = (a.id, b.id);
Ok(self.record(
out,
Box::new(move |dy| Ok(vec![(ia, dy.clone()), (ib, dy.clone())])),
))
}
pub fn matmul(
&mut self,
a: &TVar,
b: &TVar,
bias: Option<&TVar>,
spec: MatmulSpec,
) -> Result<TVar> {
if spec.trans_a || spec.b_rows.is_some() {
return Err(ForgeError::Shape(
"tape matmul: trans_a/b_rows are backward-only specs".into(),
));
}
if a.t.shape().rank() != b.t.shape().rank() {
return Err(ForgeError::Shape(
"tape matmul requires equal-rank operands (no broadcast grads)".into(),
));
}
let out = ops::matmul(&a.t, &b.t, bias.map(|b| &b.t), spec)?;
let (ia, ib) = (a.id, b.id);
let ibias = bias.map(|b| b.id);
let (at, bt) = (a.t.clone(), b.t.clone());
let alpha = spec.alpha;
let trans_b = spec.trans_b;
Ok(self.record(
out,
Box::new(move |dy| {
let da = if !trans_b {
ops::matmul(
dy,
&bt,
None,
MatmulSpec {
trans_b: true,
alpha,
..Default::default()
},
)?
} else {
ops::matmul(
dy,
&bt,
None,
MatmulSpec {
alpha,
..Default::default()
},
)?
};
let db = if !trans_b {
ops::matmul(
&at,
dy,
None,
MatmulSpec {
trans_a: true,
alpha,
..Default::default()
},
)?
} else {
ops::matmul(
dy,
&at,
None,
MatmulSpec {
trans_a: true,
alpha,
..Default::default()
},
)?
};
let mut grads = vec![(ia, da), (ib, db)];
if let Some(ibias) = ibias {
grads.push((ibias, ops::sum_rows(dy)?));
}
Ok(grads)
}),
))
}
pub fn gelu(&mut self, x: &TVar) -> Result<TVar> {
let out = ops::gelu(&x.t)?;
let ix = x.id;
let xt = x.t.clone();
Ok(self.record(
out,
Box::new(move |dy| Ok(vec![(ix, ops::gelu_bwd(&xt, dy)?)])),
))
}
pub fn layernorm(&mut self, x: &TVar, gamma: &TVar, beta: &TVar, eps: f32) -> Result<TVar> {
let out = ops::layernorm(&x.t, &gamma.t, &beta.t, eps)?;
let (ix, ig, ib) = (x.id, gamma.id, beta.id);
let (xt, gt) = (x.t.clone(), gamma.t.clone());
Ok(self.record(
out,
Box::new(move |dy| {
let (dx, dgamma, dbeta) = ops::layernorm_bwd(&xt, >, dy, eps)?;
Ok(vec![(ix, dx), (ig, dgamma), (ib, dbeta)])
}),
))
}
pub fn softmax(&mut self, x: &TVar, causal: bool, off: usize) -> Result<TVar> {
let out = ops::softmax(&x.t, causal, off)?;
let ix = x.id;
let yt = out.clone();
Ok(self.record(
out,
Box::new(move |dy| Ok(vec![(ix, ops::softmax_bwd(&yt, dy)?)])),
))
}
pub fn split_heads(&mut self, qkv: &TVar, n_head: usize) -> Result<(TVar, TVar, TVar)> {
let (q, k, v) = ops::split_heads(&qkv.t, n_head)?;
let iq = qkv.id;
let mk = move |which: usize| -> BackwardFn {
Box::new(move |dy| Ok(vec![(iq, ops::unsplit_head(dy, which)?)]))
};
Ok((
self.record(q, mk(0)),
self.record(k, mk(1)),
self.record(v, mk(2)),
))
}
pub fn merge_heads(&mut self, x: &TVar) -> Result<TVar> {
let h = x.t.shape().dim(0);
let out = ops::merge_heads(&x.t)?;
let ix = x.id;
Ok(self.record(
out,
Box::new(move |dy| Ok(vec![(ix, ops::unmerge_heads(dy, h)?)])),
))
}
pub fn embedding(&mut self, ids: &Tensor, wte: &TVar, wpe: &TVar, pos: usize) -> Result<TVar> {
let out = ops::embedding(ids, &wte.t, Some(&wpe.t), pos)?;
let (iw, ip) = (wte.id, wpe.id);
let ids = ids.clone();
let wte_shape = wte.t.shape().clone();
let (n_ctx, c) = (wpe.t.shape().dim(0), wpe.t.shape().dim(1));
Ok(self.record(
out,
Box::new(move |dy| {
let device = dy.device();
let mut dwte = Tensor::zeros(wte_shape.clone(), &device)?;
ops::scatter_add_rows(&mut dwte, &ids, dy)?;
let t = ids.shape().numel();
let mut dwpe3 = Tensor::zeros([1, n_ctx, c], &device)?;
ops::kv_append(&mut dwpe3, &dy.reshape([1, t, c])?, pos)?;
Ok(vec![(iw, dwte), (ip, dwpe3.reshape([n_ctx, c])?)])
}),
))
}
pub fn dropout(&mut self, x: &TVar, p: f32, seed: u32) -> Result<TVar> {
if p == 0.0 {
return Ok(x.clone());
}
let out = ops::dropout(&x.t, p, seed)?;
let ix = x.id;
Ok(self.record(
out,
Box::new(move |dy| Ok(vec![(ix, ops::dropout(dy, p, seed)?)])),
))
}
pub fn backward(&mut self, root: &TVar, seed_grad: Tensor) -> Result<Vec<Option<Tensor>>> {
let mut grads: Vec<Option<Tensor>> = (0..self.nodes.len()).map(|_| None).collect();
grads[root.id] = Some(seed_grad);
for id in (0..=root.id).rev() {
if grads[id].is_none() {
continue;
}
let Some(f) = self.nodes[id].backward.take() else {
continue; };
let dy = grads[id].take().expect("checked above");
for (pid, g) in f(&dy)? {
debug_assert!(pid < id, "backward edge must point to an earlier node");
grads[pid] = Some(match grads[pid].take() {
Some(acc) => ops::add(&acc, &g)?,
None => g,
});
}
}
Ok(grads)
}
}