1#![forbid(unsafe_code)]
46
47use std::cell::Cell;
48use std::collections::{BinaryHeap, HashMap, HashSet};
49use std::sync::{Arc, Mutex, PoisonError};
50
51use nuvec_core::graph::{self, Fused, FusedRef, Primitive};
52use nuvec_core::tree::{Tree, TreeDef};
53use nuvec_core::{
54 Array, Complex64, DType, DeviceKind, Error, IntoResult, Result, Stream, bf16, default_stream,
55 f16, random,
56};
57
58fn dims(a: &Array) -> Vec<usize> {
60 a.shape().iter().map(|&d| d as usize).collect()
61}
62
63pub const VERSION: &str = env!("CARGO_PKG_VERSION");
65
66const MAX_FUSED_STEPS: usize = 32;
68const MAX_FUSED_ARRAYS: usize = 24;
71
72thread_local! {
73 static TRACES: Cell<usize> = const { Cell::new(0) };
75}
76
77#[doc(hidden)]
80pub fn __trace_count() -> usize {
81 TRACES.with(Cell::get)
82}
83
84#[derive(Clone, Debug)]
87#[non_exhaustive]
88pub struct Options {
89 pub cache_capacity: usize,
92 pub shapeless: bool,
95}
96
97impl Default for Options {
98 fn default() -> Self {
99 Self {
100 cache_capacity: 64,
101 shapeless: false,
102 }
103 }
104}
105
106impl Options {
107 pub fn cache_capacity(mut self, n: usize) -> Self {
109 self.cache_capacity = n;
110 self
111 }
112
113 pub fn shapeless(mut self, on: bool) -> Self {
143 self.shapeless = on;
144 self
145 }
146
147 pub fn compile<'f, I, O, F, R>(self, f: F) -> impl Fn(&I) -> Result<O> + 'f
149 where
150 I: Tree,
151 O: Tree,
152 F: Fn(&I) -> R + 'f,
153 R: IntoResult<Ok = O>,
154 {
155 let cache: Mutex<Cache<(Tape, O)>> = Mutex::new(Cache::new(self.cache_capacity));
156 move |inputs: &I| {
157 let leaves = inputs.leaves();
158 if leaves.iter().any(|a| graph::is_tracer(a) || a.is_failed()) {
159 return f(inputs).into_result();
162 }
163 let key = Key::new(vec![inputs.def()], &leaves, self.shapeless.then_some(0));
164 let cached = lock(&cache).get(&key);
165 let entry = match cached {
166 Some(entry) => entry,
167 None => {
168 let mut out = None;
169 let mut body = |xs: &[Array]| -> Result<Vec<Array>> {
170 let o = f(&inputs.unflatten(xs.iter().cloned())?).into_result()?;
171 let leaves = o.leaves();
172 out = Some(o.map(|_, a| light(a)));
173 Ok(leaves)
174 };
175 let tape = Tape::trace(&mut body, &leaves, key.stream, self.shapeless)?;
176 let out =
177 out.ok_or_else(|| Error::InvalidArgument("compile: no output".into()))?;
178 let entry = Arc::new((tape, out));
179 if !entry.0.captures_tracer() {
180 lock(&cache).insert(key, entry.clone());
181 }
182 entry
183 }
184 };
185 let (tape, template) = &*entry;
186 template.unflatten(tape.replay(&leaves)?)
187 }
188 }
189
190 pub fn compile_mut<'f, S, I, O, F, R>(self, f: F) -> impl Fn(&mut S, &I) -> Result<O> + 'f
192 where
193 S: Tree,
194 I: Tree,
195 O: Tree,
196 F: Fn(&mut S, &I) -> R + 'f,
197 R: IntoResult<Ok = O>,
198 {
199 let cache: Mutex<Cache<MutTape<S, O>>> = Mutex::new(Cache::new(self.cache_capacity));
200 let changed_last = Cell::new(false);
201 move |state: &mut S, inputs: &I| {
202 let state_leaves = state.leaves();
203 let input_leaves = inputs.leaves();
204 let direct = state_leaves
205 .iter()
206 .chain(&input_leaves)
207 .any(|a| graph::is_tracer(a) || a.is_failed());
208 if direct {
209 return f(state, inputs).into_result();
211 }
212 let key0 = random::sequence_key();
213 let pre_def = state.def();
214 let mut all = Vec::with_capacity(1 + state_leaves.len() + input_leaves.len());
215 all.push(key0.clone());
216 all.extend(state_leaves.iter().cloned());
217 all.extend(input_leaves.iter().cloned());
218 let shapeless = self.shapeless.then_some(1 + state_leaves.len());
219 let key = Key::new(vec![pre_def.clone(), inputs.def()], &all, shapeless);
220 let cached = lock(&cache).get(&key);
221 let entry = match cached {
222 Some(entry) => entry,
223 None => {
224 let entry = Arc::new(MutTape::trace(
225 &f,
226 state,
227 inputs,
228 &all,
229 key.stream,
230 self.shapeless,
231 )?);
232 if !entry.tape.captures_tracer() {
233 lock(&cache).insert(key, entry.clone());
234 }
235 entry
236 }
237 };
238 let changed = entry.post_def != pre_def;
239 if changed && changed_last.get() {
240 return Err(Error::InvalidArgument(
241 "compile_mut: state structure keeps changing; call opt.init(&model) before \
242 the loop, or make changing host fields arrays (or `static_unchecked`)"
243 .into(),
244 ));
245 }
246 let mut outputs = entry.tape.replay(&all)?;
247 let next_key = outputs.pop().expect("the key is the last output");
248 let state_out = outputs.split_off(entry.outputs);
249 let out = entry.out.unflatten(outputs)?;
250 let next = entry.post.unflatten(state_out)?;
251 changed_last.set(changed);
252 *state = next;
253 random::set_sequence_key(next_key);
254 Ok(out)
255 }
256 }
257}
258
259pub fn compile<'f, I, O, F, R>(f: F) -> impl Fn(&I) -> Result<O> + 'f
282where
283 I: Tree,
284 O: Tree,
285 F: Fn(&I) -> R + 'f,
286 R: IntoResult<Ok = O>,
287{
288 Options::default().compile(f)
289}
290
291pub fn compile_mut<'f, S, I, O, F, R>(f: F) -> impl Fn(&mut S, &I) -> Result<O> + 'f
332where
333 S: Tree,
334 I: Tree,
335 O: Tree,
336 F: Fn(&mut S, &I) -> R + 'f,
337 R: IntoResult<Ok = O>,
338{
339 Options::default().compile_mut(f)
340}
341
342pub fn compile_flat<F>(f: F) -> impl Fn(&[Array]) -> Result<Vec<Array>>
356where
357 F: Fn(&[Array]) -> Result<Vec<Array>>,
358{
359 compile_flat_with(Options::default(), f)
360}
361
362pub fn compile_flat_with<F>(options: Options, f: F) -> impl Fn(&[Array]) -> Result<Vec<Array>>
364where
365 F: Fn(&[Array]) -> Result<Vec<Array>>,
366{
367 let cache: Mutex<Cache<Tape>> = Mutex::new(Cache::new(options.cache_capacity));
368 move |inputs| {
369 if inputs.iter().any(|a| graph::is_tracer(a) || a.is_failed()) {
370 return f(inputs);
371 }
372 let key = Key::new(Vec::new(), inputs, options.shapeless.then_some(0));
373 let cached = lock(&cache).get(&key);
374 let tape = match cached {
375 Some(tape) => tape,
376 None => {
377 let tape = Arc::new(Tape::trace(
378 &mut |xs: &[Array]| f(xs),
379 inputs,
380 key.stream,
381 options.shapeless,
382 )?);
383 if !tape.captures_tracer() {
384 lock(&cache).insert(key, tape.clone());
385 }
386 tape
387 }
388 };
389 tape.replay(inputs)
390 }
391}
392
393fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
394 m.lock().unwrap_or_else(PoisonError::into_inner)
395}
396
397fn light(a: &Array) -> Array {
399 graph::placeholder(a.dtype(), a.shape()).unwrap_or_else(|_| a.clone())
400}
401
402struct MutTape<S, O> {
405 tape: Tape,
406 outputs: usize,
407 out: O,
408 post: S,
409 post_def: TreeDef,
410}
411
412impl<S: Tree, O: Tree> MutTape<S, O> {
413 fn trace<I: Tree, R: IntoResult<Ok = O>>(
416 f: &dyn Fn(&mut S, &I) -> R,
417 state: &S,
418 inputs: &I,
419 all: &[Array],
420 stream: Stream,
421 shapeless: bool,
422 ) -> Result<Self> {
423 let ns = state.len();
424 let key0 = all[0].clone();
425 let mut traced: Option<(O, S)> = None;
426 let mut body = |xs: &[Array]| -> Result<Vec<Array>> {
427 random::set_sequence_key(xs[0].clone());
428 let mut s = state.unflatten(xs[1..1 + ns].iter().cloned())?;
429 let i = inputs.unflatten(xs[1 + ns..].iter().cloned())?;
430 let out = f(&mut s, &i).into_result();
431 let key = random::sequence_key();
432 random::set_sequence_key(key0.clone());
433 let out = out?;
434 let mut leaves = out.leaves();
435 leaves.extend(s.leaves());
436 leaves.push(key);
437 traced = Some((out, s));
438 Ok(leaves)
439 };
440 let result = Tape::trace(&mut body, all, stream, shapeless);
441 random::set_sequence_key(key0.clone());
442 let tape = result?;
443 let (out, post) =
444 traced.ok_or_else(|| Error::InvalidArgument("compile_mut: no output".into()))?;
445 Ok(Self {
446 tape,
447 outputs: out.len(),
448 out: out.map(|_, a| light(a)),
449 post_def: post.def(),
450 post: post.map(|_, a| light(a)),
451 })
452 }
453}
454
455struct Cache<T> {
457 tapes: HashMap<Key, (Arc<T>, u64)>,
458 clock: u64,
460 capacity: usize,
461}
462
463impl<T> Cache<T> {
464 fn new(capacity: usize) -> Self {
465 Self {
466 tapes: HashMap::new(),
467 clock: 0,
468 capacity,
469 }
470 }
471
472 fn get(&mut self, key: &Key) -> Option<Arc<T>> {
473 self.clock += 1;
474 let (tape, used) = self.tapes.get_mut(key)?;
475 *used = self.clock;
476 Some(tape.clone())
477 }
478
479 fn insert(&mut self, key: Key, tape: Arc<T>) {
480 if self.capacity == 0 {
481 return;
482 }
483 self.clock += 1;
484 if self.tapes.len() >= self.capacity
485 && !self.tapes.contains_key(&key)
486 && let Some(oldest) = self
487 .tapes
488 .iter()
489 .min_by_key(|(_, (_, used))| *used)
490 .map(|(k, _)| k.clone())
491 {
492 self.tapes.remove(&oldest);
493 }
494 self.tapes.insert(key, (tape, self.clock));
495 }
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Hash)]
501struct Key {
502 stream: Stream,
503 defs: Vec<TreeDef>,
504 inputs: Vec<(DType, std::result::Result<Vec<usize>, usize>)>,
506}
507
508impl Key {
509 fn new(defs: Vec<TreeDef>, inputs: &[Array], shapeless: Option<usize>) -> Self {
511 let from = shapeless.unwrap_or(inputs.len());
512 Self {
513 stream: default_stream(),
514 defs,
515 inputs: inputs
516 .iter()
517 .enumerate()
518 .map(|(i, a)| {
519 (
520 a.dtype(),
521 if i < from { Ok(dims(a)) } else { Err(a.ndim()) },
522 )
523 })
524 .collect(),
525 }
526 }
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
531enum Ref {
532 Input(usize),
534 Const(usize),
536 Step(usize, usize),
538}
539
540#[derive(Debug, Clone)]
541struct Step {
542 primitive: Primitive,
543 inputs: Vec<Ref>,
544 outputs: Vec<(DType, Vec<usize>)>,
546}
547
548#[derive(Debug)]
550struct Tape {
551 consts: Vec<Array>,
552 steps: Vec<Step>,
553 outputs: Vec<Ref>,
554 shapeless: bool,
556 views: Mutex<HashMap<(usize, Vec<usize>), Array>>,
559}
560
561const MAX_VIEWS: usize = 64;
563
564impl Tape {
565 fn trace(
568 f: &mut dyn FnMut(&[Array]) -> Result<Vec<Array>>,
569 inputs: &[Array],
570 stream: Stream,
571 shapeless: bool,
572 ) -> Result<Self> {
573 TRACES.with(|n| n.set(n.get() + 1));
574 let placeholders = inputs
575 .iter()
576 .map(|a| graph::placeholder(a.dtype(), a.shape()))
577 .collect::<Result<Vec<_>>>()?;
578 let outputs = f(&placeholders)?;
579 let mut tape = Builder::new(&placeholders).build(&outputs);
580 if shapeless {
581 tape.make_shapeless()?;
582 }
583 if stream.device.kind == DeviceKind::Gpu {
584 tape.fuse(inputs);
585 }
586 Ok(tape)
587 }
588
589 fn captures_tracer(&self) -> bool {
592 self.consts.iter().any(graph::is_tracer)
593 }
594
595 fn replay(&self, inputs: &[Array]) -> Result<Vec<Array>> {
596 let mut values: Vec<Vec<Array>> = Vec::with_capacity(self.steps.len());
597 let get = |r: &Ref, values: &[Vec<Array>]| match *r {
598 Ref::Input(i) => inputs[i].clone(),
599 Ref::Const(c) => self.consts[c].clone(),
600 Ref::Step(s, o) => values[s][o].clone(),
601 };
602 for step in &self.steps {
603 let mut args: Vec<Array> = step.inputs.iter().map(|r| get(r, &values)).collect();
604 if self.shapeless && is_element_wise(&step.primitive) {
605 self.broadcast_operands(step, &mut args)?;
606 }
607 values.push(graph::from_primitive(step.primitive.clone(), args)?);
608 }
609 Ok(self.outputs.iter().map(|r| get(r, &values)).collect())
610 }
611
612 fn broadcast_operands(&self, step: &Step, args: &mut [Array]) -> Result<()> {
615 let shapes: Vec<Vec<usize>> = args.iter().map(dims).collect();
616 if shapes.windows(2).all(|w| w[0] == w[1]) {
617 return Ok(());
618 }
619 let mut shape = Vec::new();
620 for s in &shapes {
621 shape = graph::broadcast_shapes(step.primitive.name(), &shape, s)?;
622 }
623 let expand = |a: &Array| {
624 graph::from_primitive(Primitive::Broadcast(shape.clone()), vec![a.clone()])
625 .map(|mut v| v.remove(0))
626 };
627 for ((a, s), r) in args.iter_mut().zip(&shapes).zip(&step.inputs) {
628 if *s == shape {
629 continue;
630 }
631 *a = match *r {
632 Ref::Const(c) => {
633 let mut views = lock(&self.views);
634 if views.len() >= MAX_VIEWS {
635 views.clear();
636 }
637 match views.entry((c, shape.clone())) {
638 std::collections::hash_map::Entry::Occupied(v) => v.get().clone(),
639 std::collections::hash_map::Entry::Vacant(v) => {
640 v.insert(expand(a)?).clone()
641 }
642 }
643 }
644 _ => expand(a)?,
645 };
646 }
647 Ok(())
648 }
649
650 fn make_shapeless(&mut self) -> Result<()> {
658 self.shapeless = true;
659 let mut sources: HashMap<usize, usize> = HashMap::new();
661 for c in 0..self.consts.len() {
662 let a = &self.consts[c];
663 if graph::is_tracer(a) {
664 continue;
665 }
666 if let (Some(Primitive::Broadcast(_)), [x]) =
667 (graph::primitive(a), graph::inputs(a).as_slice())
668 {
669 let x = x.clone();
670 self.consts.push(x);
671 sources.insert(c, self.consts.len() - 1);
672 }
673 }
674 for k in 0..self.steps.len() {
675 if !is_element_wise(&self.steps[k].primitive) {
676 continue;
677 }
678 for i in 0..self.steps[k].inputs.len() {
679 let mut r = self.steps[k].inputs[i];
680 loop {
681 r = match r {
682 Ref::Step(j, 0)
683 if matches!(self.steps[j].primitive, Primitive::Broadcast(_)) =>
684 {
685 self.steps[j].inputs[0]
686 }
687 Ref::Const(c) if sources.contains_key(&c) => Ref::Const(sources[&c]),
688 _ => break,
689 };
690 }
691 self.steps[k].inputs[i] = r;
692 }
693 }
694 self.drop_unused();
695 match self.steps.iter().find(|s| s.primitive.fixes_shape()) {
696 Some(s) => Err(Error::InvalidArgument(format!(
697 "compile (shapeless): `{}` fixes a traced shape; compile this function \
698 without shapeless",
699 s.primitive.name()
700 ))),
701 None => Ok(()),
702 }
703 }
704
705 fn drop_unused(&mut self) {
707 let n = self.steps.len();
708 let step_of = |r: &Ref| match *r {
709 Ref::Step(j, _) => Some(j),
710 _ => None,
711 };
712 let mut live = vec![false; n];
713 let mut stack: Vec<usize> = self.outputs.iter().filter_map(step_of).collect();
714 while let Some(j) = stack.pop() {
715 if !std::mem::replace(&mut live[j], true) {
716 stack.extend(self.steps[j].inputs.iter().filter_map(step_of));
717 }
718 }
719 let mut index = vec![0; n];
720 let remap = |r: Ref, index: &[usize]| match r {
721 Ref::Step(j, o) => Ref::Step(index[j], o),
722 r => r,
723 };
724 let mut steps = Vec::with_capacity(n);
725 for (k, mut step) in std::mem::take(&mut self.steps).into_iter().enumerate() {
726 if live[k] {
727 step.inputs = step.inputs.iter().map(|&r| remap(r, &index)).collect();
728 index[k] = steps.len();
729 steps.push(step);
730 }
731 }
732 self.outputs = self.outputs.iter().map(|&r| remap(r, &index)).collect();
733 self.steps = steps;
734 }
735
736 fn literal(&self, r: Ref) -> Option<(DType, u64)> {
739 let Ref::Const(c) = r else { return None };
740 let a = &self.consts[c];
741 if graph::is_tracer(a) || !matches!(a.dtype(), DType::F32 | DType::F16 | DType::BF16) {
742 return None;
743 }
744 let value = scalar_bits(a).or_else(|| {
745 match (graph::primitive(a), graph::inputs(a).as_slice()) {
746 (Some(Primitive::Broadcast(_)), [x]) if x.dtype() == a.dtype() => scalar_bits(x),
747 _ => None,
748 }
749 })?;
750 Some((a.dtype(), value))
751 }
752
753 fn dtype(&self, r: Ref, inputs: &[Array]) -> DType {
754 match r {
755 Ref::Input(i) => inputs[i].dtype(),
756 Ref::Const(c) => self.consts[c].dtype(),
757 Ref::Step(s, o) => self.steps[s].outputs[o].0,
758 }
759 }
760
761 fn fuse(&mut self, inputs: &[Array]) {
767 let n = self.steps.len();
768 let fusable: Vec<bool> = self
769 .steps
770 .iter()
771 .map(|s| {
772 let dtypes: Vec<DType> = s.inputs.iter().map(|&r| self.dtype(r, inputs)).collect();
773 s.outputs.len() == 1 && Fused::supports(&s.primitive, &dtypes)
774 })
775 .collect();
776 let mut consumers: Vec<Vec<usize>> = vec![Vec::new(); n];
777 for (k, s) in self.steps.iter().enumerate() {
778 for r in &s.inputs {
779 if let Ref::Step(j, _) = *r {
780 consumers[j].push(k);
781 }
782 }
783 }
784 let mut is_output = vec![false; n];
785 for r in &self.outputs {
786 if let Ref::Step(j, _) = *r {
787 is_output[j] = true;
788 }
789 }
790 let step_inputs = |k: usize| {
791 self.steps[k].inputs.iter().filter_map(|r| match *r {
792 Ref::Step(j, _) => Some(j),
793 _ => None,
794 })
795 };
796 let mut group: Vec<Option<usize>> = vec![None; n];
798 let mut groups: HashMap<usize, (Vec<usize>, Vec<Ref>)> = HashMap::new();
800 for root in (0..n).rev() {
801 if group[root].is_some() || !fusable[root] {
802 continue;
803 }
804 let mut members = HashSet::from([root]);
805 let mut external: Vec<Ref> = Vec::new();
806 let mut outputs = 1;
807 let add_inputs = |k: usize, external: &mut Vec<Ref>| {
808 for &r in &self.steps[k].inputs {
809 if !external.contains(&r) {
810 external.push(r);
811 }
812 }
813 };
814 add_inputs(root, &mut external);
815 let mut candidates: BinaryHeap<usize> = step_inputs(root).collect();
817 let mut seen = HashSet::new();
818 while let Some(j) = candidates.pop() {
819 if !seen.insert(j)
822 || group[j].is_some()
823 || !fusable[j]
824 || (self.shapeless && is_output[j])
825 || members.len() >= MAX_FUSED_STEPS
826 || !consumers[j].iter().all(|c| members.contains(c))
827 {
828 continue;
829 }
830 let new = self.steps[j]
831 .inputs
832 .iter()
833 .filter(|r| !external.contains(r))
834 .collect::<HashSet<_>>()
835 .len();
836 let arrays = external.len() - 1 + new + outputs + usize::from(is_output[j]);
837 if arrays > MAX_FUSED_ARRAYS {
838 continue;
839 }
840 members.insert(j);
841 outputs += usize::from(is_output[j]);
842 external.retain(|&r| r != Ref::Step(j, 0));
843 add_inputs(j, &mut external);
844 candidates.extend(step_inputs(j));
845 }
846 if members.len() < 2 || external.len() + outputs > MAX_FUSED_ARRAYS {
847 continue;
848 }
849 let mut members: Vec<usize> = members.into_iter().collect();
850 members.sort_unstable();
851 for &m in &members {
852 group[m] = Some(root);
853 }
854 groups.insert(root, (members, external));
855 }
856 if groups.is_empty() {
857 return;
858 }
859 let mut index: Vec<Option<(usize, usize)>> = vec![None; n];
862 let remap = |r: Ref, index: &[Option<(usize, usize)>]| match r {
863 Ref::Step(j, o) => {
864 let (k, offset) = index[j].expect("defined before use");
865 Ref::Step(k, offset + o)
866 }
867 r => r,
868 };
869 let mut steps = Vec::with_capacity(n);
870 for k in 0..n {
871 let step = match (group[k], groups.get(&k)) {
872 (Some(_), None) => continue, (Some(_), Some((members, external))) => {
874 let position: HashMap<usize, usize> =
875 members.iter().enumerate().map(|(p, &m)| (m, p)).collect();
876 let mut constants: Vec<(DType, u64)> = Vec::new();
879 let mut literal: HashMap<Ref, usize> = HashMap::new();
880 for &r in external {
881 if let Some(value) = self.literal(r) {
882 literal.insert(r, constants.len());
883 constants.push(value);
884 }
885 }
886 let external: Vec<Ref> = external
887 .iter()
888 .copied()
889 .filter(|r| !literal.contains_key(r))
890 .collect();
891 let external = &external;
892 let graph = members
893 .iter()
894 .map(|&m| {
895 let args = self.steps[m]
896 .inputs
897 .iter()
898 .map(|r| match *r {
899 Ref::Step(j, _) if position.contains_key(&j) => {
900 FusedRef::Step(position[&j])
901 }
902 r if literal.contains_key(&r) => {
903 FusedRef::Constant(literal[&r])
904 }
905 r => FusedRef::Input(
906 external.iter().position(|&e| e == r).expect("external"),
907 ),
908 })
909 .collect();
910 (self.steps[m].primitive.clone(), args)
911 })
912 .collect();
913 let outs: Vec<usize> = members
915 .iter()
916 .copied()
917 .filter(|&m| m != k && is_output[m])
918 .chain([k])
919 .collect();
920 for (o, &m) in outs.iter().enumerate() {
921 index[m] = Some((steps.len(), o));
922 }
923 let dtypes: Vec<DType> =
924 external.iter().map(|&r| self.dtype(r, inputs)).collect();
925 let positions: Vec<usize> = outs.iter().map(|m| position[m]).collect();
926 let Ok(f) = Fused::with_constants(&dtypes, &constants, graph, &positions)
927 else {
928 unreachable!("every member is fusable")
929 };
930 steps.push(Step {
931 primitive: Primitive::Compiled(f),
932 inputs: external.iter().map(|&r| remap(r, &index)).collect(),
933 outputs: outs
934 .iter()
935 .map(|&m| self.steps[m].outputs[0].clone())
936 .collect(),
937 });
938 continue;
939 }
940 (None, _) => {
941 let s = &self.steps[k];
942 Step {
943 primitive: s.primitive.clone(),
944 inputs: s.inputs.iter().map(|&r| remap(r, &index)).collect(),
945 outputs: s.outputs.clone(),
946 }
947 }
948 };
949 index[k] = Some((steps.len(), 0));
950 steps.push(step);
951 }
952 self.outputs = self.outputs.iter().map(|&r| remap(r, &index)).collect();
953 self.steps = steps;
954 }
955}
956
957struct Builder {
959 refs: HashMap<(usize, usize), Ref>,
961 consts: Vec<Array>,
962 scalars: HashMap<(DType, Vec<usize>, u64), usize>,
964 steps: Vec<Step>,
965 seen: HashMap<(Primitive, Vec<Ref>), usize>,
967}
968
969impl Builder {
970 fn new(placeholders: &[Array]) -> Self {
971 let refs = placeholders
972 .iter()
973 .enumerate()
974 .map(|(i, p)| ((graph::id(p), graph::output_index(p)), Ref::Input(i)))
975 .collect();
976 Self {
977 refs,
978 consts: Vec::new(),
979 scalars: HashMap::new(),
980 steps: Vec::new(),
981 seen: HashMap::new(),
982 }
983 }
984
985 fn key(a: &Array) -> (usize, usize) {
986 (graph::id(a), graph::output_index(a))
987 }
988
989 fn build(mut self, outputs: &[Array]) -> Tape {
992 let mut stack: Vec<(Array, bool)> = outputs.iter().map(|a| (a.clone(), false)).collect();
993 while let Some((a, expanded)) = stack.pop() {
994 if self.refs.contains_key(&Self::key(&a)) {
995 continue;
996 }
997 let inputs = graph::inputs(&a);
998 if expanded || inputs.is_empty() {
999 self.resolve(&a, &inputs);
1000 continue;
1001 }
1002 stack.push((a, true));
1003 stack.extend(
1004 inputs
1005 .into_iter()
1006 .filter(|i| !self.refs.contains_key(&Self::key(i)))
1007 .map(|i| (i, false)),
1008 );
1009 }
1010 let outputs = outputs.iter().map(|a| self.refs[&Self::key(a)]).collect();
1011 Tape {
1012 consts: self.consts,
1013 steps: self.steps,
1014 outputs,
1015 shapeless: false,
1016 views: Mutex::default(),
1017 }
1018 }
1019
1020 fn resolve(&mut self, a: &Array, inputs: &[Array]) {
1024 let siblings = graph::siblings(a);
1025 let args: Vec<Ref> = inputs.iter().map(|i| self.refs[&Self::key(i)]).collect();
1026 let primitive = graph::primitive(a);
1027 let effect = |p: &Primitive| matches!(p, Primitive::Collective { .. });
1029 let Some(primitive) =
1030 primitive.filter(|p| effect(p) || !args.iter().all(|r| matches!(r, Ref::Const(_))))
1031 else {
1032 for s in &siblings {
1033 let c = self.constant(s);
1034 self.refs.insert(Self::key(s), Ref::Const(c));
1035 }
1036 return;
1037 };
1038 if siblings.len() == 1 && is_identity(&primitive, &inputs[0], a) {
1039 self.refs.insert(Self::key(a), args[0]);
1040 return;
1041 }
1042 let merged = match effect(&primitive) {
1045 true => None,
1046 false => self.seen.get(&(primitive.clone(), args.clone())).copied(),
1047 };
1048 let k = match merged {
1049 Some(k) => k,
1050 None => {
1051 self.steps.push(Step {
1052 primitive: primitive.clone(),
1053 inputs: args.clone(),
1054 outputs: siblings.iter().map(|s| (s.dtype(), dims(s))).collect(),
1055 });
1056 let k = self.steps.len() - 1;
1057 if !effect(&primitive) {
1058 self.seen.insert((primitive, args), k);
1059 }
1060 k
1061 }
1062 };
1063 for (o, s) in siblings.iter().enumerate() {
1064 self.refs.insert(Self::key(s), Ref::Step(k, o));
1065 }
1066 }
1067
1068 fn constant(&mut self, a: &Array) -> usize {
1072 if graph::is_tracer(a) {
1073 self.consts.push(a.clone());
1074 return self.consts.len() - 1;
1075 }
1076 let value = scalar_bits(a).or_else(|| {
1077 match (graph::primitive(a), graph::inputs(a).as_slice()) {
1079 (Some(Primitive::Broadcast(_)), [x]) => scalar_bits(x),
1080 _ => None,
1081 }
1082 });
1083 let key = value.map(|v| (a.dtype(), dims(a), v));
1084 if let Some(&c) = key.as_ref().and_then(|k| self.scalars.get(k)) {
1085 return c;
1086 }
1087 self.consts.push(a.clone());
1088 let c = self.consts.len() - 1;
1089 if let Some(key) = key {
1090 self.scalars.insert(key, c);
1091 }
1092 c
1093 }
1094}
1095
1096fn is_element_wise(primitive: &Primitive) -> bool {
1099 matches!(
1100 primitive,
1101 Primitive::Binary(_) | Primitive::Select | Primitive::Compiled(_)
1102 )
1103}
1104
1105fn is_identity(primitive: &Primitive, input: &Array, output: &Array) -> bool {
1107 let same = input.shape() == output.shape() && graph::strides(input) == graph::strides(output);
1108 match primitive {
1109 Primitive::Broadcast(_) | Primitive::Reshape(_) => same,
1111 Primitive::Transpose(axes) => axes.iter().enumerate().all(|(i, &a)| i == a),
1112 Primitive::AsType(d) => *d == input.dtype(),
1113 _ => false,
1114 }
1115}
1116
1117fn scalar_bits(a: &Array) -> Option<u64> {
1119 if a.size() != 1 || !a.is_evaluated() {
1120 return None;
1121 }
1122 Some(match a.dtype() {
1123 DType::Bool => u64::from(a.item::<bool>().ok()?),
1124 DType::U8 => u64::from(a.item::<u8>().ok()?),
1125 DType::U16 => u64::from(a.item::<u16>().ok()?),
1126 DType::U32 => u64::from(a.item::<u32>().ok()?),
1127 DType::U64 => a.item::<u64>().ok()?,
1128 DType::I8 => a.item::<i8>().ok()? as u64,
1129 DType::I16 => a.item::<i16>().ok()? as u64,
1130 DType::I32 => a.item::<i32>().ok()? as u64,
1131 DType::I64 => a.item::<i64>().ok()? as u64,
1132 DType::F16 => u64::from(a.item::<f16>().ok()?.to_bits()),
1133 DType::BF16 => u64::from(a.item::<bf16>().ok()?.to_bits()),
1134 DType::F32 => u64::from(a.item::<f32>().ok()?.to_bits()),
1135 DType::F64 => a.item::<f64>().ok()?.to_bits(),
1136 DType::Complex64 => {
1137 let z = a.item::<Complex64>().ok()?;
1138 u64::from(z.re.to_bits()) << 32 | u64::from(z.im.to_bits())
1139 }
1140 _ => return None,
1141 })
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146 use nuvec_core::{Device, array, set_default_stream};
1147
1148 use super::*;
1149
1150 fn trace(f: &dyn Fn(&[Array]) -> Result<Vec<Array>>, inputs: &[Array], device: Device) -> Tape {
1151 Tape::trace(
1152 &mut |xs: &[Array]| f(xs),
1153 inputs,
1154 Stream::default_for(device),
1155 false,
1156 )
1157 .unwrap()
1158 }
1159
1160 fn names(tape: &Tape) -> Vec<&'static str> {
1161 tape.steps.iter().map(|s| s.primitive.name()).collect()
1162 }
1163
1164 #[test]
1165 fn version_is_set() {
1166 assert!(!VERSION.is_empty());
1167 }
1168
1169 #[test]
1170 fn simplifies_identities_constants_and_common_subexpressions() {
1171 set_default_stream(Stream::default_for(Device::cpu()));
1172 let x = array![1.0, 2.0];
1173 let f = |xs: &[Array]| {
1174 let x = xs[0].astype(DType::F32).reshape([2]);
1175 let a = x.multiply(2.0); let b = x.multiply(2.0); Ok(vec![a.add(&b), x.exp(), x.exp()])
1178 };
1179 let tape = trace(&f, std::slice::from_ref(&x), Device::cpu());
1180 let mut ops = names(&tape);
1181 ops.sort_unstable();
1182 assert_eq!(ops, ["add", "exp", "multiply"]);
1183 assert_eq!(tape.outputs[1], tape.outputs[2]);
1184 let outs = tape.replay(&[x]).unwrap();
1185 assert_eq!(outs[0].to_vec::<f32>().unwrap(), [4.0, 8.0]);
1186 }
1187
1188 #[test]
1189 fn fuses_element_wise_chains_on_the_gpu_only() {
1190 set_default_stream(Stream::default_for(Device::cpu()));
1191 let x = array![1.0, 2.0];
1192 let w = array![[1.0, 0.0], [0.0, 1.0]];
1193 let f = |xs: &[Array]| {
1196 let h = xs[0].reshape([1, 2]).matmul(&w);
1197 let a = h.multiply(2.0).exp();
1198 let b = a.multiply(&a).add(1.0);
1199 Ok(vec![a, b.sigmoid()])
1200 };
1201 assert_eq!(
1202 names(&trace(&f, std::slice::from_ref(&x), Device::cpu())),
1203 [
1204 "reshape", "matmul", "multiply", "exp", "multiply", "add", "sigmoid"
1205 ]
1206 );
1207 let tape = trace(&f, &[x], Device::gpu(0));
1208 assert_eq!(names(&tape), ["reshape", "matmul", "compiled"]);
1209 let Primitive::Compiled(fused) = &tape.steps[2].primitive else {
1210 unreachable!()
1211 };
1212 assert_eq!((fused.len(), fused.dtypes().len()), (5, 2));
1213 assert_eq!(tape.outputs, [Ref::Step(2, 0), Ref::Step(2, 1)]);
1214 }
1215
1216 #[test]
1217 fn copies_and_dependencies_are_kept() {
1218 set_default_stream(Stream::default_for(Device::cpu()));
1219 let x = array![1.0, 2.0];
1220 let y = array![3.0, 4.0];
1221 let f = |xs: &[Array]| {
1222 let copy = graph::from_primitive(Primitive::Copy, vec![xs[0].clone()])?.remove(0);
1223 let ordered = graph::from_primitive(
1224 Primitive::Depends { n_deps: 1 },
1225 vec![xs[0].clone(), xs[1].clone()],
1226 )?
1227 .remove(0);
1228 Ok(vec![copy.add(ordered.astype(DType::F32))])
1230 };
1231 let tape = trace(&f, &[x.clone(), y.clone()], Device::cpu());
1232 let mut ops = names(&tape);
1233 ops.sort_unstable();
1234 assert_eq!(ops, ["add", "copy", "depends"]);
1235 let out = tape.replay(&[x, y]).unwrap().remove(0);
1236 assert_eq!(out.to_vec::<f32>().unwrap(), [2.0, 4.0]);
1237 }
1238
1239 #[test]
1240 fn a_shared_value_used_outside_a_group_stays_outside() {
1241 let x = array![1.0, 2.0];
1242 let f = |xs: &[Array]| {
1245 let t = xs[0].exp();
1246 let s = t.sum_keepdims(..);
1247 Ok(vec![t.multiply(3.0).add(&s)])
1248 };
1249 let tape = trace(&f, &[x], Device::gpu(0));
1250 assert_eq!(names(&tape), ["exp", "sum", "broadcast_to", "compiled"]);
1251 }
1252}