1use std::collections::BTreeMap;
19use std::fmt::Write as _;
20
21use crate::spatial::passes::recognize_stages::{RecognizedProgram, RecognizedStage, TensorOpSpec};
22use crate::tensor_recognize::{ElementwiseOp, Epsilon, RowReduceKind};
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum VjpRefusal {
27 NoGradient { op: &'static str, why: &'static str },
30 NotDerived { op: &'static str },
32 NotDifferentiated { tensor: String, op: &'static str },
35 Shapeless { tensor: String },
37 Unrecognized { statements: Vec<(usize, String)> },
39 UnknownOutput { tensor: String },
41 Parse(String),
43}
44
45#[derive(Clone, Debug, PartialEq)]
47pub struct Backward {
48 pub source: String,
50 pub grads: BTreeMap<String, String>,
52}
53
54struct Names {
56 next: usize,
57}
58
59impl Names {
60 fn fresh(&mut self, stem: &str) -> String {
61 self.next += 1;
62 format!("{stem}_{}", self.next)
63 }
64}
65
66struct Out {
68 text: String,
69 names: Names,
70 grads: BTreeMap<String, String>,
71 shapes: BTreeMap<String, (usize, usize)>,
72 undiff: BTreeMap<String, &'static str>,
74}
75
76impl Out {
77 fn line(&mut self, depth: usize, s: impl AsRef<str>) {
78 for _ in 0..depth {
79 self.text.push_str(" ");
80 }
81 self.text.push_str(s.as_ref());
82 self.text.push('\n');
83 }
84
85 fn tensor(&mut self, stem: &str, (rows, cols): (usize, usize)) -> String {
87 let name = self.names.fresh(stem);
88 self.line(0, format!("Let {name} be a {rows} by {cols} tensor of Real."));
89 self.shapes.insert(name.clone(), (rows, cols));
90 name
91 }
92
93 fn accumulate(&mut self, tensor: &str, contribution: String) {
96 match self.grads.get(tensor).cloned() {
97 None => {
98 self.grads.insert(tensor.to_string(), contribution);
99 }
100 Some(previous) => {
101 let sum = self.names.fresh(&format!("g_{tensor}"));
102 self.line(0, format!("Let {sum} be {previous} plus {contribution}."));
103 let shape = self.shapes[&previous];
104 self.shapes.insert(sum.clone(), shape);
105 self.grads.insert(tensor.to_string(), sum);
106 }
107 }
108 }
109
110 fn shape(&self, tensor: &str) -> Result<(usize, usize), VjpRefusal> {
111 self.shapes.get(tensor).copied().ok_or_else(|| VjpRefusal::Shapeless { tensor: tensor.to_string() })
112 }
113
114 fn each(&mut self, (rows, cols): (usize, usize), body: impl FnOnce(&mut Out, usize, &str, &str, &str)) {
117 let r = self.names.fresh("r");
118 let c = self.names.fresh("c");
119 let k = self.names.fresh("k");
120 self.line(0, format!("Repeat for {r} from 1 to {rows}:"));
121 self.line(1, format!("Repeat for {c} from 1 to {cols}:"));
122 self.line(2, format!("Let {k} be ({r} - 1) * {cols} + {c}."));
123 body(self, 2, &r, &c, &k);
124 }
125}
126
127pub fn backward(forward: &str, output: &str, cotangent: &str, wrt: &[&str]) -> Result<Backward, VjpRefusal> {
131 let rp = crate::spatial::passes::recognize_stages::recognize_stages(forward)
132 .map_err(|e| VjpRefusal::Parse(format!("{e:?}")))?;
133 backward_of(&rp, output, cotangent, wrt)
134}
135
136pub fn backward_of(rp: &RecognizedProgram, output: &str, cotangent: &str, wrt: &[&str]) -> Result<Backward, VjpRefusal> {
138 if !rp.unrecognized.is_empty() {
139 return Err(VjpRefusal::Unrecognized { statements: rp.unrecognized.clone() });
140 }
141 let produced = rp.stages.iter().any(|s| s.out == output);
142 let declared = rp.declared.iter().any(|d| d.name == output);
143 if !produced && !declared {
144 return Err(VjpRefusal::UnknownOutput { tensor: output.to_string() });
145 }
146 let mut out = Out {
147 text: String::new(),
148 names: Names { next: 0 },
149 grads: BTreeMap::new(),
150 shapes: rp.shapes.iter().map(|(k, v)| (k.clone(), *v)).collect(),
151 undiff: BTreeMap::new(),
152 };
153 out.grads.insert(output.to_string(), cotangent.to_string());
154 for st in rp.stages.iter().rev() {
155 let Some(d_out) = out.grads.get(&st.out).cloned() else { continue };
156 vjp_stage(rp, st, &d_out, &mut out)?;
157 }
158 for &t in wrt {
159 if let Some(op) = out.undiff.get(t) {
160 return Err(VjpRefusal::NotDifferentiated { tensor: t.to_string(), op });
161 }
162 if !out.grads.contains_key(t) {
163 let shape = out.shape(t)?;
164 let zero = out.tensor(&format!("g_{t}"), shape);
165 out.grads.insert(t.to_string(), zero);
166 }
167 }
168 let grads = wrt.iter().map(|&t| (t.to_string(), out.grads[t].clone())).collect();
169 Ok(Backward { source: out.text, grads })
170}
171
172fn vjp_stage(rp: &RecognizedProgram, st: &RecognizedStage, d_out: &str, out: &mut Out) -> Result<(), VjpRefusal> {
175 match &st.op {
176 TensorOpSpec::Matmul(m) => {
177 let (a, b) = (rp.name(m.a).to_string(), rp.name(m.b).to_string());
179 let bt = out.names.fresh(&format!("t_{b}"));
180 out.line(0, format!("Let {bt} be transpose of {b}."));
181 out.shapes.insert(bt.clone(), (m.n, m.k));
182 let da = out.names.fresh(&format!("g_{a}"));
183 out.line(0, format!("Let {da} be {d_out} matmul {bt}."));
184 out.shapes.insert(da.clone(), (m.m, m.k));
185 let at = out.names.fresh(&format!("t_{a}"));
186 out.line(0, format!("Let {at} be transpose of {a}."));
187 out.shapes.insert(at.clone(), (m.k, m.m));
188 let db = out.names.fresh(&format!("g_{b}"));
189 out.line(0, format!("Let {db} be {at} matmul {d_out}."));
190 out.shapes.insert(db.clone(), (m.k, m.n));
191 out.accumulate(&a, da);
192 out.accumulate(&b, db);
193 Ok(())
194 }
195 TensorOpSpec::Elementwise(e) => {
196 let (a, b) = (rp.name(e.a).to_string(), rp.name(e.b).to_string());
197 match e.op {
198 ElementwiseOp::Add => {
199 out.accumulate(&a, d_out.to_string());
200 out.accumulate(&b, d_out.to_string());
201 }
202 ElementwiseOp::Mul => {
203 let shape = out.shape(&a)?;
204 let da = out.names.fresh(&format!("g_{a}"));
205 out.line(0, format!("Let {da} be {d_out} hadamard {b}."));
206 out.shapes.insert(da.clone(), shape);
207 let db = out.names.fresh(&format!("g_{b}"));
208 out.line(0, format!("Let {db} be {d_out} hadamard {a}."));
209 out.shapes.insert(db.clone(), shape);
210 out.accumulate(&a, da);
211 out.accumulate(&b, db);
212 }
213 }
214 Ok(())
215 }
216 TensorOpSpec::Scale(c) => {
217 let x = rp.name(c.x).to_string();
219 let shape = out.shape(&x)?;
220 let dx = out.names.fresh(&format!("g_{x}"));
221 out.line(0, format!("Let {dx} be {d_out} scaled by {:?}.", c.factor.get()));
222 out.shapes.insert(dx.clone(), shape);
223 out.accumulate(&x, dx);
224 Ok(())
225 }
226 TensorOpSpec::Transpose(t) => {
227 let x = rp.name(t.x).to_string();
228 let shape = out.shape(&x)?;
229 let dx = out.names.fresh(&format!("g_{x}"));
230 out.line(0, format!("Let {dx} be transpose of {d_out}."));
231 out.shapes.insert(dx.clone(), shape);
232 out.accumulate(&x, dx);
233 Ok(())
234 }
235 TensorOpSpec::FusedRow(r) => {
236 let x = rp.name(r.x).to_string();
237 let y = rp.name(r.y).to_string();
238 let shape = (r.rows, r.cols);
239 match r.reduce {
240 RowReduceKind::Softmax => softmax_rule(out, &y, d_out, &x, shape),
241 RowReduceKind::RmsNorm => rmsnorm_rule(rp, r, out, d_out, shape),
242 RowReduceKind::LayerNorm => layernorm_rule(rp, r, out, d_out, shape),
243 }
244 }
245 TensorOpSpec::Sigmoid(s) => {
246 let (x, y) = (rp.name(s.x).to_string(), rp.name(s.out).to_string());
248 pointwise(out, &x, d_out, &[&y], |v| format!("{0} * {1} * (1.0 - {1})", v[0], v[1]))
249 }
250 TensorOpSpec::Silu(s) => {
251 let x = rp.name(s.x).to_string();
253 pointwise_with_sigma(out, &x, d_out, |dy, x, sg| format!("{dy} * ({sg} + {x} * {sg} * (1.0 - {sg}))"))
254 }
255 TensorOpSpec::Gelu(g) => {
256 let x = rp.name(g.x).to_string();
258 gelu_rule(out, &x, d_out)
259 }
260 TensorOpSpec::SwiGlu(s) => {
261 let (g, u) = (rp.name(s.gate).to_string(), rp.name(s.up).to_string());
263 swiglu_rule(out, &g, &u, d_out)
264 }
265 TensorOpSpec::L2Norm(l) => l2norm_rule(out, rp.name(l.x), l.eps, d_out, (l.rows, l.cols)),
266 TensorOpSpec::RowGather(g) => {
267 let table = rp.name(g.table).to_string();
268 row_scatter_rule(rp, out, &table, d_out, &RowIndexText::of(rp, &g.index))
269 }
270 TensorOpSpec::RowsGather(g) => {
271 let table = rp.name(g.table).to_string();
272 let indices = rp.name(g.indices).to_string();
273 out.undiff.insert(indices.clone(), "RowsGather");
274 rows_scatter_rule(out, &table, &indices, d_out)
275 }
276 TensorOpSpec::ArgMax(_) => Err(VjpRefusal::NoGradient {
277 op: "ArgMax",
278 why: "the index of the largest element is piecewise constant in every input",
279 }),
280 TensorOpSpec::Rope(r) => rope_rule(rp, r, out, d_out),
281 TensorOpSpec::Decay(_) => Err(VjpRefusal::NotDerived { op: "Decay" }),
282 TensorOpSpec::CausalConv(_) => Err(VjpRefusal::NotDerived { op: "CausalConv" }),
283 TensorOpSpec::GatedDelta(_) => Err(VjpRefusal::NotDerived { op: "GatedDelta" }),
284 TensorOpSpec::Softcap(c) => {
285 let (x, y, cap) = (rp.name(c.x).to_string(), rp.name(c.out).to_string(), c.cap.get());
287 pointwise(out, &x, d_out, &[&y], |v| format!("{0} * (1.0 - ({1} / {cap:?}) * ({1} / {cap:?}))", v[0], v[1]))
288 }
289 TensorOpSpec::Attention(a) => {
290 let attended = match &a.pos {
291 None => Attended::All(a.l),
292 Some(p) => Attended::From(RowIndexText::of(rp, p).0),
293 };
294 let shape = AttentionShape {
295 q: rp.name(a.q),
296 k: rp.name(a.kc),
297 v: rp.name(a.vc),
298 rows: a.rows,
299 h: a.h,
300 kv: a.kv,
301 hd: a.hd,
302 vd: a.vd,
303 l: a.l,
304 attended,
305 scale: a.scale.map_or(1.0 / (a.hd as f64).sqrt(), |s| s.get()),
306 cap: a.softcap.map(|c| c.get()),
307 window: a.window,
308 sinks: a.sinks.map(|s| rp.name(s)),
309 };
310 attention_rule(out, shape, d_out)
311 }
312 TensorOpSpec::FullAttention(a) => attention_rule(
313 out,
314 AttentionShape {
315 q: rp.name(a.q),
316 k: rp.name(a.k),
317 v: rp.name(a.v),
318 rows: a.t,
319 h: a.h,
320 kv: a.h,
321 hd: a.hd,
322 vd: a.hd,
323 l: a.t,
324 attended: Attended::All(a.t),
325 scale: 1.0 / (a.hd as f64).sqrt(),
326 cap: None,
327 window: None,
328 sinks: None,
329 },
330 d_out,
331 ),
332 TensorOpSpec::Moe(_) => Err(VjpRefusal::NotDerived { op: "Moe" }),
333 TensorOpSpec::SetRow(w) => set_row_rule(rp, w, out, d_out),
334 TensorOpSpec::HeadSlice(h) => head_slice_rule(out, rp.name(h.x), h.every, h.from, h.width, h.n, d_out),
335 TensorOpSpec::Join(j) => join_rule(out, (rp.name(j.a), rp.name(j.b)), (j.rows, j.heads, j.wa, j.wb), d_out),
336 TensorOpSpec::HeadMatmul(m) => head_matmul_rule(out, (rp.name(m.x), rp.name(m.w)), (m.m, m.heads, m.a, m.b), d_out),
337 TensorOpSpec::IntRequant(_) => Err(VjpRefusal::NoGradient {
338 op: "IntRequant",
339 why: "integer arithmetic: a floored shift of an integer is a step function of it",
340 }),
341 TensorOpSpec::IntClamp(_) => Err(VjpRefusal::NoGradient {
342 op: "IntClamp",
343 why: "integer arithmetic: a saturation of integers is a step function of them",
344 }),
345 TensorOpSpec::Relu(r) if r.int => Err(VjpRefusal::NoGradient {
346 op: "Relu",
347 why: "integer arithmetic: a ReLU of integers is a step function of them",
348 }),
349 TensorOpSpec::Relu(_) => Err(VjpRefusal::NotDerived { op: "Relu" }),
350 TensorOpSpec::LutAttention(_) => Err(VjpRefusal::NoGradient {
351 op: "LutAttention",
352 why: "integer arithmetic: a table-weighted, floored mean of integers is a step function of them",
353 }),
354 }
355}
356
357fn head_slice_rule(out: &mut Out, x: &str, every: usize, from: usize, width: usize, n: usize, d_out: &str) -> Result<(), VjpRefusal> {
360 let shape = out.shape(x)?;
361 let dx = out.tensor(&format!("g_{x}"), shape);
362 let (g, i, src, dst) = (out.names.fresh("g"), out.names.fresh("i"), out.names.fresh("k"), out.names.fresh("k"));
363 out.line(0, format!("Repeat for {g} from 1 to {}:", n / every.max(1)));
364 out.line(1, format!("Repeat for {i} from 1 to {width}:"));
365 out.line(2, format!("Let {dst} be ({g} - 1) * {every} + {from} + {i}."));
366 out.line(2, format!("Let {src} be ({g} - 1) * {width} + {i}."));
367 out.line(2, format!("Set item {dst} of {dx} to item {src} of {d_out}."));
368 out.accumulate(x, dx);
369 Ok(())
370}
371
372fn join_rule(out: &mut Out, (a, b): (&str, &str), (rows, heads, wa, wb): (usize, usize, usize, usize), d_out: &str) -> Result<(), VjpRefusal> {
375 let (sa, sb) = (out.shape(a)?, out.shape(b)?);
376 let (da, db) = (out.tensor(&format!("g_{a}"), sa), out.tensor(&format!("g_{b}"), sb));
377 let (ha, hb) = (wa / heads.max(1), wb / heads.max(1));
378 let (r, h, i, j) = (out.names.fresh("r"), out.names.fresh("h"), out.names.fresh("i"), out.names.fresh("j"));
379 let (at, ka, kj, kb, kk) = (out.names.fresh("k"), out.names.fresh("k"), out.names.fresh("k"), out.names.fresh("k"), out.names.fresh("k"));
380 out.line(0, format!("Repeat for {r} from 1 to {rows}:"));
381 out.line(1, format!("Repeat for {h} from 1 to {heads}:"));
382 out.line(2, format!("Let {at} be ({r} - 1) * {} + ({h} - 1) * {}.", wa + wb, ha + hb));
383 out.line(2, format!("Repeat for {i} from 1 to {ha}:"));
384 out.line(3, format!("Let {ka} be ({r} - 1) * {wa} + ({h} - 1) * {ha} + {i}."));
385 out.line(3, format!("Let {kj} be {at} + {i}."));
386 out.line(3, format!("Set item {ka} of {da} to item {kj} of {d_out}."));
387 out.line(2, format!("Repeat for {j} from 1 to {hb}:"));
388 out.line(3, format!("Let {kb} be ({r} - 1) * {wb} + ({h} - 1) * {hb} + {j}."));
389 out.line(3, format!("Let {kk} be {at} + {ha} + {j}."));
390 out.line(3, format!("Set item {kb} of {db} to item {kk} of {d_out}."));
391 out.accumulate(a, da);
392 out.accumulate(b, db);
393 Ok(())
394}
395
396fn head_matmul_rule(out: &mut Out, (x, w): (&str, &str), (m, heads, a, b): (usize, usize, usize, usize), d_out: &str) -> Result<(), VjpRefusal> {
399 let (sx, sw) = (out.shape(x)?, out.shape(w)?);
400 let (dx, dw) = (out.tensor(&format!("g_{x}"), sx), out.tensor(&format!("g_{w}"), sw));
401 let (t, h, e, r) = (out.names.fresh("t"), out.names.fresh("h"), out.names.fresh("e"), out.names.fresh("r"));
402 let (xk, wk, yk, dy) = (out.names.fresh("k"), out.names.fresh("k"), out.names.fresh("k"), out.names.fresh("y"));
403 out.line(0, format!("Repeat for {t} from 1 to {m}:"));
404 out.line(1, format!("Repeat for {h} from 1 to {heads}:"));
405 out.line(2, format!("Repeat for {e} from 1 to {a}:"));
406 out.line(3, format!("Let {xk} be ({t} - 1) * {} + ({h} - 1) * {a} + {e}.", heads * a));
407 out.line(3, format!("Repeat for {r} from 1 to {b}:"));
408 out.line(4, format!("Let {wk} be (({h} - 1) * {a} + {e} - 1) * {b} + {r}."));
409 out.line(4, format!("Let {yk} be ({t} - 1) * {} + ({h} - 1) * {b} + {r}.", heads * b));
410 out.line(4, format!("Let {dy} be item {yk} of {d_out}."));
411 out.line(4, format!("Set item {xk} of {dx} to (item {xk} of {dx}) + {dy} * (item {wk} of {w})."));
412 out.line(4, format!("Set item {wk} of {dw} to (item {wk} of {dw}) + {dy} * (item {xk} of {x})."));
413 out.accumulate(x, dx);
414 out.accumulate(w, dw);
415 Ok(())
416}
417
418fn pointwise(out: &mut Out, x: &str, d_out: &str, operands: &[&str], combine: impl Fn(&[String]) -> String) -> Result<(), VjpRefusal> {
420 let shape = out.shape(x)?;
421 let dx = out.tensor(&format!("g_{x}"), shape);
422 let operands: Vec<String> = operands.iter().map(|s| s.to_string()).collect();
423 let d_out = d_out.to_string();
424 out.each(shape, |o, depth, _, _, k| {
425 let mut vals = vec![format!("(item {k} of {d_out})")];
426 vals.extend(operands.iter().map(|t| format!("(item {k} of {t})")));
427 o.line(depth, format!("Set item {k} of {dx} to {}.", combine(&vals)));
428 });
429 out.accumulate(x, dx);
430 Ok(())
431}
432
433fn sigma(x: &str) -> String {
435 format!("(1.0 / (1.0 + {} ** (0.0 - {x})))", std::f64::consts::E)
436}
437
438fn pointwise_with_sigma(out: &mut Out, x: &str, d_out: &str, rule: impl Fn(&str, &str, &str) -> String) -> Result<(), VjpRefusal> {
439 let shape = out.shape(x)?;
440 let dx = out.tensor(&format!("g_{x}"), shape);
441 let d_out = d_out.to_string();
442 let x_name = x.to_string();
443 out.each(shape, |o, depth, _, _, k| {
444 let xv = o.names.fresh("x");
445 let sg = o.names.fresh("s");
446 o.line(depth, format!("Let {xv} be item {k} of {x_name}."));
447 o.line(depth, format!("Let {sg} be {}.", sigma(&xv)));
448 o.line(depth, format!("Set item {k} of {dx} to {}.", rule(&format!("(item {k} of {d_out})"), &xv, &sg)));
449 });
450 out.accumulate(x, dx);
451 Ok(())
452}
453
454fn gelu_rule(out: &mut Out, x: &str, d_out: &str) -> Result<(), VjpRefusal> {
455 const SQRT_2_OVER_PI: f64 = 0.797_884_560_802_865_4;
456 const K: f64 = 0.044_715;
457 let shape = out.shape(x)?;
458 let dx = out.tensor(&format!("g_{x}"), shape);
459 let d_out = d_out.to_string();
460 let x_name = x.to_string();
461 out.each(shape, |o, depth, _, _, k| {
462 let xv = o.names.fresh("x");
463 let z2 = o.names.fresh("z");
464 let s = o.names.fresh("s");
465 o.line(depth, format!("Let {xv} be item {k} of {x_name}."));
466 o.line(depth, format!("Let {z2} be 2.0 * {SQRT_2_OVER_PI:?} * ({xv} + {K:?} * {xv} * {xv} * {xv})."));
467 o.line(depth, format!("Let {s} be {}.", sigma(&z2)));
468 o.line(
469 depth,
470 format!(
471 "Set item {k} of {dx} to (item {k} of {d_out}) * ({s} + {xv} * {s} * (1.0 - {s}) * 2.0 * {SQRT_2_OVER_PI:?} * (1.0 + 3.0 * {K:?} * {xv} * {xv}))."
472 ),
473 );
474 });
475 out.accumulate(x, dx);
476 Ok(())
477}
478
479fn swiglu_rule(out: &mut Out, g: &str, u: &str, d_out: &str) -> Result<(), VjpRefusal> {
480 let shape = out.shape(g)?;
481 let dg = out.tensor(&format!("g_{g}"), shape);
482 let du = out.tensor(&format!("g_{u}"), shape);
483 let (g_name, u_name, d_out) = (g.to_string(), u.to_string(), d_out.to_string());
484 out.each(shape, |o, depth, _, _, k| {
485 let gv = o.names.fresh("x");
486 let sg = o.names.fresh("s");
487 o.line(depth, format!("Let {gv} be item {k} of {g_name}."));
488 o.line(depth, format!("Let {sg} be {}.", sigma(&gv)));
489 o.line(
490 depth,
491 format!("Set item {k} of {dg} to (item {k} of {d_out}) * (item {k} of {u_name}) * ({sg} + {gv} * {sg} * (1.0 - {sg}))."),
492 );
493 o.line(depth, format!("Set item {k} of {du} to (item {k} of {d_out}) * {gv} * {sg}."));
494 });
495 out.accumulate(g, dg);
496 out.accumulate(u, du);
497 Ok(())
498}
499
500fn each_row(out: &mut Out, (rows, cols): (usize, usize)) -> (String, String) {
504 let r = out.names.fresh("r");
505 let base = out.names.fresh("b");
506 out.line(0, format!("Repeat for {r} from 1 to {rows}:"));
507 out.line(1, format!("Let {base} be ({r} - 1) * {cols}."));
508 (r, base)
509}
510
511fn row_sum(out: &mut Out, cols: usize, base: &str, term: impl Fn(&str) -> String) -> String {
513 let acc = out.names.fresh("a");
514 let c = out.names.fresh("c");
515 let k = out.names.fresh("k");
516 out.line(1, format!("Let mutable {acc} be 0.0."));
517 out.line(1, format!("Repeat for {c} from 1 to {cols}:"));
518 out.line(2, format!("Let {k} be {base} + {c}."));
519 out.line(2, format!("Set {acc} to {acc} + {}.", term(&k)));
520 acc
521}
522
523fn row_write(out: &mut Out, cols: usize, base: &str, target: &str, value: impl Fn(&str) -> String) {
525 let c = out.names.fresh("c");
526 let k = out.names.fresh("k");
527 out.line(1, format!("Repeat for {c} from 1 to {cols}:"));
528 out.line(2, format!("Let {k} be {base} + {c}."));
529 out.line(2, format!("Set item {k} of {target} to {}.", value(&k)));
530}
531
532fn softmax_rule(out: &mut Out, y: &str, d_out: &str, x: &str, shape: (usize, usize)) -> Result<(), VjpRefusal> {
533 let dx = out.tensor(&format!("g_{x}"), out.shape(x)?);
535 let (_, base) = each_row(out, shape);
536 let (y, d_out) = (y.to_string(), d_out.to_string());
537 let dot = row_sum(out, shape.1, &base, |k| format!("(item {k} of {d_out}) * (item {k} of {y})"));
538 row_write(out, shape.1, &base, &dx, |k| format!("(item {k} of {y}) * ((item {k} of {d_out}) - {dot})"));
539 out.accumulate(x, dx);
540 Ok(())
541}
542
543fn l2norm_rule(out: &mut Out, x: &str, eps: Epsilon, d_out: &str, shape: (usize, usize)) -> Result<(), VjpRefusal> {
544 let dx = out.tensor(&format!("g_{x}"), out.shape(x)?);
546 let (_, base) = each_row(out, shape);
547 let (x, d_out) = (x.to_string(), d_out.to_string());
548 let ss = row_sum(out, shape.1, &base, |k| format!("(item {k} of {x}) * (item {k} of {x})"));
549 let dot = row_sum(out, shape.1, &base, |k| format!("(item {k} of {d_out}) * (item {k} of {x})"));
550 let n = out.names.fresh("n");
551 out.line(1, format!("Let {n} be sqrt({ss} + {:?}).", eps.get()));
552 row_write(out, shape.1, &base, &dx, |k| {
553 format!("(item {k} of {d_out}) / {n} - (item {k} of {x}) * {dot} / ({n} * {n} * {n})")
554 });
555 out.accumulate(&x, dx);
556 Ok(())
557}
558
559fn rmsnorm_rule(
560 rp: &RecognizedProgram,
561 r: &crate::tensor_recognize::FusedRowSpec,
562 out: &mut Out,
563 d_out: &str,
564 shape: (usize, usize),
565) -> Result<(), VjpRefusal> {
566 let x = rp.name(r.x).to_string();
569 let residual = r.residual.map(|s| rp.name(s).to_string());
570 let weight = r.weight.map(|s| rp.name(s).to_string());
571 let cols = shape.1;
572 let du = out.tensor(&format!("g_{x}"), out.shape(&x)?);
573 let dw = weight.as_ref().map(|w| {
574 let s = out.shape(w).unwrap_or((1, cols));
575 out.tensor(&format!("g_{w}"), s)
576 });
577 let d_out = d_out.to_string();
578 let u_of = |k: &str| match &residual {
579 Some(res) => format!("((item {k} of {x}) + (item {k} of {res}))"),
580 None => format!("(item {k} of {x})"),
581 };
582 let w_of = |c_index: &str| match &weight {
583 Some(w) => format!("(item {c_index} of {w})"),
584 None => "1.0".to_string(),
585 };
586 let (_, base) = each_row(out, shape);
587 let ss = row_sum(out, cols, &base, |k| format!("{} * {}", u_of(k), u_of(k)));
588 let q = out.names.fresh("q");
589 out.line(1, format!("Let {q} be 1.0 / sqrt({ss} / {cols}.0 + {:?}).", r.eps.get()));
590 let acc = out.names.fresh("a");
592 let c = out.names.fresh("c");
593 let k = out.names.fresh("k");
594 out.line(1, format!("Let mutable {acc} be 0.0."));
595 out.line(1, format!("Repeat for {c} from 1 to {cols}:"));
596 out.line(2, format!("Let {k} be {base} + {c}."));
597 out.line(2, format!("Set {acc} to {acc} + {} * (item {k} of {d_out}) * {}.", w_of(&c), u_of(&k)));
598 let c2 = out.names.fresh("c");
599 let k2 = out.names.fresh("k");
600 out.line(1, format!("Repeat for {c2} from 1 to {cols}:"));
601 out.line(2, format!("Let {k2} be {base} + {c2}."));
602 out.line(
603 2,
604 format!(
605 "Set item {k2} of {du} to {q} * {} * (item {k2} of {d_out}) - {q} * {q} * {q} / {cols}.0 * {} * {acc}.",
606 w_of(&c2),
607 u_of(&k2)
608 ),
609 );
610 if let Some(dw) = &dw {
611 out.line(2, format!("Set item {c2} of {dw} to (item {c2} of {dw}) + (item {k2} of {d_out}) * {} * {q}.", u_of(&k2)));
612 }
613 out.accumulate(&x, du.clone());
614 if let Some(res) = residual {
615 out.accumulate(&res, du);
616 }
617 if let (Some(w), Some(dw)) = (weight, dw) {
618 out.accumulate(&w, dw);
619 }
620 Ok(())
621}
622
623fn layernorm_rule(
624 rp: &RecognizedProgram,
625 r: &crate::tensor_recognize::FusedRowSpec,
626 out: &mut Out,
627 d_out: &str,
628 shape: (usize, usize),
629) -> Result<(), VjpRefusal> {
630 let x = rp.name(r.x).to_string();
633 let weight = r.weight.map(|s| rp.name(s).to_string());
634 let bias = r.bias.map(|s| rp.name(s).to_string());
635 let cols = shape.1;
636 let dx = out.tensor(&format!("g_{x}"), out.shape(&x)?);
637 let dw = weight.as_ref().map(|w| {
638 let s = out.shape(w).unwrap_or((1, cols));
639 out.tensor(&format!("g_{w}"), s)
640 });
641 let db = bias.as_ref().map(|b| {
642 let s = out.shape(b).unwrap_or((1, cols));
643 out.tensor(&format!("g_{b}"), s)
644 });
645 let d_out = d_out.to_string();
646 let w_of = |c_index: &str| match &weight {
647 Some(w) => format!("(item {c_index} of {w})"),
648 None => "1.0".to_string(),
649 };
650 let (_, base) = each_row(out, shape);
651 let sum = row_sum(out, cols, &base, |k| format!("(item {k} of {x})"));
652 let mu = out.names.fresh("m");
653 out.line(1, format!("Let {mu} be {sum} / {cols}.0."));
654 let var = row_sum(out, cols, &base, |k| format!("((item {k} of {x}) - {mu}) * ((item {k} of {x}) - {mu})"));
655 let q = out.names.fresh("q");
656 out.line(1, format!("Let {q} be 1.0 / sqrt({var} / {cols}.0 + {:?}).", r.eps.get()));
657 let (g_sum, gx_sum) = (out.names.fresh("a"), out.names.fresh("a"));
658 let c = out.names.fresh("c");
659 let k = out.names.fresh("k");
660 out.line(1, format!("Let mutable {g_sum} be 0.0."));
661 out.line(1, format!("Let mutable {gx_sum} be 0.0."));
662 out.line(1, format!("Repeat for {c} from 1 to {cols}:"));
663 out.line(2, format!("Let {k} be {base} + {c}."));
664 out.line(2, format!("Set {g_sum} to {g_sum} + (item {k} of {d_out}) * {}.", w_of(&c)));
665 out.line(2, format!("Set {gx_sum} to {gx_sum} + (item {k} of {d_out}) * {} * ((item {k} of {x}) - {mu}) * {q}.", w_of(&c)));
666 let c2 = out.names.fresh("c");
667 let k2 = out.names.fresh("k");
668 out.line(1, format!("Repeat for {c2} from 1 to {cols}:"));
669 out.line(2, format!("Let {k2} be {base} + {c2}."));
670 out.line(
671 2,
672 format!(
673 "Set item {k2} of {dx} to {q} * ((item {k2} of {d_out}) * {} - {g_sum} / {cols}.0 - ((item {k2} of {x}) - {mu}) * {q} * {gx_sum} / {cols}.0).",
674 w_of(&c2)
675 ),
676 );
677 if let Some(dw) = &dw {
678 out.line(2, format!("Set item {c2} of {dw} to (item {c2} of {dw}) + (item {k2} of {d_out}) * ((item {k2} of {x}) - {mu}) * {q}."));
679 }
680 if let Some(db) = &db {
681 out.line(2, format!("Set item {c2} of {db} to (item {c2} of {db}) + (item {k2} of {d_out})."));
682 }
683 out.accumulate(&x, dx);
684 if let (Some(w), Some(dw)) = (weight, dw) {
685 out.accumulate(&w, dw);
686 }
687 if let (Some(b), Some(db)) = (bias, db) {
688 out.accumulate(&b, db);
689 }
690 Ok(())
691}
692
693struct RowIndexText(String);
695
696impl RowIndexText {
697 fn of(rp: &RecognizedProgram, index: &crate::tensor_recognize::qwen::RowIndex) -> RowIndexText {
698 use crate::tensor_recognize::qwen::RowIndex;
699 RowIndexText(match index {
700 RowIndex::Literal(n) => n.to_string(),
701 RowIndex::Named(s) => rp.name(*s).to_string(),
702 })
703 }
704}
705
706fn row_scatter_rule(_rp: &RecognizedProgram, out: &mut Out, table: &str, d_out: &str, index: &RowIndexText) -> Result<(), VjpRefusal> {
707 let shape = out.shape(table)?;
709 let dt = out.tensor(&format!("g_{table}"), shape);
710 let c = out.names.fresh("c");
711 let k = out.names.fresh("k");
712 out.line(0, format!("Repeat for {c} from 1 to {}:", shape.1));
713 out.line(1, format!("Let {k} be ({} - 1) * {} + {c}.", index.0, shape.1));
714 out.line(1, format!("Set item {k} of {dt} to (item {k} of {dt}) + (item {c} of {d_out})."));
715 out.accumulate(table, dt);
716 Ok(())
717}
718
719fn rows_scatter_rule(out: &mut Out, table: &str, indices: &str, d_out: &str) -> Result<(), VjpRefusal> {
720 let shape = out.shape(table)?;
722 let (rows, _) = out.shape(d_out).unwrap_or((1, shape.1));
723 let dt = out.tensor(&format!("g_{table}"), shape);
724 let r = out.names.fresh("r");
725 let c = out.names.fresh("c");
726 let k = out.names.fresh("k");
727 let src = out.names.fresh("k");
728 let mut header = String::new();
729 let _ = write!(header, "Repeat for {r} from 1 to {rows}:");
730 out.line(0, header);
731 out.line(1, format!("Repeat for {c} from 1 to {}:", shape.1));
732 out.line(2, format!("Let {k} be ((item {r} of {indices}) - 1) * {} + {c}.", shape.1));
733 out.line(2, format!("Let {src} be ({r} - 1) * {} + {c}.", shape.1));
734 out.line(2, format!("Set item {k} of {dt} to (item {k} of {dt}) + (item {src} of {d_out})."));
735 out.accumulate(table, dt);
736 Ok(())
737}
738
739fn rope_rule(rp: &RecognizedProgram, r: &crate::tensor_recognize::qwen::RopeSpec, out: &mut Out, d_out: &str) -> Result<(), VjpRefusal> {
740 let (q, rc, rs) = (rp.name(r.q).to_string(), rp.name(r.rc).to_string(), rp.name(r.rs).to_string());
743 let table = out.shape(&rs)?;
744 let neg = out.tensor(&format!("n_{rs}"), table);
745 let rs_name = rs.clone();
746 out.each(table, |o, depth, _, _, k| {
747 o.line(depth, format!("Set item {k} of {neg} to 0.0 - (item {k} of {rs_name})."));
748 });
749 let shape = out.shape(&q)?;
750 let dq = out.names.fresh(&format!("g_{q}"));
751 let mut stmt = format!("Let {dq} be rope of {d_out} with cos {rc} and sin {neg}");
752 if let Some((index, _)) = &r.from {
753 stmt.push_str(&format!(" from position {}", RowIndexText::of(rp, index).0));
754 }
755 if r.hd != 2 * r.half {
756 stmt.push_str(&format!(" across {} heads", r.h));
757 }
758 stmt.push('.');
759 out.line(0, stmt);
760 out.shapes.insert(dq.clone(), shape);
761 out.accumulate(&q, dq);
762 out.undiff.insert(rc, "Rope");
763 out.undiff.insert(rs, "Rope");
764 Ok(())
765}
766
767enum Attended {
769 All(usize),
771 From(String),
774}
775
776struct AttentionShape<'n> {
777 q: &'n str,
778 k: &'n str,
779 v: &'n str,
780 rows: usize,
781 h: usize,
782 kv: usize,
783 hd: usize,
784 vd: usize,
786 l: usize,
787 attended: Attended,
788 scale: f64,
790 cap: Option<f64>,
792 window: Option<crate::tensor_recognize::qwen::Window>,
793 sinks: Option<&'n str>,
795}
796
797fn attention_rule(out: &mut Out, a: AttentionShape, d_out: &str) -> Result<(), VjpRefusal> {
798 let (q, k, v) = (a.q.to_string(), a.k.to_string(), a.v.to_string());
805 let (width, owidth) = (a.h * a.hd, a.h * a.vd);
806 let (kwidth, vwidth) = (a.kv * a.hd, a.kv * a.vd);
807 let group = a.h / a.kv;
808 let alpha = a.scale;
809 let e = std::f64::consts::E;
810 let dq = out.tensor(&format!("g_{q}"), (a.rows, width));
811 let dk = out.tensor(&format!("g_{k}"), (a.l, kwidth));
812 let dv = out.tensor(&format!("g_{v}"), (a.l, vwidth));
813 let dsink = a.sinks.map(|s| out.tensor(&format!("g_{s}"), (1, a.h)));
814 let n = |o: &mut Names| o.fresh("n");
815 let (r, g, kvh, cnt) = (out.names.fresh("r"), out.names.fresh("h"), out.names.fresh("kh"), n(&mut out.names));
816 let (qo, oo, j, ko, vo, d) =
817 (out.names.fresh("qo"), out.names.fresh("oo"), out.names.fresh("j"), out.names.fresh("ko"), out.names.fresh("vo"), out.names.fresh("d"));
818 let (lo, sc, ps, dps) = (out.names.fresh("lo"), out.names.fresh("sc"), out.names.fresh("ps"), out.names.fresh("dp"));
819 let (acc, mx, den, dot) = (out.names.fresh("a"), out.names.fresh("m"), out.names.fresh("z"), out.names.fresh("t"));
820 out.line(0, format!("Repeat for {r} from 1 to {}:", a.rows));
821 out.line(1, format!("Repeat for {g} from 1 to {}:", a.h));
822 out.line(2, format!("Let {kvh} be ({g} - 1) / {group} + 1."));
823 match &a.attended {
824 Attended::All(l) => out.line(2, format!("Let {cnt} be {l}.")),
825 Attended::From(p) => out.line(2, format!("Let {cnt} be {p} + {r} - 1.")),
826 }
827 use crate::tensor_recognize::qwen::Window;
829 match a.window {
830 None => out.line(2, format!("Let {lo} be 1.")),
831 Some(Window::Sliding(w)) => {
832 out.line(2, format!("Let mutable {lo} be {cnt} - {w} + 1."));
833 out.line(2, format!("If {lo} < 1:"));
834 out.line(3, format!("Set {lo} to 1."));
835 }
836 Some(Window::Chunked(c)) => out.line(2, format!("Let {lo} be (({cnt} - 1) / {c}) * {c} + 1.")),
837 }
838 out.line(2, format!("Let {qo} be ({r} - 1) * {width} + ({g} - 1) * {}.", a.hd));
839 out.line(2, format!("Let {oo} be ({r} - 1) * {owidth} + ({g} - 1) * {}.", a.vd));
840 out.line(2, format!("Let mutable {sc} be a new Seq of Real."));
842 match a.sinks {
843 Some(sinks) => out.line(2, format!("Let mutable {mx} be item {g} of {sinks}.")),
844 None => out.line(2, format!("Let mutable {mx} be 0.0.")),
845 }
846 out.line(2, format!("Repeat for {j} from {lo} to {cnt}:"));
847 out.line(3, format!("Let {ko} be ({j} - 1) * {kwidth} + ({kvh} - 1) * {}.", a.hd));
848 out.line(3, format!("Let mutable {acc} be 0.0."));
849 out.line(3, format!("Repeat for {d} from 1 to {}:", a.hd));
850 out.line(4, format!("Set {acc} to {acc} + (item ({qo} + {d}) of {q}) * (item ({ko} + {d}) of {k})."));
851 out.line(3, format!("Set {acc} to {acc} * {alpha:?}."));
852 if let Some(c) = a.cap {
853 out.line(3, format!("Set {acc} to {c:?} * (1.0 - 2.0 / ({e:?} ** ({acc} * {:?}) + 1.0)).", 2.0 / c));
854 }
855 let first = if a.sinks.is_some() { String::new() } else { format!("{j} == {lo} or ") };
856 out.line(3, format!("If {first}{acc} > {mx}:"));
857 out.line(4, format!("Set {mx} to {acc}."));
858 out.line(3, format!("Push {acc} to {sc}."));
859 match a.sinks {
861 Some(sinks) => out.line(2, format!("Let mutable {den} be {e:?} ** ((item {g} of {sinks}) - {mx}).")),
862 None => out.line(2, format!("Let mutable {den} be 0.0.")),
863 }
864 out.line(2, format!("Let mutable {ps} be a new Seq of Real."));
865 out.line(2, format!("Repeat for {j} from {lo} to {cnt}:"));
866 out.line(3, format!("Let {acc}_e be {e:?} ** ((item ({j} - {lo} + 1) of {sc}) - {mx})."));
867 out.line(3, format!("Set {den} to {den} + {acc}_e."));
868 out.line(3, format!("Push {acc}_e to {ps}."));
869 out.line(2, format!("Let mutable {dps} be a new Seq of Real."));
871 out.line(2, format!("Let mutable {dot} be 0.0."));
872 out.line(2, format!("Repeat for {j} from {lo} to {cnt}:"));
873 out.line(3, format!("Let {vo} be ({j} - 1) * {vwidth} + ({kvh} - 1) * {}.", a.vd));
874 out.line(3, format!("Let {acc}_p be (item ({j} - {lo} + 1) of {ps}) / {den}."));
875 out.line(3, format!("Let mutable {acc} be 0.0."));
876 out.line(3, format!("Repeat for {d} from 1 to {}:", a.vd));
877 out.line(4, format!("Set {acc} to {acc} + (item ({oo} + {d}) of {d_out}) * (item ({vo} + {d}) of {v})."));
878 out.line(4, format!("Set item ({vo} + {d}) of {dv} to (item ({vo} + {d}) of {dv}) + {acc}_p * (item ({oo} + {d}) of {d_out})."));
879 out.line(3, format!("Push {acc} to {dps}."));
880 out.line(3, format!("Set {dot} to {dot} + {acc}_p * {acc}."));
881 if let (Some(sinks), Some(dsink)) = (a.sinks, &dsink) {
883 out.line(2, format!("Let {acc}_sink be {e:?} ** ((item {g} of {sinks}) - {mx}) / {den}."));
884 out.line(2, format!("Set item {g} of {dsink} to (item {g} of {dsink}) - {acc}_sink * {dot}."));
885 }
886 out.line(2, format!("Repeat for {j} from {lo} to {cnt}:"));
888 out.line(3, format!("Let {ko} be ({j} - 1) * {kwidth} + ({kvh} - 1) * {}.", a.hd));
889 let through_cap = match a.cap {
890 Some(c) => format!(" * (1.0 - ((item ({j} - {lo} + 1) of {sc}) / {c:?}) * ((item ({j} - {lo} + 1) of {sc}) / {c:?}))"),
891 None => String::new(),
892 };
893 out.line(
894 3,
895 format!("Let {acc}_s be (item ({j} - {lo} + 1) of {ps}) / {den} * ((item ({j} - {lo} + 1) of {dps}) - {dot}){through_cap} * {alpha:?}."),
896 );
897 out.line(3, format!("Repeat for {d} from 1 to {}:", a.hd));
898 out.line(4, format!("Set item ({qo} + {d}) of {dq} to (item ({qo} + {d}) of {dq}) + {acc}_s * (item ({ko} + {d}) of {k})."));
899 out.line(4, format!("Set item ({ko} + {d}) of {dk} to (item ({ko} + {d}) of {dk}) + {acc}_s * (item ({qo} + {d}) of {q})."));
900 out.accumulate(&q, dq);
901 out.accumulate(&k, dk);
902 out.accumulate(&v, dv);
903 if let (Some(sinks), Some(dsink)) = (a.sinks, dsink) {
904 out.accumulate(sinks, dsink);
905 }
906 Ok(())
907}
908
909fn set_row_rule(rp: &RecognizedProgram, w: &crate::tensor_recognize::qwen::SetRowSpec, out: &mut Out, d_out: &str) -> Result<(), VjpRefusal> {
910 let (table, source) = (rp.name(w.table).to_string(), rp.name(w.source).to_string());
914 let p = RowIndexText::of(rp, &w.index).0;
915 let cols = w.cols;
916 let dx = out.tensor(&format!("g_{source}"), (w.count, cols));
917 let before = out.tensor(&format!("g_{table}"), (w.rows, cols));
918 let (r, c, k) = (out.names.fresh("r"), out.names.fresh("c"), out.names.fresh("k"));
919 out.line(0, format!("Repeat for {r} from 1 to {}:", w.rows));
920 out.line(1, format!("Repeat for {c} from 1 to {cols}:"));
921 out.line(2, format!("Let {k} be ({r} - 1) * {cols} + {c}."));
922 out.line(2, format!("If {r} >= {p} and {r} < {p} + {}:", w.count));
923 out.line(3, format!("Set item (({r} - {p}) * {cols} + {c}) of {dx} to item {k} of {d_out}."));
924 out.line(2, "Otherwise:".to_string());
925 out.line(3, format!("Set item {k} of {before} to item {k} of {d_out}."));
926 out.grads.insert(table.clone(), before);
927 out.accumulate(&source, dx);
928 if let crate::tensor_recognize::qwen::RowIndex::Named(s) = &w.index {
929 out.undiff.insert(rp.name(*s).to_string(), "SetRow");
930 }
931 Ok(())
932}