1use crate::shape::Dim;
9use crate::{DType, Graph, NodeId, Op, Shape};
10
11#[derive(Debug, Clone)]
17pub struct MultiOutputHandle {
18 pub source: NodeId,
21 pub sub_shapes: Vec<Shape>,
23 pub offsets: Vec<usize>,
26}
27
28impl MultiOutputHandle {
29 pub fn n_outputs(&self) -> usize {
31 self.sub_shapes.len()
32 }
33
34 pub fn output(&self, g: &mut Graph, idx: usize) -> NodeId {
39 assert!(idx < self.sub_shapes.len(), "output index out of range");
40 let sub = &self.sub_shapes[idx];
41 let n_elems: usize = sub
42 .dims()
43 .iter()
44 .map(|d| match d {
45 Dim::Static(k) => *k,
46 Dim::Dynamic(_) => panic!("dynamic sub-output dim"),
47 })
48 .product();
49 let flat_shape = Shape::from_dims(&[Dim::Static(n_elems)], sub.dtype());
50 let narrowed = g.add_node(
51 Op::Narrow {
52 axis: 0,
53 start: self.offsets[idx],
54 len: n_elems,
55 },
56 vec![self.source],
57 flat_shape,
58 );
59 if sub.rank() == 1 {
60 narrowed
62 } else {
63 let dims: Vec<i64> = sub
64 .dims()
65 .iter()
66 .map(|d| match d {
67 Dim::Static(k) => *k as i64,
68 Dim::Dynamic(_) => unreachable!(),
69 })
70 .collect();
71 g.add_node(Op::Reshape { new_shape: dims }, vec![narrowed], sub.clone())
72 }
73 }
74}
75
76impl Graph {
77 pub fn selective_scan(
81 &mut self,
82 x: NodeId,
83 delta: NodeId,
84 a: NodeId,
85 b: NodeId,
86 c: NodeId,
87 state_size: usize,
88 shape: Shape,
89 ) -> NodeId {
90 self.push(
91 Op::SelectiveScan { state_size },
92 vec![x, delta, a, b, c],
93 shape,
94 None,
95 )
96 }
97
98 pub fn gated_delta_net(
105 &mut self,
106 q: NodeId,
107 k: NodeId,
108 v: NodeId,
109 g: NodeId,
110 beta: NodeId,
111 state_size: usize,
112 shape: Shape,
113 ) -> NodeId {
114 self.push(
115 Op::GatedDeltaNet {
116 state_size,
117 carry_state: false,
118 gate_per_channel: false,
119 },
120 vec![q, k, v, g, beta],
121 shape,
122 None,
123 )
124 }
125
126 pub fn gated_delta_net_pc(
129 &mut self,
130 q: NodeId,
131 k: NodeId,
132 v: NodeId,
133 g: NodeId,
134 beta: NodeId,
135 state_size: usize,
136 shape: Shape,
137 ) -> NodeId {
138 self.push(
139 Op::GatedDeltaNet {
140 state_size,
141 carry_state: false,
142 gate_per_channel: true,
143 },
144 vec![q, k, v, g, beta],
145 shape,
146 None,
147 )
148 }
149
150 pub fn gated_delta_net_carry(
153 &mut self,
154 q: NodeId,
155 k: NodeId,
156 v: NodeId,
157 g: NodeId,
158 beta: NodeId,
159 state: NodeId,
160 state_size: usize,
161 shape: Shape,
162 ) -> NodeId {
163 self.push(
164 Op::GatedDeltaNet {
165 state_size,
166 carry_state: true,
167 gate_per_channel: false,
168 },
169 vec![q, k, v, g, beta, state],
170 shape,
171 None,
172 )
173 }
174
175 #[allow(clippy::too_many_arguments)]
177 pub fn gated_delta_net_carry_pc(
178 &mut self,
179 q: NodeId,
180 k: NodeId,
181 v: NodeId,
182 g: NodeId,
183 beta: NodeId,
184 state: NodeId,
185 state_size: usize,
186 shape: Shape,
187 ) -> NodeId {
188 self.push(
189 Op::GatedDeltaNet {
190 state_size,
191 carry_state: true,
192 gate_per_channel: true,
193 },
194 vec![q, k, v, g, beta, state],
195 shape,
196 None,
197 )
198 }
199
200 #[allow(clippy::too_many_arguments)]
205 pub fn lstm(
206 &mut self,
207 x: NodeId,
208 w_ih: NodeId,
209 w_hh: NodeId,
210 bias: NodeId,
211 hidden_size: usize,
212 num_layers: usize,
213 bidirectional: bool,
214 shape: Shape,
215 ) -> NodeId {
216 self.push(
217 Op::Lstm {
218 hidden_size,
219 num_layers,
220 bidirectional,
221 carry: false,
222 },
223 vec![x, w_ih, w_hh, bias],
224 shape,
225 None,
226 )
227 }
228
229 #[allow(clippy::too_many_arguments)]
232 pub fn lstm_carry(
233 &mut self,
234 x: NodeId,
235 w_ih: NodeId,
236 w_hh: NodeId,
237 bias: NodeId,
238 h0: NodeId,
239 c0: NodeId,
240 hidden_size: usize,
241 num_layers: usize,
242 bidirectional: bool,
243 shape: Shape,
244 ) -> NodeId {
245 self.push(
246 Op::Lstm {
247 hidden_size,
248 num_layers,
249 bidirectional,
250 carry: true,
251 },
252 vec![x, w_ih, w_hh, bias, h0, c0],
253 shape,
254 None,
255 )
256 }
257
258 #[allow(clippy::too_many_arguments)]
262 pub fn gru(
263 &mut self,
264 x: NodeId,
265 w_ih: NodeId,
266 w_hh: NodeId,
267 b_ih: NodeId,
268 b_hh: NodeId,
269 h0: Option<NodeId>,
270 hidden_size: usize,
271 num_layers: usize,
272 bidirectional: bool,
273 shape: Shape,
274 ) -> NodeId {
275 let mut inputs = vec![x, w_ih, w_hh, b_ih, b_hh];
276 let carry = h0.is_some();
277 if let Some(h) = h0 {
278 inputs.push(h);
279 }
280 self.push(
281 Op::Gru {
282 hidden_size,
283 num_layers,
284 bidirectional,
285 carry,
286 },
287 inputs,
288 shape,
289 None,
290 )
291 }
292
293 #[allow(clippy::too_many_arguments)]
297 pub fn rnn(
298 &mut self,
299 x: NodeId,
300 w_ih: NodeId,
301 w_hh: NodeId,
302 bias: NodeId,
303 h0: Option<NodeId>,
304 hidden_size: usize,
305 num_layers: usize,
306 bidirectional: bool,
307 relu: bool,
308 shape: Shape,
309 ) -> NodeId {
310 let mut inputs = vec![x, w_ih, w_hh, bias];
311 let carry = h0.is_some();
312 if let Some(h) = h0 {
313 inputs.push(h);
314 }
315 self.push(
316 Op::Rnn {
317 hidden_size,
318 num_layers,
319 bidirectional,
320 carry,
321 relu,
322 },
323 inputs,
324 shape,
325 None,
326 )
327 }
328
329 pub fn mamba2(
332 &mut self,
333 x: NodeId,
334 dt: NodeId,
335 a: NodeId,
336 b: NodeId,
337 c: NodeId,
338 head_dim: usize,
339 state_size: usize,
340 shape: Shape,
341 ) -> NodeId {
342 self.push(
343 Op::Mamba2 {
344 head_dim,
345 state_size,
346 },
347 vec![x, dt, a, b, c],
348 shape,
349 None,
350 )
351 }
352
353 pub fn scan(&mut self, init: NodeId, body: Graph, length: u32) -> NodeId {
357 let init_shape = self.shape(init).clone();
358 self.push(
359 Op::Scan {
360 body: Box::new(body),
361 length,
362 save_trajectory: false,
363 num_bcast: 0,
364 num_xs: 0,
365 num_checkpoints: 0,
366 },
367 vec![init],
368 init_shape,
369 None,
370 )
371 }
372
373 pub fn scan_checkpointed(
386 &mut self,
387 init: NodeId,
388 body: Graph,
389 length: u32,
390 num_checkpoints: u32,
391 ) -> NodeId {
392 assert!(
393 num_checkpoints > 0 && num_checkpoints <= length,
394 "scan_checkpointed: num_checkpoints={num_checkpoints} \
395 must be in 1..=length={length}"
396 );
397 let init_shape = self.shape(init).clone();
398 self.push(
399 Op::Scan {
400 body: Box::new(body),
401 length,
402 save_trajectory: false,
403 num_bcast: 0,
404 num_xs: 0,
405 num_checkpoints,
406 },
407 vec![init],
408 init_shape,
409 None,
410 )
411 }
412
413 pub fn scan_with_bcasts_and_xs(
421 &mut self,
422 init: NodeId,
423 bcasts: &[NodeId],
424 xs: &[NodeId],
425 body: Graph,
426 length: u32,
427 ) -> NodeId {
428 let init_shape = self.shape(init).clone();
429 let mut inputs = vec![init];
430 inputs.extend_from_slice(bcasts);
431 inputs.extend_from_slice(xs);
432 self.push(
433 Op::Scan {
434 body: Box::new(body),
435 length,
436 save_trajectory: false,
437 num_bcast: bcasts.len() as u32,
438 num_xs: xs.len() as u32,
439 num_checkpoints: 0,
440 },
441 inputs,
442 init_shape,
443 None,
444 )
445 }
446
447 pub fn scan_with_xs(
453 &mut self,
454 init: NodeId,
455 xs: &[NodeId],
456 body: Graph,
457 length: u32,
458 ) -> NodeId {
459 let init_shape = self.shape(init).clone();
460 let mut inputs = vec![init];
461 inputs.extend_from_slice(xs);
462 self.push(
463 Op::Scan {
464 body: Box::new(body),
465 length,
466 save_trajectory: false,
467 num_bcast: 0,
468 num_xs: xs.len() as u32,
469 num_checkpoints: 0,
470 },
471 inputs,
472 init_shape,
473 None,
474 )
475 }
476
477 pub fn scan_trajectory_with_xs(
486 &mut self,
487 init: NodeId,
488 xs: &[NodeId],
489 body: Graph,
490 length: u32,
491 ) -> NodeId {
492 let init_shape = self.shape(init).clone();
493 let mut traj_dims: Vec<crate::Dim> = Vec::with_capacity(init_shape.rank() + 1);
494 traj_dims.push(crate::Dim::Static(length as usize));
495 for i in 0..init_shape.rank() {
496 traj_dims.push(init_shape.dim(i));
497 }
498 let traj_shape = crate::Shape::from_dims(&traj_dims, init_shape.dtype());
499 let mut inputs = vec![init];
500 inputs.extend_from_slice(xs);
501 self.push(
502 Op::Scan {
503 body: Box::new(body),
504 length,
505 save_trajectory: true,
506 num_bcast: 0,
507 num_xs: xs.len() as u32,
508 num_checkpoints: 0,
509 },
510 inputs,
511 traj_shape,
512 None,
513 )
514 }
515
516 pub fn scan_backward(
525 &mut self,
526 init: NodeId,
527 trajectory: NodeId,
528 upstream: NodeId,
529 xs: &[NodeId],
530 body_vjp: Graph,
531 length: u32,
532 save_trajectory: bool,
533 out_shape: Shape,
534 ) -> NodeId {
535 self.scan_backward_with_checkpoints(
536 init,
537 trajectory,
538 upstream,
539 xs,
540 body_vjp,
541 length,
542 save_trajectory,
543 0,
544 None,
545 out_shape,
546 )
547 }
548
549 #[allow(clippy::too_many_arguments)]
555 pub fn scan_backward_with_checkpoints(
556 &mut self,
557 init: NodeId,
558 trajectory: NodeId,
559 upstream: NodeId,
560 xs: &[NodeId],
561 body_vjp: Graph,
562 length: u32,
563 save_trajectory: bool,
564 num_checkpoints: u32,
565 forward_body: Option<Graph>,
566 out_shape: Shape,
567 ) -> NodeId {
568 let mut inputs = vec![init, trajectory, upstream];
569 inputs.extend_from_slice(xs);
570 self.push(
571 Op::ScanBackward {
572 body_vjp: Box::new(body_vjp),
573 length,
574 save_trajectory,
575 num_xs: xs.len() as u32,
576 num_checkpoints,
577 forward_body: forward_body.map(Box::new),
578 },
579 inputs,
580 out_shape,
581 None,
582 )
583 }
584
585 pub fn scan_backward_xs(
590 &mut self,
591 init: NodeId,
592 trajectory: NodeId,
593 upstream: NodeId,
594 xs: &[NodeId],
595 body_vjp: Graph,
596 length: u32,
597 save_trajectory: bool,
598 xs_idx: u32,
599 out_shape: Shape,
600 ) -> NodeId {
601 self.scan_backward_xs_with_checkpoints(
602 init,
603 trajectory,
604 upstream,
605 xs,
606 body_vjp,
607 length,
608 save_trajectory,
609 xs_idx,
610 0,
611 None,
612 out_shape,
613 )
614 }
615
616 #[allow(clippy::too_many_arguments)]
617 pub fn scan_backward_xs_with_checkpoints(
618 &mut self,
619 init: NodeId,
620 trajectory: NodeId,
621 upstream: NodeId,
622 xs: &[NodeId],
623 body_vjp: Graph,
624 length: u32,
625 save_trajectory: bool,
626 xs_idx: u32,
627 num_checkpoints: u32,
628 forward_body: Option<Graph>,
629 out_shape: Shape,
630 ) -> NodeId {
631 let mut inputs = vec![init, trajectory, upstream];
632 inputs.extend_from_slice(xs);
633 self.push(
634 Op::ScanBackwardXs {
635 body_vjp: Box::new(body_vjp),
636 length,
637 save_trajectory,
638 num_xs: xs.len() as u32,
639 xs_idx,
640 num_checkpoints,
641 forward_body: forward_body.map(Box::new),
642 },
643 inputs,
644 out_shape,
645 None,
646 )
647 }
648
649 pub fn custom_fn(
658 &mut self,
659 inputs: Vec<NodeId>,
660 fwd_body: Graph,
661 vjp_body: Option<Graph>,
662 jvp_body: Option<Graph>,
663 ) -> NodeId {
664 let n_in = inputs.len();
665 let fwd_inputs: usize = fwd_body
667 .nodes()
668 .iter()
669 .filter(|n| matches!(n.op, Op::Input { .. }))
670 .count();
671 assert_eq!(
672 fwd_inputs, n_in,
673 "custom_fn: fwd_body has {fwd_inputs} Op::Input(s); outer call \
674 provides {n_in}. Counts must match.",
675 );
676 let fwd_out_id = fwd_body
677 .outputs
678 .first()
679 .copied()
680 .expect("custom_fn: fwd_body must declare exactly one output");
681 let out_shape = fwd_body.node(fwd_out_id).shape.clone();
682
683 if let Some(vjp) = vjp_body.as_ref() {
684 let primal_count = vjp
685 .nodes()
686 .iter()
687 .filter(|n| {
688 matches!(&n.op,
689 Op::Input { name } if name != "primal_output" && name != "d_output")
690 })
691 .count();
692 assert_eq!(
693 primal_count, n_in,
694 "custom_fn: vjp_body has {primal_count} primal Op::Input(s) \
695 (excluding 'primal_output' / 'd_output'); expected {n_in}",
696 );
697 let has_primal_out = vjp
698 .nodes()
699 .iter()
700 .any(|n| matches!(&n.op, Op::Input { name } if name == "primal_output"));
701 let has_d_output = vjp
702 .nodes()
703 .iter()
704 .any(|n| matches!(&n.op, Op::Input { name } if name == "d_output"));
705 assert!(
706 has_primal_out,
707 "custom_fn: vjp_body must declare an Op::Input named 'primal_output'"
708 );
709 assert!(
710 has_d_output,
711 "custom_fn: vjp_body must declare an Op::Input named 'd_output'"
712 );
713 assert_eq!(
714 vjp.outputs.len(),
715 n_in,
716 "custom_fn: vjp_body has {} outputs; expected {n_in} \
717 (one gradient per primal input)",
718 vjp.outputs.len(),
719 );
720 }
721 if let Some(jvp) = jvp_body.as_ref() {
722 let primal_count = jvp
723 .nodes()
724 .iter()
725 .filter(|n| {
726 matches!(&n.op,
727 Op::Input { name }
728 if !name.starts_with("tangent_") && name != "primal_output")
729 })
730 .count();
731 assert_eq!(
732 primal_count, n_in,
733 "custom_fn: jvp_body has {primal_count} primal Op::Input(s) \
734 (excluding 'primal_output' / 'tangent_*'); expected {n_in}",
735 );
736 for i in 0..n_in {
737 let want = format!("tangent_{i}");
738 let has = jvp
739 .nodes()
740 .iter()
741 .any(|n| matches!(&n.op, Op::Input { name } if name == &want));
742 assert!(
743 has,
744 "custom_fn: jvp_body must declare an Op::Input named '{want}'"
745 );
746 }
747 assert_eq!(
748 jvp.outputs.len(),
749 1,
750 "custom_fn: jvp_body has {} outputs; expected 1 (output tangent)",
751 jvp.outputs.len(),
752 );
753 }
754
755 self.push(
756 Op::CustomFn {
757 fwd_body: Box::new(fwd_body),
758 vjp_body: vjp_body.map(Box::new),
759 jvp_body: jvp_body.map(Box::new),
760 num_inputs: n_in as u32,
761 },
762 inputs,
763 out_shape,
764 None,
765 )
766 }
767
768 pub fn custom_fn_multi(
789 &mut self,
790 inputs: Vec<NodeId>,
791 mut fwd_body: Graph,
792 ) -> MultiOutputHandle {
793 use crate::op::BinaryOp;
794 let original_outputs = fwd_body.outputs.clone();
798 assert!(
799 !original_outputs.is_empty(),
800 "custom_fn_multi: fwd_body must have ≥ 1 declared output"
801 );
802 let mut sub_shapes: Vec<Shape> = Vec::with_capacity(original_outputs.len());
803 let mut offsets: Vec<usize> = Vec::with_capacity(original_outputs.len());
804 let mut total_len: usize = 0;
805 for &out_id in &original_outputs {
806 let s = fwd_body.node(out_id).shape.clone();
807 assert_eq!(
808 s.dtype(),
809 DType::F32,
810 "custom_fn_multi MVP: all sub-outputs must be F32, got {:?} \
811 (sub-output #{})",
812 s.dtype(),
813 sub_shapes.len()
814 );
815 let n_elems: usize = s
816 .dims()
817 .iter()
818 .map(|d| match d {
819 Dim::Static(k) => *k,
820 Dim::Dynamic(_) => {
821 panic!("custom_fn_multi MVP: dynamic dims not supported")
822 }
823 })
824 .product();
825 offsets.push(total_len);
826 total_len += n_elems;
827 sub_shapes.push(s);
828 }
829 let mut flats: Vec<NodeId> = Vec::with_capacity(original_outputs.len());
831 for (out_id, sh) in original_outputs.iter().zip(sub_shapes.iter()) {
832 let n: usize = sh
833 .dims()
834 .iter()
835 .map(|d| match d {
836 Dim::Static(k) => *k,
837 Dim::Dynamic(_) => unreachable!(),
838 })
839 .product();
840 let flat_shape = Shape::from_dims(&[Dim::Static(n)], DType::F32);
841 let flat = fwd_body.add_node(
842 Op::Reshape {
843 new_shape: vec![n as i64],
844 },
845 vec![*out_id],
846 flat_shape,
847 );
848 flats.push(flat);
849 }
850 let concat_shape = Shape::from_dims(&[Dim::Static(total_len)], DType::F32);
851 let concat = fwd_body.add_node(Op::Concat { axis: 0 }, flats.clone(), concat_shape);
852 let _ = BinaryOp::Add; fwd_body.set_outputs(vec![concat]);
854
855 let source = self.custom_fn(inputs, fwd_body, None, None);
858
859 MultiOutputHandle {
860 source,
861 sub_shapes,
862 offsets,
863 }
864 }
865
866 pub fn scan_trajectory(&mut self, init: NodeId, body: Graph, length: u32) -> NodeId {
871 let init_shape = self.shape(init).clone();
872 let mut traj_dims: Vec<crate::Dim> = Vec::with_capacity(init_shape.rank() + 1);
873 traj_dims.push(crate::Dim::Static(length as usize));
874 for i in 0..init_shape.rank() {
875 traj_dims.push(init_shape.dim(i));
876 }
877 let traj_shape = crate::Shape::from_dims(&traj_dims, init_shape.dtype());
878 self.push(
879 Op::Scan {
880 body: Box::new(body),
881 length,
882 save_trajectory: true,
883 num_xs: 0,
884 num_bcast: 0,
885 num_checkpoints: 0,
886 },
887 vec![init],
888 traj_shape,
889 None,
890 )
891 }
892}