1#![forbid(unsafe_code)]
33
34use std::collections::{BinaryHeap, HashMap, HashSet};
35use std::sync::{Arc, Mutex, PoisonError};
36
37use nuvec_core::{
38 Array, Complex64, DType, DeviceKind, Fused, FusedRef, Primitive, Result, Stream, bf16,
39 default_stream, f16,
40};
41
42pub const VERSION: &str = env!("CARGO_PKG_VERSION");
44
45const MAX_FUSED_STEPS: usize = 32;
47const MAX_FUSED_ARRAYS: usize = 24;
50
51pub fn compile<F>(f: F) -> impl Fn(&[Array]) -> Result<Vec<Array>>
64where
65 F: Fn(&[Array]) -> Result<Vec<Array>>,
66{
67 let cache: Mutex<HashMap<Key, Arc<Tape>>> = Mutex::default();
68 move |inputs| {
69 if inputs.iter().any(Array::is_tracer) {
70 return f(inputs);
71 }
72 let key = Key::new(inputs);
73 let cached = cache
74 .lock()
75 .unwrap_or_else(PoisonError::into_inner)
76 .get(&key)
77 .cloned();
78 let tape = match cached {
79 Some(tape) => tape,
80 None => {
81 let tape = Arc::new(Tape::trace(&f, inputs, key.stream)?);
82 cache
83 .lock()
84 .unwrap_or_else(PoisonError::into_inner)
85 .insert(key, tape.clone());
86 tape
87 }
88 };
89 tape.replay(inputs)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95struct Key {
96 stream: Stream,
97 inputs: Vec<(DType, Vec<usize>)>,
98}
99
100impl Key {
101 fn new(inputs: &[Array]) -> Self {
102 Self {
103 stream: default_stream(),
104 inputs: inputs
105 .iter()
106 .map(|a| (a.dtype(), a.shape().to_vec()))
107 .collect(),
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114enum Ref {
115 Input(usize),
117 Const(usize),
119 Step(usize, usize),
121}
122
123#[derive(Debug, Clone)]
124struct Step {
125 primitive: Primitive,
126 inputs: Vec<Ref>,
127 outputs: Vec<(DType, Vec<usize>)>,
129}
130
131#[derive(Debug)]
133struct Tape {
134 consts: Vec<Array>,
135 steps: Vec<Step>,
136 outputs: Vec<Ref>,
137}
138
139impl Tape {
140 fn trace(
143 f: &dyn Fn(&[Array]) -> Result<Vec<Array>>,
144 inputs: &[Array],
145 stream: Stream,
146 ) -> Result<Self> {
147 let placeholders = inputs
148 .iter()
149 .map(|a| Array::placeholder(a.dtype(), a.shape()))
150 .collect::<Result<Vec<_>>>()?;
151 let outputs = f(&placeholders)?;
152 let mut tape = Builder::new(&placeholders).build(&outputs);
153 if stream.device.kind == DeviceKind::Gpu {
154 tape.fuse(inputs);
155 }
156 Ok(tape)
157 }
158
159 fn replay(&self, inputs: &[Array]) -> Result<Vec<Array>> {
160 let mut values: Vec<Vec<Array>> = Vec::with_capacity(self.steps.len());
161 let get = |r: &Ref, values: &[Vec<Array>]| match *r {
162 Ref::Input(i) => inputs[i].clone(),
163 Ref::Const(c) => self.consts[c].clone(),
164 Ref::Step(s, o) => values[s][o].clone(),
165 };
166 for step in &self.steps {
167 let args = step.inputs.iter().map(|r| get(r, &values)).collect();
168 values.push(Array::from_primitive(step.primitive.clone(), args)?);
169 }
170 Ok(self.outputs.iter().map(|r| get(r, &values)).collect())
171 }
172
173 fn dtype(&self, r: Ref, inputs: &[Array]) -> DType {
174 match r {
175 Ref::Input(i) => inputs[i].dtype(),
176 Ref::Const(c) => self.consts[c].dtype(),
177 Ref::Step(s, o) => self.steps[s].outputs[o].0,
178 }
179 }
180
181 fn fuse(&mut self, inputs: &[Array]) {
187 let n = self.steps.len();
188 let fusable: Vec<bool> = self
189 .steps
190 .iter()
191 .map(|s| {
192 let dtypes: Vec<DType> = s.inputs.iter().map(|&r| self.dtype(r, inputs)).collect();
193 s.outputs.len() == 1 && Fused::supports(&s.primitive, &dtypes)
194 })
195 .collect();
196 let mut consumers: Vec<Vec<usize>> = vec![Vec::new(); n];
197 for (k, s) in self.steps.iter().enumerate() {
198 for r in &s.inputs {
199 if let Ref::Step(j, _) = *r {
200 consumers[j].push(k);
201 }
202 }
203 }
204 let mut is_output = vec![false; n];
205 for r in &self.outputs {
206 if let Ref::Step(j, _) = *r {
207 is_output[j] = true;
208 }
209 }
210 let step_inputs = |k: usize| {
211 self.steps[k].inputs.iter().filter_map(|r| match *r {
212 Ref::Step(j, _) => Some(j),
213 _ => None,
214 })
215 };
216 let mut group: Vec<Option<usize>> = vec![None; n];
218 let mut groups: HashMap<usize, (Vec<usize>, Vec<Ref>)> = HashMap::new();
220 for root in (0..n).rev() {
221 if group[root].is_some() || !fusable[root] {
222 continue;
223 }
224 let mut members = HashSet::from([root]);
225 let mut external: Vec<Ref> = Vec::new();
226 let mut outputs = 1;
227 let add_inputs = |k: usize, external: &mut Vec<Ref>| {
228 for &r in &self.steps[k].inputs {
229 if !external.contains(&r) {
230 external.push(r);
231 }
232 }
233 };
234 add_inputs(root, &mut external);
235 let mut candidates: BinaryHeap<usize> = step_inputs(root).collect();
237 let mut seen = HashSet::new();
238 while let Some(j) = candidates.pop() {
239 if !seen.insert(j)
240 || group[j].is_some()
241 || !fusable[j]
242 || members.len() >= MAX_FUSED_STEPS
243 || !consumers[j].iter().all(|c| members.contains(c))
244 {
245 continue;
246 }
247 let new = self.steps[j]
248 .inputs
249 .iter()
250 .filter(|r| !external.contains(r))
251 .collect::<HashSet<_>>()
252 .len();
253 let arrays = external.len() - 1 + new + outputs + usize::from(is_output[j]);
254 if arrays > MAX_FUSED_ARRAYS {
255 continue;
256 }
257 members.insert(j);
258 outputs += usize::from(is_output[j]);
259 external.retain(|&r| r != Ref::Step(j, 0));
260 add_inputs(j, &mut external);
261 candidates.extend(step_inputs(j));
262 }
263 if members.len() < 2 || external.len() + outputs > MAX_FUSED_ARRAYS {
264 continue;
265 }
266 let mut members: Vec<usize> = members.into_iter().collect();
267 members.sort_unstable();
268 for &m in &members {
269 group[m] = Some(root);
270 }
271 groups.insert(root, (members, external));
272 }
273 if groups.is_empty() {
274 return;
275 }
276 let mut index: Vec<Option<(usize, usize)>> = vec![None; n];
279 let remap = |r: Ref, index: &[Option<(usize, usize)>]| match r {
280 Ref::Step(j, o) => {
281 let (k, offset) = index[j].expect("defined before use");
282 Ref::Step(k, offset + o)
283 }
284 r => r,
285 };
286 let mut steps = Vec::with_capacity(n);
287 for k in 0..n {
288 let step = match (group[k], groups.get(&k)) {
289 (Some(_), None) => continue, (Some(_), Some((members, external))) => {
291 let position: HashMap<usize, usize> =
292 members.iter().enumerate().map(|(p, &m)| (m, p)).collect();
293 let graph = members
294 .iter()
295 .map(|&m| {
296 let args = self.steps[m]
297 .inputs
298 .iter()
299 .map(|r| match *r {
300 Ref::Step(j, _) if position.contains_key(&j) => {
301 FusedRef::Step(position[&j])
302 }
303 r => FusedRef::Input(
304 external.iter().position(|&e| e == r).expect("external"),
305 ),
306 })
307 .collect();
308 (self.steps[m].primitive.clone(), args)
309 })
310 .collect();
311 let outs: Vec<usize> = members
313 .iter()
314 .copied()
315 .filter(|&m| m != k && is_output[m])
316 .chain([k])
317 .collect();
318 for (o, &m) in outs.iter().enumerate() {
319 index[m] = Some((steps.len(), o));
320 }
321 let dtypes: Vec<DType> =
322 external.iter().map(|&r| self.dtype(r, inputs)).collect();
323 let positions: Vec<usize> = outs.iter().map(|m| position[m]).collect();
324 let Ok(f) = Fused::new(&dtypes, graph, &positions) else {
325 unreachable!("every member is fusable")
326 };
327 steps.push(Step {
328 primitive: Primitive::Compiled(f),
329 inputs: external.iter().map(|&r| remap(r, &index)).collect(),
330 outputs: outs
331 .iter()
332 .map(|&m| self.steps[m].outputs[0].clone())
333 .collect(),
334 });
335 continue;
336 }
337 (None, _) => {
338 let s = &self.steps[k];
339 Step {
340 primitive: s.primitive.clone(),
341 inputs: s.inputs.iter().map(|&r| remap(r, &index)).collect(),
342 outputs: s.outputs.clone(),
343 }
344 }
345 };
346 index[k] = Some((steps.len(), 0));
347 steps.push(step);
348 }
349 self.outputs = self.outputs.iter().map(|&r| remap(r, &index)).collect();
350 self.steps = steps;
351 }
352}
353
354struct Builder {
356 refs: HashMap<(usize, usize), Ref>,
358 consts: Vec<Array>,
359 scalars: HashMap<(DType, Vec<usize>, u64), usize>,
361 steps: Vec<Step>,
362 seen: HashMap<(Primitive, Vec<Ref>), usize>,
364}
365
366impl Builder {
367 fn new(placeholders: &[Array]) -> Self {
368 let refs = placeholders
369 .iter()
370 .enumerate()
371 .map(|(i, p)| ((p.id(), p.output_index()), Ref::Input(i)))
372 .collect();
373 Self {
374 refs,
375 consts: Vec::new(),
376 scalars: HashMap::new(),
377 steps: Vec::new(),
378 seen: HashMap::new(),
379 }
380 }
381
382 fn key(a: &Array) -> (usize, usize) {
383 (a.id(), a.output_index())
384 }
385
386 fn build(mut self, outputs: &[Array]) -> Tape {
389 let mut stack: Vec<(Array, bool)> = outputs.iter().map(|a| (a.clone(), false)).collect();
390 while let Some((a, expanded)) = stack.pop() {
391 if self.refs.contains_key(&Self::key(&a)) {
392 continue;
393 }
394 let inputs = a.inputs();
395 if expanded || inputs.is_empty() {
396 self.resolve(&a, &inputs);
397 continue;
398 }
399 stack.push((a, true));
400 stack.extend(
401 inputs
402 .into_iter()
403 .filter(|i| !self.refs.contains_key(&Self::key(i)))
404 .map(|i| (i, false)),
405 );
406 }
407 let outputs = outputs.iter().map(|a| self.refs[&Self::key(a)]).collect();
408 Tape {
409 consts: self.consts,
410 steps: self.steps,
411 outputs,
412 }
413 }
414
415 fn resolve(&mut self, a: &Array, inputs: &[Array]) {
419 let siblings = a.siblings();
420 let args: Vec<Ref> = inputs.iter().map(|i| self.refs[&Self::key(i)]).collect();
421 let primitive = a.primitive();
422 let Some(primitive) =
423 primitive.filter(|_| !args.iter().all(|r| matches!(r, Ref::Const(_))))
424 else {
425 for s in &siblings {
426 let c = self.constant(s);
427 self.refs.insert(Self::key(s), Ref::Const(c));
428 }
429 return;
430 };
431 if siblings.len() == 1 && is_identity(&primitive, &inputs[0], a) {
432 self.refs.insert(Self::key(a), args[0]);
433 return;
434 }
435 let k = match self.seen.get(&(primitive.clone(), args.clone())) {
436 Some(&k) => k,
437 None => {
438 self.steps.push(Step {
439 primitive: primitive.clone(),
440 inputs: args.clone(),
441 outputs: siblings
442 .iter()
443 .map(|s| (s.dtype(), s.shape().to_vec()))
444 .collect(),
445 });
446 let k = self.steps.len() - 1;
447 self.seen.insert((primitive, args), k);
448 k
449 }
450 };
451 for (o, s) in siblings.iter().enumerate() {
452 self.refs.insert(Self::key(s), Ref::Step(k, o));
453 }
454 }
455
456 fn constant(&mut self, a: &Array) -> usize {
458 let value = scalar_bits(a).or_else(|| {
459 match (a.primitive(), a.inputs().as_slice()) {
461 (Some(Primitive::Broadcast(_)), [x]) => scalar_bits(x),
462 _ => None,
463 }
464 });
465 let key = value.map(|v| (a.dtype(), a.shape().to_vec(), v));
466 if let Some(&c) = key.as_ref().and_then(|k| self.scalars.get(k)) {
467 return c;
468 }
469 self.consts.push(a.clone());
470 let c = self.consts.len() - 1;
471 if let Some(key) = key {
472 self.scalars.insert(key, c);
473 }
474 c
475 }
476}
477
478fn is_identity(primitive: &Primitive, input: &Array, output: &Array) -> bool {
480 let same = input.shape() == output.shape() && input.strides() == output.strides();
481 match primitive {
482 Primitive::Broadcast(_) | Primitive::Reshape(_) | Primitive::StopGradient => same,
483 Primitive::Transpose(axes) => axes.iter().enumerate().all(|(i, &a)| i == a),
484 Primitive::AsType(d) => *d == input.dtype(),
485 _ => false,
486 }
487}
488
489fn scalar_bits(a: &Array) -> Option<u64> {
491 if a.size() != 1 || !a.is_evaluated() {
492 return None;
493 }
494 Some(match a.dtype() {
495 DType::Bool => u64::from(a.item::<bool>().ok()?),
496 DType::U8 => u64::from(a.item::<u8>().ok()?),
497 DType::U16 => u64::from(a.item::<u16>().ok()?),
498 DType::U32 => u64::from(a.item::<u32>().ok()?),
499 DType::U64 => a.item::<u64>().ok()?,
500 DType::I8 => a.item::<i8>().ok()? as u64,
501 DType::I16 => a.item::<i16>().ok()? as u64,
502 DType::I32 => a.item::<i32>().ok()? as u64,
503 DType::I64 => a.item::<i64>().ok()? as u64,
504 DType::F16 => u64::from(a.item::<f16>().ok()?.to_bits()),
505 DType::BF16 => u64::from(a.item::<bf16>().ok()?.to_bits()),
506 DType::F32 => u64::from(a.item::<f32>().ok()?.to_bits()),
507 DType::F64 => a.item::<f64>().ok()?.to_bits(),
508 DType::Complex64 => {
509 let z = a.item::<Complex64>().ok()?;
510 u64::from(z.re.to_bits()) << 32 | u64::from(z.im.to_bits())
511 }
512 _ => return None,
513 })
514}
515
516#[cfg(test)]
517mod tests {
518 use nuvec_core::{Device, add, exp, multiply, set_default_stream};
519
520 use super::*;
521
522 fn trace(f: &dyn Fn(&[Array]) -> Result<Vec<Array>>, inputs: &[Array], device: Device) -> Tape {
523 Tape::trace(f, inputs, Stream::default_for(device)).unwrap()
524 }
525
526 fn names(tape: &Tape) -> Vec<&'static str> {
527 tape.steps.iter().map(|s| s.primitive.name()).collect()
528 }
529
530 #[test]
531 fn version_is_set() {
532 assert!(!VERSION.is_empty());
533 }
534
535 #[test]
536 fn simplifies_identities_constants_and_common_subexpressions() {
537 set_default_stream(Stream::default_for(Device::cpu()));
538 let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
539 let f = |xs: &[Array]| {
540 let x = xs[0].astype(DType::F32)?.reshape(&[2])?;
541 let a = multiply(&x, 2.0)?; let b = multiply(&x, 2.0)?; Ok(vec![add(&a, &b)?, exp(&x)?, exp(&x)?])
544 };
545 let tape = trace(&f, std::slice::from_ref(&x), Device::cpu());
546 let mut ops = names(&tape);
547 ops.sort_unstable();
548 assert_eq!(ops, ["add", "exp", "multiply"]);
549 assert_eq!(tape.outputs[1], tape.outputs[2]);
550 let outs = tape.replay(&[x]).unwrap();
551 assert_eq!(outs[0].to_vec::<f32>().unwrap(), [4.0, 8.0]);
552 }
553
554 #[test]
555 fn fuses_element_wise_chains_on_the_gpu_only() {
556 set_default_stream(Stream::default_for(Device::cpu()));
557 let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
558 let w = Array::from_slice(&[1.0f32, 0.0, 0.0, 1.0], &[2, 2]).unwrap();
559 let f = |xs: &[Array]| {
562 let h = nuvec_core::matmul(&xs[0].reshape(&[1, 2])?, &w)?;
563 let a = exp(&multiply(&h, 2.0)?)?;
564 let b = add(&multiply(&a, &a)?, 1.0)?;
565 Ok(vec![a, nuvec_core::sigmoid(&b)?])
566 };
567 assert_eq!(
568 names(&trace(&f, std::slice::from_ref(&x), Device::cpu())),
569 [
570 "reshape", "matmul", "multiply", "exp", "multiply", "add", "sigmoid"
571 ]
572 );
573 let tape = trace(&f, &[x], Device::gpu(0));
574 assert_eq!(names(&tape), ["reshape", "matmul", "compiled"]);
575 let Primitive::Compiled(fused) = &tape.steps[2].primitive else {
576 unreachable!()
577 };
578 assert_eq!((fused.len(), fused.dtypes().len()), (5, 2));
579 assert_eq!(tape.outputs, [Ref::Step(2, 0), Ref::Step(2, 1)]);
580 }
581
582 #[test]
583 fn a_shared_value_used_outside_a_group_stays_outside() {
584 let x = Array::from_slice(&[1.0f32, 2.0], &[2]).unwrap();
585 let f = |xs: &[Array]| {
588 let t = exp(&xs[0])?;
589 let s = nuvec_core::sum(&t, &[], true)?;
590 Ok(vec![add(&multiply(&t, 3.0)?, &s)?])
591 };
592 let tape = trace(&f, &[x], Device::gpu(0));
593 assert_eq!(names(&tape), ["exp", "sum", "broadcast_to", "compiled"]);
594 }
595}