1use rlx_ir::op::*;
114use rlx_ir::shape::Dim;
115use rlx_ir::*;
116use std::collections::HashMap;
117
118pub use crate::prepare_ad::{
119 AutodiffError, PrepareForAutodiff, grad_with_loss_module, jvp_module, prepare_graph_for_ad,
120 prepare_mir_for_ad, prepare_module_for_ad,
121};
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct GradWithLossOptions {
150 pub zero_missing_wrt: bool,
153}
154
155impl GradWithLossOptions {
156 pub const STRICT: Self = Self {
157 zero_missing_wrt: false,
158 };
159 pub const TRAINING: Self = Self {
160 zero_missing_wrt: true,
161 };
162}
163
164pub fn grad_with_loss(forward: &Graph, wrt: &[NodeId]) -> Graph {
170 grad_with_loss_opts(forward, wrt, GradWithLossOptions::STRICT)
171}
172
173pub fn grad_with_loss_opts(forward: &Graph, wrt: &[NodeId], opts: GradWithLossOptions) -> Graph {
175 assert!(
176 !forward.outputs.is_empty(),
177 "grad_with_loss: forward must have at least one output (the loss)"
178 );
179
180 let forward_owned = crate::prepare_ad::prepare_graph_for_ad(forward.clone());
193 let forward = &forward_owned;
194
195 let mut bwd = Graph::new(format!("{}_grad", forward.name));
196
197 let mut fwd_to_bwd: HashMap<NodeId, NodeId> = HashMap::new();
202 for node in forward.nodes() {
203 let inputs: Vec<NodeId> = node.inputs.iter().map(|i| fwd_to_bwd[i]).collect();
204 let new_id = bwd.add_node(node.op.clone(), inputs, node.shape.clone());
205 fwd_to_bwd.insert(node.id, new_id);
206 }
207
208 let loss_fwd = forward.outputs[0];
211 let loss_bwd = fwd_to_bwd[&loss_fwd];
212 let loss_shape = forward.node(loss_fwd).shape.clone();
213 let d_output = bwd.input("d_output", loss_shape);
214
215 let mut grads: HashMap<NodeId, NodeId> = HashMap::new();
216 grads.insert(loss_bwd, d_output);
217
218 for fwd_node in forward.nodes().iter().rev() {
219 let bwd_id = fwd_to_bwd[&fwd_node.id];
220 let upstream = match grads.get(&bwd_id) {
221 Some(g) => *g,
222 None => continue,
223 };
224 let input_grads = vjp(fwd_node, upstream, &fwd_to_bwd, &mut bwd);
225 for (idx, grad_id) in input_grads {
226 let target = fwd_node.inputs[idx];
227 let bwd_target = fwd_to_bwd[&target];
228 let new_grad = if let Some(&prev) = grads.get(&bwd_target) {
230 let shape = bwd.node(prev).shape.clone();
231 bwd.binary(BinaryOp::Add, prev, grad_id, shape)
232 } else {
233 grad_id
234 };
235 grads.insert(bwd_target, new_grad);
236 }
237 }
238
239 let n_aux = forward.outputs.len().saturating_sub(1);
240 let mut outputs = Vec::with_capacity(1 + n_aux + wrt.len());
241 outputs.push(loss_bwd);
242 for &aux in &forward.outputs[1..] {
245 outputs.push(fwd_to_bwd[&aux]);
246 }
247 for &id in wrt {
248 let g = match grads.get(&fwd_to_bwd[&id]).copied() {
249 Some(g) => g,
250 None if opts.zero_missing_wrt => {
251 let shape = forward.node(id).shape.clone();
252 let n = shape.num_elements().unwrap_or(0);
253 let data: Vec<u8> = (0..n).flat_map(|_| 0.0f32.to_le_bytes()).collect();
254 bwd.add_node(Op::Constant { data }, vec![], shape)
255 }
256 None => {
257 panic!(
258 "no gradient flowed to {id} — \
259 either the forward graph doesn't depend on it, or one \
260 of its consumer ops has no VJP rule"
261 )
262 }
263 };
264 outputs.push(g);
265 }
266 bwd.set_outputs(outputs);
267 bwd
268}
269
270pub fn grad(forward: &Graph, wrt: &[NodeId]) -> Graph {
274 let g = grad_with_loss(forward, wrt);
275 let mut g = g;
276 let outs = g.outputs.iter().skip(1).copied().collect();
278 g.set_outputs(outs);
279 g
280}
281
282pub fn quantized_weight_bits(forward: &Graph, node_id: NodeId) -> Option<u8> {
295 match &forward.node(node_id).op {
296 Op::FakeQuantize { bits, .. } => Some(*bits),
297 Op::FakeQuantizeLSQ { bits, .. } => Some(*bits),
298 _ => None,
299 }
300}
301
302fn unbroadcast(grad: NodeId, target_shape: &Shape, bwd: &mut Graph) -> NodeId {
303 let grad_shape = bwd.node(grad).shape.clone();
304 if grad_shape == *target_shape {
305 return grad;
306 }
307 let g_rank = grad_shape.rank();
308 let t_rank = target_shape.rank();
309 let extra = g_rank.saturating_sub(t_rank);
310
311 let mut axes: Vec<usize> = (0..extra).collect();
313 for i in 0..t_rank {
314 let g_dim = grad_shape.dim(extra + i);
315 let t_dim = target_shape.dim(i);
316 if matches!(t_dim, Dim::Static(1)) && !matches!(g_dim, Dim::Static(1)) {
317 axes.push(extra + i);
318 }
319 }
320
321 let mut current = grad;
322 if !axes.is_empty() {
323 let mut running_dims: Vec<Dim> = (0..g_rank).map(|i| grad_shape.dim(i)).collect();
330 for &ax in &axes {
331 running_dims[ax] = Dim::Static(1);
332 let step_shape = Shape::from_dims(&running_dims, target_shape.dtype());
333 current = bwd.add_node(
334 Op::Reduce {
335 op: ReduceOp::Sum,
336 axes: vec![ax],
337 keep_dim: true,
338 },
339 vec![current],
340 step_shape,
341 );
342 }
343 }
344
345 if bwd.node(current).shape.rank() != t_rank {
347 let new_shape: Vec<i64> = target_shape
348 .dims()
349 .iter()
350 .map(|d| match d {
351 Dim::Static(n) => *n as i64,
352 Dim::Dynamic(_) => -1,
353 })
354 .collect();
355 current = bwd.add_node(
356 Op::Reshape { new_shape },
357 vec![current],
358 target_shape.clone(),
359 );
360 }
361 current
362}
363
364fn reshape_to(grad: NodeId, target_shape: &Shape, bwd: &mut Graph) -> NodeId {
366 if bwd.node(grad).shape == *target_shape {
367 return grad;
368 }
369 let new_shape: Vec<i64> = target_shape
370 .dims()
371 .iter()
372 .map(|d| match d {
373 Dim::Static(n) => *n as i64,
374 Dim::Dynamic(_) => -1,
375 })
376 .collect();
377 bwd.add_node(Op::Reshape { new_shape }, vec![grad], target_shape.clone())
378}
379
380fn grouped_matmul_vjp(
382 bwd: &mut Graph,
383 upstream: NodeId,
384 x_bwd: NodeId,
385 w_bwd: NodeId,
386 expert_bwd: NodeId,
387 x_shape: &Shape,
388 w_shape: &Shape,
389) -> (NodeId, NodeId) {
390 let dtype = x_shape.dtype();
391 let m = x_shape.dim(0);
392 let k = x_shape.dim(1);
393 let e = w_shape.dim(0);
394 let n_out = w_shape.dim(2);
395 let m_static = match m {
396 Dim::Static(v) => v,
397 _ => panic!("GroupedMatMul VJP: M must be static"),
398 };
399 let k_static = match k {
400 Dim::Static(v) => v,
401 _ => panic!("GroupedMatMul VJP: K must be static"),
402 };
403 let n_static = match n_out {
404 Dim::Static(v) => v,
405 _ => panic!("GroupedMatMul VJP: N must be static"),
406 };
407
408 let w_per = bwd.add_node(
409 Op::Gather { axis: 0 },
410 vec![w_bwd, expert_bwd],
411 Shape::from_dims(&[m, k, n_out], dtype),
412 );
413
414 let up_3d_shape = Shape::from_dims(&[m, Dim::Static(1), n_out], dtype);
415 let up_3d = bwd.reshape(
416 upstream,
417 vec![m_static as i64, 1, n_static as i64],
418 up_3d_shape,
419 );
420 let w_per_t = bwd.add_node(
421 Op::Transpose {
422 perm: vec![0, 2, 1],
423 },
424 vec![w_per],
425 Shape::from_dims(&[m, n_out, k], dtype),
426 );
427 let dx_3d_shape = Shape::from_dims(&[m, Dim::Static(1), k], dtype);
428 let dx_3d = bwd.matmul(up_3d, w_per_t, dx_3d_shape);
429 let dx = bwd.reshape(
430 dx_3d,
431 vec![m_static as i64, k_static as i64],
432 x_shape.clone(),
433 );
434
435 let x_3d = bwd.reshape(
436 x_bwd,
437 vec![m_static as i64, k_static as i64, 1],
438 Shape::from_dims(&[m, k, Dim::Static(1)], dtype),
439 );
440 let up_for_outer = bwd.reshape(
441 upstream,
442 vec![m_static as i64, 1, n_static as i64],
443 Shape::from_dims(&[m, Dim::Static(1), n_out], dtype),
444 );
445 let dw_per = bwd.matmul(x_3d, up_for_outer, Shape::from_dims(&[m, k, n_out], dtype));
446 let dw = bwd.add_node(
447 Op::ScatterAdd,
448 vec![dw_per, expert_bwd],
449 Shape::from_dims(&[e, k, n_out], dtype),
450 );
451 (dx, dw)
452}
453
454fn scalar_const(value: f32, bwd: &mut Graph) -> NodeId {
456 let bytes = value.to_le_bytes().to_vec();
457 let shape = Shape::from_dims(&[Dim::Static(1)], DType::F32);
458 bwd.add_node(Op::Constant { data: bytes }, vec![], shape)
459}
460
461#[allow(unused_variables)]
465fn vjp(
466 node: &Node,
467 upstream: NodeId,
468 fwd_map: &HashMap<NodeId, NodeId>,
469 bwd: &mut Graph,
470) -> Vec<(usize, NodeId)> {
471 let upstream_shape = bwd.node(upstream).shape.clone();
472 match &node.op {
473 Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => vec![],
475
476 Op::Binary(BinaryOp::Add) => vjp_binary_add(node, upstream, upstream_shape, fwd_map, bwd),
477 Op::Binary(BinaryOp::Sub) => vjp_binary_sub(node, upstream, upstream_shape, fwd_map, bwd),
478 Op::Binary(BinaryOp::Mul) => vjp_binary_mul(node, upstream, upstream_shape, fwd_map, bwd),
479 Op::Activation(kind) => vjp_activation(node, upstream, upstream_shape, fwd_map, bwd),
480 Op::MatMul => vjp_mat_mul(node, upstream, upstream_shape, fwd_map, bwd),
481 Op::DenseSolve => vjp_dense_solve(node, upstream, upstream_shape, fwd_map, bwd),
482 Op::BatchedDenseSolve => {
483 vjp_batched_dense_solve(node, upstream, upstream_shape, fwd_map, bwd)
484 }
485 Op::Scan {
486 body,
487 length,
488 save_trajectory,
489 num_bcast: _,
490 num_xs,
491 num_checkpoints,
492 } => vjp_scan(node, upstream, upstream_shape, fwd_map, bwd),
493 Op::Conv {
494 kernel_size,
495 stride,
496 padding,
497 dilation,
498 groups,
499 } => vjp_conv(node, upstream, upstream_shape, fwd_map, bwd),
500 Op::Pool {
501 kind: ReduceOp::Max,
502 kernel_size,
503 stride,
504 padding,
505 } => vjp_pool(node, upstream, upstream_shape, fwd_map, bwd),
506 Op::SoftmaxCrossEntropyWithLogits => {
507 vjp_softmax_cross_entropy_with_logits(node, upstream, upstream_shape, fwd_map, bwd)
508 }
509 Op::SoftmaxCrossEntropy => {
510 vjp_softmax_cross_entropy(node, upstream, upstream_shape, fwd_map, bwd)
511 }
512 Op::Reduce {
513 op: ReduceOp::Sum,
514 axes,
515 keep_dim,
516 } => vjp_reduce(node, upstream, upstream_shape, fwd_map, bwd),
517 Op::Reduce {
518 op: ReduceOp::Mean,
519 axes,
520 keep_dim,
521 } => vjp_reduce_2(node, upstream, upstream_shape, fwd_map, bwd),
522 Op::Reshape { .. } => vjp_reshape(node, upstream, upstream_shape, fwd_map, bwd),
523 Op::ComplexNormSq => vjp_complex_norm_sq(node, upstream, upstream_shape, fwd_map, bwd),
524 Op::Conjugate => vjp_conjugate(node, upstream, upstream_shape, fwd_map, bwd),
525 Op::Cast { .. } => vjp_cast(node, upstream, upstream_shape, fwd_map, bwd),
526 Op::StopGradient => vjp_stop_gradient(node, upstream, upstream_shape, fwd_map, bwd),
527 Op::Quantize { .. } | Op::Dequantize { .. } => {
539 vec![(0, upstream)]
540 }
541
542 Op::FakeQuantizeLSQ { bits, axis } => {
543 vjp_fake_quantize_l_s_q(node, upstream, upstream_shape, fwd_map, bwd)
544 }
545 Op::FakeQuantize {
546 bits, axis, ste, ..
547 } => vjp_fake_quantize(node, upstream, upstream_shape, fwd_map, bwd),
548 Op::Expand { .. } => vjp_expand(node, upstream, upstream_shape, fwd_map, bwd),
549 Op::BatchNormInference { eps } => {
550 vjp_batch_norm_inference(node, upstream, upstream_shape, fwd_map, bwd)
551 }
552 Op::LayerNorm { axis, eps } => vjp_layer_norm(node, upstream, upstream_shape, fwd_map, bwd),
553 Op::Softmax { axis } => vjp_softmax(node, upstream, upstream_shape, fwd_map, bwd),
554 Op::Transpose { perm } => vjp_transpose(node, upstream, upstream_shape, fwd_map, bwd),
555 Op::Concat { axis } => vjp_concat(node, upstream, upstream_shape, fwd_map, bwd),
556 Op::Narrow { axis, start, len } => vjp_narrow(node, upstream, upstream_shape, fwd_map, bwd),
557 Op::Gather { axis } => vjp_gather(node, upstream, upstream_shape, fwd_map, bwd),
558 Op::Compare(_) => vjp_compare(node, upstream, upstream_shape, fwd_map, bwd),
559 Op::Where => vjp_where(node, upstream, upstream_shape, fwd_map, bwd),
560 Op::Binary(BinaryOp::Div) => vjp_binary_div(node, upstream, upstream_shape, fwd_map, bwd),
561 Op::Reduce {
563 op: ReduceOp::Max,
564 axes,
565 keep_dim,
566 }
567 | Op::Reduce {
568 op: ReduceOp::Min,
569 axes,
570 keep_dim,
571 } => {
572 let is_max = matches!(
576 node.op,
577 Op::Reduce {
578 op: ReduceOp::Max,
579 ..
580 }
581 );
582 let _ = is_max;
583 let x_bwd = fwd_map[&node.inputs[0]];
584 let y_bwd = fwd_map[&node.id];
585 let x_shape = bwd.node(x_bwd).shape.clone();
586 let y_expanded = expand_to(y_bwd, &x_shape, axes, *keep_dim, bwd);
587 let mask_bool = bwd.add_node(
588 Op::Compare(CmpOp::Eq),
589 vec![x_bwd, y_expanded],
590 Shape::from_dims(x_shape.dims(), DType::Bool),
591 );
592 let mask_f32 = bwd.add_node(
596 Op::Cast {
597 to: x_shape.dtype(),
598 },
599 vec![mask_bool],
600 x_shape.clone(),
601 );
602 let upstream_expanded = expand_to(upstream, &x_shape, axes, *keep_dim, bwd);
603 let dx = bwd.binary(BinaryOp::Mul, upstream_expanded, mask_f32, x_shape);
604 vec![(0, dx)]
605 }
606
607 Op::Rope {
608 head_dim, n_rot, ..
609 } => vjp_rope(node, upstream, upstream_shape, fwd_map, bwd),
610 Op::RmsNorm { axis, eps } => vjp_rms_norm(node, upstream, upstream_shape, fwd_map, bwd),
611 Op::GroupNorm { num_groups, eps } => {
612 vjp_group_norm(node, upstream, upstream_shape, fwd_map, bwd)
613 }
614 Op::Attention {
615 num_heads,
616 head_dim,
617 mask_kind,
618 score_scale: _,
619 attn_logit_softcap: _,
620 } => vjp_attention(node, upstream, upstream_shape, fwd_map, bwd),
621 Op::Reduce {
622 op: ReduceOp::Prod,
623 axes,
624 keep_dim,
625 } => vjp_reduce_3(node, upstream, upstream_shape, fwd_map, bwd),
626 Op::Pool {
627 kind: ReduceOp::Mean,
628 kernel_size,
629 stride,
630 padding,
631 } => vjp_pool_2(node, upstream, upstream_shape, fwd_map, bwd),
632 Op::Binary(BinaryOp::Min) | Op::Binary(BinaryOp::Max) => {
639 let a_bwd = fwd_map[&node.inputs[0]];
640 let b_bwd = fwd_map[&node.inputs[1]];
641 let y_bwd = fwd_map[&node.id];
642 let a_shape = bwd.node(a_bwd).shape.clone();
643 let b_shape = bwd.node(b_bwd).shape.clone();
644 let dtype = upstream_shape.dtype();
645
646 let bool_shape = Shape::from_dims(upstream_shape.dims(), DType::Bool);
647 let mask_pred = bwd.add_node(Op::Compare(CmpOp::Eq), vec![a_bwd, y_bwd], bool_shape);
648 let mask_f32 = bwd.add_node(
649 Op::Cast { to: dtype },
650 vec![mask_pred],
651 upstream_shape.clone(),
652 );
653 let zero_bytes = vec![
654 0u8;
655 upstream_shape
656 .num_elements()
657 .expect("Min/Max VJP: dyn shape")
658 * 4
659 ];
660 let zero = bwd.add_node(
661 Op::Constant { data: zero_bytes },
662 vec![],
663 upstream_shape.clone(),
664 );
665 let g_a_full = bwd.add_node(
666 Op::Where,
667 vec![mask_f32, upstream, zero],
668 upstream_shape.clone(),
669 );
670 let g_b_full = bwd.add_node(Op::Where, vec![mask_f32, zero, upstream], upstream_shape);
671 let g_a = unbroadcast(g_a_full, &a_shape, bwd);
672 let g_b = unbroadcast(g_b_full, &b_shape, bwd);
673 vec![(0, g_a), (1, g_b)]
674 }
675
676 Op::Binary(BinaryOp::Pow) => vjp_binary_pow(node, upstream, upstream_shape, fwd_map, bwd),
677 Op::ScaledMatMul {
678 lhs_format,
679 rhs_format,
680 scale_layout,
681 has_bias,
682 } => vjp_scaled_mat_mul(node, upstream, upstream_shape, fwd_map, bwd),
683 Op::ScaledQuantize { .. } => {
684 vjp_scaled_quantize(node, upstream, upstream_shape, fwd_map, bwd)
685 }
686 Op::ScaledQuantScale { .. } => {
687 vjp_scaled_quant_scale(node, upstream, upstream_shape, fwd_map, bwd)
688 }
689 Op::DequantMatMul { scheme: _ } => {
690 vjp_dequant_mat_mul(node, upstream, upstream_shape, fwd_map, bwd)
691 }
692 Op::ScatterAdd => vjp_scatter_add(node, upstream, upstream_shape, fwd_map, bwd),
693 Op::Cumsum { axis, exclusive } => vjp_cumsum(node, upstream, upstream_shape, fwd_map, bwd),
694 Op::GroupedMatMul => vjp_grouped_mat_mul(node, upstream, upstream_shape, fwd_map, bwd),
695 Op::DequantGroupedMatMul { scheme } => {
696 vjp_dequant_grouped_mat_mul(node, upstream, upstream_shape, fwd_map, bwd)
697 }
698 Op::QMatMul {
699 x_zp,
700 w_zp,
701 out_zp: _,
702 mult,
703 } => vjp_q_mat_mul(node, upstream, upstream_shape, fwd_map, bwd),
704 Op::QConv2d {
705 kernel_size,
706 stride,
707 padding,
708 dilation,
709 groups,
710 x_zp,
711 w_zp,
712 out_zp: _,
713 mult,
714 } => vjp_q_conv2d(node, upstream, upstream_shape, fwd_map, bwd),
715 Op::TopK { .. } | Op::Sample { .. } | Op::RngNormal { .. } | Op::RngUniform { .. } => {
717 vec![]
721 }
722
723 Op::GaussianSplatRender {
724 width,
725 height,
726 tile_size,
727 radius_scale,
728 alpha_cutoff,
729 max_splat_steps,
730 transmittance_threshold,
731 max_list_entries,
732 ..
733 } => vjp_gaussian_splat_render(node, upstream, upstream_shape, fwd_map, bwd),
734 Op::GaussianSplatRenderBackward { .. } => {
735 vjp_gaussian_splat_render_backward(node, upstream, upstream_shape, fwd_map, bwd)
736 }
737 Op::GaussianSplatPrepare { .. } | Op::GaussianSplatRasterize { .. } => {
738 panic!(
739 "autodiff: decomposed splat ops must be fused before AD — \
740 `prepare_graph_for_ad` rewrites Prepare→Rasterize into \
741 `GaussianSplatRender`, or use `Op::GaussianSplatRender` directly"
742 );
743 }
744
745 Op::CustomFn {
746 vjp_body: Some(vjp_body),
747 num_inputs,
748 ..
749 } => vjp_custom_fn(node, upstream, upstream_shape, fwd_map, bwd),
750 Op::CustomFn { vjp_body: None, .. } => {
751 vjp_custom_fn_2(node, upstream, upstream_shape, fwd_map, bwd)
752 }
753 Op::Custom { name, .. } => vjp_custom(node, upstream, upstream_shape, fwd_map, bwd),
754 Op::Conv2dBackwardInput {
755 kernel_size,
756 stride,
757 padding,
758 dilation,
759 groups,
760 } => vjp_conv2d_backward_input(node, upstream, upstream_shape, fwd_map, bwd),
761 Op::Conv2dBackwardWeight {
762 kernel_size,
763 stride,
764 padding,
765 dilation,
766 groups,
767 } => vjp_conv2d_backward_weight(node, upstream, upstream_shape, fwd_map, bwd),
768 Op::Fft { inverse, norm } => vjp_fft(node, upstream, upstream_shape, fwd_map, bwd),
769 Op::LogMel => vjp_log_mel(node, upstream, upstream_shape, fwd_map, bwd),
770 other => panic!(
774 "autodiff: no VJP rule for {other}. See the matching \
775 entry in rlx-opt/src/autodiff.rs (catch-all panic) for \
776 a pointer to what's needed to differentiate this op.",
777 ),
778 }
779}
780
781#[allow(unused_variables)]
782fn vjp_binary_add(
783 node: &Node,
784 upstream: NodeId,
785 upstream_shape: Shape,
786 fwd_map: &HashMap<NodeId, NodeId>,
787 bwd: &mut Graph,
788) -> Vec<(usize, NodeId)> {
789 let Op::Binary(BinaryOp::Add) = &node.op else {
790 unreachable!()
791 };
792 {
793 let a_bwd = fwd_map[&node.inputs[0]];
794 let b_bwd = fwd_map[&node.inputs[1]];
795 let a_shape = bwd.node(a_bwd).shape.clone();
796 let b_shape = bwd.node(b_bwd).shape.clone();
797 let g_a = unbroadcast(upstream, &a_shape, bwd);
798 let g_b = unbroadcast(upstream, &b_shape, bwd);
799 vec![(0, g_a), (1, g_b)]
800 }
801}
802
803#[allow(unused_variables)]
804fn vjp_binary_sub(
805 node: &Node,
806 upstream: NodeId,
807 upstream_shape: Shape,
808 fwd_map: &HashMap<NodeId, NodeId>,
809 bwd: &mut Graph,
810) -> Vec<(usize, NodeId)> {
811 let Op::Binary(BinaryOp::Sub) = &node.op else {
812 unreachable!()
813 };
814 {
815 let a_bwd = fwd_map[&node.inputs[0]];
816 let b_bwd = fwd_map[&node.inputs[1]];
817 let a_shape = bwd.node(a_bwd).shape.clone();
818 let b_shape = bwd.node(b_bwd).shape.clone();
819 let neg = bwd.activation(Activation::Neg, upstream, upstream_shape.clone());
820 let g_a = unbroadcast(upstream, &a_shape, bwd);
821 let g_b = unbroadcast(neg, &b_shape, bwd);
822 vec![(0, g_a), (1, g_b)]
823 }
824}
825
826#[allow(unused_variables)]
827fn vjp_binary_mul(
828 node: &Node,
829 upstream: NodeId,
830 upstream_shape: Shape,
831 fwd_map: &HashMap<NodeId, NodeId>,
832 bwd: &mut Graph,
833) -> Vec<(usize, NodeId)> {
834 let Op::Binary(BinaryOp::Mul) = &node.op else {
835 unreachable!()
836 };
837 {
838 let a_bwd = fwd_map[&node.inputs[0]];
839 let b_bwd = fwd_map[&node.inputs[1]];
840 let a_shape = bwd.node(a_bwd).shape.clone();
841 let b_shape = bwd.node(b_bwd).shape.clone();
842 let is_c64 = upstream_shape.dtype() == DType::C64;
848 let b_for_a = if is_c64 { bwd.conjugate(b_bwd) } else { b_bwd };
849 let a_for_b = if is_c64 { bwd.conjugate(a_bwd) } else { a_bwd };
850 let g_a_full = bwd.binary(BinaryOp::Mul, upstream, b_for_a, upstream_shape.clone());
851 let g_b_full = bwd.binary(BinaryOp::Mul, upstream, a_for_b, upstream_shape);
852 let g_a = unbroadcast(g_a_full, &a_shape, bwd);
853 let g_b = unbroadcast(g_b_full, &b_shape, bwd);
854 vec![(0, g_a), (1, g_b)]
855 }
856}
857
858#[allow(unused_variables)]
859fn vjp_activation(
860 node: &Node,
861 upstream: NodeId,
862 upstream_shape: Shape,
863 fwd_map: &HashMap<NodeId, NodeId>,
864 bwd: &mut Graph,
865) -> Vec<(usize, NodeId)> {
866 let Op::Activation(kind) = &node.op else {
867 unreachable!()
868 };
869 {
870 let x_bwd = fwd_map[&node.inputs[0]];
871 let dx = match kind {
876 Activation::Relu => bwd.relu_backward(x_bwd, upstream),
877 _ => bwd.activation_backward(*kind, x_bwd, upstream),
878 };
879 vec![(0, dx)]
880 }
881}
882
883#[allow(unused_variables)]
884fn vjp_mat_mul(
885 node: &Node,
886 upstream: NodeId,
887 upstream_shape: Shape,
888 fwd_map: &HashMap<NodeId, NodeId>,
889 bwd: &mut Graph,
890) -> Vec<(usize, NodeId)> {
891 let Op::MatMul = &node.op else { unreachable!() };
892 {
893 let a_bwd = fwd_map[&node.inputs[0]];
904 let b_bwd = fwd_map[&node.inputs[1]];
905 let a_shape = bwd.node(a_bwd).shape.clone();
906 let b_shape = bwd.node(b_bwd).shape.clone();
907 assert!(
908 a_shape.rank() >= 2 && b_shape.rank() >= 2,
909 "MatMul VJP: rank must be ≥ 2, got {} and {}",
910 a_shape.rank(),
911 b_shape.rank()
912 );
913 let dtype = upstream_shape.dtype();
914
915 let trans_last_two = |bwd: &mut Graph, x: NodeId| -> NodeId {
917 let s = bwd.node(x).shape.clone();
918 let r = s.rank();
919 let mut perm: Vec<usize> = (0..r).collect();
920 perm.swap(r - 2, r - 1);
921 let mut dims: Vec<Dim> = s.dims().to_vec();
922 dims.swap(r - 2, r - 1);
923 let new_shape = Shape::from_dims(&dims, s.dtype());
924 bwd.add_node(Op::Transpose { perm }, vec![x], new_shape)
925 };
926
927 let upstream_dims: Vec<Dim> = upstream_shape.dims().to_vec();
930 let r_up = upstream_dims.len();
931
932 let is_c64 = dtype == DType::C64;
938
939 let b_t = trans_last_two(bwd, b_bwd);
941 let b_t = if is_c64 { bwd.conjugate(b_t) } else { b_t };
942 let mut ga_dims = upstream_dims.clone();
943 ga_dims[r_up - 1] = a_shape.dim(a_shape.rank() - 1); let ga_shape = Shape::from_dims(&ga_dims, dtype);
945 let g_a_full = bwd.matmul(upstream, b_t, ga_shape);
946 let g_a = unbroadcast(g_a_full, &a_shape, bwd);
947
948 let a_t = trans_last_two(bwd, a_bwd);
950 let a_t = if is_c64 { bwd.conjugate(a_t) } else { a_t };
951 let mut gb_dims = upstream_dims.clone();
952 gb_dims[r_up - 2] = a_shape.dim(a_shape.rank() - 1); let gb_shape = Shape::from_dims(&gb_dims, dtype);
954 let g_b_full = bwd.matmul(a_t, upstream, gb_shape);
955 let g_b = unbroadcast(g_b_full, &b_shape, bwd);
956
957 vec![(0, g_a), (1, g_b)]
958 }
959}
960
961#[allow(unused_variables)]
962fn vjp_dense_solve(
963 node: &Node,
964 upstream: NodeId,
965 upstream_shape: Shape,
966 fwd_map: &HashMap<NodeId, NodeId>,
967 bwd: &mut Graph,
968) -> Vec<(usize, NodeId)> {
969 let Op::DenseSolve = &node.op else {
970 unreachable!()
971 };
972 {
973 let a_bwd = fwd_map[&node.inputs[0]];
982 let x_bwd = fwd_map[&node.id];
983 let a_shape = bwd.node(a_bwd).shape.clone();
984 let x_shape = bwd.node(x_bwd).shape.clone();
985 assert_eq!(a_shape.rank(), 2, "DenseSolve VJP: A must be 2-D");
986 let n = match a_shape.dim(0) {
987 Dim::Static(n) => n,
988 Dim::Dynamic(_) => panic!("DenseSolve VJP: dynamic N not supported"),
989 };
990 let dtype = a_shape.dtype();
991
992 let mut a_t_dims: Vec<Dim> = a_shape.dims().to_vec();
994 a_t_dims.swap(0, 1);
995 let a_t_shape = Shape::from_dims(&a_t_dims, dtype);
996 let a_t = bwd.add_node(Op::Transpose { perm: vec![1, 0] }, vec![a_bwd], a_t_shape);
997
998 let d_b = bwd.dense_solve(a_t, upstream, x_shape.clone());
1000
1001 let neg_outer = match x_shape.rank() {
1003 1 => {
1004 let col_shape = Shape::from_dims(&[Dim::Static(n), Dim::Static(1)], dtype);
1006 let row_shape = Shape::from_dims(&[Dim::Static(1), Dim::Static(n)], dtype);
1007 let db_col = bwd.add_node(
1008 Op::Reshape {
1009 new_shape: vec![n as i64, 1],
1010 },
1011 vec![d_b],
1012 col_shape,
1013 );
1014 let x_row = bwd.add_node(
1015 Op::Reshape {
1016 new_shape: vec![1, n as i64],
1017 },
1018 vec![x_bwd],
1019 row_shape,
1020 );
1021 let outer = bwd.matmul(db_col, x_row, a_shape.clone());
1022 bwd.activation(Activation::Neg, outer, a_shape)
1023 }
1024 2 => {
1025 let k = match x_shape.dim(1) {
1027 Dim::Static(k) => k,
1028 Dim::Dynamic(_) => panic!("DenseSolve VJP: dynamic K not supported"),
1029 };
1030 let xt_dims = vec![Dim::Static(k), Dim::Static(n)];
1031 let xt_shape = Shape::from_dims(&xt_dims, dtype);
1032 let x_t = bwd.add_node(Op::Transpose { perm: vec![1, 0] }, vec![x_bwd], xt_shape);
1033 let outer = bwd.matmul(d_b, x_t, a_shape.clone());
1034 bwd.activation(Activation::Neg, outer, a_shape)
1035 }
1036 r => panic!("DenseSolve VJP: B must be rank 1 or 2, got rank {r}"),
1037 };
1038
1039 vec![(0, neg_outer), (1, d_b)]
1040 }
1041}
1042
1043#[allow(unused_variables)]
1044fn vjp_batched_dense_solve(
1045 node: &Node,
1046 upstream: NodeId,
1047 upstream_shape: Shape,
1048 fwd_map: &HashMap<NodeId, NodeId>,
1049 bwd: &mut Graph,
1050) -> Vec<(usize, NodeId)> {
1051 let Op::BatchedDenseSolve = &node.op else {
1052 unreachable!()
1053 };
1054 {
1055 let a_bwd = fwd_map[&node.inputs[0]];
1061 let x_bwd = fwd_map[&node.id];
1062 let a_shape = bwd.node(a_bwd).shape.clone();
1063 let x_shape = bwd.node(x_bwd).shape.clone();
1064 assert_eq!(
1065 a_shape.rank(),
1066 3,
1067 "BatchedDenseSolve VJP: A must be rank-3 [B, N, N]"
1068 );
1069 let b_dim = match a_shape.dim(0) {
1070 Dim::Static(b) => b,
1071 Dim::Dynamic(_) => panic!("BatchedDenseSolve VJP: dynamic B not supported"),
1072 };
1073 let n = match a_shape.dim(1) {
1074 Dim::Static(n) => n,
1075 Dim::Dynamic(_) => panic!("BatchedDenseSolve VJP: dynamic N not supported"),
1076 };
1077 let dtype = a_shape.dtype();
1078
1079 let a_t = bwd.add_node(
1082 Op::Transpose {
1083 perm: vec![0, 2, 1],
1084 },
1085 vec![a_bwd],
1086 a_shape.clone(),
1087 );
1088
1089 let d_b = bwd.batched_dense_solve(a_t, upstream, x_shape.clone());
1091
1092 let neg_outer = match x_shape.rank() {
1094 2 => {
1095 let col_shape =
1098 Shape::from_dims(&[Dim::Static(b_dim), Dim::Static(n), Dim::Static(1)], dtype);
1099 let row_shape =
1100 Shape::from_dims(&[Dim::Static(b_dim), Dim::Static(1), Dim::Static(n)], dtype);
1101 let db_col = bwd.add_node(
1102 Op::Reshape {
1103 new_shape: vec![b_dim as i64, n as i64, 1],
1104 },
1105 vec![d_b],
1106 col_shape,
1107 );
1108 let x_row = bwd.add_node(
1109 Op::Reshape {
1110 new_shape: vec![b_dim as i64, 1, n as i64],
1111 },
1112 vec![x_bwd],
1113 row_shape,
1114 );
1115 let outer = bwd.matmul(db_col, x_row, a_shape.clone());
1116 bwd.activation(Activation::Neg, outer, a_shape)
1117 }
1118 3 => {
1119 let k = match x_shape.dim(2) {
1122 Dim::Static(k) => k,
1123 Dim::Dynamic(_) => panic!("BatchedDenseSolve VJP: dynamic K not supported"),
1124 };
1125 let xt_shape =
1126 Shape::from_dims(&[Dim::Static(b_dim), Dim::Static(k), Dim::Static(n)], dtype);
1127 let x_t = bwd.add_node(
1128 Op::Transpose {
1129 perm: vec![0, 2, 1],
1130 },
1131 vec![x_bwd],
1132 xt_shape,
1133 );
1134 let outer = bwd.matmul(d_b, x_t, a_shape.clone());
1135 bwd.activation(Activation::Neg, outer, a_shape)
1136 }
1137 r => panic!("BatchedDenseSolve VJP: b must be rank 2 or 3, got rank {r}"),
1138 };
1139
1140 vec![(0, neg_outer), (1, d_b)]
1141 }
1142}
1143
1144#[allow(unused_variables)]
1145fn vjp_scan(
1146 node: &Node,
1147 upstream: NodeId,
1148 upstream_shape: Shape,
1149 fwd_map: &HashMap<NodeId, NodeId>,
1150 bwd: &mut Graph,
1151) -> Vec<(usize, NodeId)> {
1152 let Op::Scan {
1153 body,
1154 length,
1155 save_trajectory,
1156 num_bcast: _,
1157 num_xs,
1158 num_checkpoints,
1159 } = &node.op
1160 else {
1161 unreachable!()
1162 };
1163 {
1164 let init_bwd = fwd_map[&node.inputs[0]];
1172 let traj_bwd = fwd_map[&node.id];
1173 let init_shape = bwd.node(init_bwd).shape.clone();
1174
1175 let mut body_input_ids: Vec<NodeId> = body
1177 .nodes()
1178 .iter()
1179 .filter(|n| matches!(n.op, Op::Input { .. }))
1180 .map(|n| n.id)
1181 .collect();
1182 body_input_ids.sort();
1183
1184 let body_vjp = grad(body, &body_input_ids);
1185
1186 let xs_bwd: Vec<NodeId> = (0..*num_xs as usize)
1187 .map(|i| fwd_map[&node.inputs[1 + i]])
1188 .collect();
1189
1190 let is_checkpointed = *num_checkpoints != 0 && *num_checkpoints != *length;
1196 let forward_body_for_bwd = if is_checkpointed {
1197 Some((**body).clone())
1198 } else {
1199 None
1200 };
1201
1202 let dinit = bwd.scan_backward_with_checkpoints(
1203 init_bwd,
1204 traj_bwd,
1205 upstream,
1206 &xs_bwd,
1207 body_vjp.clone(),
1208 *length,
1209 *save_trajectory,
1210 *num_checkpoints,
1211 forward_body_for_bwd.clone(),
1212 init_shape,
1213 );
1214
1215 let mut grads: Vec<(usize, NodeId)> = vec![(0, dinit)];
1216 for i in 0..*num_xs as usize {
1217 let outer_xs_id = node.inputs[1 + i];
1218 let xs_shape = bwd.node(fwd_map[&outer_xs_id]).shape.clone();
1219 let dxs_i = bwd.scan_backward_xs_with_checkpoints(
1220 init_bwd,
1221 traj_bwd,
1222 upstream,
1223 &xs_bwd,
1224 body_vjp.clone(),
1225 *length,
1226 *save_trajectory,
1227 i as u32,
1228 *num_checkpoints,
1229 forward_body_for_bwd.clone(),
1230 xs_shape,
1231 );
1232 grads.push((1 + i, dxs_i));
1233 }
1234 grads
1235 }
1236}
1237
1238#[allow(unused_variables)]
1239fn vjp_conv(
1240 node: &Node,
1241 upstream: NodeId,
1242 upstream_shape: Shape,
1243 fwd_map: &HashMap<NodeId, NodeId>,
1244 bwd: &mut Graph,
1245) -> Vec<(usize, NodeId)> {
1246 let Op::Conv {
1247 kernel_size,
1248 stride,
1249 padding,
1250 dilation,
1251 groups,
1252 } = &node.op
1253 else {
1254 unreachable!()
1255 };
1256 {
1257 let x_bwd = fwd_map[&node.inputs[0]];
1258 let w_bwd = fwd_map[&node.inputs[1]];
1259 let x_shape = bwd.node(x_bwd).shape.clone();
1260 let w_shape = bwd.node(w_bwd).shape.clone();
1261 let dx = bwd.conv2d_backward_input(
1262 upstream,
1263 w_bwd,
1264 x_shape,
1265 kernel_size.clone(),
1266 stride.clone(),
1267 padding.clone(),
1268 dilation.clone(),
1269 *groups,
1270 );
1271 let _qat_bits: Option<u8> = None;
1281 let dw = bwd.conv2d_backward_weight(
1282 x_bwd,
1283 upstream,
1284 w_shape,
1285 kernel_size.clone(),
1286 stride.clone(),
1287 padding.clone(),
1288 dilation.clone(),
1289 *groups,
1290 );
1291 vec![(0, dx), (1, dw)]
1292 }
1293}
1294
1295#[allow(unused_variables)]
1296fn vjp_pool(
1297 node: &Node,
1298 upstream: NodeId,
1299 upstream_shape: Shape,
1300 fwd_map: &HashMap<NodeId, NodeId>,
1301 bwd: &mut Graph,
1302) -> Vec<(usize, NodeId)> {
1303 let Op::Pool {
1304 kind: ReduceOp::Max,
1305 kernel_size,
1306 stride,
1307 padding,
1308 } = &node.op
1309 else {
1310 unreachable!()
1311 };
1312 {
1313 let x_bwd = fwd_map[&node.inputs[0]];
1314 let dx = bwd.maxpool2d_backward(
1315 x_bwd,
1316 upstream,
1317 kernel_size.clone(),
1318 stride.clone(),
1319 padding.clone(),
1320 );
1321 vec![(0, dx)]
1322 }
1323}
1324
1325#[allow(unused_variables)]
1326fn vjp_softmax_cross_entropy_with_logits(
1327 node: &Node,
1328 upstream: NodeId,
1329 upstream_shape: Shape,
1330 fwd_map: &HashMap<NodeId, NodeId>,
1331 bwd: &mut Graph,
1332) -> Vec<(usize, NodeId)> {
1333 let Op::SoftmaxCrossEntropyWithLogits = &node.op else {
1334 unreachable!()
1335 };
1336 {
1337 let logits_bwd = fwd_map[&node.inputs[0]];
1338 let labels_bwd = fwd_map[&node.inputs[1]];
1339 let dlogits = bwd.softmax_cross_entropy_backward(logits_bwd, labels_bwd, upstream);
1340 vec![(0, dlogits)]
1342 }
1343}
1344
1345#[allow(unused_variables)]
1346fn vjp_softmax_cross_entropy(
1347 node: &Node,
1348 upstream: NodeId,
1349 upstream_shape: Shape,
1350 fwd_map: &HashMap<NodeId, NodeId>,
1351 bwd: &mut Graph,
1352) -> Vec<(usize, NodeId)> {
1353 let Op::SoftmaxCrossEntropy = &node.op else {
1354 unreachable!()
1355 };
1356 {
1357 let logits_bwd = fwd_map[&node.inputs[0]];
1363 let targets_bwd = fwd_map[&node.inputs[1]];
1364 let logits_shape = bwd.node(logits_bwd).shape.clone();
1365 let upstream_2d = bwd.reshape_(upstream, vec![-1, 1]);
1367 let sm = bwd.softmax(logits_bwd, -1, logits_shape.clone());
1368 let diff = bwd.sub(sm, targets_bwd);
1369 let dlogits = bwd.mul(diff, upstream_2d);
1370 let neg_logits = bwd.neg(logits_bwd);
1371 let dtargets = bwd.mul(neg_logits, upstream_2d);
1372 vec![(0, dlogits), (1, dtargets)]
1373 }
1374}
1375
1376#[allow(unused_variables)]
1377fn vjp_reduce(
1378 node: &Node,
1379 upstream: NodeId,
1380 upstream_shape: Shape,
1381 fwd_map: &HashMap<NodeId, NodeId>,
1382 bwd: &mut Graph,
1383) -> Vec<(usize, NodeId)> {
1384 let Op::Reduce {
1385 op: ReduceOp::Sum,
1386 axes,
1387 keep_dim,
1388 } = &node.op
1389 else {
1390 unreachable!()
1391 };
1392 {
1393 let x_bwd = fwd_map[&node.inputs[0]];
1394 let x_shape = bwd.node(x_bwd).shape.clone();
1395 let g = expand_to(upstream, &x_shape, axes, *keep_dim, bwd);
1396 vec![(0, g)]
1397 }
1398}
1399
1400#[allow(unused_variables)]
1401fn vjp_reduce_2(
1402 node: &Node,
1403 upstream: NodeId,
1404 upstream_shape: Shape,
1405 fwd_map: &HashMap<NodeId, NodeId>,
1406 bwd: &mut Graph,
1407) -> Vec<(usize, NodeId)> {
1408 let Op::Reduce {
1409 op: ReduceOp::Mean,
1410 axes,
1411 keep_dim,
1412 } = &node.op
1413 else {
1414 unreachable!()
1415 };
1416 {
1417 let x_bwd = fwd_map[&node.inputs[0]];
1423 let x_shape = bwd.node(x_bwd).shape.clone();
1424 let count: usize = axes
1425 .iter()
1426 .map(|&a| match x_shape.dim(a) {
1427 Dim::Static(n) => n,
1428 _ => panic!("Reduce::Mean VJP requires static reduced dims"),
1429 })
1430 .product();
1431 let expanded = expand_to(upstream, &x_shape, axes, *keep_dim, bwd);
1432 let inv_count = scalar_const(1.0 / count as f32, bwd);
1433 let g = bwd.binary(BinaryOp::Mul, expanded, inv_count, x_shape);
1434 vec![(0, g)]
1435 }
1436}
1437
1438#[allow(unused_variables)]
1439fn vjp_reshape(
1440 node: &Node,
1441 upstream: NodeId,
1442 upstream_shape: Shape,
1443 fwd_map: &HashMap<NodeId, NodeId>,
1444 bwd: &mut Graph,
1445) -> Vec<(usize, NodeId)> {
1446 let Op::Reshape { .. } = &node.op else {
1447 unreachable!()
1448 };
1449 {
1450 let x_bwd = fwd_map[&node.inputs[0]];
1451 let x_shape = bwd.node(x_bwd).shape.clone();
1452 let dx = reshape_to(upstream, &x_shape, bwd);
1453 vec![(0, dx)]
1454 }
1455}
1456
1457#[allow(unused_variables)]
1458fn vjp_complex_norm_sq(
1459 node: &Node,
1460 upstream: NodeId,
1461 upstream_shape: Shape,
1462 fwd_map: &HashMap<NodeId, NodeId>,
1463 bwd: &mut Graph,
1464) -> Vec<(usize, NodeId)> {
1465 let Op::ComplexNormSq = &node.op else {
1466 unreachable!()
1467 };
1468 {
1469 let z_bwd = fwd_map[&node.inputs[0]];
1472 let dz = bwd.complex_norm_sq_backward(z_bwd, upstream);
1473 vec![(0, dz)]
1474 }
1475}
1476
1477#[allow(unused_variables)]
1478fn vjp_conjugate(
1479 node: &Node,
1480 upstream: NodeId,
1481 upstream_shape: Shape,
1482 fwd_map: &HashMap<NodeId, NodeId>,
1483 bwd: &mut Graph,
1484) -> Vec<(usize, NodeId)> {
1485 let Op::Conjugate = &node.op else {
1486 unreachable!()
1487 };
1488 {
1489 let dz = bwd.conjugate(upstream);
1495 vec![(0, dz)]
1496 }
1497}
1498
1499#[allow(unused_variables)]
1500fn vjp_cast(
1501 node: &Node,
1502 upstream: NodeId,
1503 upstream_shape: Shape,
1504 fwd_map: &HashMap<NodeId, NodeId>,
1505 bwd: &mut Graph,
1506) -> Vec<(usize, NodeId)> {
1507 let Op::Cast { .. } = &node.op else {
1508 unreachable!()
1509 };
1510 {
1511 let x_bwd = fwd_map[&node.inputs[0]];
1512 let x_shape = bwd.node(x_bwd).shape.clone();
1513 let dx = bwd.add_node(
1514 Op::Cast {
1515 to: x_shape.dtype(),
1516 },
1517 vec![upstream],
1518 x_shape,
1519 );
1520 vec![(0, dx)]
1521 }
1522}
1523
1524#[allow(unused_variables)]
1529fn vjp_stop_gradient(
1530 node: &Node,
1531 upstream: NodeId,
1532 upstream_shape: Shape,
1533 fwd_map: &HashMap<NodeId, NodeId>,
1534 bwd: &mut Graph,
1535) -> Vec<(usize, NodeId)> {
1536 let Op::StopGradient = &node.op else {
1537 unreachable!()
1538 };
1539 vec![]
1540}
1541
1542#[allow(unused_variables)]
1543fn vjp_fake_quantize_l_s_q(
1544 node: &Node,
1545 upstream: NodeId,
1546 upstream_shape: Shape,
1547 fwd_map: &HashMap<NodeId, NodeId>,
1548 bwd: &mut Graph,
1549) -> Vec<(usize, NodeId)> {
1550 let Op::FakeQuantizeLSQ { bits, axis } = &node.op else {
1551 unreachable!()
1552 };
1553 {
1554 let x_bwd = fwd_map[&node.inputs[0]];
1558 let scale_bwd = fwd_map[&node.inputs[1]];
1559 let x_shape = bwd.node(x_bwd).shape.clone();
1560 let scale_shape = bwd.node(scale_bwd).shape.clone();
1561 let dx = bwd.add_node(
1562 Op::FakeQuantizeLSQBackwardX {
1563 bits: *bits,
1564 axis: *axis,
1565 },
1566 vec![x_bwd, scale_bwd, upstream],
1567 x_shape,
1568 );
1569 let dscale = bwd.add_node(
1570 Op::FakeQuantizeLSQBackwardScale {
1571 bits: *bits,
1572 axis: *axis,
1573 },
1574 vec![x_bwd, scale_bwd, upstream],
1575 scale_shape,
1576 );
1577 vec![(0, dx), (1, dscale)]
1578 }
1579}
1580
1581#[allow(unused_variables)]
1586fn vjp_fake_quantize(
1587 node: &Node,
1588 upstream: NodeId,
1589 upstream_shape: Shape,
1590 fwd_map: &HashMap<NodeId, NodeId>,
1591 bwd: &mut Graph,
1592) -> Vec<(usize, NodeId)> {
1593 let Op::FakeQuantize {
1594 bits, axis, ste, ..
1595 } = &node.op
1596 else {
1597 unreachable!()
1598 };
1599 {
1600 use rlx_ir::op::SteKind;
1601 match ste {
1602 SteKind::Identity => vec![(0, upstream)],
1603 _ => {
1604 let x_bwd = fwd_map[&node.inputs[0]];
1605 let x_shape = bwd.node(x_bwd).shape.clone();
1606 let dx = bwd.add_node(
1607 Op::FakeQuantizeBackward {
1608 bits: *bits,
1609 axis: *axis,
1610 ste: *ste,
1611 },
1612 vec![x_bwd, upstream],
1613 x_shape,
1614 );
1615 vec![(0, dx)]
1616 }
1617 }
1618 }
1619}
1620
1621#[allow(unused_variables)]
1622fn vjp_expand(
1623 node: &Node,
1624 upstream: NodeId,
1625 upstream_shape: Shape,
1626 fwd_map: &HashMap<NodeId, NodeId>,
1627 bwd: &mut Graph,
1628) -> Vec<(usize, NodeId)> {
1629 let Op::Expand { .. } = &node.op else {
1630 unreachable!()
1631 };
1632 {
1633 let x_bwd = fwd_map[&node.inputs[0]];
1634 let x_shape = bwd.node(x_bwd).shape.clone();
1635 let dx = unbroadcast(upstream, &x_shape, bwd);
1636 vec![(0, dx)]
1637 }
1638}
1639
1640#[allow(unused_variables)]
1641fn vjp_batch_norm_inference(
1642 node: &Node,
1643 upstream: NodeId,
1644 upstream_shape: Shape,
1645 fwd_map: &HashMap<NodeId, NodeId>,
1646 bwd: &mut Graph,
1647) -> Vec<(usize, NodeId)> {
1648 let Op::BatchNormInference { eps } = &node.op else {
1649 unreachable!()
1650 };
1651 {
1652 let x_bwd = fwd_map[&node.inputs[0]];
1653 let gamma_bwd = fwd_map[&node.inputs[1]];
1654 let _beta_bwd = fwd_map[&node.inputs[2]];
1655 let mean_bwd = fwd_map[&node.inputs[3]];
1656 let var_bwd = fwd_map[&node.inputs[4]];
1657 let gamma_shape = bwd.node(gamma_bwd).shape.clone();
1658 let dx = bwd.batch_norm_inference_backward_input(
1659 x_bwd, gamma_bwd, mean_bwd, var_bwd, upstream, *eps,
1660 );
1661 let dgamma = bwd.batch_norm_inference_backward_gamma(
1662 x_bwd,
1663 mean_bwd,
1664 var_bwd,
1665 upstream,
1666 gamma_shape.clone(),
1667 *eps,
1668 );
1669 let dbeta = bwd.batch_norm_inference_backward_beta(upstream, gamma_shape);
1670 vec![(0, dx), (1, dgamma), (2, dbeta)]
1672 }
1673}
1674
1675#[allow(unused_variables)]
1676fn vjp_layer_norm(
1677 node: &Node,
1678 upstream: NodeId,
1679 upstream_shape: Shape,
1680 fwd_map: &HashMap<NodeId, NodeId>,
1681 bwd: &mut Graph,
1682) -> Vec<(usize, NodeId)> {
1683 let Op::LayerNorm { axis, eps } = &node.op else {
1684 unreachable!()
1685 };
1686 {
1687 let x_bwd = fwd_map[&node.inputs[0]];
1694 let gamma_bwd = fwd_map[&node.inputs[1]];
1695 let _beta_bwd = fwd_map[&node.inputs[2]];
1696 let gamma_shape = bwd.node(gamma_bwd).shape.clone();
1697
1698 let dx = bwd.layer_norm_backward_input(x_bwd, gamma_bwd, upstream, *axis, *eps);
1699 let dgamma =
1700 bwd.layer_norm_backward_gamma(x_bwd, upstream, gamma_shape.clone(), *axis, *eps);
1701 let dbeta = unbroadcast(upstream, &gamma_shape, bwd);
1702 vec![(0, dx), (1, dgamma), (2, dbeta)]
1703 }
1704}
1705
1706#[allow(unused_variables)]
1707fn vjp_softmax(
1708 node: &Node,
1709 upstream: NodeId,
1710 upstream_shape: Shape,
1711 fwd_map: &HashMap<NodeId, NodeId>,
1712 bwd: &mut Graph,
1713) -> Vec<(usize, NodeId)> {
1714 let Op::Softmax { axis } = &node.op else {
1715 unreachable!()
1716 };
1717 {
1718 let y_bwd = fwd_map[&node.id];
1734 let y_shape = bwd.node(y_bwd).shape.clone();
1735 let dtype = y_shape.dtype();
1736 let rank = y_shape.rank();
1737 let axis_pos = if *axis < 0 {
1738 (rank as i32 + *axis) as usize
1739 } else {
1740 *axis as usize
1741 };
1742
1743 let yg = bwd.binary(BinaryOp::Mul, y_bwd, upstream, y_shape.clone());
1744
1745 let mut kept_dims: Vec<Dim> = y_shape.dims().to_vec();
1746 kept_dims[axis_pos] = Dim::Static(1);
1747 let kept_shape = Shape::from_dims(&kept_dims, dtype);
1748 let s = bwd.add_node(
1749 Op::Reduce {
1750 op: ReduceOp::Sum,
1751 axes: vec![axis_pos],
1752 keep_dim: true,
1753 },
1754 vec![yg],
1755 kept_shape,
1756 );
1757
1758 let target_dims: Vec<i64> = y_shape
1759 .dims()
1760 .iter()
1761 .map(|d| match d {
1762 Dim::Static(n) => *n as i64,
1763 Dim::Dynamic(_) => -1,
1764 })
1765 .collect();
1766 let s_expanded = bwd.add_node(
1767 Op::Expand {
1768 target_shape: target_dims,
1769 },
1770 vec![s],
1771 y_shape.clone(),
1772 );
1773
1774 let diff = bwd.binary(BinaryOp::Sub, upstream, s_expanded, y_shape.clone());
1775 let dx = bwd.binary(BinaryOp::Mul, y_bwd, diff, y_shape);
1776 vec![(0, dx)]
1777 }
1778}
1779
1780#[allow(unused_variables)]
1782fn vjp_transpose(
1783 node: &Node,
1784 upstream: NodeId,
1785 upstream_shape: Shape,
1786 fwd_map: &HashMap<NodeId, NodeId>,
1787 bwd: &mut Graph,
1788) -> Vec<(usize, NodeId)> {
1789 let Op::Transpose { perm } = &node.op else {
1790 unreachable!()
1791 };
1792 {
1793 let inv: Vec<usize> = {
1796 let mut v = vec![0usize; perm.len()];
1797 for (i, &p) in perm.iter().enumerate() {
1798 v[p] = i;
1799 }
1800 v
1801 };
1802 let x_bwd = fwd_map[&node.inputs[0]];
1803 let x_shape = bwd.node(x_bwd).shape.clone();
1804 let dx = bwd.add_node(Op::Transpose { perm: inv }, vec![upstream], x_shape);
1805 vec![(0, dx)]
1806 }
1807}
1808
1809#[allow(unused_variables)]
1810fn vjp_concat(
1811 node: &Node,
1812 upstream: NodeId,
1813 upstream_shape: Shape,
1814 fwd_map: &HashMap<NodeId, NodeId>,
1815 bwd: &mut Graph,
1816) -> Vec<(usize, NodeId)> {
1817 let Op::Concat { axis } = &node.op else {
1818 unreachable!()
1819 };
1820 {
1821 let mut grads = Vec::with_capacity(node.inputs.len());
1824 let mut offset: usize = 0;
1825 for (i, &input_id) in node.inputs.iter().enumerate() {
1826 let x_bwd = fwd_map[&input_id];
1827 let x_shape = bwd.node(x_bwd).shape.clone();
1828 let len = match x_shape.dim(*axis) {
1829 Dim::Static(n) => n,
1830 _ => panic!("Concat VJP: dynamic concat dim"),
1831 };
1832 let dx = bwd.add_node(
1833 Op::Narrow {
1834 axis: *axis,
1835 start: offset,
1836 len,
1837 },
1838 vec![upstream],
1839 x_shape,
1840 );
1841 grads.push((i, dx));
1842 offset += len;
1843 }
1844 grads
1845 }
1846}
1847
1848#[allow(unused_variables)]
1849fn vjp_narrow(
1850 node: &Node,
1851 upstream: NodeId,
1852 upstream_shape: Shape,
1853 fwd_map: &HashMap<NodeId, NodeId>,
1854 bwd: &mut Graph,
1855) -> Vec<(usize, NodeId)> {
1856 let Op::Narrow { axis, start, len } = &node.op else {
1857 unreachable!()
1858 };
1859 {
1860 let x_bwd = fwd_map[&node.inputs[0]];
1864 let x_shape = bwd.node(x_bwd).shape.clone();
1865 let full_n = match x_shape.dim(*axis) {
1866 Dim::Static(n) => n,
1867 _ => panic!("Narrow VJP: dynamic axis"),
1868 };
1869 let pre = *start;
1870 let post = full_n - *start - *len;
1871
1872 let zero_buf = |bwd: &mut Graph, len_axis: usize| -> NodeId {
1873 if len_axis == 0 {
1874 return upstream; }
1876 let dtype = x_shape.dtype();
1877 let mut dims: Vec<Dim> = x_shape.dims().to_vec();
1878 dims[*axis] = Dim::Static(len_axis);
1879 let s = Shape::from_dims(&dims, dtype);
1880 let n_elems = dims.iter().fold(1usize, |a, d| match d {
1881 Dim::Static(k) => a * k,
1882 _ => a,
1883 });
1884 let bytes = vec![0u8; n_elems * dtype.size_bytes()];
1888 bwd.add_node(Op::Constant { data: bytes }, vec![], s)
1889 };
1890
1891 let mut parts: Vec<NodeId> = Vec::new();
1892 if pre > 0 {
1893 parts.push(zero_buf(bwd, pre));
1894 }
1895 parts.push(upstream);
1896 if post > 0 {
1897 parts.push(zero_buf(bwd, post));
1898 }
1899
1900 let dx = if parts.len() == 1 {
1901 parts[0]
1902 } else {
1903 bwd.add_node(Op::Concat { axis: *axis }, parts, x_shape)
1904 };
1905 vec![(0, dx)]
1906 }
1907}
1908
1909#[allow(unused_variables)]
1910fn vjp_gather(
1911 node: &Node,
1912 upstream: NodeId,
1913 upstream_shape: Shape,
1914 fwd_map: &HashMap<NodeId, NodeId>,
1915 bwd: &mut Graph,
1916) -> Vec<(usize, NodeId)> {
1917 let Op::Gather { axis } = &node.op else {
1918 unreachable!()
1919 };
1920 {
1921 let table_bwd = fwd_map[&node.inputs[0]];
1922 let indices_bwd = fwd_map[&node.inputs[1]];
1923 let table_shape = bwd.node(table_bwd).shape.clone();
1924 if *axis == 0 {
1925 let dtable = bwd.add_node(Op::ScatterAdd, vec![upstream, indices_bwd], table_shape);
1926 vec![(0, dtable)]
1927 } else {
1928 let dtable = bwd.gather_backward(
1929 upstream,
1930 indices_bwd,
1931 table_shape,
1932 (*axis).try_into().unwrap(),
1933 );
1934 vec![(0, dtable)]
1935 }
1936 }
1937}
1938
1939#[allow(unused_variables)]
1941fn vjp_compare(
1942 node: &Node,
1943 upstream: NodeId,
1944 upstream_shape: Shape,
1945 fwd_map: &HashMap<NodeId, NodeId>,
1946 bwd: &mut Graph,
1947) -> Vec<(usize, NodeId)> {
1948 let Op::Compare(_) = &node.op else {
1949 unreachable!()
1950 };
1951 {
1952 vec![]
1957 }
1958}
1959
1960#[allow(unused_variables)]
1961fn vjp_where(
1962 node: &Node,
1963 upstream: NodeId,
1964 upstream_shape: Shape,
1965 fwd_map: &HashMap<NodeId, NodeId>,
1966 bwd: &mut Graph,
1967) -> Vec<(usize, NodeId)> {
1968 let Op::Where = &node.op else { unreachable!() };
1969 {
1970 let cond = fwd_map[&node.inputs[0]];
1974 let a_bwd = fwd_map[&node.inputs[1]];
1975 let b_bwd = fwd_map[&node.inputs[2]];
1976 let a_shape = bwd.node(a_bwd).shape.clone();
1977 let b_shape = bwd.node(b_bwd).shape.clone();
1978 let out_shape = upstream_shape.clone();
1979
1980 let zero_a_bytes = vec![0u8; a_shape.num_elements().expect("Where VJP: dynamic a") * 4];
1981 let zero_b_bytes = vec![0u8; b_shape.num_elements().expect("Where VJP: dynamic b") * 4];
1982 let zero_a = bwd.add_node(Op::Constant { data: zero_a_bytes }, vec![], a_shape.clone());
1983 let zero_b = bwd.add_node(Op::Constant { data: zero_b_bytes }, vec![], b_shape.clone());
1984 let zero_a_bcast = unbroadcast_inverse(zero_a, &out_shape, bwd);
1987 let zero_b_bcast = unbroadcast_inverse(zero_b, &out_shape, bwd);
1988 let g_a_full = bwd.add_node(
1989 Op::Where,
1990 vec![cond, upstream, zero_a_bcast],
1991 out_shape.clone(),
1992 );
1993 let g_b_full = bwd.add_node(Op::Where, vec![cond, zero_b_bcast, upstream], out_shape);
1994 let g_a = unbroadcast(g_a_full, &a_shape, bwd);
1995 let g_b = unbroadcast(g_b_full, &b_shape, bwd);
1996 vec![(1, g_a), (2, g_b)]
1997 }
1998}
1999
2000#[allow(unused_variables)]
2002fn vjp_binary_div(
2003 node: &Node,
2004 upstream: NodeId,
2005 upstream_shape: Shape,
2006 fwd_map: &HashMap<NodeId, NodeId>,
2007 bwd: &mut Graph,
2008) -> Vec<(usize, NodeId)> {
2009 let Op::Binary(BinaryOp::Div) = &node.op else {
2010 unreachable!()
2011 };
2012 {
2013 let a_bwd = fwd_map[&node.inputs[0]];
2021 let b_bwd = fwd_map[&node.inputs[1]];
2022 let y_bwd = fwd_map[&node.id];
2023 let a_shape = bwd.node(a_bwd).shape.clone();
2024 let b_shape = bwd.node(b_bwd).shape.clone();
2025 let is_c64 = upstream_shape.dtype() == DType::C64;
2026
2027 let b_term = if is_c64 { bwd.conjugate(b_bwd) } else { b_bwd };
2028 let y_term = if is_c64 { bwd.conjugate(y_bwd) } else { y_bwd };
2029
2030 let g_a_full = bwd.binary(BinaryOp::Div, upstream, b_term, upstream_shape.clone());
2032 let g_a = unbroadcast(g_a_full, &a_shape, bwd);
2033
2034 let neg_up = bwd.activation(Activation::Neg, upstream, upstream_shape.clone());
2036 let neg_up_y = bwd.binary(BinaryOp::Mul, neg_up, y_term, upstream_shape.clone());
2037 let g_b_full = bwd.binary(BinaryOp::Div, neg_up_y, b_term, upstream_shape);
2038 let g_b = unbroadcast(g_b_full, &b_shape, bwd);
2039
2040 vec![(0, g_a), (1, g_b)]
2041 }
2042}
2043
2044#[allow(unused_variables)]
2050fn vjp_rope(
2051 node: &Node,
2052 upstream: NodeId,
2053 upstream_shape: Shape,
2054 fwd_map: &HashMap<NodeId, NodeId>,
2055 bwd: &mut Graph,
2056) -> Vec<(usize, NodeId)> {
2057 let Op::Rope {
2058 head_dim, n_rot, ..
2059 } = &node.op
2060 else {
2061 unreachable!()
2062 };
2063 {
2064 let cos = fwd_map[&node.inputs[1]];
2065 let sin = fwd_map[&node.inputs[2]];
2066 let dx = bwd.rope_backward(upstream, cos, sin, *head_dim, *n_rot);
2067 vec![(0, dx)]
2068 }
2069}
2070
2071#[allow(unused_variables)]
2072fn vjp_rms_norm(
2073 node: &Node,
2074 upstream: NodeId,
2075 upstream_shape: Shape,
2076 fwd_map: &HashMap<NodeId, NodeId>,
2077 bwd: &mut Graph,
2078) -> Vec<(usize, NodeId)> {
2079 let Op::RmsNorm { axis, eps } = &node.op else {
2080 unreachable!()
2081 };
2082 {
2083 let x = fwd_map[&node.inputs[0]];
2084 let gamma = fwd_map[&node.inputs[1]];
2085 let beta = fwd_map[&node.inputs[2]];
2086 let dx = bwd.rms_norm_backward_input(x, gamma, beta, upstream, *axis, *eps);
2087 let dgamma = bwd.rms_norm_backward_gamma(x, gamma, beta, upstream, *axis, *eps);
2088 let dbeta = bwd.rms_norm_backward_beta(x, gamma, beta, upstream, *axis, *eps);
2089 vec![(0, dx), (1, dgamma), (2, dbeta)]
2090 }
2091}
2092
2093#[allow(unused_variables)]
2094fn vjp_group_norm(
2095 node: &Node,
2096 upstream: NodeId,
2097 upstream_shape: Shape,
2098 fwd_map: &HashMap<NodeId, NodeId>,
2099 bwd: &mut Graph,
2100) -> Vec<(usize, NodeId)> {
2101 let Op::GroupNorm { num_groups, eps } = &node.op else {
2102 unreachable!()
2103 };
2104 {
2105 let x = fwd_map[&node.inputs[0]];
2106 let gamma = fwd_map[&node.inputs[1]];
2107 let beta = fwd_map[&node.inputs[2]];
2108 let gamma_shape = bwd.node(gamma).shape.clone();
2109 let beta_shape = bwd.node(beta).shape.clone();
2110 let dx = bwd.group_norm_backward_input(x, gamma, beta, upstream, *num_groups, *eps);
2111 let dgamma = bwd.group_norm_backward_gamma(x, upstream, gamma_shape, *num_groups, *eps);
2112 let dbeta = bwd.group_norm_backward_beta(x, upstream, beta_shape, *num_groups, *eps);
2113 vec![(0, dx), (1, dgamma), (2, dbeta)]
2114 }
2115}
2116
2117#[allow(unused_variables)]
2119fn vjp_attention(
2120 node: &Node,
2121 upstream: NodeId,
2122 upstream_shape: Shape,
2123 fwd_map: &HashMap<NodeId, NodeId>,
2124 bwd: &mut Graph,
2125) -> Vec<(usize, NodeId)> {
2126 let Op::Attention {
2127 num_heads,
2128 head_dim,
2129 mask_kind,
2130 score_scale: _,
2131 attn_logit_softcap: _,
2132 } = &node.op
2133 else {
2134 unreachable!()
2135 };
2136 {
2137 let q = fwd_map[&node.inputs[0]];
2138 let k = fwd_map[&node.inputs[1]];
2139 let v = fwd_map[&node.inputs[2]];
2140 let mask = match mask_kind {
2141 MaskKind::Custom | MaskKind::Bias => Some(fwd_map[&node.inputs[3]]),
2142 _ => None,
2143 };
2144 let (dq, dk, dv) =
2145 bwd.attention_backward_all(q, k, v, upstream, *num_heads, *head_dim, *mask_kind, mask);
2146 vec![(0, dq), (1, dk), (2, dv)]
2147 }
2148}
2149
2150#[allow(unused_variables)]
2157fn vjp_reduce_3(
2158 node: &Node,
2159 upstream: NodeId,
2160 upstream_shape: Shape,
2161 fwd_map: &HashMap<NodeId, NodeId>,
2162 bwd: &mut Graph,
2163) -> Vec<(usize, NodeId)> {
2164 let Op::Reduce {
2165 op: ReduceOp::Prod,
2166 axes,
2167 keep_dim,
2168 } = &node.op
2169 else {
2170 unreachable!()
2171 };
2172 {
2173 let x_bwd = fwd_map[&node.inputs[0]];
2174 let y_bwd = fwd_map[&node.id];
2175 let x_shape = bwd.node(x_bwd).shape.clone();
2176 let y_expanded = expand_to(y_bwd, &x_shape, axes, *keep_dim, bwd);
2177 let upstream_expanded = expand_to(upstream, &x_shape, axes, *keep_dim, bwd);
2178 let num = bwd.binary(
2180 BinaryOp::Mul,
2181 upstream_expanded,
2182 y_expanded,
2183 x_shape.clone(),
2184 );
2185 let dx = bwd.binary(BinaryOp::Div, num, x_bwd, x_shape);
2186 vec![(0, dx)]
2187 }
2188}
2189
2190#[allow(unused_variables)]
2202fn vjp_pool_2(
2203 node: &Node,
2204 upstream: NodeId,
2205 upstream_shape: Shape,
2206 fwd_map: &HashMap<NodeId, NodeId>,
2207 bwd: &mut Graph,
2208) -> Vec<(usize, NodeId)> {
2209 let Op::Pool {
2210 kind: ReduceOp::Mean,
2211 kernel_size,
2212 stride,
2213 padding,
2214 } = &node.op
2215 else {
2216 unreachable!()
2217 };
2218 {
2219 assert_eq!(kernel_size.len(), 2, "Pool(Mean) VJP: 2-D pool only");
2220 let x_bwd = fwd_map[&node.inputs[0]];
2221 let x_shape = bwd.node(x_bwd).shape.clone();
2222 let dtype = x_shape.dtype();
2223 let c = match x_shape.dim(1) {
2225 Dim::Static(n) => n,
2226 _ => panic!("Pool(Mean) VJP: dynamic channel dim"),
2227 };
2228 let kh = kernel_size[0];
2229 let kw = kernel_size[1];
2230 let inv_n = 1.0_f32 / (kh as f32 * kw as f32);
2231 let kernel_n = c * kh * kw;
2232 let mut bytes: Vec<u8> = Vec::with_capacity(kernel_n * 4);
2233 for _ in 0..kernel_n {
2234 bytes.extend_from_slice(&inv_n.to_le_bytes());
2235 }
2236 let kernel_shape = Shape::from_dims(
2237 &[
2238 Dim::Static(c),
2239 Dim::Static(1),
2240 Dim::Static(kh),
2241 Dim::Static(kw),
2242 ],
2243 dtype,
2244 );
2245 let kernel = bwd.add_node(Op::Constant { data: bytes }, vec![], kernel_shape);
2246 let dx = bwd.conv2d_backward_input(
2247 upstream,
2248 kernel,
2249 x_shape,
2250 kernel_size.clone(),
2251 stride.clone(),
2252 padding.clone(),
2253 vec![1, 1],
2254 c, );
2256 vec![(0, dx)]
2257 }
2258}
2259
2260#[allow(unused_variables)]
2269fn vjp_binary_pow(
2270 node: &Node,
2271 upstream: NodeId,
2272 upstream_shape: Shape,
2273 fwd_map: &HashMap<NodeId, NodeId>,
2274 bwd: &mut Graph,
2275) -> Vec<(usize, NodeId)> {
2276 let Op::Binary(BinaryOp::Pow) = &node.op else {
2277 unreachable!()
2278 };
2279 {
2280 let a_bwd = fwd_map[&node.inputs[0]];
2281 let b_bwd = fwd_map[&node.inputs[1]];
2282 let y_bwd = fwd_map[&node.id]; let a_shape = bwd.node(a_bwd).shape.clone();
2284 let b_shape = bwd.node(b_bwd).shape.clone();
2285
2286 let yb = bwd.binary(BinaryOp::Mul, y_bwd, b_bwd, upstream_shape.clone());
2289 let yb_over_a = bwd.binary(BinaryOp::Div, yb, a_bwd, upstream_shape.clone());
2290 let g_a_full = bwd.binary(BinaryOp::Mul, upstream, yb_over_a, upstream_shape.clone());
2291 let g_a = unbroadcast(g_a_full, &a_shape, bwd);
2292
2293 let ln_a = bwd.activation(Activation::Log, a_bwd, a_shape);
2295 let ln_a_b = unbroadcast_inverse(ln_a, &upstream_shape, bwd);
2296 let yln = bwd.binary(BinaryOp::Mul, y_bwd, ln_a_b, upstream_shape.clone());
2297 let g_b_full = bwd.binary(BinaryOp::Mul, upstream, yln, upstream_shape);
2298 let g_b = unbroadcast(g_b_full, &b_shape, bwd);
2299
2300 vec![(0, g_a), (1, g_b)]
2301 }
2302}
2303
2304#[allow(unused_variables)]
2330fn vjp_scaled_mat_mul(
2331 node: &Node,
2332 upstream: NodeId,
2333 upstream_shape: Shape,
2334 fwd_map: &HashMap<NodeId, NodeId>,
2335 bwd: &mut Graph,
2336) -> Vec<(usize, NodeId)> {
2337 let Op::ScaledMatMul {
2338 lhs_format,
2339 rhs_format,
2340 scale_layout,
2341 has_bias,
2342 } = &node.op
2343 else {
2344 unreachable!()
2345 };
2346 {
2347 let lhs_codes = fwd_map[&node.inputs[0]];
2348 let rhs_codes = fwd_map[&node.inputs[1]];
2349 let lhs_scale = fwd_map[&node.inputs[2]];
2350 let rhs_scale = fwd_map[&node.inputs[3]];
2351 let lhs_shape = bwd.node(lhs_codes).shape.clone();
2352 let rhs_shape = bwd.node(rhs_codes).shape.clone();
2353 let m = lhs_shape.dim(0);
2354 let k = lhs_shape.dim(1);
2355 let n = rhs_shape.dim(0);
2356 let f32 = DType::F32;
2357
2358 let lhs_recon = bwd.add_node(
2359 Op::ScaledDequantize {
2360 format: *lhs_format,
2361 scale_layout: *scale_layout,
2362 },
2363 vec![lhs_codes, lhs_scale],
2364 Shape::from_dims(&[m, k], f32),
2365 );
2366 let rhs_recon = bwd.add_node(
2367 Op::ScaledDequantize {
2368 format: *rhs_format,
2369 scale_layout: *scale_layout,
2370 },
2371 vec![rhs_codes, rhs_scale],
2372 Shape::from_dims(&[n, k], f32),
2373 );
2374
2375 let d_lhs = bwd.matmul(upstream, rhs_recon, Shape::from_dims(&[m, k], f32));
2376 let up_t = bwd.add_node(
2377 Op::Transpose { perm: vec![1, 0] },
2378 vec![upstream],
2379 Shape::from_dims(&[n, m], f32),
2380 );
2381 let d_rhs = bwd.matmul(up_t, lhs_recon, Shape::from_dims(&[n, k], f32));
2382
2383 let mut grads = vec![(0usize, d_lhs), (1usize, d_rhs)];
2384 if *has_bias {
2385 let d_bias = bwd.add_node(
2386 Op::Reduce {
2387 op: ReduceOp::Sum,
2388 axes: vec![0],
2389 keep_dim: false,
2390 },
2391 vec![upstream],
2392 Shape::from_dims(&[n], f32),
2393 );
2394 grads.push((4usize, d_bias));
2395 }
2396 grads
2397 }
2398}
2399
2400#[allow(unused_variables)]
2403fn vjp_scaled_quantize(
2404 node: &Node,
2405 upstream: NodeId,
2406 upstream_shape: Shape,
2407 fwd_map: &HashMap<NodeId, NodeId>,
2408 bwd: &mut Graph,
2409) -> Vec<(usize, NodeId)> {
2410 let Op::ScaledQuantize { .. } = &node.op else {
2411 unreachable!()
2412 };
2413 vec![(0, upstream)]
2414}
2415
2416#[allow(unused_variables)]
2418fn vjp_scaled_quant_scale(
2419 node: &Node,
2420 upstream: NodeId,
2421 upstream_shape: Shape,
2422 fwd_map: &HashMap<NodeId, NodeId>,
2423 bwd: &mut Graph,
2424) -> Vec<(usize, NodeId)> {
2425 let Op::ScaledQuantScale { .. } = &node.op else {
2426 unreachable!()
2427 };
2428 vec![]
2429}
2430
2431#[allow(unused_variables)]
2432fn vjp_dequant_mat_mul(
2433 node: &Node,
2434 upstream: NodeId,
2435 upstream_shape: Shape,
2436 fwd_map: &HashMap<NodeId, NodeId>,
2437 bwd: &mut Graph,
2438) -> Vec<(usize, NodeId)> {
2439 let Op::DequantMatMul { scheme: _ } = &node.op else {
2440 unreachable!()
2441 };
2442 {
2443 let x_bwd = fwd_map[&node.inputs[0]];
2444 let w_q_bwd = fwd_map[&node.inputs[1]];
2445 let scale_bwd = fwd_map[&node.inputs[2]];
2446 let zp_bwd = fwd_map[&node.inputs[3]];
2447 let x_shape = bwd.node(x_bwd).shape.clone();
2448 let w_shape = bwd.node(w_q_bwd).shape.clone();
2449 let scale_shape = bwd.node(scale_bwd).shape.clone();
2450 let zp_shape = bwd.node(zp_bwd).shape.clone();
2451
2452 let dtype = x_shape.dtype();
2456 let w_q_f32 = bwd.add_node(
2457 Op::Cast { to: dtype },
2458 vec![w_q_bwd],
2459 Shape::from_dims(w_shape.dims(), dtype),
2460 );
2461 let scale_b = unbroadcast_inverse(scale_bwd, &Shape::from_dims(w_shape.dims(), dtype), bwd);
2463 let zp_b = unbroadcast_inverse(zp_bwd, &Shape::from_dims(w_shape.dims(), dtype), bwd);
2464 let w_centered = bwd.binary(
2465 BinaryOp::Sub,
2466 w_q_f32,
2467 zp_b,
2468 Shape::from_dims(w_shape.dims(), dtype),
2469 );
2470 let w_dq = bwd.binary(
2471 BinaryOp::Mul,
2472 w_centered,
2473 scale_b,
2474 Shape::from_dims(w_shape.dims(), dtype),
2475 );
2476
2477 let w_rank = w_shape.rank();
2479 let mut perm: Vec<usize> = (0..w_rank).collect();
2480 perm.swap(w_rank - 2, w_rank - 1);
2481 let mut wdt_dims: Vec<Dim> = w_shape.dims().to_vec();
2482 wdt_dims.swap(w_rank - 2, w_rank - 1);
2483 let w_dq_t_shape = Shape::from_dims(&wdt_dims, dtype);
2484 let w_dq_t = bwd.add_node(Op::Transpose { perm }, vec![w_dq], w_dq_t_shape);
2485 let dx = bwd.matmul(upstream, w_dq_t, x_shape.clone());
2486
2487 let x_rank = x_shape.rank();
2492 let mut x_perm: Vec<usize> = (0..x_rank).collect();
2493 x_perm.swap(x_rank - 2, x_rank - 1);
2494 let mut x_t_dims: Vec<Dim> = x_shape.dims().to_vec();
2495 x_t_dims.swap(x_rank - 2, x_rank - 1);
2496 let x_t = bwd.add_node(
2497 Op::Transpose { perm: x_perm },
2498 vec![x_bwd],
2499 Shape::from_dims(&x_t_dims, dtype),
2500 );
2501 let dw_unscaled = bwd.matmul(x_t, upstream, Shape::from_dims(w_shape.dims(), dtype));
2502 let dw_q_f32 = bwd.binary(
2503 BinaryOp::Mul,
2504 dw_unscaled,
2505 scale_b,
2506 Shape::from_dims(w_shape.dims(), dtype),
2507 );
2508 let dw_q = bwd.add_node(
2510 Op::Cast {
2511 to: w_shape.dtype(),
2512 },
2513 vec![dw_q_f32],
2514 w_shape,
2515 );
2516
2517 let zero_scale_bytes =
2519 vec![0u8; scale_shape.num_elements().expect("DQMM VJP: dyn scale") * 4];
2520 let zero_zp_bytes = vec![0u8; zp_shape.num_elements().expect("DQMM VJP: dyn zp") * 4];
2521 let dscale = bwd.add_node(
2522 Op::Constant {
2523 data: zero_scale_bytes,
2524 },
2525 vec![],
2526 scale_shape,
2527 );
2528 let dzp = bwd.add_node(
2529 Op::Constant {
2530 data: zero_zp_bytes,
2531 },
2532 vec![],
2533 zp_shape,
2534 );
2535
2536 vec![(0, dx), (1, dw_q), (2, dscale), (3, dzp)]
2537 }
2538}
2539
2540#[allow(unused_variables)]
2546fn vjp_scatter_add(
2547 node: &Node,
2548 upstream: NodeId,
2549 upstream_shape: Shape,
2550 fwd_map: &HashMap<NodeId, NodeId>,
2551 bwd: &mut Graph,
2552) -> Vec<(usize, NodeId)> {
2553 let Op::ScatterAdd = &node.op else {
2554 unreachable!()
2555 };
2556 {
2557 let updates_bwd = fwd_map[&node.inputs[0]];
2558 let indices_bwd = fwd_map[&node.inputs[1]];
2559 let updates_shape = bwd.node(updates_bwd).shape.clone();
2560 let dupdates = bwd.add_node(
2561 Op::Gather { axis: 0 },
2562 vec![upstream, indices_bwd],
2563 updates_shape,
2564 );
2565 vec![(0, dupdates)]
2566 }
2567}
2568
2569#[allow(unused_variables)]
2572fn vjp_cumsum(
2573 node: &Node,
2574 upstream: NodeId,
2575 upstream_shape: Shape,
2576 fwd_map: &HashMap<NodeId, NodeId>,
2577 bwd: &mut Graph,
2578) -> Vec<(usize, NodeId)> {
2579 let Op::Cumsum { axis, exclusive } = &node.op else {
2580 unreachable!()
2581 };
2582 {
2583 let x_bwd = fwd_map[&node.inputs[0]];
2584 let x_shape = bwd.node(x_bwd).shape.clone();
2585 let dx = bwd.cumsum_backward(upstream, x_shape, *axis, *exclusive);
2586 vec![(0, dx)]
2587 }
2588}
2589
2590#[allow(unused_variables)]
2603fn vjp_grouped_mat_mul(
2604 node: &Node,
2605 upstream: NodeId,
2606 upstream_shape: Shape,
2607 fwd_map: &HashMap<NodeId, NodeId>,
2608 bwd: &mut Graph,
2609) -> Vec<(usize, NodeId)> {
2610 let Op::GroupedMatMul = &node.op else {
2611 unreachable!()
2612 };
2613 {
2614 let x_bwd = fwd_map[&node.inputs[0]];
2615 let w_bwd = fwd_map[&node.inputs[1]];
2616 let expert_bwd = fwd_map[&node.inputs[2]];
2617 let x_shape = bwd.node(x_bwd).shape.clone();
2618 let w_shape = bwd.node(w_bwd).shape.clone();
2619 let (dx, dw) =
2620 grouped_matmul_vjp(bwd, upstream, x_bwd, w_bwd, expert_bwd, &x_shape, &w_shape);
2621 vec![(0, dx), (1, dw)]
2622 }
2623}
2624
2625#[allow(unused_variables)]
2631fn vjp_dequant_grouped_mat_mul(
2632 node: &Node,
2633 upstream: NodeId,
2634 upstream_shape: Shape,
2635 fwd_map: &HashMap<NodeId, NodeId>,
2636 bwd: &mut Graph,
2637) -> Vec<(usize, NodeId)> {
2638 let Op::DequantGroupedMatMul { scheme } = &node.op else {
2639 unreachable!()
2640 };
2641 {
2642 let x_bwd = fwd_map[&node.inputs[0]];
2643 let w_packed = fwd_map[&node.inputs[1]];
2644 let expert_bwd = fwd_map[&node.inputs[2]];
2645 let x_shape = bwd.node(x_bwd).shape.clone();
2646 let w_packed_shape = bwd.node(w_packed).shape.clone();
2647 let dtype = x_shape.dtype();
2648 let k = x_shape.dim(1);
2649 let n_out = node.shape.dim(node.shape.rank() - 1);
2650 let k_static = match k {
2651 Dim::Static(v) => v,
2652 _ => panic!("DequantGroupedMatMul VJP: K must be static"),
2653 };
2654 let n_static = match n_out {
2655 Dim::Static(v) => v,
2656 _ => panic!("DequantGroupedMatMul VJP: N must be static"),
2657 };
2658 let block_elems = scheme.gguf_block_size() as usize;
2659 let block_bytes = scheme.gguf_block_bytes() as usize;
2660 let slab_bytes = (k_static * n_static) / block_elems * block_bytes;
2661 let total_bytes = w_packed_shape
2662 .num_elements()
2663 .expect("DequantGroupedMatMul VJP: dyn packed");
2664 let e_static = total_bytes / slab_bytes.max(1);
2665 let w_shape = Shape::from_dims(
2666 &[
2667 Dim::Static(e_static),
2668 Dim::Static(k_static),
2669 Dim::Static(n_static),
2670 ],
2671 dtype,
2672 );
2673 let w_dq = bwd.add_node(
2674 Op::DequantMoEWeights { scheme: *scheme },
2675 vec![w_packed],
2676 w_shape.clone(),
2677 );
2678 let (dx, _dw) =
2679 grouped_matmul_vjp(bwd, upstream, x_bwd, w_dq, expert_bwd, &x_shape, &w_shape);
2680 vec![(0, dx)]
2681 }
2682}
2683
2684#[allow(unused_variables)]
2697fn vjp_q_mat_mul(
2698 node: &Node,
2699 upstream: NodeId,
2700 upstream_shape: Shape,
2701 fwd_map: &HashMap<NodeId, NodeId>,
2702 bwd: &mut Graph,
2703) -> Vec<(usize, NodeId)> {
2704 let Op::QMatMul {
2705 x_zp,
2706 w_zp,
2707 out_zp: _,
2708 mult,
2709 } = &node.op
2710 else {
2711 unreachable!()
2712 };
2713 {
2714 let x_bwd = fwd_map[&node.inputs[0]];
2715 let w_bwd = fwd_map[&node.inputs[1]];
2716 let bias_bwd = fwd_map[&node.inputs[2]];
2717 let x_shape = bwd.node(x_bwd).shape.clone();
2718 let w_shape = bwd.node(w_bwd).shape.clone();
2719 let bias_shape = bwd.node(bias_bwd).shape.clone();
2720 let dtype = upstream_shape.dtype();
2721
2722 let x_f32 = bwd.add_node(
2724 Op::Cast { to: dtype },
2725 vec![x_bwd],
2726 Shape::from_dims(x_shape.dims(), dtype),
2727 );
2728 let w_f32 = bwd.add_node(
2729 Op::Cast { to: dtype },
2730 vec![w_bwd],
2731 Shape::from_dims(w_shape.dims(), dtype),
2732 );
2733 let xzp_c = scalar_const(*x_zp as f32, bwd);
2734 let xzp_b = unbroadcast_inverse(xzp_c, &Shape::from_dims(x_shape.dims(), dtype), bwd);
2735 let _ = bwd.binary(
2736 BinaryOp::Sub,
2737 x_f32,
2738 xzp_b,
2739 Shape::from_dims(x_shape.dims(), dtype),
2740 );
2741 let wzp_c = scalar_const(*w_zp as f32, bwd);
2742 let wzp_b = unbroadcast_inverse(wzp_c, &Shape::from_dims(w_shape.dims(), dtype), bwd);
2743 let w_centered = bwd.binary(
2744 BinaryOp::Sub,
2745 w_f32,
2746 wzp_b,
2747 Shape::from_dims(w_shape.dims(), dtype),
2748 );
2749
2750 let mult_c = scalar_const(*mult, bwd);
2752 let mult_b = unbroadcast_inverse(mult_c, &upstream_shape, bwd);
2753 let upstream_scaled = bwd.binary(BinaryOp::Mul, upstream, mult_b, upstream_shape.clone());
2754
2755 let w_rank = w_shape.rank();
2758 let mut perm: Vec<usize> = (0..w_rank).collect();
2759 perm.swap(w_rank - 2, w_rank - 1);
2760 let mut wt_dims: Vec<Dim> = w_shape.dims().to_vec();
2761 wt_dims.swap(w_rank - 2, w_rank - 1);
2762 let w_t = bwd.add_node(
2763 Op::Transpose { perm },
2764 vec![w_centered],
2765 Shape::from_dims(&wt_dims, dtype),
2766 );
2767 let dx_f32 = bwd.matmul(
2768 upstream_scaled,
2769 w_t,
2770 Shape::from_dims(x_shape.dims(), dtype),
2771 );
2772 let dx = bwd.add_node(
2773 Op::Cast {
2774 to: x_shape.dtype(),
2775 },
2776 vec![dx_f32],
2777 x_shape.clone(),
2778 );
2779
2780 let x_rank = x_shape.rank();
2782 let mut x_perm: Vec<usize> = (0..x_rank).collect();
2783 x_perm.swap(x_rank - 2, x_rank - 1);
2784 let mut xt_dims: Vec<Dim> = x_shape.dims().to_vec();
2785 xt_dims.swap(x_rank - 2, x_rank - 1);
2786 let x_f32_2 = bwd.add_node(
2788 Op::Cast { to: dtype },
2789 vec![x_bwd],
2790 Shape::from_dims(x_shape.dims(), dtype),
2791 );
2792 let x_centered = bwd.binary(
2793 BinaryOp::Sub,
2794 x_f32_2,
2795 xzp_b,
2796 Shape::from_dims(x_shape.dims(), dtype),
2797 );
2798 let x_t = bwd.add_node(
2799 Op::Transpose { perm: x_perm },
2800 vec![x_centered],
2801 Shape::from_dims(&xt_dims, dtype),
2802 );
2803 let dw_f32 = bwd.matmul(
2804 x_t,
2805 upstream_scaled,
2806 Shape::from_dims(w_shape.dims(), dtype),
2807 );
2808 let dw = bwd.add_node(
2809 Op::Cast {
2810 to: w_shape.dtype(),
2811 },
2812 vec![dw_f32],
2813 w_shape,
2814 );
2815
2816 let bias_rank = bias_shape.rank();
2819 let reduce_axes: Vec<usize> = (0..upstream_shape.rank())
2820 .filter(|&i| i + bias_rank < upstream_shape.rank() || i == 0)
2821 .collect();
2822 let dbias_f32 = bwd.add_node(
2823 Op::Reduce {
2824 op: ReduceOp::Sum,
2825 axes: reduce_axes,
2826 keep_dim: false,
2827 },
2828 vec![upstream_scaled],
2829 Shape::from_dims(bias_shape.dims(), dtype),
2830 );
2831 let dbias = bwd.add_node(
2832 Op::Cast {
2833 to: bias_shape.dtype(),
2834 },
2835 vec![dbias_f32],
2836 bias_shape,
2837 );
2838
2839 vec![(0, dx), (1, dw), (2, dbias)]
2840 }
2841}
2842
2843#[allow(unused_variables)]
2844fn vjp_q_conv2d(
2845 node: &Node,
2846 upstream: NodeId,
2847 upstream_shape: Shape,
2848 fwd_map: &HashMap<NodeId, NodeId>,
2849 bwd: &mut Graph,
2850) -> Vec<(usize, NodeId)> {
2851 let Op::QConv2d {
2852 kernel_size,
2853 stride,
2854 padding,
2855 dilation,
2856 groups,
2857 x_zp,
2858 w_zp,
2859 out_zp: _,
2860 mult,
2861 } = &node.op
2862 else {
2863 unreachable!()
2864 };
2865 {
2866 let x_bwd = fwd_map[&node.inputs[0]];
2870 let w_bwd = fwd_map[&node.inputs[1]];
2871 let bias_bwd = fwd_map[&node.inputs[2]];
2872 let x_shape = bwd.node(x_bwd).shape.clone();
2873 let w_shape = bwd.node(w_bwd).shape.clone();
2874 let bias_shape = bwd.node(bias_bwd).shape.clone();
2875 let dtype = upstream_shape.dtype();
2876
2877 let x_f32 = bwd.add_node(
2879 Op::Cast { to: dtype },
2880 vec![x_bwd],
2881 Shape::from_dims(x_shape.dims(), dtype),
2882 );
2883 let w_f32 = bwd.add_node(
2884 Op::Cast { to: dtype },
2885 vec![w_bwd],
2886 Shape::from_dims(w_shape.dims(), dtype),
2887 );
2888 let xzp_c = scalar_const(*x_zp as f32, bwd);
2889 let xzp_b = unbroadcast_inverse(xzp_c, &Shape::from_dims(x_shape.dims(), dtype), bwd);
2890 let x_centered = bwd.binary(
2891 BinaryOp::Sub,
2892 x_f32,
2893 xzp_b,
2894 Shape::from_dims(x_shape.dims(), dtype),
2895 );
2896 let wzp_c = scalar_const(*w_zp as f32, bwd);
2897 let wzp_b = unbroadcast_inverse(wzp_c, &Shape::from_dims(w_shape.dims(), dtype), bwd);
2898 let w_centered = bwd.binary(
2899 BinaryOp::Sub,
2900 w_f32,
2901 wzp_b,
2902 Shape::from_dims(w_shape.dims(), dtype),
2903 );
2904
2905 let mult_c = scalar_const(*mult, bwd);
2907 let mult_b = unbroadcast_inverse(mult_c, &upstream_shape, bwd);
2908 let upstream_scaled = bwd.binary(BinaryOp::Mul, upstream, mult_b, upstream_shape.clone());
2909
2910 let dx_f32 = bwd.conv2d_backward_input(
2912 upstream_scaled,
2913 w_centered,
2914 Shape::from_dims(x_shape.dims(), dtype),
2915 kernel_size.clone(),
2916 stride.clone(),
2917 padding.clone(),
2918 dilation.clone(),
2919 *groups,
2920 );
2921 let dx = bwd.add_node(
2922 Op::Cast {
2923 to: x_shape.dtype(),
2924 },
2925 vec![dx_f32],
2926 x_shape,
2927 );
2928 let dw_f32 = bwd.conv2d_backward_weight(
2929 x_centered,
2930 upstream_scaled,
2931 Shape::from_dims(w_shape.dims(), dtype),
2932 kernel_size.clone(),
2933 stride.clone(),
2934 padding.clone(),
2935 dilation.clone(),
2936 *groups,
2937 );
2938 let dw = bwd.add_node(
2939 Op::Cast {
2940 to: w_shape.dtype(),
2941 },
2942 vec![dw_f32],
2943 w_shape,
2944 );
2945
2946 let dbias_f32 = bwd.add_node(
2948 Op::Reduce {
2949 op: ReduceOp::Sum,
2950 axes: vec![0, 2, 3],
2951 keep_dim: false,
2952 },
2953 vec![upstream_scaled],
2954 Shape::from_dims(bias_shape.dims(), dtype),
2955 );
2956 let dbias = bwd.add_node(
2957 Op::Cast {
2958 to: bias_shape.dtype(),
2959 },
2960 vec![dbias_f32],
2961 bias_shape,
2962 );
2963
2964 vec![(0, dx), (1, dw), (2, dbias)]
2965 }
2966}
2967
2968#[allow(unused_variables)]
2969fn vjp_gaussian_splat_render(
2970 node: &Node,
2971 upstream: NodeId,
2972 upstream_shape: Shape,
2973 fwd_map: &HashMap<NodeId, NodeId>,
2974 bwd: &mut Graph,
2975) -> Vec<(usize, NodeId)> {
2976 let Op::GaussianSplatRender {
2977 width,
2978 height,
2979 tile_size,
2980 radius_scale,
2981 alpha_cutoff,
2982 max_splat_steps,
2983 transmittance_threshold,
2984 max_list_entries,
2985 ..
2986 } = &node.op
2987 else {
2988 unreachable!()
2989 };
2990 {
2991 use rlx_ir::ops::splat::{
2992 GaussianSplatBackwardParams, GaussianSplatInputs, GaussianSplatRenderParams,
2993 unpack_gaussian_splat_packed_grads,
2994 };
2995 let render = GaussianSplatRenderParams {
2996 width: *width,
2997 height: *height,
2998 tile_size: *tile_size,
2999 radius_scale: *radius_scale,
3000 alpha_cutoff: *alpha_cutoff,
3001 max_splat_steps: *max_splat_steps,
3002 transmittance_threshold: *transmittance_threshold,
3003 max_list_entries: *max_list_entries,
3004 };
3005 let inputs = GaussianSplatInputs {
3006 positions: fwd_map[&node.inputs[0]],
3007 scales: fwd_map[&node.inputs[1]],
3008 rotations: fwd_map[&node.inputs[2]],
3009 opacities: fwd_map[&node.inputs[3]],
3010 colors: fwd_map[&node.inputs[4]],
3011 sh_coeffs: fwd_map[&node.inputs[5]],
3012 meta: fwd_map[&node.inputs[6]],
3013 };
3014 let count = bwd.shape(inputs.positions).num_elements().unwrap_or(0) / 3;
3015 let sh_len = bwd.shape(inputs.sh_coeffs).num_elements().unwrap_or(0);
3016 let meta_shape = bwd.shape(inputs.meta).clone();
3017 let packed = bwd.gaussian_splat_render_backward(
3018 inputs,
3019 upstream,
3020 GaussianSplatBackwardParams {
3021 render,
3022 loss_grad_clip: 1.0,
3023 sh_band: 0,
3024 max_anisotropy: 10.0,
3025 },
3026 );
3027 let sh_coeff_count = if count == 0 {
3028 1
3029 } else {
3030 (sh_len / (count * 3)).max(1)
3031 };
3032 let grads = unpack_gaussian_splat_packed_grads(bwd, packed, count, sh_coeff_count);
3033 let meta_n = meta_shape.num_elements().unwrap_or(0);
3034 let zero_meta = bwd.add_node(
3035 Op::Constant {
3036 data: vec![0u8; meta_n * meta_shape.dtype().size_bytes()],
3037 },
3038 vec![],
3039 meta_shape,
3040 );
3041 vec![
3042 (0, grads.positions),
3043 (1, grads.scales),
3044 (2, grads.rotations),
3045 (3, grads.opacities),
3046 (4, grads.colors),
3047 (5, grads.sh_coeffs),
3048 (6, zero_meta),
3049 ]
3050 }
3051}
3052
3053#[allow(unused_variables)]
3054fn vjp_gaussian_splat_render_backward(
3055 node: &Node,
3056 upstream: NodeId,
3057 upstream_shape: Shape,
3058 fwd_map: &HashMap<NodeId, NodeId>,
3059 bwd: &mut Graph,
3060) -> Vec<(usize, NodeId)> {
3061 let Op::GaussianSplatRenderBackward { .. } = &node.op else {
3062 unreachable!()
3063 };
3064 {
3065 vec![]
3067 }
3068}
3069
3070#[allow(unused_variables)]
3092fn vjp_custom_fn(
3093 node: &Node,
3094 upstream: NodeId,
3095 upstream_shape: Shape,
3096 fwd_map: &HashMap<NodeId, NodeId>,
3097 bwd: &mut Graph,
3098) -> Vec<(usize, NodeId)> {
3099 let Op::CustomFn {
3100 vjp_body: Some(vjp_body),
3101 num_inputs,
3102 ..
3103 } = &node.op
3104 else {
3105 unreachable!()
3106 };
3107 {
3108 let mut sub_to_bwd: HashMap<NodeId, NodeId> = HashMap::new();
3110
3111 let mut primal_input_ids: Vec<NodeId> = vjp_body
3115 .nodes()
3116 .iter()
3117 .filter_map(|n| match &n.op {
3118 Op::Input { name } if name != "primal_output" && name != "d_output" => Some(n.id),
3119 _ => None,
3120 })
3121 .collect();
3122 primal_input_ids.sort();
3123 assert_eq!(primal_input_ids.len(), *num_inputs as usize);
3124
3125 for sub_node in vjp_body.nodes() {
3128 let new_id = match &sub_node.op {
3129 Op::Input { name } if name == "primal_output" => fwd_map[&node.id],
3130 Op::Input { name } if name == "d_output" => upstream,
3131 Op::Input { .. } => {
3132 let idx = primal_input_ids
3134 .iter()
3135 .position(|&id| id == sub_node.id)
3136 .expect(
3137 "custom_fn vjp_body: primal Input \
3138 not found in primal list",
3139 );
3140 fwd_map[&node.inputs[idx]]
3141 }
3142 _ => {
3143 let new_inputs: Vec<NodeId> =
3144 sub_node.inputs.iter().map(|i| sub_to_bwd[i]).collect();
3145 bwd.add_node(sub_node.op.clone(), new_inputs, sub_node.shape.clone())
3146 }
3147 };
3148 sub_to_bwd.insert(sub_node.id, new_id);
3149 }
3150
3151 let mut grads: Vec<(usize, NodeId)> = Vec::with_capacity(*num_inputs as usize);
3154 for (i, out_id) in vjp_body.outputs.iter().enumerate() {
3155 grads.push((i, sub_to_bwd[out_id]));
3156 }
3157 grads
3158 }
3159}
3160
3161#[allow(unused_variables)]
3164fn vjp_custom_fn_2(
3165 node: &Node,
3166 upstream: NodeId,
3167 upstream_shape: Shape,
3168 fwd_map: &HashMap<NodeId, NodeId>,
3169 bwd: &mut Graph,
3170) -> Vec<(usize, NodeId)> {
3171 let Op::CustomFn { vjp_body: None, .. } = &node.op else {
3172 unreachable!()
3173 };
3174 {
3175 panic!(
3176 "autodiff: Op::CustomFn has no vjp_body and was not inlined. \
3177 This is an internal error in inline_custom_fn_for_autodiff."
3178 )
3179 }
3180}
3181
3182#[allow(unused_variables)]
3187fn vjp_custom(
3188 node: &Node,
3189 upstream: NodeId,
3190 upstream_shape: Shape,
3191 fwd_map: &HashMap<NodeId, NodeId>,
3192 bwd: &mut Graph,
3193) -> Vec<(usize, NodeId)> {
3194 let Op::Custom { name, .. } = &node.op else {
3195 unreachable!()
3196 };
3197 {
3198 let ext = rlx_ir::lookup_op(name).unwrap_or_else(|| {
3199 panic!(
3200 "autodiff: Op::Custom('{name}') is not registered \
3201 in the op registry — register it via \
3202 rlx_ir::register_op before compiling the graph"
3203 )
3204 });
3205 let mut ctx = rlx_ir::VjpContext {
3206 upstream,
3207 fwd_map,
3208 bwd,
3209 };
3210 ext.vjp(node, &mut ctx)
3211 }
3212}
3213
3214#[allow(unused_variables)]
3215fn vjp_conv2d_backward_input(
3216 node: &Node,
3217 upstream: NodeId,
3218 upstream_shape: Shape,
3219 fwd_map: &HashMap<NodeId, NodeId>,
3220 bwd: &mut Graph,
3221) -> Vec<(usize, NodeId)> {
3222 let Op::Conv2dBackwardInput {
3223 kernel_size,
3224 stride,
3225 padding,
3226 dilation,
3227 groups,
3228 } = &node.op
3229 else {
3230 unreachable!()
3231 };
3232 {
3233 let dy_bwd = fwd_map[&node.inputs[0]];
3234 let w_bwd = fwd_map[&node.inputs[1]];
3235 let dy_shape = bwd.node(dy_bwd).shape.clone();
3236 let _x_shape = node.shape.clone();
3237 let d_dy = bwd.add_node(
3238 Op::Conv {
3239 kernel_size: kernel_size.clone(),
3240 stride: stride.clone(),
3241 padding: padding.clone(),
3242 dilation: dilation.clone(),
3243 groups: *groups,
3244 },
3245 vec![upstream, w_bwd],
3246 dy_shape,
3247 );
3248 vec![(0, d_dy)]
3249 }
3250}
3251
3252#[allow(unused_variables)]
3253fn vjp_conv2d_backward_weight(
3254 node: &Node,
3255 upstream: NodeId,
3256 upstream_shape: Shape,
3257 fwd_map: &HashMap<NodeId, NodeId>,
3258 bwd: &mut Graph,
3259) -> Vec<(usize, NodeId)> {
3260 let Op::Conv2dBackwardWeight {
3261 kernel_size,
3262 stride,
3263 padding,
3264 dilation,
3265 groups,
3266 } = &node.op
3267 else {
3268 unreachable!()
3269 };
3270 {
3271 let x_bwd = fwd_map[&node.inputs[0]];
3272 let dy_bwd = fwd_map[&node.inputs[1]];
3273 let x_shape = bwd.node(x_bwd).shape.clone();
3274 let dy_shape = bwd.node(dy_bwd).shape.clone();
3275 let d_x = bwd.conv2d_backward_input(
3276 dy_bwd,
3277 upstream,
3278 x_shape,
3279 kernel_size.clone(),
3280 stride.clone(),
3281 padding.clone(),
3282 dilation.clone(),
3283 *groups,
3284 );
3285 let d_dy = bwd.add_node(
3286 Op::Conv {
3287 kernel_size: kernel_size.clone(),
3288 stride: stride.clone(),
3289 padding: padding.clone(),
3290 dilation: dilation.clone(),
3291 groups: *groups,
3292 },
3293 vec![x_bwd, upstream],
3294 dy_shape,
3295 );
3296 vec![(0, d_x), (1, d_dy)]
3297 }
3298}
3299
3300#[allow(unused_variables)]
3309fn vjp_fft(
3310 node: &Node,
3311 upstream: NodeId,
3312 upstream_shape: Shape,
3313 fwd_map: &HashMap<NodeId, NodeId>,
3314 bwd: &mut Graph,
3315) -> Vec<(usize, NodeId)> {
3316 let Op::Fft { inverse, norm } = &node.op else {
3317 unreachable!()
3318 };
3319 {
3320 let n = rlx_ir::fft::fft_meta(bwd.shape(node.inputs[0])).n_complex;
3321 let s = norm.output_scale(n, *inverse) as f32;
3322 let z = if s != 1.0 {
3323 let sc = scalar_const(s, bwd);
3324 bwd.mul(upstream, sc)
3325 } else {
3326 upstream
3327 };
3328 let dx = bwd.fft(z, !*inverse);
3329 vec![(0, dx)]
3330 }
3331}
3332
3333#[allow(unused_variables)]
3334fn vjp_log_mel(
3335 node: &Node,
3336 upstream: NodeId,
3337 upstream_shape: Shape,
3338 fwd_map: &HashMap<NodeId, NodeId>,
3339 bwd: &mut Graph,
3340) -> Vec<(usize, NodeId)> {
3341 let Op::LogMel = &node.op else { unreachable!() };
3342 {
3343 let spec_bwd = fwd_map[&node.inputs[0]];
3344 let filt_bwd = fwd_map[&node.inputs[1]];
3345 let dx = bwd.log_mel_backward(spec_bwd, filt_bwd, upstream);
3346 vec![(0, dx)]
3347 }
3348}
3349
3350fn materialize_bcasts_for_ad(g: Graph) -> Graph {
3383 use rlx_ir::op::BinaryOp;
3384
3385 let needs = g.nodes().iter().any(|n| {
3386 matches!(
3387 &n.op, Op::Scan { num_bcast, .. } if *num_bcast > 0
3388 )
3389 });
3390 if !needs {
3391 return g;
3392 }
3393
3394 let mut out = Graph::new(g.name.clone());
3395 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
3396
3397 for node in g.nodes() {
3398 let new_inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
3399 match &node.op {
3400 Op::Scan {
3401 body,
3402 length,
3403 save_trajectory,
3404 num_bcast,
3405 num_xs,
3406 num_checkpoints,
3407 } if *num_bcast > 0 => {
3408 let bcast_base = 1;
3413 let xs_base = 1 + *num_bcast as usize;
3414
3415 let mut new_scan_inputs = vec![new_inputs[0]];
3416
3417 let mut materialised_xs: Vec<NodeId> = Vec::new();
3419 for i in 0..*num_bcast as usize {
3420 let b_id = new_inputs[bcast_base + i];
3421 let b_shape = out.node(b_id).shape.clone();
3422 let dtype = b_shape.dtype();
3423
3424 let mut ones_dims: Vec<rlx_ir::Dim> =
3428 vec![rlx_ir::Dim::Static(*length as usize)];
3429 for _ in 0..b_shape.rank() {
3430 ones_dims.push(rlx_ir::Dim::Static(1));
3431 }
3432 let ones_shape = rlx_ir::Shape::from_dims(&ones_dims, dtype);
3433 let n_elems: usize = ones_dims
3434 .iter()
3435 .map(|d| match d {
3436 rlx_ir::Dim::Static(n) => *n,
3437 rlx_ir::Dim::Dynamic(_) => 1,
3438 })
3439 .product();
3440 let elem_size = dtype.size_bytes();
3441 let mut data = Vec::with_capacity(n_elems * elem_size);
3442 match dtype {
3443 rlx_ir::DType::F64 => {
3444 for _ in 0..n_elems {
3445 data.extend_from_slice(&1.0_f64.to_le_bytes());
3446 }
3447 }
3448 rlx_ir::DType::F32 => {
3449 for _ in 0..n_elems {
3450 data.extend_from_slice(&1.0_f32.to_le_bytes());
3451 }
3452 }
3453 other => {
3454 panic!("materialize_bcasts_for_ad: unsupported bcast dtype {other:?}")
3455 }
3456 }
3457 let ones = out.add_node(Op::Constant { data }, vec![], ones_shape);
3458
3459 let mut xs_dims: Vec<rlx_ir::Dim> = vec![rlx_ir::Dim::Static(*length as usize)];
3461 for i in 0..b_shape.rank() {
3462 xs_dims.push(b_shape.dim(i));
3463 }
3464 let xs_shape = rlx_ir::Shape::from_dims(&xs_dims, dtype);
3465 let xs_id = out.add_node(Op::Binary(BinaryOp::Mul), vec![ones, b_id], xs_shape);
3466 materialised_xs.push(xs_id);
3467 }
3468
3469 new_scan_inputs.extend_from_slice(&materialised_xs);
3470 for i in 0..*num_xs as usize {
3471 new_scan_inputs.push(new_inputs[xs_base + i]);
3472 }
3473
3474 let new_id = out.add_node(
3475 Op::Scan {
3476 body: body.clone(),
3477 length: *length,
3478 save_trajectory: *save_trajectory,
3479 num_bcast: 0,
3480 num_xs: *num_bcast + *num_xs,
3481 num_checkpoints: *num_checkpoints,
3482 },
3483 new_scan_inputs,
3484 node.shape.clone(),
3485 );
3486 id_map.insert(node.id, new_id);
3487 }
3488 _ => {
3489 let new_id = out.add_node(node.op.clone(), new_inputs, node.shape.clone());
3490 id_map.insert(node.id, new_id);
3491 }
3492 }
3493 }
3494
3495 let new_outputs: Vec<NodeId> = g.outputs.iter().map(|o| id_map[o]).collect();
3496 out.set_outputs(new_outputs);
3497 out
3498}
3499
3500pub fn convert_scans_for_ad(g: Graph) -> Graph {
3501 use rlx_ir::shape::Shape as IrShape;
3502
3503 let g = materialize_bcasts_for_ad(g);
3508
3509 let needs = g.nodes().iter().any(|n| {
3512 matches!(
3513 &n.op,
3514 Op::Scan {
3515 save_trajectory: false,
3516 ..
3517 }
3518 )
3519 });
3520 if !needs {
3521 return g;
3522 }
3523
3524 let mut out = Graph::new(g.name.clone());
3525 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
3526
3527 for node in g.nodes() {
3528 let new_inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
3529 match &node.op {
3530 Op::Scan {
3531 body,
3532 length,
3533 save_trajectory: false,
3534 num_xs,
3535 num_checkpoints,
3536 ..
3537 } => {
3538 let carry_shape = node.shape.clone();
3539 let mut traj_dims: Vec<Dim> = Vec::with_capacity(carry_shape.rank() + 1);
3560 traj_dims.push(Dim::Static(*length as usize));
3561 for i in 0..carry_shape.rank() {
3562 traj_dims.push(carry_shape.dim(i));
3563 }
3564 let traj_shape = IrShape::from_dims(&traj_dims, carry_shape.dtype());
3565 let traj = out.add_node(
3566 Op::Scan {
3567 body: body.clone(),
3568 length: *length,
3569 save_trajectory: true,
3570 num_bcast: 0,
3571 num_xs: *num_xs,
3572 num_checkpoints: *num_checkpoints,
3573 },
3574 new_inputs,
3575 traj_shape,
3576 );
3577 let mut narrow_dims: Vec<Dim> = Vec::with_capacity(carry_shape.rank() + 1);
3579 narrow_dims.push(Dim::Static(1));
3580 for i in 0..carry_shape.rank() {
3581 narrow_dims.push(carry_shape.dim(i));
3582 }
3583 let narrow_shape = IrShape::from_dims(&narrow_dims, carry_shape.dtype());
3584 let narrowed = out.add_node(
3585 Op::Narrow {
3586 axis: 0,
3587 start: (*length as usize).saturating_sub(1),
3588 len: 1,
3589 },
3590 vec![traj],
3591 narrow_shape,
3592 );
3593 let new_shape: Vec<i64> = (0..carry_shape.rank())
3595 .map(|i| match carry_shape.dim(i) {
3596 Dim::Static(n) => n as i64,
3597 Dim::Dynamic(_) => -1,
3598 })
3599 .collect();
3600 let final_id = out.add_node(Op::Reshape { new_shape }, vec![narrowed], carry_shape);
3601 id_map.insert(node.id, final_id);
3602 }
3603 _ => {
3604 let new_id = out.add_node(node.op.clone(), new_inputs, node.shape.clone());
3605 id_map.insert(node.id, new_id);
3606 }
3607 }
3608 }
3609
3610 let new_outputs: Vec<NodeId> = g.outputs.iter().map(|o| id_map[o]).collect();
3611 out.set_outputs(new_outputs);
3612 out
3613}
3614
3615pub fn inline_custom_fn_for_autodiff(g: Graph) -> Graph {
3620 use rlx_fusion::control_flow::inline_subgraph_into;
3621
3622 let mut out = Graph::new(g.name.clone());
3623 let mut id_map: HashMap<NodeId, NodeId> = HashMap::new();
3624 let nodes: Vec<rlx_ir::Node> = g.nodes().to_vec();
3625
3626 for node in &nodes {
3627 let new_inputs: Vec<NodeId> = node.inputs.iter().map(|i| id_map[i]).collect();
3628 let new_id = match &node.op {
3629 Op::CustomFn {
3630 vjp_body: None,
3631 jvp_body: None,
3632 fwd_body,
3633 num_inputs,
3634 ..
3635 } => {
3636 assert_eq!(
3637 new_inputs.len(),
3638 *num_inputs as usize,
3639 "custom_fn: outer input count mismatch"
3640 );
3641 inline_subgraph_into(fwd_body, &new_inputs, &mut out)
3642 }
3643 _ => out.add_node(node.op.clone(), new_inputs, node.shape.clone()),
3644 };
3645 id_map.insert(node.id, new_id);
3646 }
3647
3648 let new_outputs: Vec<NodeId> = g.outputs.iter().map(|i| id_map[i]).collect();
3649 out.set_outputs(new_outputs);
3650 out
3651}
3652
3653pub(crate) fn unbroadcast_inverse(x: NodeId, target: &Shape, bwd: &mut Graph) -> NodeId {
3657 let target_dims: Vec<i64> = target
3658 .dims()
3659 .iter()
3660 .map(|d| match d {
3661 Dim::Static(n) => *n as i64,
3662 Dim::Dynamic(_) => -1,
3663 })
3664 .collect();
3665 bwd.add_node(
3666 Op::Expand {
3667 target_shape: target_dims,
3668 },
3669 vec![x],
3670 target.clone(),
3671 )
3672}
3673
3674fn expand_to(
3679 grad: NodeId,
3680 x_shape: &Shape,
3681 axes: &[usize],
3682 keep_dim: bool,
3683 bwd: &mut Graph,
3684) -> NodeId {
3685 let mut current = grad;
3686 if !keep_dim {
3687 let kept_dims: Vec<Dim> = (0..x_shape.rank())
3690 .map(|i| {
3691 if axes.contains(&i) {
3692 Dim::Static(1)
3693 } else {
3694 x_shape.dim(i)
3695 }
3696 })
3697 .collect();
3698 let kept = Shape::from_dims(&kept_dims, x_shape.dtype());
3699 current = reshape_to(current, &kept, bwd);
3700 }
3701 let target_shape: Vec<i64> = x_shape
3702 .dims()
3703 .iter()
3704 .map(|d| match d {
3705 Dim::Static(n) => *n as i64,
3706 Dim::Dynamic(_) => -1,
3707 })
3708 .collect();
3709 bwd.add_node(Op::Expand { target_shape }, vec![current], x_shape.clone())
3710}
3711
3712#[cfg(test)]
3713mod tests {
3714 use super::*;
3715
3716 #[test]
3717 fn grad_of_add_is_identity() {
3718 let mut g = Graph::new("test");
3719 let x = g.input("x", Shape::new(&[4], DType::F32));
3720 let y = g.input("y", Shape::new(&[4], DType::F32));
3721 let z = g.binary(BinaryOp::Add, x, y, Shape::new(&[4], DType::F32));
3722 g.set_outputs(vec![z]);
3723
3724 let bwd = grad(&g, &[x, y]);
3725 assert_eq!(bwd.outputs.len(), 2);
3727 }
3728
3729 #[test]
3730 fn grad_of_mul_uses_other_operand() {
3731 let mut g = Graph::new("test");
3732 let x = g.input("x", Shape::new(&[4], DType::F32));
3733 let y = g.input("y", Shape::new(&[4], DType::F32));
3734 let z = g.binary(BinaryOp::Mul, x, y, Shape::new(&[4], DType::F32));
3735 g.set_outputs(vec![z]);
3736
3737 let bwd = grad(&g, &[x, y]);
3738 assert!(
3740 bwd.nodes()
3741 .iter()
3742 .filter(|n| matches!(n.op, Op::Binary(BinaryOp::Mul)))
3743 .count()
3744 >= 2
3745 );
3746 }
3747
3748 #[test]
3749 fn grad_with_loss_returns_loss_first() {
3750 let mut g = Graph::new("loss");
3751 let x = g.input("x", Shape::new(&[4], DType::F32));
3752 let y = g.input("y", Shape::new(&[4], DType::F32));
3753 let z = g.binary(BinaryOp::Add, x, y, Shape::new(&[4], DType::F32));
3754 g.set_outputs(vec![z]);
3755
3756 let bwd = grad_with_loss(&g, &[x, y]);
3757 assert_eq!(bwd.outputs.len(), 3);
3759 }
3760
3761 #[test]
3762 fn grad_of_dense_solve_emits_implicit_function_rule() {
3763 let mut g = Graph::new("solve_test");
3777 let a = g.param("A", Shape::new(&[2, 2], DType::F32));
3778 let b = g.input("b", Shape::new(&[2], DType::F32));
3779 let x = g.dense_solve(a, b, Shape::new(&[2], DType::F32));
3780 let loss = g.reduce(
3781 x,
3782 ReduceOp::Sum,
3783 vec![0],
3784 false,
3785 Shape::new(&[1], DType::F32),
3786 );
3787 g.set_outputs(vec![loss]);
3788
3789 let bwd = grad_with_loss(&g, &[a, b]);
3790 assert_eq!(bwd.outputs.len(), 3, "expect [loss, dA, db]");
3791
3792 let count =
3793 |pred: fn(&Op) -> bool| -> usize { bwd.nodes().iter().filter(|n| pred(&n.op)).count() };
3794
3795 assert!(
3798 count(|o| matches!(o, Op::DenseSolve)) >= 2,
3799 "expected ≥2 DenseSolve nodes (forward mirror + reverse), got\n{bwd}"
3800 );
3801 assert!(
3802 count(|o| matches!(o, Op::Transpose { .. })) >= 1,
3803 "expected a Transpose for Aᵀ, got\n{bwd}"
3804 );
3805 assert!(
3806 count(|o| matches!(o, Op::MatMul)) >= 1,
3807 "expected a MatMul for the outer product, got\n{bwd}"
3808 );
3809 assert!(
3810 count(|o| matches!(o, Op::Activation(Activation::Neg))) >= 1,
3811 "expected a Neg for −outer, got\n{bwd}"
3812 );
3813 }
3814
3815 #[test]
3816 fn inline_if_replaces_with_where() {
3817 let s = Shape::new(&[4], DType::F32);
3824 let pred_s = Shape::new(&[1], DType::F32);
3825
3826 let mut then_g = Graph::new("then_branch");
3827 let then_in = then_g.input("captured", s.clone());
3828 let then_out = then_g.activation(Activation::Relu, then_in, s.clone());
3829 then_g.set_outputs(vec![then_out]);
3830
3831 let mut else_g = Graph::new("else_branch");
3832 let else_in = else_g.input("captured", s.clone());
3833 let else_out = else_g.activation(Activation::Sigmoid, else_in, s.clone());
3834 else_g.set_outputs(vec![else_out]);
3835
3836 let mut g = Graph::new("parent");
3837 let x = g.input("x", s.clone());
3838 let pred = g.input("pred", pred_s);
3839 let if_out = g.add_node(
3840 Op::If {
3841 then_branch: Box::new(then_g),
3842 else_branch: Box::new(else_g),
3843 },
3844 vec![pred, x],
3845 s,
3846 );
3847 g.set_outputs(vec![if_out]);
3848
3849 let inlined = rlx_fusion::control_flow::inline_if(g);
3850
3851 let has_if = inlined
3855 .nodes()
3856 .iter()
3857 .any(|n| matches!(n.op, Op::If { .. }));
3858 let has_where = inlined.nodes().iter().any(|n| matches!(n.op, Op::Where));
3859 let has_relu = inlined
3860 .nodes()
3861 .iter()
3862 .any(|n| matches!(n.op, Op::Activation(Activation::Relu)));
3863 let has_sigmoid = inlined
3864 .nodes()
3865 .iter()
3866 .any(|n| matches!(n.op, Op::Activation(Activation::Sigmoid)));
3867 assert!(!has_if, "Op::If should be inlined away");
3868 assert!(has_where, "Op::Where should replace the Op::If");
3869 assert!(has_relu, "then_branch's Activation(Relu) should be inlined");
3870 assert!(
3871 has_sigmoid,
3872 "else_branch's Activation(Sigmoid) should be inlined"
3873 );
3874 assert_eq!(inlined.outputs.len(), 1);
3875 }
3876
3877 #[test]
3878 fn grad_through_if_propagates() {
3879 let s = Shape::new(&[4], DType::F32);
3882 let pred_s = Shape::new(&[1], DType::F32);
3883
3884 let mut then_g = Graph::new("th");
3885 let ti = then_g.input("c", s.clone());
3886 let to = then_g.binary(BinaryOp::Mul, ti, ti, s.clone());
3887 then_g.set_outputs(vec![to]);
3888
3889 let mut else_g = Graph::new("el");
3890 let ei = else_g.input("c", s.clone());
3891 let eo = else_g.activation(Activation::Relu, ei, s.clone());
3892 else_g.set_outputs(vec![eo]);
3893
3894 let mut g = Graph::new("parent");
3895 let x = g.input("x", s.clone());
3896 let pred = g.input("pred", pred_s);
3897 let z = g.add_node(
3898 Op::If {
3899 then_branch: Box::new(then_g),
3900 else_branch: Box::new(else_g),
3901 },
3902 vec![pred, x],
3903 s,
3904 );
3905 g.set_outputs(vec![z]);
3906
3907 let bwd = grad_with_loss(&g, &[x]);
3908 assert_eq!(bwd.outputs.len(), 2, "expected loss + 1 grad output");
3910 }
3911
3912 #[test]
3913 fn unroll_while_replicates_body_n_times() {
3914 let s = Shape::new(&[4], DType::F32);
3920 let bool_s = Shape::new(&[1], DType::F32);
3921
3922 let mut cond_g = Graph::new("cond");
3923 let ci = cond_g.input("c", s.clone());
3924 cond_g.set_outputs(vec![ci]);
3927 let _ = bool_s;
3930
3931 let mut body_g = Graph::new("body");
3932 let bi = body_g.input("c", s.clone());
3933 let bo = body_g.activation(Activation::Relu, bi, s.clone());
3934 body_g.set_outputs(vec![bo]);
3935
3936 let mut g = Graph::new("parent");
3937 let x = g.input("x", s.clone());
3938 let w = g.add_node(
3939 Op::While {
3940 cond: Box::new(cond_g),
3941 body: Box::new(body_g),
3942 max_iterations: Some(3),
3943 },
3944 vec![x],
3945 s,
3946 );
3947 g.set_outputs(vec![w]);
3948
3949 let unrolled = rlx_fusion::control_flow::unroll_while(g);
3950
3951 let has_while = unrolled
3952 .nodes()
3953 .iter()
3954 .any(|n| matches!(n.op, Op::While { .. }));
3955 let relu_count = unrolled
3956 .nodes()
3957 .iter()
3958 .filter(|n| matches!(n.op, Op::Activation(Activation::Relu)))
3959 .count();
3960 assert!(!has_while, "Op::While should be unrolled away");
3961 assert_eq!(
3962 relu_count, 3,
3963 "body's Activation(Relu) should appear once per iteration"
3964 );
3965 assert_eq!(unrolled.outputs.len(), 1);
3966 }
3967
3968 #[test]
3969 fn grad_through_while_propagates() {
3970 let s = Shape::new(&[4], DType::F32);
3974
3975 let mut cond_g = Graph::new("cond");
3976 let ci = cond_g.input("c", s.clone());
3977 cond_g.set_outputs(vec![ci]);
3978
3979 let mut body_g = Graph::new("body");
3980 let bi = body_g.input("c", s.clone());
3981 let bo = body_g.binary(BinaryOp::Mul, bi, bi, s.clone());
3982 body_g.set_outputs(vec![bo]);
3983
3984 let mut g = Graph::new("parent");
3985 let x = g.input("x", s.clone());
3986 let w = g.add_node(
3987 Op::While {
3988 cond: Box::new(cond_g),
3989 body: Box::new(body_g),
3990 max_iterations: Some(2),
3991 },
3992 vec![x],
3993 s,
3994 );
3995 g.set_outputs(vec![w]);
3996
3997 let bwd = grad_with_loss(&g, &[x]);
3998 assert_eq!(bwd.outputs.len(), 2, "expected loss + 1 grad output");
3999 }
4000
4001 fn build_ftl_graph(has_bias: bool) -> (Graph, NodeId, Vec<NodeId>) {
4004 let mut g = Graph::new("ftl_test");
4006 let h_shape = Shape::new(&[1, 2, 4], DType::F32);
4007 let h = g.input("h", h_shape.clone());
4008 let qkv_w = g.param("qkv_w", Shape::new(&[4, 12], DType::F32));
4009 let out_w = g.param("out_w", Shape::new(&[4, 4], DType::F32));
4010 let ln1_g = g.param("ln1_g", Shape::new(&[4], DType::F32));
4011 let fc1_w = g.param("fc1_w", Shape::new(&[4, 8], DType::F32));
4012 let fc2_w = g.param("fc2_w", Shape::new(&[8, 4], DType::F32));
4013 let ln2_g = g.param("ln2_g", Shape::new(&[4], DType::F32));
4014 let mask = g.input("mask", Shape::new(&[1, 2, 2, 2], DType::F32));
4015
4016 let (inputs, params) = if has_bias {
4017 let qkv_b = g.param("qkv_b", Shape::new(&[12], DType::F32));
4018 let out_b = g.param("out_b", Shape::new(&[4], DType::F32));
4019 let ln1_b = g.param("ln1_b", Shape::new(&[4], DType::F32));
4020 let fc1_b = g.param("fc1_b", Shape::new(&[8], DType::F32));
4021 let fc2_b = g.param("fc2_b", Shape::new(&[4], DType::F32));
4022 let ln2_b = g.param("ln2_b", Shape::new(&[4], DType::F32));
4023 (
4024 vec![
4025 h, qkv_w, qkv_b, out_w, out_b, ln1_g, ln1_b, fc1_w, fc1_b, fc2_w, fc2_b, ln2_g,
4026 ln2_b, mask,
4027 ],
4028 vec![
4029 qkv_w, qkv_b, out_w, out_b, ln1_g, ln1_b, fc1_w, fc1_b, fc2_w, fc2_b, ln2_g,
4030 ln2_b,
4031 ],
4032 )
4033 } else {
4034 (
4035 vec![h, qkv_w, out_w, ln1_g, fc1_w, fc2_w, ln2_g, mask],
4036 vec![qkv_w, out_w, ln1_g, fc1_w, fc2_w, ln2_g],
4037 )
4038 };
4039 let y = g.add_node(
4040 Op::FusedTransformerLayer {
4041 num_heads: 2,
4042 head_dim: 2,
4043 intermediate_size: 8,
4044 eps1: 1e-5,
4045 eps2: 1e-5,
4046 activation: rlx_ir::op::Activation::Gelu,
4047 has_bias,
4048 },
4049 inputs,
4050 h_shape,
4051 );
4052 g.set_outputs(vec![y]);
4053 (g, h, params)
4054 }
4055
4056 #[test]
4057 fn unfuse_decomposes_fused_transformer_layer() {
4058 let (g, _h, _params) = build_ftl_graph(true);
4062 let unfused = rlx_fusion::unfuse_fused_for_autodiff(g);
4063
4064 let has_ftl = unfused
4065 .nodes()
4066 .iter()
4067 .any(|n| matches!(n.op, Op::FusedTransformerLayer { .. }));
4068 assert!(!has_ftl, "Op::FusedTransformerLayer should be unfused");
4069
4070 let count = |pred: fn(&Op) -> bool| -> usize {
4071 unfused.nodes().iter().filter(|n| pred(&n.op)).count()
4072 };
4073 assert!(
4074 count(|o| matches!(o, Op::MatMul)) >= 4,
4075 "expected >=4 MatMul after FTL unfuse"
4076 );
4077 assert_eq!(
4078 count(|o| matches!(o, Op::Attention { .. })),
4079 1,
4080 "expected exactly 1 Attention after FTL unfuse"
4081 );
4082 assert_eq!(
4083 count(|o| matches!(o, Op::LayerNorm { .. })),
4084 2,
4085 "expected exactly 2 LayerNorm after FTL unfuse"
4086 );
4087 assert!(
4088 count(|o| matches!(o, Op::Narrow { .. })) >= 3,
4089 "expected >=3 Narrow (Q/K/V split) after FTL unfuse"
4090 );
4091 assert_eq!(
4092 count(|o| matches!(o, Op::Activation(_))),
4093 1,
4094 "expected exactly 1 Activation (FFN) after FTL unfuse"
4095 );
4096 }
4097
4098 #[test]
4099 fn grad_through_fused_transformer_layer_propagates() {
4100 let (g, _h, params) = build_ftl_graph(true);
4104 let bwd = grad_with_loss(&g, ¶ms);
4105 assert_eq!(
4106 bwd.outputs.len(),
4107 1 + params.len(),
4108 "expected loss + {} param grads",
4109 params.len()
4110 );
4111 }
4112
4113 #[test]
4114 fn grad_through_fused_transformer_layer_no_bias() {
4115 let (g, _h, params) = build_ftl_graph(false);
4118 let bwd = grad_with_loss(&g, ¶ms);
4119 assert_eq!(
4120 bwd.outputs.len(),
4121 1 + params.len(),
4122 "expected loss + {} param grads (no-bias)",
4123 params.len()
4124 );
4125 }
4126
4127 fn build_ssm_graph() -> (Graph, NodeId, Vec<NodeId>) {
4130 let mut g = Graph::new("ssm_test");
4131 let bsh = Shape::new(&[1, 3, 2], DType::F32);
4132 let hn = Shape::new(&[2, 4], DType::F32);
4133 let bsn = Shape::new(&[1, 3, 4], DType::F32);
4134
4135 let x = g.input("x", bsh.clone());
4136 let delta = g.input("delta", bsh.clone());
4137 let a = g.param("a", hn);
4138 let b = g.input("b", bsn.clone());
4139 let c = g.input("c", bsn);
4140 let y = g.selective_scan(x, delta, a, b, c, 4, bsh);
4141 g.set_outputs(vec![y]);
4142 (g, x, vec![a])
4143 }
4144
4145 #[test]
4146 fn unfuse_decomposes_selective_scan() {
4147 let (g, _x, _params) = build_ssm_graph();
4152 let unfused = rlx_fusion::unfuse_fused_for_autodiff(g);
4153
4154 let has_ssm = unfused
4155 .nodes()
4156 .iter()
4157 .any(|n| matches!(n.op, Op::SelectiveScan { .. }));
4158 assert!(!has_ssm, "Op::SelectiveScan should be unfused");
4159
4160 let count = |pred: fn(&Op) -> bool| -> usize {
4161 unfused.nodes().iter().filter(|n| pred(&n.op)).count()
4162 };
4163 assert_eq!(
4164 count(|o| matches!(o, Op::Concat { .. })),
4165 1,
4166 "expected 1 Concat (over the 3 time steps)"
4167 );
4168 assert_eq!(
4169 count(|o| matches!(
4170 o,
4171 Op::Reduce {
4172 op: ReduceOp::Sum,
4173 ..
4174 }
4175 )),
4176 3,
4177 "expected one Reduce(Sum) per time step (S=3)"
4178 );
4179 assert_eq!(
4180 count(|o| matches!(o, Op::Activation(Activation::Exp))),
4181 3,
4182 "expected one exp(δA) per time step (S=3)"
4183 );
4184 assert!(
4185 count(|o| matches!(o, Op::Narrow { .. })) >= 12,
4186 "expected >=12 Narrows (4 per step × 3 steps)"
4187 );
4188 }
4189
4190 #[test]
4191 fn grad_through_selective_scan_propagates() {
4192 let (g, _x, params) = build_ssm_graph();
4198 let bwd = grad_with_loss(&g, ¶ms);
4199 assert_eq!(
4200 bwd.outputs.len(),
4201 1 + params.len(),
4202 "expected loss + {} param grads",
4203 params.len()
4204 );
4205 }
4206
4207 fn build_gdn_graph() -> (Graph, NodeId, Vec<NodeId>) {
4209 let (b, s, h, n) = (1usize, 3, 2, 4);
4210 let mut g = Graph::new("gdn_test");
4211 let bshn = Shape::new(&[b, s, h, n], DType::F32);
4212 let bsh = Shape::new(&[b, s, h], DType::F32);
4213 let q = g.input("q", bshn.clone());
4214 let k = g.input("k", bshn.clone());
4215 let v = g.input("v", bshn.clone());
4216 let g_in = g.input("g", bsh.clone());
4217 let beta = g.input("beta", bsh);
4218 let y = g.gated_delta_net(q, k, v, g_in, beta, n, bshn);
4219 g.set_outputs(vec![y]);
4220 (g, q, vec![q, k, v, g_in, beta])
4221 }
4222
4223 #[test]
4224 fn unfuse_decomposes_gated_delta_net() {
4225 let (g, _q, _params) = build_gdn_graph();
4226 let unfused = rlx_fusion::unfuse_fused_for_autodiff(g);
4227
4228 let has_gdn = unfused
4229 .nodes()
4230 .iter()
4231 .any(|n| matches!(n.op, Op::GatedDeltaNet { .. }));
4232 assert!(!has_gdn, "Op::GatedDeltaNet should be unfused");
4233
4234 let count = |pred: fn(&Op) -> bool| -> usize {
4235 unfused.nodes().iter().filter(|n| pred(&n.op)).count()
4236 };
4237 assert_eq!(
4238 count(|o| matches!(o, Op::Concat { .. })),
4239 1,
4240 "expected 1 Concat over S=3 steps"
4241 );
4242 assert!(
4243 count(|o| matches!(o, Op::MatMul)) >= 3,
4244 "expected >=3 MatMul per step (sk + out) × S=3"
4245 );
4246 assert_eq!(
4247 count(|o| matches!(o, Op::Activation(Activation::Exp))),
4248 3,
4249 "expected one exp(g) per time step"
4250 );
4251 }
4252
4253 #[test]
4254 fn grad_through_gated_delta_net_propagates() {
4255 let (g, _q, params) = build_gdn_graph();
4256 let bwd = grad_with_loss(&g, ¶ms);
4257 assert_eq!(
4258 bwd.outputs.len(),
4259 1 + params.len(),
4260 "expected loss + {} input grads",
4261 params.len()
4262 );
4263 }
4264
4265 #[test]
4266 fn custom_fn_vjp_body_is_inlined_into_bwd() {
4267 let n = 4usize;
4275 let shape = Shape::new(&[n], DType::F32);
4276
4277 let mut fwd_body = Graph::new("square_fwd");
4279 let xb = fwd_body.input("x", shape.clone());
4280 let yb = fwd_body.binary(BinaryOp::Mul, xb, xb, shape.clone());
4281 fwd_body.set_outputs(vec![yb]);
4282
4283 let mut vjp_body = Graph::new("square_vjp");
4285 let _vx = vjp_body.input("x", shape.clone());
4286 let _vp = vjp_body.input("primal_output", shape.clone());
4287 let vd = vjp_body.input("d_output", shape.clone());
4288 let dx = vjp_body.activation(Activation::Sin, vd, shape.clone());
4289 vjp_body.set_outputs(vec![dx]);
4290
4291 let mut g = Graph::new("custom_fn_test");
4292 let x = g.input("x", shape.clone());
4293 let y = g.custom_fn(vec![x], fwd_body, Some(vjp_body), None);
4294 let loss = g.reduce(
4295 y,
4296 ReduceOp::Sum,
4297 vec![0],
4298 false,
4299 Shape::new(&[1], DType::F32),
4300 );
4301 g.set_outputs(vec![loss]);
4302
4303 let bwd = grad_with_loss(&g, &[x]);
4304 assert_eq!(bwd.outputs.len(), 2, "expect [loss, dx]");
4305 let sin_count = bwd
4306 .nodes()
4307 .iter()
4308 .filter(|n| matches!(n.op, Op::Activation(Activation::Sin)))
4309 .count();
4310 assert!(
4311 sin_count >= 1,
4312 "expected the vjp_body's Sin to be inlined into bwd, got\n{bwd}"
4313 );
4314 }
4315
4316 #[test]
4317 fn custom_fn_without_vjp_inlines_fwd_body_for_autodiff() {
4318 let n = 4usize;
4322 let shape = Shape::new(&[n], DType::F32);
4323
4324 let mut fwd_body = Graph::new("square_fwd");
4325 let xb = fwd_body.input("x", shape.clone());
4326 let yb = fwd_body.binary(BinaryOp::Mul, xb, xb, shape.clone());
4327 fwd_body.set_outputs(vec![yb]);
4328
4329 let mut g = Graph::new("custom_fn_no_vjp");
4330 let x = g.input("x", shape.clone());
4331 let y = g.custom_fn(vec![x], fwd_body, None, None);
4332 let loss = g.reduce(
4333 y,
4334 ReduceOp::Sum,
4335 vec![0],
4336 false,
4337 Shape::new(&[1], DType::F32),
4338 );
4339 g.set_outputs(vec![loss]);
4340
4341 let bwd = grad_with_loss(&g, &[x]);
4342 assert_eq!(bwd.outputs.len(), 2, "expect [loss, dx]");
4343 let custom_fn_count = bwd
4344 .nodes()
4345 .iter()
4346 .filter(|n| matches!(n.op, Op::CustomFn { .. }))
4347 .count();
4348 assert_eq!(
4349 custom_fn_count, 0,
4350 "CustomFn should be inlined away before autodiff"
4351 );
4352 let mul_count = bwd
4353 .nodes()
4354 .iter()
4355 .filter(|n| matches!(n.op, Op::Binary(BinaryOp::Mul)))
4356 .count();
4357 assert!(mul_count >= 2, "expected Mul-based VJP for x², got\n{bwd}");
4358 }
4359
4360 #[test]
4361 fn convert_scans_for_ad_forces_save_trajectory_true() {
4362 let n = 2usize;
4369 let length = 3u32;
4370 let carry = Shape::new(&[n], DType::F32);
4371 let xs_shape = Shape::new(&[length as usize, n], DType::F32);
4372
4373 let mut body = Graph::new("scan_body");
4375 let bc = body.input("carry", carry.clone());
4376 let bx = body.input("x_t", carry.clone());
4377 let by = body.binary(BinaryOp::Add, bc, bx, carry.clone());
4378 body.set_outputs(vec![by]);
4379
4380 let mut g = Graph::new("scan_save_false");
4381 let init = g.input("init", carry.clone());
4382 let xs = g.input("xs", xs_shape);
4383 let scan_out = g.add_node(
4384 Op::Scan {
4385 body: Box::new(body),
4386 length,
4387 save_trajectory: false,
4388 num_bcast: 0,
4389 num_xs: 1,
4390 num_checkpoints: 0,
4391 },
4392 vec![init, xs],
4393 carry.clone(),
4394 );
4395 let loss = g.reduce(
4396 scan_out,
4397 ReduceOp::Sum,
4398 vec![0],
4399 false,
4400 Shape::new(&[1], DType::F32),
4401 );
4402 g.set_outputs(vec![loss]);
4403
4404 let bwd = grad_with_loss(&g, &[init, xs]);
4405 let saved_traj = bwd.nodes().iter().any(|n| {
4406 matches!(
4407 &n.op,
4408 Op::Scan {
4409 save_trajectory: true,
4410 ..
4411 }
4412 )
4413 });
4414 assert!(
4415 saved_traj,
4416 "convert_scans_for_ad should rewrite save_trajectory=false → \
4417 save_trajectory=true in the AD-prepared graph; got\n{bwd}"
4418 );
4419 }
4420}