use crate::env::Value;
use crate::parser::Expr;
use std::collections::HashMap;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Op {
Const(u64), Param(usize),
Add, Sub, Mul, Div,
Lt, Gt, Le, Ge, Eq,
If, TAdd, TSub, TMul, TDiv, MatMul, Transpose, TSum, Relu, Step, SumTo, Expand, }
#[derive(Clone, Debug)]
pub struct Node {
pub op: Op,
pub args: Vec<usize>, }
#[derive(Clone, Debug, Default)]
pub struct Graph {
pub nodes: Vec<Node>,
pub output: usize,
}
struct Interner {
nodes: Vec<Node>,
memo: HashMap<(Op, Vec<usize>), usize>,
}
impl Interner {
fn new() -> Self { Interner { nodes: Vec::new(), memo: HashMap::new() } }
fn from_graph(g: &Graph) -> Self {
let mut memo = HashMap::new();
for (i, n) in g.nodes.iter().enumerate() {
memo.entry((n.op.clone(), n.args.clone())).or_insert(i);
}
Interner { nodes: g.nodes.clone(), memo }
}
fn intern(&mut self, op: Op, args: Vec<usize>) -> usize {
let key = (op.clone(), args.clone());
if let Some(&i) = self.memo.get(&key) { return i; }
let i = self.nodes.len();
self.nodes.push(Node { op, args });
self.memo.insert(key, i);
i
}
fn const_val(&self, idx: usize) -> Option<f64> {
match &self.nodes[idx].op {
Op::Const(bits) => Some(f64::from_bits(*bits)),
_ => None,
}
}
fn konst(&mut self, v: f64) -> usize { self.intern(Op::Const(v.to_bits()), vec![]) }
}
fn sym_of(e: &Expr) -> Option<&str> {
match e {
Expr::Symbol(s) => Some(s.as_str()),
Expr::LocalRef { name, .. } | Expr::GlobalRef { name, .. } => Some(&**name),
_ => None,
}
}
fn build_num(ib: &mut Interner, params: &[String], expr: &Expr) -> Result<usize, String> {
match expr {
Expr::Number(n) => Ok(ib.konst(*n)),
Expr::Symbol(_) | Expr::LocalRef { .. } | Expr::GlobalRef { .. } => {
let s = sym_of(expr).unwrap();
if let Some(i) = params.iter().position(|p| p == s) {
Ok(ib.intern(Op::Param(i), vec![]))
} else {
Err(format!(
"graph-ir: unsupported reference to '{}' — only params, numbers, + - * /, \
if, and the tensor ops are supported", s
))
}
}
Expr::List(items) if !items.is_empty() => {
if let Some(head) = sym_of(&items[0]) {
match head {
"+" | "-" | "*" | "/" if items.len() >= 2 => {
let mut arg_ids = items[1..].iter()
.map(|e| build_num(ib, params, e))
.collect::<Result<Vec<_>, _>>()?;
let op = match head { "+" => Op::Add, "-" => Op::Sub, "*" => Op::Mul, "/" => Op::Div, _ => unreachable!() };
if head == "-" && arg_ids.len() == 1 {
let zero = ib.konst(0.0);
return Ok(ib.intern(Op::Sub, vec![zero, arg_ids[0]]));
}
let mut acc = arg_ids.remove(0);
for a in arg_ids { acc = ib.intern(op.clone(), vec![acc, a]); }
Ok(acc)
}
"if" if items.len() == 4 => {
let c = build_bool(ib, params, &items[1])?;
let t = build_num(ib, params, &items[2])?;
let e = build_num(ib, params, &items[3])?;
Ok(ib.intern(Op::If, vec![c, t, e]))
}
"tensor-add" | "tensor-sub" | "tensor-mul" | "tensor-div"
if items.len() == 3 =>
{
let a = build_num(ib, params, &items[1])?;
let b = build_num(ib, params, &items[2])?;
let op = match head {
"tensor-add" => Op::TAdd, "tensor-sub" => Op::TSub,
"tensor-mul" => Op::TMul, _ => Op::TDiv,
};
Ok(ib.intern(op, vec![a, b]))
}
"matmul" if items.len() == 3 => {
let a = build_num(ib, params, &items[1])?;
let b = build_num(ib, params, &items[2])?;
Ok(ib.intern(Op::MatMul, vec![a, b]))
}
"transpose" if items.len() == 2 => {
let a = build_num(ib, params, &items[1])?;
Ok(ib.intern(Op::Transpose, vec![a]))
}
"relu" if items.len() == 2 => {
let a = build_num(ib, params, &items[1])?;
Ok(ib.intern(Op::Relu, vec![a]))
}
"tensor-sum" if items.len() == 2 => {
let a = build_num(ib, params, &items[1])?;
Ok(ib.intern(Op::TSum, vec![a]))
}
other => Err(format!("graph-ir: unsupported operator '{}'", other)),
}
} else {
Err("graph-ir: unsupported expression".into())
}
}
_ => Err("graph-ir: only numbers, params, + - * /, if, and the tensor ops are supported".into()),
}
}
fn build_bool(ib: &mut Interner, params: &[String], expr: &Expr) -> Result<usize, String> {
if let Expr::List(items) = expr {
if let Some(head) = items.first().and_then(sym_of) {
let op = match head {
"<" => Some(Op::Lt), ">" => Some(Op::Gt), "<=" => Some(Op::Le),
">=" => Some(Op::Ge), "=" => Some(Op::Eq), _ => None,
};
if let (Some(op), 3) = (op, items.len()) {
let a = build_num(ib, params, &items[1])?;
let b = build_num(ib, params, &items[2])?;
return Ok(ib.intern(op, vec![a, b]));
}
}
}
Err("graph-ir: an `if` condition must be a comparison (< > <= >= =)".into())
}
pub fn build(params: &[String], body: &Expr) -> Result<Graph, String> {
let mut ib = Interner::new();
let output = build_num(&mut ib, params, body)?;
Ok(Graph { nodes: ib.nodes, output })
}
fn fold_binop(ib: &mut Interner, op: Op, a: usize, b: usize, f: impl Fn(f64, f64) -> f64) -> usize {
match (ib.const_val(a), ib.const_val(b)) {
(Some(x), Some(y)) => ib.konst(f(x, y)),
_ => ib.intern(op, vec![a, b]),
}
}
fn fold_cmp(ib: &mut Interner, op: Op, a: usize, b: usize, f: impl Fn(f64, f64) -> bool) -> usize {
match (ib.const_val(a), ib.const_val(b)) {
(Some(x), Some(y)) => ib.konst(if f(x, y) { 1.0 } else { 0.0 }),
_ => ib.intern(op, vec![a, b]),
}
}
fn fold_nodes(graph: &Graph) -> (Vec<Node>, Vec<usize>) {
let mut ib = Interner::new();
let mut remap = vec![0usize; graph.nodes.len()];
for (i, node) in graph.nodes.iter().enumerate() {
let args: Vec<usize> = node.args.iter().map(|&a| remap[a]).collect();
let new_idx = match (&node.op, args.as_slice()) {
(Op::Add, [a, b]) => fold_binop(&mut ib, Op::Add, *a, *b, |x, y| x + y),
(Op::Sub, [a, b]) => fold_binop(&mut ib, Op::Sub, *a, *b, |x, y| x - y),
(Op::Mul, [a, b]) => fold_binop(&mut ib, Op::Mul, *a, *b, |x, y| x * y),
(Op::Div, [a, b]) => fold_binop(&mut ib, Op::Div, *a, *b, |x, y| x / y),
(Op::Lt, [a, b]) => fold_cmp(&mut ib, Op::Lt, *a, *b, |x, y| x < y),
(Op::Gt, [a, b]) => fold_cmp(&mut ib, Op::Gt, *a, *b, |x, y| x > y),
(Op::Le, [a, b]) => fold_cmp(&mut ib, Op::Le, *a, *b, |x, y| x <= y),
(Op::Ge, [a, b]) => fold_cmp(&mut ib, Op::Ge, *a, *b, |x, y| x >= y),
(Op::Eq, [a, b]) => fold_cmp(&mut ib, Op::Eq, *a, *b, |x, y| x == y),
(Op::If, [c, t, e]) => match ib.const_val(*c) {
Some(cv) => if cv != 0.0 { *t } else { *e },
None => ib.intern(Op::If, args.clone()),
},
(other, _) => ib.intern(other.clone(), args.clone()),
};
remap[i] = new_idx;
}
(ib.nodes, remap)
}
fn dce_nodes(nodes: &[Node], outputs: &[usize]) -> (Vec<Node>, Vec<usize>) {
let mut reachable = vec![false; nodes.len()];
let mut stack: Vec<usize> = outputs.to_vec();
while let Some(i) = stack.pop() {
if reachable[i] { continue; }
reachable[i] = true;
for &a in &nodes[i].args { stack.push(a); }
}
let mut new_index = vec![usize::MAX; nodes.len()];
let mut new_nodes = Vec::new();
for (i, node) in nodes.iter().enumerate() {
if reachable[i] {
let args = node.args.iter().map(|&a| new_index[a]).collect();
new_index[i] = new_nodes.len();
new_nodes.push(Node { op: node.op.clone(), args });
}
}
let new_outputs = outputs.iter().map(|&o| new_index[o]).collect();
(new_nodes, new_outputs)
}
pub fn optimize(graph: &Graph) -> Graph {
let (g, outs) = optimize_outputs(graph, &[graph.output]);
Graph { nodes: g.nodes, output: outs[0] }
}
pub fn optimize_outputs(graph: &Graph, outputs: &[usize]) -> (Graph, Vec<usize>) {
let (nodes, remap) = fold_nodes(graph);
let folded_outs: Vec<usize> = outputs.iter().map(|&o| remap[o]).collect();
let (nodes, outs) = dce_nodes(&nodes, &folded_outs);
(Graph { nodes, output: outs[0] }, outs)
}
fn accum(ib: &mut Interner, adj: &mut [Option<usize>], target: usize, contrib: usize) {
adj[target] = Some(match adj[target] {
Some(prev) => ib.intern(Op::TAdd, vec![prev, contrib]),
None => contrib,
});
}
pub fn backward(graph: &Graph, nparams: usize) -> Result<(Graph, Vec<usize>), String> {
let mut ib = Interner::from_graph(graph);
let n = graph.nodes.len();
let mut adj: Vec<Option<usize>> = vec![None; n];
let seed = ib.konst(1.0);
adj[graph.output] = Some(seed);
for i in (0..n).rev() {
let g = match adj[i] { Some(g) => g, None => continue };
let node = ib.nodes[i].clone();
match (&node.op, node.args.as_slice()) {
(Op::Const(_), _) | (Op::Param(_), _) => {} (Op::Add, &[a, b]) => {
accum(&mut ib, &mut adj, a, g);
accum(&mut ib, &mut adj, b, g);
}
(Op::Sub, &[a, b]) => {
accum(&mut ib, &mut adj, a, g);
let zero = ib.konst(0.0);
let neg = ib.intern(Op::Sub, vec![zero, g]);
accum(&mut ib, &mut adj, b, neg);
}
(Op::Mul, &[a, b]) => {
let da = ib.intern(Op::Mul, vec![g, b]);
let db = ib.intern(Op::Mul, vec![g, a]);
accum(&mut ib, &mut adj, a, da);
accum(&mut ib, &mut adj, b, db);
}
(Op::Div, &[a, b]) => {
let da = ib.intern(Op::Div, vec![g, b]);
accum(&mut ib, &mut adj, a, da);
let ga = ib.intern(Op::Mul, vec![g, a]);
let bb = ib.intern(Op::Mul, vec![b, b]);
let q = ib.intern(Op::Div, vec![ga, bb]);
let zero = ib.konst(0.0);
let db = ib.intern(Op::Sub, vec![zero, q]);
accum(&mut ib, &mut adj, b, db);
}
(Op::TAdd, &[a, b]) => {
let da = ib.intern(Op::SumTo, vec![g, a]);
let db = ib.intern(Op::SumTo, vec![g, b]);
accum(&mut ib, &mut adj, a, da);
accum(&mut ib, &mut adj, b, db);
}
(Op::TSub, &[a, b]) => {
let da = ib.intern(Op::SumTo, vec![g, a]);
accum(&mut ib, &mut adj, a, da);
let neg1 = ib.konst(-1.0);
let ng = ib.intern(Op::TMul, vec![g, neg1]);
let db = ib.intern(Op::SumTo, vec![ng, b]);
accum(&mut ib, &mut adj, b, db);
}
(Op::TMul, &[a, b]) => {
let gb = ib.intern(Op::TMul, vec![g, b]);
let da = ib.intern(Op::SumTo, vec![gb, a]);
accum(&mut ib, &mut adj, a, da);
let ga = ib.intern(Op::TMul, vec![g, a]);
let db = ib.intern(Op::SumTo, vec![ga, b]);
accum(&mut ib, &mut adj, b, db);
}
(Op::TDiv, &[a, b]) => {
let gb = ib.intern(Op::TDiv, vec![g, b]);
let da = ib.intern(Op::SumTo, vec![gb, a]);
accum(&mut ib, &mut adj, a, da);
let ga = ib.intern(Op::TMul, vec![g, a]);
let bb = ib.intern(Op::TMul, vec![b, b]);
let q = ib.intern(Op::TDiv, vec![ga, bb]);
let neg1 = ib.konst(-1.0);
let nq = ib.intern(Op::TMul, vec![q, neg1]);
let db = ib.intern(Op::SumTo, vec![nq, b]);
accum(&mut ib, &mut adj, b, db);
}
(Op::MatMul, &[a, b]) => {
let bt = ib.intern(Op::Transpose, vec![b]);
let da = ib.intern(Op::MatMul, vec![g, bt]);
accum(&mut ib, &mut adj, a, da);
let at = ib.intern(Op::Transpose, vec![a]);
let db = ib.intern(Op::MatMul, vec![at, g]);
accum(&mut ib, &mut adj, b, db);
}
(Op::Transpose, &[a]) => {
let da = ib.intern(Op::Transpose, vec![g]);
accum(&mut ib, &mut adj, a, da);
}
(Op::TSum, &[a]) => {
let da = ib.intern(Op::Expand, vec![g, a]);
accum(&mut ib, &mut adj, a, da);
}
(Op::Relu, &[a]) => {
let mask = ib.intern(Op::Step, vec![a]);
let da = ib.intern(Op::TMul, vec![g, mask]);
accum(&mut ib, &mut adj, a, da);
}
(op, _) => {
return Err(format!(
"graph-grad: cannot differentiate through '{}' — comparisons and \
data-dependent `if` are not differentiable (a constant-condition if \
is pruned before this pass and is fine)", op_name(op)
));
}
}
}
let grads = (0..nparams).map(|p| {
let pn = ib.intern(Op::Param(p), vec![]);
match adj.get(pn).copied().flatten() {
Some(g) => g,
None => {
let zero = ib.konst(0.0);
ib.intern(Op::Expand, vec![zero, pn])
}
}
}).collect();
Ok((Graph { nodes: ib.nodes, output: graph.output }, grads))
}
#[derive(Clone, Debug)]
pub enum GVal {
Num(f64),
Tensor { data: Rc<Vec<f64>>, shape: Vec<usize> },
}
fn num(v: &GVal, op: &str) -> Result<f64, String> {
match v {
GVal::Num(n) => Ok(*n),
GVal::Tensor { shape, .. } => Err(format!(
"graph-eval: {} expects a number, got a {} tensor — use the tensor-* ops",
op, shape.iter().map(|d| d.to_string()).collect::<Vec<_>>().join("x")
)),
}
}
fn t_binop(a: &GVal, b: &GVal, name: &str, f: fn(f64, f64) -> f64) -> Result<GVal, String> {
match (a, b) {
(GVal::Tensor { data: xd, shape: xs }, GVal::Tensor { data: yd, shape: ys }) => {
if xs != ys {
return Err(format!("graph-eval: {}: shape mismatch {:?} vs {:?}", name, xs, ys));
}
let data: Vec<f64> = xd.iter().zip(yd.iter()).map(|(x, y)| f(*x, *y)).collect();
Ok(GVal::Tensor { data: Rc::new(data), shape: xs.clone() })
}
(GVal::Tensor { data, shape }, GVal::Num(s)) =>
Ok(GVal::Tensor { data: Rc::new(data.iter().map(|x| f(*x, *s)).collect()), shape: shape.clone() }),
(GVal::Num(s), GVal::Tensor { data, shape }) =>
Ok(GVal::Tensor { data: Rc::new(data.iter().map(|x| f(*s, *x)).collect()), shape: shape.clone() }),
(GVal::Num(x), GVal::Num(y)) => Ok(GVal::Num(f(*x, *y))),
}
}
#[inline(always)]
fn matmul_ikj_rows(xd: &[f64], yd: &[f64], i0: usize, rows: usize, k: usize, m: usize, out: &mut [f64]) {
for li in 0..rows {
let x_row = &xd[(i0 + li) * k..(i0 + li + 1) * k];
let o_row = &mut out[li * m..(li + 1) * m];
for p in 0..k {
let x = x_row[p];
let y_row = &yd[p * m..(p + 1) * m];
for j in 0..m {
o_row[j] += x * y_row[j];
}
}
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn matmul_ikj_rows_avx2(xd: &[f64], yd: &[f64], i0: usize, rows: usize, k: usize, m: usize, out: &mut [f64]) {
matmul_ikj_rows(xd, yd, i0, rows, k, m, out)
}
fn matmul_rows_dispatch(xd: &[f64], yd: &[f64], i0: usize, rows: usize, k: usize, m: usize, out: &mut [f64]) {
#[cfg(target_arch = "x86_64")]
if std::arch::is_x86_feature_detected!("avx2") {
return unsafe { matmul_ikj_rows_avx2(xd, yd, i0, rows, k, m, out) };
}
matmul_ikj_rows(xd, yd, i0, rows, k, m, out)
}
mod mm_pool {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, OnceLock};
#[derive(Clone, Copy)]
pub struct Job {
a: *const f64, a_len: usize,
b: *const f64, b_len: usize,
out: *mut f64,
n: usize, k: usize, m: usize,
nchunks: usize, chunk_rows: usize,
}
unsafe impl Send for Job {}
struct Shared {
slot: Mutex<(u64, Option<Job>)>, cv: Condvar,
done: AtomicUsize, }
pub struct Pool {
sh: Arc<Shared>,
pub workers: usize,
}
fn worker(sh: Arc<Shared>, w: usize) {
let mut seen = 0u64;
loop {
let job = {
let mut g = sh.slot.lock().unwrap();
while g.0 == seen {
g = sh.cv.wait(g).unwrap();
}
seen = g.0;
g.1.unwrap()
};
let ci = w + 1; if ci < job.nchunks {
let i0 = ci * job.chunk_rows;
let rows = job.chunk_rows.min(job.n - i0);
unsafe {
let a = std::slice::from_raw_parts(job.a, job.a_len);
let b = std::slice::from_raw_parts(job.b, job.b_len);
let out = std::slice::from_raw_parts_mut(job.out.add(i0 * job.m), rows * job.m);
super::matmul_rows_dispatch(a, b, i0, rows, job.k, job.m, out);
}
sh.done.fetch_add(1, Ordering::Release);
}
}
}
impl Pool {
fn new(workers: usize) -> Pool {
let sh = Arc::new(Shared {
slot: Mutex::new((0, None)),
cv: Condvar::new(),
done: AtomicUsize::new(0),
});
for w in 0..workers {
let s = sh.clone();
std::thread::Builder::new()
.name(format!("rusty-mm-{}", w))
.spawn(move || worker(s, w))
.expect("rusty-mm: failed to spawn pool worker");
}
Pool { sh, workers }
}
pub fn matmul(&self, a: &[f64], b: &[f64], n: usize, k: usize, m: usize, out: &mut [f64]) {
let nchunks = (self.workers + 1).min(n);
let chunk_rows = (n + nchunks - 1) / nchunks;
let nchunks = (n + chunk_rows - 1) / chunk_rows;
let job = Job {
a: a.as_ptr(), a_len: a.len(),
b: b.as_ptr(), b_len: b.len(),
out: out.as_mut_ptr(),
n, k, m, nchunks, chunk_rows,
};
self.sh.done.store(0, Ordering::Relaxed);
{
let mut g = self.sh.slot.lock().unwrap();
g.0 += 1;
g.1 = Some(job);
}
self.sh.cv.notify_all();
let rows0 = chunk_rows.min(n);
super::matmul_rows_dispatch(a, b, 0, rows0, k, m, &mut out[..rows0 * m]);
while self.sh.done.load(Ordering::Acquire) < nchunks - 1 {
std::hint::spin_loop();
}
}
}
pub fn get() -> Option<&'static Pool> {
static POOL: OnceLock<Option<Pool>> = OnceLock::new();
POOL.get_or_init(|| {
let nthreads = std::env::var("RUSTY_MM_THREADS")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or_else(|| std::thread::available_parallelism().map(|x| x.get()).unwrap_or(1));
if nthreads <= 1 {
return None;
}
Some(Pool::new(nthreads - 1)) })
.as_ref()
}
}
pub const MM_POOL_MIN_MULADDS: usize = 65536;
pub fn matmul_ikj_into(xd: &[f64], yd: &[f64], n: usize, k: usize, m: usize, out: &mut [f64]) {
if n >= 2 && n * k * m >= MM_POOL_MIN_MULADDS {
if let Some(pool) = mm_pool::get() {
pool.matmul(xd, yd, n, k, m, out);
return;
}
}
matmul_rows_dispatch(xd, yd, 0, n, k, m, out);
}
pub fn matmul_ikj(xd: &[f64], yd: &[f64], n: usize, k: usize, m: usize) -> Vec<f64> {
let mut data = vec![0.0; n * m];
matmul_ikj_into(xd, yd, n, k, m, &mut data);
data
}
pub extern "C" fn mm_bridge(
a: *const f64, a_len: usize,
b: *const f64, b_len: usize,
out: *mut f64,
rows: usize, k: usize, cols: usize,
) {
unsafe {
let a = std::slice::from_raw_parts(a, a_len);
let b = std::slice::from_raw_parts(b, b_len);
let out = std::slice::from_raw_parts_mut(out, rows * cols);
matmul_ikj_into(a, b, rows, k, cols, out);
}
}
fn t_matmul(a: &GVal, b: &GVal) -> Result<GVal, String> {
match (a, b) {
(GVal::Tensor { data: xd, shape: xs }, GVal::Tensor { data: yd, shape: ys }) => {
if xs.len() != 2 || ys.len() != 2 {
return Err("graph-eval: matmul: both arguments must be rank-2 tensors".into());
}
let (n, k) = (xs[0], xs[1]);
let (k2, m) = (ys[0], ys[1]);
if k != k2 {
return Err(format!("graph-eval: matmul: inner dimensions differ ({} vs {})", k, k2));
}
let data = matmul_ikj(xd, yd, n, k, m);
Ok(GVal::Tensor { data: Rc::new(data), shape: vec![n, m] })
}
_ => Err("graph-eval: matmul: both arguments must be tensors".into()),
}
}
fn t_transpose(a: &GVal) -> Result<GVal, String> {
match a {
GVal::Tensor { data, shape } if shape.len() == 2 => {
let (n, m) = (shape[0], shape[1]);
let mut out = vec![0.0; n * m];
for i in 0..n {
for j in 0..m {
out[j * n + i] = data[i * m + j];
}
}
Ok(GVal::Tensor { data: Rc::new(out), shape: vec![m, n] })
}
_ => Err("graph-eval: transpose: argument must be a rank-2 tensor".into()),
}
}
pub fn eval_graph(graph: &Graph, inputs: &[GVal]) -> Result<GVal, String> {
Ok(eval_nodes(graph, inputs)?[graph.output].clone())
}
pub fn eval_graph_outputs(graph: &Graph, inputs: &[GVal], outputs: &[usize]) -> Result<Vec<GVal>, String> {
let vals = eval_nodes(graph, inputs)?;
Ok(outputs.iter().map(|&o| vals[o].clone()).collect())
}
fn eval_nodes(graph: &Graph, inputs: &[GVal]) -> Result<Vec<GVal>, String> {
let mut vals: Vec<GVal> = Vec::with_capacity(graph.nodes.len());
for node in &graph.nodes {
let a = node.args.first().map(|&x| &vals[x]);
let b = node.args.get(1).map(|&x| &vals[x]);
let v = match &node.op {
Op::Const(bits) => GVal::Num(f64::from_bits(*bits)),
Op::Param(p) => inputs[*p].clone(),
Op::Add => GVal::Num(num(a.unwrap(), "+")? + num(b.unwrap(), "+")?),
Op::Sub => GVal::Num(num(a.unwrap(), "-")? - num(b.unwrap(), "-")?),
Op::Mul => GVal::Num(num(a.unwrap(), "*")? * num(b.unwrap(), "*")?),
Op::Div => GVal::Num(num(a.unwrap(), "/")? / num(b.unwrap(), "/")?),
Op::Lt => GVal::Num(if num(a.unwrap(), "<")? < num(b.unwrap(), "<")? { 1.0 } else { 0.0 }),
Op::Gt => GVal::Num(if num(a.unwrap(), ">")? > num(b.unwrap(), ">")? { 1.0 } else { 0.0 }),
Op::Le => GVal::Num(if num(a.unwrap(), "<=")? <= num(b.unwrap(), "<=")? { 1.0 } else { 0.0 }),
Op::Ge => GVal::Num(if num(a.unwrap(), ">=")? >= num(b.unwrap(), ">=")? { 1.0 } else { 0.0 }),
Op::Eq => GVal::Num(if num(a.unwrap(), "=")? == num(b.unwrap(), "=")? { 1.0 } else { 0.0 }),
Op::If => {
let c = num(&vals[node.args[0]], "if")?;
if c != 0.0 { vals[node.args[1]].clone() } else { vals[node.args[2]].clone() }
}
Op::TAdd => t_binop(a.unwrap(), b.unwrap(), "tensor-add", |x, y| x + y)?,
Op::TSub => t_binop(a.unwrap(), b.unwrap(), "tensor-sub", |x, y| x - y)?,
Op::TMul => t_binop(a.unwrap(), b.unwrap(), "tensor-mul", |x, y| x * y)?,
Op::TDiv => t_binop(a.unwrap(), b.unwrap(), "tensor-div", |x, y| x / y)?,
Op::MatMul => t_matmul(a.unwrap(), b.unwrap())?,
Op::Transpose => t_transpose(a.unwrap())?,
Op::TSum => match a.unwrap() {
GVal::Tensor { data, .. } => GVal::Num(data.iter().sum()),
GVal::Num(n) => GVal::Num(*n),
},
Op::Relu => match a.unwrap() {
GVal::Num(n) => GVal::Num(n.max(0.0)),
GVal::Tensor { data, shape } => GVal::Tensor {
data: Rc::new(data.iter().map(|x| x.max(0.0)).collect()),
shape: shape.clone(),
},
},
Op::Step => match a.unwrap() {
GVal::Num(n) => GVal::Num(if *n > 0.0 { 1.0 } else { 0.0 }),
GVal::Tensor { data, shape } => GVal::Tensor {
data: Rc::new(data.iter().map(|x| if *x > 0.0 { 1.0 } else { 0.0 }).collect()),
shape: shape.clone(),
},
},
Op::SumTo => match (a.unwrap(), b.unwrap()) {
(g, GVal::Num(_)) => match g {
GVal::Num(n) => GVal::Num(*n),
GVal::Tensor { data, .. } => GVal::Num(data.iter().sum()),
},
(GVal::Tensor { data, shape }, GVal::Tensor { shape: like, .. }) => {
if shape != like {
return Err(format!("graph-grad: internal SumTo shape mismatch {:?} vs {:?}", shape, like));
}
GVal::Tensor { data: data.clone(), shape: shape.clone() }
}
(GVal::Num(_), GVal::Tensor { .. }) =>
return Err("graph-grad: the loss must evaluate to a scalar (use tensor-sum or a mean)".into()),
},
Op::Expand => match (a.unwrap(), b.unwrap()) {
(GVal::Num(n), GVal::Num(_)) => GVal::Num(*n),
(GVal::Num(n), GVal::Tensor { shape, data }) =>
GVal::Tensor { data: Rc::new(vec![*n; data.len()]), shape: shape.clone() },
(GVal::Tensor { .. }, _) =>
return Err("graph-grad: internal Expand — expected a scalar gradient".into()),
},
};
vals.push(v);
}
Ok(vals)
}
pub type SShape = Option<Vec<usize>>;
pub fn infer_shapes(graph: &Graph, inputs: &[SShape]) -> Result<Vec<SShape>, String> {
let mut shapes: Vec<SShape> = Vec::with_capacity(graph.nodes.len());
for node in &graph.nodes {
let a = node.args.first().map(|&x| shapes[x].clone());
let b = node.args.get(1).map(|&x| shapes[x].clone());
let s = match &node.op {
Op::Const(_) => None,
Op::Param(p) => inputs.get(*p)
.ok_or_else(|| format!("graph-compile-grad: missing shape for param {}", p))?
.clone(),
Op::Add | Op::Sub | Op::Mul | Op::Div
| Op::Lt | Op::Gt | Op::Le | Op::Ge | Op::Eq => {
if a.clone().flatten().is_some() || b.clone().flatten().is_some() {
return Err("graph-compile-grad: scalar op applied to a tensor (use tensor-add etc.)".into());
}
None
}
Op::If => {
let t = shapes[node.args[1]].clone();
let e = shapes[node.args[2]].clone();
if shapes[node.args[0]].is_some() {
return Err("graph-compile-grad: `if` condition must be a scalar".into());
}
if t != e {
return Err("graph-compile-grad: `if` branches must have the same shape".into());
}
t
}
Op::TAdd | Op::TSub | Op::TMul | Op::TDiv => {
match (a.clone().unwrap(), b.clone().unwrap()) {
(Some(x), Some(y)) if x == y => Some(x),
(Some(x), Some(y)) => return Err(format!(
"graph-compile-grad: elementwise op on mismatched shapes {:?} vs {:?}", x, y)),
(Some(x), None) | (None, Some(x)) => Some(x), (None, None) => None,
}
}
Op::MatMul => match (a.clone().unwrap(), b.clone().unwrap()) {
(Some(x), Some(y)) if x.len() == 2 && y.len() == 2 && x[1] == y[0] =>
Some(vec![x[0], y[1]]),
(x, y) => return Err(format!(
"graph-compile-grad: matmul needs rank-2 tensors with matching inner dim, got {:?} × {:?}", x, y)),
},
Op::Transpose => match a.clone().unwrap() {
Some(x) if x.len() == 2 => Some(vec![x[1], x[0]]),
x => return Err(format!("graph-compile-grad: transpose needs a rank-2 tensor, got {:?}", x)),
},
Op::TSum => None,
Op::Relu | Op::Step => a.clone().unwrap(),
Op::SumTo => match (a.clone().unwrap(), b.clone().unwrap()) {
(_, None) => None,
(Some(x), Some(like)) if x == like => Some(x),
(Some(x), Some(like)) => return Err(format!(
"graph-grad: internal SumTo shape mismatch {:?} vs {:?}", x, like)),
(None, Some(_)) =>
return Err("graph-grad: the loss must evaluate to a scalar (use tensor-sum or a mean)".into()),
},
Op::Expand => match (a.clone().unwrap(), b.clone().unwrap()) {
(None, like) => like,
(Some(_), _) =>
return Err("graph-grad: internal Expand — expected a scalar gradient".into()),
},
};
shapes.push(s);
}
Ok(shapes)
}
pub fn op_name(op: &Op) -> &'static str {
match op {
Op::Const(_) => "const", Op::Param(_) => "param",
Op::Add => "add", Op::Sub => "sub", Op::Mul => "mul", Op::Div => "div",
Op::Lt => "lt", Op::Gt => "gt", Op::Le => "le", Op::Ge => "ge", Op::Eq => "eq",
Op::If => "if",
Op::TAdd => "tensor-add", Op::TSub => "tensor-sub",
Op::TMul => "tensor-mul", Op::TDiv => "tensor-div",
Op::MatMul => "matmul", Op::Transpose => "transpose", Op::TSum => "tensor-sum",
Op::Relu => "relu", Op::Step => "step", Op::SumTo => "sum-to", Op::Expand => "expand",
}
}
pub fn to_value(graph: &Graph) -> Value {
let nodes: Vec<Value> = graph.nodes.iter().enumerate().map(|(i, node)| {
let mut row = vec![Value::Number(i as f64), Value::Symbol(op_name(&node.op).to_string())];
match &node.op {
Op::Const(bits) => row.push(Value::Number(f64::from_bits(*bits))),
Op::Param(p) => row.push(Value::Number(*p as f64)),
_ => row.extend(node.args.iter().map(|&a| Value::Number(a as f64))),
}
crate::env::list(row)
}).collect();
crate::env::list(vec![crate::env::list(nodes), Value::Number(graph.output as f64)])
}