1use std::collections::HashMap;
2use std::fmt;
3use std::num::NonZeroUsize;
4use std::sync::Arc;
5
6use computegraph::compile::{compile, CompiledProgram};
7use computegraph::materialize::materialize_merge;
8use computegraph::resolve::resolve;
9use computegraph::types::ValueKey;
10use lru::LruCache;
11use num_complex::{Complex32, Complex64};
12use tenferro_ops::dim_expr::DimExpr;
13use tenferro_ops::input_key::TensorInputKey;
14use tenferro_ops::std_tensor_op::StdTensorOp;
15use tenferro_tensor::{DType, Tensor, TensorScalar};
16
17use super::cache::{
18 compile_cache_stats, compute_cache_key, CacheKey, GraphCompilerCacheStats,
19 DEFAULT_COMPILE_CACHE_CAPACITY,
20};
21use super::program::{GraphProgram, GraphProgramInput};
22use crate::compiler::{compile_std_to_exec_with_options, CompilerOptions};
23use crate::error::{Error, Result};
24use crate::exec::ExecProgram;
25use crate::extension_cache::{ExtensionCacheSelector, ExtensionCacheStore};
26use crate::traced::{try_concrete_shape, TracedTensor};
27
28#[derive(Clone)]
29struct InputDescriptor {
30 key: TensorInputKey,
31 dtype: DType,
32 shape: Vec<usize>,
33 default_tensor: Option<Arc<Tensor>>,
34}
35
36pub struct GraphCompiler {
53 compile_cache: LruCache<CacheKey, ExecProgram>,
54 extension_cache: ExtensionCacheStore,
55 compiler_options: CompilerOptions,
56}
57
58impl fmt::Debug for GraphCompiler {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.debug_struct("GraphCompiler")
61 .field("cache_stats", &self.cache_stats())
62 .field("compile_cache_capacity", &self.compile_cache_capacity())
63 .field("compiler_options", &self.compiler_options)
64 .field("extension_cache", &self.extension_cache)
65 .finish_non_exhaustive()
66 }
67}
68
69impl GraphCompiler {
70 pub fn new() -> Self {
81 Self {
82 compile_cache: LruCache::new(
83 NonZeroUsize::new(DEFAULT_COMPILE_CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN),
84 ),
85 extension_cache: ExtensionCacheStore::new(),
86 compiler_options: CompilerOptions::default(),
87 }
88 }
89
90 pub fn with_compiler_options(compiler_options: CompilerOptions) -> Self {
107 Self {
108 compile_cache: LruCache::new(
109 NonZeroUsize::new(DEFAULT_COMPILE_CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN),
110 ),
111 extension_cache: ExtensionCacheStore::new(),
112 compiler_options,
113 }
114 }
115
116 pub fn compile(&mut self, output: &TracedTensor) -> Result<GraphProgram> {
130 self.compile_many(&[output])
131 }
132
133 pub fn compile_many(&mut self, outputs: &[&TracedTensor]) -> Result<GraphProgram> {
147 let mut all_inputs = HashMap::new();
148 for output in outputs {
149 for (key, tensor) in output.inputs_map.iter() {
150 if let Some(existing) = all_inputs.get(key) {
151 if !default_tensors_equivalent(existing, tensor) {
152 return Err(Error::DuplicateBinding {
153 input_key: format!("{:?}", key),
154 });
155 }
156 continue;
157 }
158 all_inputs.insert(key.clone(), tensor.clone());
159 }
160 }
161 self.compile_many_with_descriptors(outputs, &HashMap::new(), &all_inputs)
162 }
163
164 pub fn compile_with_input_specs(
180 &mut self,
181 output: &TracedTensor,
182 bindings: &[(&TracedTensor, DType, &[usize])],
183 ) -> Result<GraphProgram> {
184 let mut binding_specs = HashMap::new();
185 for (index, (placeholder, dtype, shape)) in bindings.iter().enumerate() {
186 validate_placeholder_spec(index, placeholder, *dtype, shape)?;
187 let key = placeholder.input_key().ok_or(Error::UnexpectedBinding {
188 binding_index: index,
189 })?;
190 if binding_specs
191 .insert(
192 key.clone(),
193 InputDescriptor {
194 key: key.clone(),
195 dtype: *dtype,
196 shape: (*shape).to_vec(),
197 default_tensor: None,
198 },
199 )
200 .is_some()
201 {
202 return Err(Error::DuplicateBinding {
203 input_key: format!("{:?}", key),
204 });
205 }
206 }
207
208 self.compile_many_with_descriptors(&[output], &binding_specs, output.inputs_map.as_ref())
209 }
210
211 pub fn compile_cache_len(&self) -> usize {
222 self.compile_cache.len()
223 }
224
225 pub fn compile_cache_capacity(&self) -> NonZeroUsize {
236 self.compile_cache.cap()
237 }
238
239 pub fn set_compile_cache_capacity(&mut self, capacity: NonZeroUsize) {
252 self.compile_cache.resize(capacity);
253 }
254
255 pub fn compiler_options(&self) -> CompilerOptions {
267 self.compiler_options
268 }
269
270 pub fn set_compiler_options(&mut self, compiler_options: CompilerOptions) {
290 if self.compiler_options == compiler_options {
291 return;
292 }
293 self.compiler_options = compiler_options;
294 self.clear_compile_cache();
295 }
296
297 pub fn clear_compile_cache(&mut self) {
309 self.compile_cache.clear();
310 }
311
312 pub fn clear_extension_caches(&mut self) {
324 self.extension_cache.clear();
325 }
326
327 pub fn clear_caches(&mut self) {
339 self.clear_compile_cache();
340 self.clear_extension_caches();
341 }
342
343 pub fn cache_stats(&self) -> GraphCompilerCacheStats {
355 GraphCompilerCacheStats {
356 compile: compile_cache_stats(&self.compile_cache),
357 extensions: self.extension_cache.stats(ExtensionCacheSelector::All),
358 }
359 }
360
361 pub fn extension_caches(&self) -> &ExtensionCacheStore {
372 &self.extension_cache
373 }
374
375 pub fn extension_caches_mut(&mut self) -> &mut ExtensionCacheStore {
386 &mut self.extension_cache
387 }
388
389 fn compile_many_with_descriptors(
390 &mut self,
391 outputs: &[&TracedTensor],
392 binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
393 default_inputs: &HashMap<TensorInputKey, Arc<Tensor>>,
394 ) -> Result<GraphProgram> {
395 let mut roots = Vec::new();
396 let mut output_keys = Vec::with_capacity(outputs.len());
397 for output in outputs {
398 roots.extend(output.resolve_roots());
399 output_keys.push(output.graph.values()[output.val].key.clone());
400 }
401
402 let view = resolve(roots);
403 let graph = materialize_merge(&view, &output_keys);
404 let mut compiled = compile(&graph);
405 prune_compiled_extension_outputs(&mut compiled)?;
406
407 let mut descriptors = Vec::with_capacity(graph.inputs.len());
408 let mut input_dtypes = Vec::with_capacity(graph.inputs.len());
409 let mut input_shapes = Vec::with_capacity(graph.inputs.len());
410 for key in &graph.inputs {
411 let ValueKey::Input(input_key) = key else {
412 return Err(Error::Internal(
413 "expected Input key in graph inputs".to_string(),
414 ));
415 };
416 let descriptor = descriptor_for_input(input_key, binding_specs, default_inputs)?;
417 input_dtypes.push(descriptor.dtype);
418 input_shapes.push(DimExpr::from_concrete(&descriptor.shape));
419 descriptors.push(GraphProgramInput::new(
420 descriptor.key,
421 descriptor.dtype,
422 descriptor.shape.clone(),
423 DimExpr::from_concrete(&descriptor.shape),
424 descriptor.default_tensor,
425 ));
426 }
427
428 let exec = compile_std_to_exec_with_options(
429 &compiled,
430 &input_dtypes,
431 &input_shapes,
432 self.compiler_options,
433 )?;
434 let exec = self.get_or_compile(exec);
435 Ok(GraphProgram::new(exec, descriptors))
436 }
437
438 fn get_or_compile(&mut self, exec: ExecProgram) -> ExecProgram {
439 let key = compute_cache_key(&exec);
440 if let Some(cached) = self.compile_cache.get(&key) {
441 return cached.clone();
442 }
443 self.compile_cache.put(key, exec.clone());
444 exec
445 }
446}
447
448impl Default for GraphCompiler {
449 fn default() -> Self {
450 Self::new()
451 }
452}
453
454fn validate_placeholder_spec(
455 index: usize,
456 placeholder: &TracedTensor,
457 dtype: DType,
458 shape: &[usize],
459) -> Result<()> {
460 if placeholder.data.is_some() {
461 return Err(Error::UnexpectedBinding {
462 binding_index: index,
463 });
464 }
465 placeholder.input_key().ok_or(Error::UnexpectedBinding {
466 binding_index: index,
467 })?;
468
469 if placeholder.dtype != dtype {
470 return Err(Error::PlaceholderDtypeMismatch {
471 expected: placeholder.dtype,
472 actual: dtype,
473 });
474 }
475 validate_placeholder_shape(placeholder, shape)
476}
477
478fn validate_placeholder_shape(placeholder: &TracedTensor, shape: &[usize]) -> Result<()> {
479 match try_concrete_shape(placeholder) {
480 Some(expected_shape) => {
481 if expected_shape.as_slice() != shape {
482 return Err(Error::PlaceholderShapeMismatch {
483 expected: expected_shape,
484 actual: shape.to_vec(),
485 });
486 }
487 }
488 None => {
489 if placeholder.rank != shape.len() {
490 return Err(Error::PlaceholderRankMismatch {
491 expected: placeholder.rank,
492 actual: shape.len(),
493 });
494 }
495 }
496 }
497 Ok(())
498}
499
500fn descriptor_for_input(
501 key: &TensorInputKey,
502 binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
503 default_inputs: &HashMap<TensorInputKey, Arc<Tensor>>,
504) -> Result<InputDescriptor> {
505 if let Some(tensor) = default_inputs.get(key) {
506 return Ok(InputDescriptor {
507 key: key.clone(),
508 dtype: tensor.dtype(),
509 shape: tensor.shape().to_vec(),
510 default_tensor: Some(tensor.clone()),
511 });
512 }
513 if let Some(spec) = binding_specs.get(key) {
514 return Ok(spec.clone());
515 }
516 Err(Error::UnboundPlaceholder {
517 input_key: format!("{:?}", key),
518 })
519}
520
521fn prune_compiled_extension_outputs(prog: &mut CompiledProgram<StdTensorOp>) -> Result<()> {
522 let mut live_slots = vec![false; prog.n_slots];
523 for &slot in &prog.output_slots {
524 let Some(live) = live_slots.get_mut(slot) else {
525 return Err(invalid_compiled_graph(format!(
526 "program output slot {slot} is outside slot table of length {}",
527 prog.n_slots
528 )));
529 };
530 *live = true;
531 }
532
533 for instr in prog.instructions.iter_mut().rev() {
534 let live_outputs = instr
535 .outputs
536 .iter()
537 .map(|&slot| {
538 live_slots.get(slot).copied().ok_or_else(|| {
539 invalid_compiled_graph(format!(
540 "instruction output slot {slot} is outside slot table of length {}",
541 prog.n_slots
542 ))
543 })
544 })
545 .collect::<Result<Vec<_>>>()?;
546
547 if let StdTensorOp::Extension(ext) = &instr.operation {
548 if let Some(pruned) = ext.prune_outputs(&live_outputs) {
549 let kept_outputs = instr
550 .outputs
551 .iter()
552 .zip(live_outputs.iter())
553 .filter_map(|(&slot, &live)| live.then_some(slot))
554 .collect::<Vec<_>>();
555 if pruned.output_count() != kept_outputs.len() {
556 return Err(invalid_compiled_graph(format!(
557 "extension family_id={:?} pruned to {} outputs for {} live slots",
558 ext.family_id(),
559 pruned.output_count(),
560 kept_outputs.len()
561 )));
562 }
563 instr.operation = StdTensorOp::Extension(pruned);
564 instr.outputs = kept_outputs;
565 }
566 }
567
568 if live_outputs.iter().any(|&live| live) {
569 for &slot in &instr.inputs {
570 let Some(live) = live_slots.get_mut(slot) else {
571 return Err(invalid_compiled_graph(format!(
572 "instruction input slot {slot} is outside slot table of length {}",
573 prog.n_slots
574 )));
575 };
576 *live = true;
577 }
578 }
579 }
580
581 Ok(())
582}
583
584fn invalid_compiled_graph(message: impl Into<String>) -> Error {
585 Error::InvalidCompiledGraph {
586 message: message.into(),
587 }
588}
589
590fn default_tensors_equivalent(lhs: &Arc<Tensor>, rhs: &Arc<Tensor>) -> bool {
591 if Arc::ptr_eq(lhs, rhs) {
592 return true;
593 }
594 if lhs.dtype() != rhs.dtype() || lhs.shape() != rhs.shape() {
595 return false;
596 }
597 match lhs.dtype() {
598 DType::F32 => default_slices_equivalent::<f32>(lhs, rhs),
599 DType::F64 => default_slices_equivalent::<f64>(lhs, rhs),
600 DType::I32 => default_slices_equivalent::<i32>(lhs, rhs),
601 DType::I64 => default_slices_equivalent::<i64>(lhs, rhs),
602 DType::Bool => default_slices_equivalent::<bool>(lhs, rhs),
603 DType::C32 => default_slices_equivalent::<Complex32>(lhs, rhs),
604 DType::C64 => default_slices_equivalent::<Complex64>(lhs, rhs),
605 }
606}
607
608fn default_slices_equivalent<T: TensorScalar + PartialEq>(lhs: &Tensor, rhs: &Tensor) -> bool {
609 match (lhs.as_slice::<T>(), rhs.as_slice::<T>()) {
610 (Ok(lhs), Ok(rhs)) => lhs == rhs,
611 _ => false,
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use std::any::Any;
621 use std::hash::Hasher;
622 use std::sync::Arc;
623 use tenferro_ops::{ext_op::ExtensionOp, SymDim};
624 use tenferro_tensor::{
625 Buffer, BufferHandle, DeviceId, DeviceKind, GpuBackendKind, MemoryKind, Placement,
626 TypedTensor,
627 };
628
629 #[test]
630 fn compile_many_rejects_conflicting_default_inputs_for_same_key() {
631 let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
632 let y1 = x.neg().unwrap();
633 let mut y2 = x.neg().unwrap();
634 let key = x.input_key().expect("concrete traced tensor has input key");
635 let replacement = Arc::new(Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap());
636 let mut inputs = (*y2.inputs_map).clone();
637 inputs.insert(key.clone(), replacement);
638 y2.inputs_map = Arc::new(inputs);
639
640 let err = GraphCompiler::new().compile_many(&[&y1, &y2]).unwrap_err();
641
642 assert!(matches!(
643 err,
644 Error::DuplicateBinding { ref input_key } if input_key.contains(&format!("{key:?}"))
645 ));
646 }
647
648 #[test]
649 fn default_tensors_equivalent_rejects_distinct_backend_buffers() {
650 let placement = Placement {
651 memory_kind: MemoryKind::Device,
652 device: Some(DeviceId {
653 kind: DeviceKind::Gpu(GpuBackendKind::Cuda),
654 ordinal: 0,
655 }),
656 };
657 let lhs = Arc::new(Tensor::F64(
658 TypedTensor::from_buffer_col_major(
659 vec![2],
660 Buffer::Backend(Arc::new(BufferHandle::<f64>::new_with_len(1, 2))),
661 placement.clone(),
662 )
663 .unwrap(),
664 ));
665 let rhs = Arc::new(Tensor::F64(
666 TypedTensor::from_buffer_col_major(
667 vec![2],
668 Buffer::Backend(Arc::new(BufferHandle::<f64>::new_with_len(2, 2))),
669 placement,
670 )
671 .unwrap(),
672 ));
673
674 assert!(
675 !default_tensors_equivalent(&lhs, &rhs),
676 "distinct backend-resident default tensors must not compare equal just because both are unreadable on host"
677 );
678 assert!(default_tensors_equivalent(&lhs, &lhs));
679 }
680
681 #[derive(Clone, Debug, PartialEq, Eq)]
682 struct PrunableTestOp {
683 pruned: bool,
684 }
685
686 impl ExtensionOp for PrunableTestOp {
687 fn family_id(&self) -> &'static str {
688 "tenferro-runtime.test-prunable.v1"
689 }
690
691 fn payload_hash(&self, hasher: &mut dyn Hasher) {
692 hasher.write_u8(u8::from(self.pruned));
693 }
694
695 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
696 other
697 .as_any()
698 .downcast_ref::<Self>()
699 .is_some_and(|that| self == that)
700 }
701
702 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
703 Arc::new(self.clone())
704 }
705
706 fn as_any(&self) -> &dyn Any {
707 self
708 }
709
710 fn input_count(&self) -> usize {
711 1
712 }
713
714 fn output_count(&self) -> usize {
715 if self.pruned {
716 1
717 } else {
718 3
719 }
720 }
721
722 fn infer_output_meta(
723 &self,
724 input_dtypes: &[DType],
725 input_shapes: &[&[SymDim]],
726 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
727 Ok((0..self.output_count())
728 .map(|_| (input_dtypes[0], input_shapes[0].to_vec()))
729 .collect())
730 }
731
732 fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
733 (!self.pruned && live_outputs == [false, true, false])
734 .then(|| Arc::new(Self { pruned: true }) as Arc<dyn ExtensionOp>)
735 }
736 }
737
738 #[test]
739 fn compile_prunes_extension_outputs_with_replacement_op() {
740 let input = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
741 let outputs =
742 crate::extension::apply(Arc::new(PrunableTestOp { pruned: false }), &[&input]).unwrap();
743
744 let program = GraphCompiler::new().compile(&outputs[1]).unwrap();
745 let pruned_instruction = program
746 .lowering_view()
747 .instructions()
748 .find_map(|inst| match inst.op() {
749 crate::GraphOpView::Extension { op }
750 if op.family_id() == "tenferro-runtime.test-prunable.v1" =>
751 {
752 Some((
753 inst.output_slots().to_vec(),
754 format!("{op:?}"),
755 op.output_count(),
756 ))
757 }
758 _ => None,
759 })
760 .expect("compiled program should contain the test extension");
761
762 assert_eq!(pruned_instruction.0.len(), 1);
763 assert!(pruned_instruction.1.contains("pruned: true"));
764 assert_eq!(pruned_instruction.2, 1);
765 }
766}