1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4
5use computegraph::compile::{compile, CompiledProgram};
6use computegraph::materialize::{
7 materialize_merge, MaterializedGraph, MaterializedOperation, MaterializedValue,
8};
9use computegraph::resolve::{resolve, ResolvedView, ValueDef};
10use computegraph::types::{OperationKey, ValueKey};
11use computegraph::GraphOperation;
12use num_complex::{Complex32, Complex64};
13use tenferro_ops::dim_expr::{DimExpr, DimExprEvalError};
14use tenferro_ops::input_key::TensorInputKey;
15use tenferro_ops::std_tensor_op::StdTensorOp;
16use tenferro_ops::{ShapeExtent, ShapeRelation, SymDim};
17use tenferro_tensor::{CacheStats, DType, SliceConfig, Tensor, TensorScalar};
18
19use super::program::CompiledGraph;
20#[cfg(test)]
21use crate::compiler::semantic_staging::stage_semantic_program;
22use crate::compiler::{lower_scoped_dim_expr, CompilerOptions};
23use crate::error::{Error, Result};
24use crate::extension_cache::{ExtensionCacheSelector, ExtensionCacheStore};
25use crate::metadata::registered_meta;
26use crate::program::{
27 CoreSemanticOp, FrozenProgram, ProgramInputSpec, ProgramShapeRelation, ProgramValueMetadata,
28 SemanticOpRef, SemanticProgramBuilder, ShapeGuard as ProgramShapeGuard,
29};
30use crate::shape_constraint::{discharge, LocalShapeConstraint, SlotScopedShapeConstraint};
31use crate::shape_infer::{infer_extension_output_meta, infer_output_shapes};
32use crate::trace::TracedGraph;
33use crate::traced::{try_concrete_shape, TracedTensor};
34
35#[derive(Clone)]
36struct InputDescriptor {
37 dtype: DType,
38 shape: Vec<usize>,
39 extent_identity: InputExtentIdentity,
40 default_tensor: Option<Arc<Tensor>>,
41}
42
43#[derive(Clone, Copy)]
44enum InputExtentIdentity {
45 Concrete,
46 Symbolic,
47}
48
49impl InputDescriptor {
50 fn semantic_shape(&self, input_idx: usize) -> Vec<DimExpr> {
51 match self.extent_identity {
52 InputExtentIdentity::Concrete => DimExpr::from_concrete(&self.shape),
53 InputExtentIdentity::Symbolic => (0..self.shape.len())
54 .map(|axis| DimExpr::InputDim { input_idx, axis })
55 .collect(),
56 }
57 }
58
59 fn constraint_guard_shape(&self, input_idx: usize) -> Vec<DimExpr> {
60 if self.default_tensor.is_some() {
61 return input_dim_shape(input_idx, self.shape.len());
62 }
63 self.semantic_shape(input_idx)
64 }
65}
66
67fn input_dim_shape(input_idx: usize, rank: usize) -> Vec<DimExpr> {
68 (0..rank)
69 .map(|axis| DimExpr::InputDim { input_idx, axis })
70 .collect()
71}
72
73pub struct GraphCompiler {
90 extension_cache: ExtensionCacheStore,
91 compiler_options: CompilerOptions,
92}
93
94impl fmt::Debug for GraphCompiler {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.debug_struct("GraphCompiler")
97 .field("extension_cache_stats", &self.cache_stats())
98 .field("compiler_options", &self.compiler_options)
99 .field("extension_cache", &self.extension_cache)
100 .finish_non_exhaustive()
101 }
102}
103
104impl GraphCompiler {
105 pub fn new() -> Self {
116 Self {
117 extension_cache: ExtensionCacheStore::new(),
118 compiler_options: CompilerOptions::default(),
119 }
120 }
121
122 pub fn with_compiler_options(compiler_options: CompilerOptions) -> Self {
139 Self {
140 extension_cache: ExtensionCacheStore::new(),
141 compiler_options,
142 }
143 }
144
145 pub fn compile(&mut self, output: &TracedTensor) -> Result<CompiledGraph> {
168 self.compile_many(&[output])
169 }
170
171 pub fn compile_traced_graph(&mut self, graph: &TracedGraph) -> Result<CompiledGraph> {
184 self.compile_frozen(graph.frozen())
185 }
186
187 pub fn compile_frozen_program(&mut self, frozen: &FrozenProgram) -> Result<CompiledGraph> {
221 self.compile_frozen(frozen)
222 }
223
224 fn compile_frozen(&mut self, frozen: &FrozenProgram) -> Result<CompiledGraph> {
225 validate_bound_shape_guards(frozen)?;
226 Ok(CompiledGraph::new(
227 frozen.clone(),
228 self.compiler_options,
229 [],
230 ))
231 }
232
233 pub fn compile_many(&mut self, outputs: &[&TracedTensor]) -> Result<CompiledGraph> {
256 let all_inputs = collect_default_inputs(outputs)?;
257 self.compile_many_with_descriptors(
258 outputs,
259 &HashMap::new(),
260 &all_inputs,
261 None,
262 false,
263 false,
264 )
265 }
266
267 pub(crate) fn compile_ad_source(&mut self, output: &TracedTensor) -> Result<CompiledGraph> {
268 let all_inputs = collect_default_inputs(&[output])?;
269 self.compile_many_with_descriptors(
270 &[output],
271 &HashMap::new(),
272 &all_inputs,
273 None,
274 true,
275 true,
276 )
277 }
278
279 pub fn compile_with_input_specs(
306 &mut self,
307 output: &TracedTensor,
308 bindings: &[(&TracedTensor, DType, &[usize])],
309 ) -> Result<CompiledGraph> {
310 let mut binding_specs = HashMap::new();
311 let mut input_order = Vec::with_capacity(bindings.len());
312 for (index, (placeholder, dtype, shape)) in bindings.iter().enumerate() {
313 validate_placeholder_spec(index, placeholder, *dtype, shape)?;
314 let key = placeholder.input_key().ok_or(Error::UnexpectedBinding {
315 binding_index: index,
316 })?;
317 if binding_specs
318 .insert(
319 key.clone(),
320 InputDescriptor {
321 dtype: *dtype,
322 shape: (*shape).to_vec(),
323 extent_identity: InputExtentIdentity::Concrete,
324 default_tensor: None,
325 },
326 )
327 .is_some()
328 {
329 return Err(Error::DuplicateBinding {
330 input_key: format!("{:?}", key),
331 });
332 }
333 input_order.push(key);
334 }
335
336 self.compile_many_with_descriptors(
337 &[output],
338 &binding_specs,
339 output.inputs_map.as_ref(),
340 Some(&input_order),
341 false,
342 false,
343 )
344 }
345
346 pub fn compiler_options(&self) -> CompilerOptions {
358 self.compiler_options
359 }
360
361 pub fn set_compiler_options(&mut self, compiler_options: CompilerOptions) {
381 if self.compiler_options == compiler_options {
382 return;
383 }
384 self.compiler_options = compiler_options;
385 self.clear_extension_caches();
386 }
387
388 pub fn clear_extension_caches(&mut self) {
400 self.extension_cache.clear();
401 }
402
403 pub fn clear_caches(&mut self) {
415 self.clear_extension_caches();
416 }
417
418 pub fn cache_stats(&self) -> CacheStats {
430 self.extension_cache.stats(ExtensionCacheSelector::All)
431 }
432
433 pub fn extension_caches(&self) -> &ExtensionCacheStore {
444 &self.extension_cache
445 }
446
447 pub fn extension_caches_mut(&mut self) -> &mut ExtensionCacheStore {
458 &mut self.extension_cache
459 }
460
461 fn compile_many_with_descriptors(
462 &mut self,
463 outputs: &[&TracedTensor],
464 binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
465 default_inputs: &HashMap<TensorInputKey, Arc<Tensor>>,
466 explicit_input_order: Option<&[TensorInputKey]>,
467 include_checkpoint_aliases: bool,
468 allow_unbound_placeholders: bool,
469 ) -> Result<CompiledGraph> {
470 let mut constraint_scopes = Vec::new();
471 let mut seen_constraint_scopes = std::collections::HashSet::new();
472 for output in outputs {
473 for scope in output.constraint_scopes.as_slice() {
474 if seen_constraint_scopes.insert(Arc::as_ptr(scope)) {
475 #[cfg(test)]
476 test_support::record_constraint_scope_clones(1);
477 constraint_scopes.push(Arc::clone(scope));
478 }
479 }
480 }
481
482 let mut roots = Vec::new();
483 let mut checkpoint_aliases = HashMap::new();
484 let mut output_keys = Vec::with_capacity(outputs.len());
485 for output in outputs {
486 roots.extend(output.resolve_roots());
487 if include_checkpoint_aliases {
488 if let Some(chain) = &output.checkpoint_chain {
489 roots.extend(chain.collect_graphs());
490 for (alias_key, target_key) in chain.collect_aliases() {
491 insert_checkpoint_alias(&mut checkpoint_aliases, alias_key, target_key)?;
492 }
493 }
494 }
495 output_keys.push(output.graph.values()[output.val].key.clone());
496 }
497
498 let view = resolve(roots);
499 let graph = if checkpoint_aliases.is_empty() {
500 materialize_merge(&view, &output_keys)
501 } else {
502 let checkpoint_alias_shapes = checkpoint_aliases
503 .keys()
504 .filter_map(|key| {
505 default_inputs
506 .get(key)
507 .map(|tensor| (key.clone(), tensor.shape().to_vec()))
508 })
509 .collect::<HashMap<_, _>>();
510 materialize_merge_with_input_aliases(
511 &view,
512 &output_keys,
513 &checkpoint_aliases,
514 &checkpoint_alias_shapes,
515 )
516 };
517 let mut compiled = compile(&graph);
518 prune_compiled_extension_outputs(&mut compiled)?;
519 let slot_by_key: HashMap<_, _> = graph
520 .values
521 .iter()
522 .enumerate()
523 .map(|(slot, value)| (value.key.clone(), slot))
524 .collect();
525 let mut scoped_constraints = Vec::new();
526 for scope in constraint_scopes {
527 for scoped in scope.constraints() {
528 let origin_slots: Vec<_> = scoped
529 .origins
530 .iter()
531 .filter_map(|key| slot_by_key.get(key).copied())
532 .collect();
533 if origin_slots.is_empty() {
534 continue;
535 }
536 let origin_instruction = origin_slots.iter().find_map(|&slot| {
537 graph
538 .values
539 .get(slot)
540 .and_then(|value| value.producer.map(|p| p.0))
541 });
542 let mut local = scoped.local.clone();
543 if let Some(instruction_index) = origin_instruction {
544 local.source = local.source.with_instruction(instruction_index);
545 }
546 let mut input_slots = Vec::with_capacity(scoped.inputs.len());
547 for (input_idx, key) in scoped.inputs.iter().enumerate() {
548 let Some(slot) = slot_by_key.get(key).copied() else {
549 return Err(Error::ShapeConstraintEvaluation {
550 family: local.source.family_id,
551 instruction_index: local.source.instruction_index,
552 relation: local.relation,
553 expression: format!("{:?}", local.lhs),
554 cause: crate::ShapeConstraintEvalError::MissingInput {
555 input_idx,
556 input_count: scoped.inputs.len(),
557 },
558 });
559 };
560 input_slots.push(slot);
561 }
562 scoped_constraints.push(SlotScopedShapeConstraint {
563 origin_slots,
564 input_slots,
565 local,
566 });
567 }
568 }
569
570 let mut descriptors = Vec::with_capacity(graph.inputs.len());
571 let mut input_keys = Vec::with_capacity(graph.inputs.len());
572 for key in &graph.inputs {
573 let ValueKey::Input(input_key) = key else {
574 return Err(Error::Internal(
575 "expected Input key in graph inputs".to_string(),
576 ));
577 };
578 let descriptor = descriptor_for_input(
579 input_key,
580 binding_specs,
581 default_inputs,
582 allow_unbound_placeholders,
583 )?;
584 descriptors.push(descriptor);
585 input_keys.push(input_key.clone());
586 }
587 if let Some(explicit_input_order) = explicit_input_order {
588 let input_position_by_key: HashMap<_, _> = graph
589 .inputs
590 .iter()
591 .enumerate()
592 .filter_map(|(position, key)| match key {
593 ValueKey::Input(key) => Some((key.clone(), position)),
594 _ => None,
595 })
596 .collect();
597 let mut ordered_positions = Vec::with_capacity(graph.inputs.len());
598 let mut selected_positions = vec![false; graph.inputs.len()];
599 for key in explicit_input_order {
600 if let Some(&position) = input_position_by_key.get(key) {
601 ordered_positions.push(position);
602 selected_positions[position] = true;
603 }
604 }
605 for (position, selected) in selected_positions.iter().enumerate() {
606 if !selected {
607 ordered_positions.push(position);
608 }
609 }
610 compiled.input_slots = ordered_positions
611 .iter()
612 .map(|&position| compiled.input_slots[position])
613 .collect();
614 descriptors = ordered_positions
615 .iter()
616 .map(|&position| descriptors[position].clone())
617 .collect();
618 input_keys = ordered_positions
619 .into_iter()
620 .map(|position| input_keys[position].clone())
621 .collect();
622 }
623
624 let semantic =
625 compile_materialized_semantic_program(&compiled, &descriptors, &scoped_constraints)?;
626 validate_bound_shape_guards(&semantic)?;
627 Ok(CompiledGraph::new(
628 semantic,
629 self.compiler_options,
630 input_keys,
631 ))
632 }
633}
634
635fn validate_bound_shape_guards(frozen: &FrozenProgram) -> Result<()> {
636 let input_shapes = compile_time_input_shapes(frozen)?;
637 for operation in frozen.program.operations() {
638 let fallback_family = match operation.op() {
639 SemanticOpRef::Core(_) => "tenferro-runtime.core.v1",
640 SemanticOpRef::Extension(extension) => extension.family_id(),
641 };
642 for guard in operation.shape_guards() {
643 if guard.source_family().is_some() {
644 continue;
645 }
646 validate_bound_shape_guard(guard, fallback_family, &input_shapes)?;
647 }
648 }
649 Ok(())
650}
651
652fn compile_time_input_shapes(frozen: &FrozenProgram) -> Result<Vec<Option<Vec<usize>>>> {
653 let metadata = frozen.input_metadata_with_bound_shapes();
654 frozen
655 .program
656 .inputs()
657 .iter()
658 .enumerate()
659 .map(|(input_idx, &input)| {
660 if let Some(tensor) = frozen.bindings.tensor_ref_for_input(input) {
661 return Ok(Some(tensor.shape().to_vec()));
662 }
663 let Some(metadata) = metadata.get(input_idx) else {
664 return Err(invalid_compiled_graph(format!(
665 "semantic input metadata index {input_idx} is outside metadata table"
666 )));
667 };
668 concrete_shape_from_input_metadata(metadata)
669 })
670 .collect()
671}
672
673fn concrete_shape_from_input_metadata(
674 metadata: &ProgramValueMetadata,
675) -> Result<Option<Vec<usize>>> {
676 let mut shape = Vec::with_capacity(metadata.shape().len());
677 for extent in metadata.shape() {
678 let ShapeExtent::Exact(expression) = extent else {
679 return Ok(None);
680 };
681 let Some(value) = evaluate_static_dim_expr(expression)? else {
682 return Ok(None);
683 };
684 shape.push(value);
685 }
686 Ok(Some(shape))
687}
688
689fn validate_bound_shape_guard(
690 guard: &ProgramShapeGuard,
691 fallback_family: &'static str,
692 input_shapes: &[Option<Vec<usize>>],
693) -> Result<()> {
694 let ProgramShapeRelation::Equal = guard.relation() else {
695 return Ok(());
696 };
697 let family = guard.source_family().unwrap_or(fallback_family);
698 let relation = ShapeRelation::Equal;
699 let lhs = evaluate_bound_shape_guard_expression(family, relation, guard.lhs(), input_shapes)?;
700 let rhs = evaluate_bound_shape_guard_expression(family, relation, guard.rhs(), input_shapes)?;
701 let (Some(lhs), Some(rhs)) = (lhs, rhs) else {
702 return Ok(());
703 };
704 if lhs == rhs {
705 return Ok(());
706 }
707 Err(Error::ShapeConstraintViolation {
708 family,
709 instruction_index: None,
710 relation,
711 lhs_expr: format!("{:?}", guard.lhs()),
712 rhs_expr: format!("{:?}", guard.rhs()),
713 lhs_value: lhs,
714 rhs_value: rhs,
715 })
716}
717
718fn evaluate_bound_shape_guard_expression(
719 family: &'static str,
720 relation: ShapeRelation,
721 expression: &DimExpr,
722 input_shapes: &[Option<Vec<usize>>],
723) -> Result<Option<usize>> {
724 evaluate_static_dim_expr_with_inputs(expression, input_shapes).map_err(|cause| {
725 Error::ShapeConstraintEvaluation {
726 family,
727 instruction_index: None,
728 relation,
729 expression: format!("{expression:?}"),
730 cause: cause.into(),
731 }
732 })
733}
734
735fn evaluate_static_dim_expr(expression: &DimExpr) -> Result<Option<usize>> {
736 evaluate_static_dim_expr_without_inputs(expression).map_err(|cause| {
737 Error::ShapeConstraintEvaluation {
738 family: "tenferro-runtime.input.v1",
739 instruction_index: None,
740 relation: ShapeRelation::Equal,
741 expression: format!("{expression:?}"),
742 cause: cause.into(),
743 }
744 })
745}
746
747fn evaluate_static_dim_expr_without_inputs(
748 expression: &DimExpr,
749) -> std::result::Result<Option<usize>, DimExprEvalError> {
750 match expression {
751 DimExpr::Const(value) => Ok(Some(*value)),
752 DimExpr::InputDim { .. } => Ok(None),
753 DimExpr::Add(a, b) => {
754 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
755 return Ok(None);
756 };
757 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
758 return Ok(None);
759 };
760 lhs.checked_add(rhs)
761 .map(Some)
762 .ok_or(DimExprEvalError::AddOverflow { lhs, rhs })
763 }
764 DimExpr::Sub(a, b) => {
765 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
766 return Ok(None);
767 };
768 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
769 return Ok(None);
770 };
771 lhs.checked_sub(rhs)
772 .map(Some)
773 .ok_or(DimExprEvalError::SubUnderflow { lhs, rhs })
774 }
775 DimExpr::Mul(a, b) => {
776 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
777 return Ok(None);
778 };
779 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
780 return Ok(None);
781 };
782 lhs.checked_mul(rhs)
783 .map(Some)
784 .ok_or(DimExprEvalError::MulOverflow { lhs, rhs })
785 }
786 DimExpr::FloorDiv(a, b) => {
787 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
788 return Ok(None);
789 };
790 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
791 return Ok(None);
792 };
793 if rhs == 0 {
794 return Err(DimExprEvalError::FloorDivByZero { lhs, rhs });
795 }
796 Ok(Some(lhs / rhs))
797 }
798 DimExpr::Min(a, b) => {
799 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
800 return Ok(None);
801 };
802 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
803 return Ok(None);
804 };
805 Ok(Some(lhs.min(rhs)))
806 }
807 DimExpr::Max(a, b) => {
808 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
809 return Ok(None);
810 };
811 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
812 return Ok(None);
813 };
814 Ok(Some(lhs.max(rhs)))
815 }
816 }
817}
818
819fn evaluate_static_dim_expr_with_inputs(
820 expression: &DimExpr,
821 input_shapes: &[Option<Vec<usize>>],
822) -> std::result::Result<Option<usize>, DimExprEvalError> {
823 match expression {
824 DimExpr::Const(value) => Ok(Some(*value)),
825 DimExpr::InputDim { input_idx, axis } => match input_shapes.get(*input_idx) {
826 Some(Some(shape)) => {
827 shape
828 .get(*axis)
829 .copied()
830 .map(Some)
831 .ok_or(DimExprEvalError::AxisOutOfBounds {
832 input_idx: *input_idx,
833 axis: *axis,
834 rank: shape.len(),
835 })
836 }
837 Some(None) => Ok(None),
838 None => Err(DimExprEvalError::InputOutOfBounds {
839 input_idx: *input_idx,
840 input_count: input_shapes.len(),
841 }),
842 },
843 DimExpr::Add(a, b) => {
844 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
845 return Ok(None);
846 };
847 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
848 return Ok(None);
849 };
850 lhs.checked_add(rhs)
851 .map(Some)
852 .ok_or(DimExprEvalError::AddOverflow { lhs, rhs })
853 }
854 DimExpr::Sub(a, b) => {
855 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
856 return Ok(None);
857 };
858 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
859 return Ok(None);
860 };
861 lhs.checked_sub(rhs)
862 .map(Some)
863 .ok_or(DimExprEvalError::SubUnderflow { lhs, rhs })
864 }
865 DimExpr::Mul(a, b) => {
866 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
867 return Ok(None);
868 };
869 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
870 return Ok(None);
871 };
872 lhs.checked_mul(rhs)
873 .map(Some)
874 .ok_or(DimExprEvalError::MulOverflow { lhs, rhs })
875 }
876 DimExpr::FloorDiv(a, b) => {
877 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
878 return Ok(None);
879 };
880 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
881 return Ok(None);
882 };
883 if rhs == 0 {
884 return Err(DimExprEvalError::FloorDivByZero { lhs, rhs });
885 }
886 Ok(Some(lhs / rhs))
887 }
888 DimExpr::Min(a, b) => {
889 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
890 return Ok(None);
891 };
892 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
893 return Ok(None);
894 };
895 Ok(Some(lhs.min(rhs)))
896 }
897 DimExpr::Max(a, b) => {
898 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
899 return Ok(None);
900 };
901 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
902 return Ok(None);
903 };
904 Ok(Some(lhs.max(rhs)))
905 }
906 }
907}
908
909fn compile_materialized_semantic_program(
910 compiled: &CompiledProgram<StdTensorOp>,
911 descriptors: &[InputDescriptor],
912 scoped_constraints: &[SlotScopedShapeConstraint],
913) -> Result<FrozenProgram> {
914 if compiled.input_slots.len() != descriptors.len() {
915 return Err(Error::runtime_state(
916 "graph_compile_semantic",
917 crate::ErrorPhase::Compile,
918 "materialized input count does not match semantic descriptors",
919 ));
920 }
921
922 let mut builder = SemanticProgramBuilder::new();
923 let mut values = vec![None; compiled.n_slots];
924 let mut slot_shapes = vec![None; compiled.n_slots];
925 let mut guard_slot_shapes = vec![None; compiled.n_slots];
926 let mut slot_dtypes = vec![None; compiled.n_slots];
927 for (input_idx, (&slot, descriptor)) in compiled.input_slots.iter().zip(descriptors).enumerate()
928 {
929 let Some(value_slot) = values.get_mut(slot) else {
930 return Err(invalid_compiled_graph(format!(
931 "semantic input slot {slot} is outside slot table of length {}",
932 compiled.n_slots
933 )));
934 };
935 let semantic_shape = descriptor.semantic_shape(input_idx);
936 let value = builder
937 .input(ProgramInputSpec::new(
938 descriptor.dtype,
939 semantic_shape.clone(),
940 ))
941 .map_err(semantic_build_error)?;
942 if let Some(tensor) = &descriptor.default_tensor {
943 builder
944 .bind_input(value, Arc::clone(tensor))
945 .map_err(semantic_build_error)?;
946 }
947 *value_slot = Some(value);
948 slot_shapes[slot] = Some(semantic_shape);
949 guard_slot_shapes[slot] = Some(descriptor.constraint_guard_shape(input_idx));
950 slot_dtypes[slot] = Some(descriptor.dtype);
951 }
952
953 for instruction in &compiled.instructions {
954 let inputs = instruction
955 .inputs
956 .iter()
957 .map(|&slot| {
958 values.get(slot).and_then(|value| *value).ok_or_else(|| {
959 invalid_compiled_graph(format!(
960 "semantic operation input slot {slot} is unavailable"
961 ))
962 })
963 })
964 .collect::<Result<Vec<_>>>()?;
965 let guard_input_shapes = instruction
966 .inputs
967 .iter()
968 .map(|&slot| {
969 guard_slot_shapes
970 .get(slot)
971 .and_then(|shape| shape.as_deref())
972 .ok_or_else(|| {
973 invalid_compiled_graph(format!(
974 "semantic operation guard-shape input slot {slot} is unavailable"
975 ))
976 })
977 })
978 .collect::<Result<Vec<_>>>()?;
979 let guard_output_shapes = match &instruction.operation {
980 StdTensorOp::Extension(extension) => {
981 let input_dtypes = instruction
982 .inputs
983 .iter()
984 .map(|&slot| {
985 slot_dtypes
986 .get(slot)
987 .and_then(|dtype| *dtype)
988 .ok_or_else(|| {
989 invalid_compiled_graph(format!(
990 "semantic operation dtype input slot {slot} is unavailable"
991 ))
992 })
993 })
994 .collect::<Result<Vec<_>>>()?;
995 infer_extension_output_meta(extension.as_ref(), &input_dtypes, &guard_input_shapes)?
996 .into_iter()
997 .map(|(_dtype, shape)| shape)
998 .collect::<Vec<_>>()
999 }
1000 operation => infer_output_shapes(operation, &guard_input_shapes)?,
1001 };
1002 let outputs = match &instruction.operation {
1003 StdTensorOp::Extension(extension) => builder
1004 .add_extension(Arc::clone(extension), &inputs)
1005 .map_err(semantic_build_error)?,
1006 operation => builder
1007 .add_op(
1008 CoreSemanticOp::try_from(operation).map_err(|source| {
1009 Error::runtime_state_source(
1010 "graph_compile_semantic",
1011 crate::ErrorPhase::Compile,
1012 source,
1013 )
1014 })?,
1015 &inputs,
1016 )
1017 .map_err(semantic_build_error)?,
1018 };
1019 if outputs.len() != instruction.outputs.len() {
1020 return Err(invalid_compiled_graph(format!(
1021 "semantic operation produced {} outputs for {} materialized slots",
1022 outputs.len(),
1023 instruction.outputs.len()
1024 )));
1025 }
1026 if guard_output_shapes.len() != instruction.outputs.len() {
1027 return Err(invalid_compiled_graph(format!(
1028 "semantic operation inferred {} guard shapes for {} materialized slots",
1029 guard_output_shapes.len(),
1030 instruction.outputs.len()
1031 )));
1032 }
1033 for ((&slot, &value), guard_shape) in instruction
1034 .outputs
1035 .iter()
1036 .zip(outputs.iter())
1037 .zip(guard_output_shapes.iter())
1038 {
1039 let Some(value_slot) = values.get_mut(slot) else {
1040 return Err(invalid_compiled_graph(format!(
1041 "semantic output slot {slot} is outside slot table of length {}",
1042 compiled.n_slots
1043 )));
1044 };
1045 if value_slot.replace(value).is_some() {
1046 return Err(invalid_compiled_graph(format!(
1047 "semantic output slot {slot} has multiple producers"
1048 )));
1049 }
1050 let metadata = builder
1051 .value_metadata(value)
1052 .map_err(semantic_build_error)?;
1053 slot_shapes[slot] = Some(
1054 metadata
1055 .shape()
1056 .iter()
1057 .enumerate()
1058 .map(|(axis, extent)| match extent {
1059 tenferro_ops::ShapeExtent::Exact(expression)
1060 | tenferro_ops::ShapeExtent::UpperBound(expression) => expression.clone(),
1061 tenferro_ops::ShapeExtent::Unknown => DimExpr::InputDim {
1062 input_idx: slot,
1063 axis,
1064 },
1065 })
1066 .collect(),
1067 );
1068 guard_slot_shapes[slot] = Some(guard_shape.clone());
1069 slot_dtypes[slot] = Some(metadata.dtype());
1070 }
1071 }
1072
1073 for scoped in scoped_constraints {
1074 let target = scoped
1075 .origin_slots
1076 .iter()
1077 .find_map(|&slot| values.get(slot).and_then(|value| *value))
1078 .ok_or_else(|| {
1079 invalid_compiled_graph(
1080 "semantic shape constraint has no available origin".to_string(),
1081 )
1082 })?;
1083 let lhs = lower_scoped_dim_expr(
1084 &scoped.local.lhs,
1085 &scoped.input_slots,
1086 &guard_slot_shapes,
1087 &scoped.local,
1088 )?;
1089 let rhs = lower_scoped_dim_expr(
1090 &scoped.local.rhs,
1091 &scoped.input_slots,
1092 &guard_slot_shapes,
1093 &scoped.local,
1094 )?;
1095 let relation = match scoped.local.relation {
1096 tenferro_ops::ShapeRelation::Equal => ProgramShapeRelation::Equal,
1097 };
1098 let lowered = LocalShapeConstraint {
1099 source: scoped.local.source.clone(),
1100 relation: scoped.local.relation,
1101 lhs,
1102 rhs,
1103 };
1104 let retained_guards = discharge(vec![lowered])?;
1105 if retained_guards.is_empty() {
1106 continue;
1107 }
1108 let guards = retained_guards.into_iter().map(|guard| {
1109 ProgramShapeGuard::new(relation, guard.lhs, guard.rhs)
1110 .with_source_family(guard.source.family_id)
1111 });
1112 builder
1113 .add_shape_guards_to_output(target, guards)
1114 .map_err(semantic_build_error)?;
1115 }
1116
1117 let outputs = compiled
1118 .output_slots
1119 .iter()
1120 .map(|&slot| {
1121 values.get(slot).and_then(|value| *value).ok_or_else(|| {
1122 invalid_compiled_graph(format!(
1123 "semantic program output slot {slot} is unavailable"
1124 ))
1125 })
1126 })
1127 .collect::<Result<Vec<_>>>()?;
1128 builder.finish(&outputs).map_err(|source| {
1129 Error::runtime_state_source("graph_compile_semantic", crate::ErrorPhase::Compile, source)
1130 })
1131}
1132
1133fn semantic_build_error(source: crate::program::ProgramBuildError) -> Error {
1134 Error::runtime_state_source("graph_compile_semantic", crate::ErrorPhase::Compile, source)
1135}
1136
1137impl Default for GraphCompiler {
1138 fn default() -> Self {
1139 Self::new()
1140 }
1141}
1142
1143fn collect_default_inputs(
1144 outputs: &[&TracedTensor],
1145) -> Result<HashMap<TensorInputKey, Arc<Tensor>>> {
1146 let mut all_inputs = HashMap::new();
1147 for output in outputs {
1148 for (key, tensor) in output.inputs_map.iter() {
1149 if let Some(existing) = all_inputs.get(key) {
1150 if !default_tensors_equivalent(existing, tensor) {
1151 return Err(Error::DuplicateBinding {
1152 input_key: format!("{:?}", key),
1153 });
1154 }
1155 continue;
1156 }
1157 all_inputs.insert(key.clone(), tensor.clone());
1158 }
1159 }
1160 Ok(all_inputs)
1161}
1162
1163fn insert_checkpoint_alias(
1164 aliases: &mut HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1165 alias_key: TensorInputKey,
1166 target_key: ValueKey<StdTensorOp>,
1167) -> Result<()> {
1168 if let Some(existing) = aliases.get(&alias_key) {
1169 if existing != &target_key {
1170 return Err(Error::Internal(format!(
1171 "checkpoint alias {alias_key:?} targets both {existing:?} and {target_key:?}"
1172 )));
1173 }
1174 return Ok(());
1175 }
1176 aliases.insert(alias_key, target_key);
1177 Ok(())
1178}
1179
1180struct AliasAwareMaterializer<'a> {
1181 view: &'a ResolvedView<StdTensorOp>,
1182 aliases: &'a HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1183 alias_shapes: &'a HashMap<TensorInputKey, Vec<usize>>,
1184 val_map: HashMap<ValueKey<StdTensorOp>, usize>,
1185 op_map: HashMap<Arc<OperationKey<StdTensorOp>>, usize>,
1186 values: Vec<MaterializedValue<StdTensorOp>>,
1187 operations: Vec<MaterializedOperation<StdTensorOp>>,
1188 input_keys: Vec<ValueKey<StdTensorOp>>,
1189}
1190
1191impl<'a> AliasAwareMaterializer<'a> {
1192 fn new(
1193 view: &'a ResolvedView<StdTensorOp>,
1194 aliases: &'a HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1195 alias_shapes: &'a HashMap<TensorInputKey, Vec<usize>>,
1196 ) -> Self {
1197 Self {
1198 view,
1199 aliases,
1200 alias_shapes,
1201 val_map: HashMap::new(),
1202 op_map: HashMap::new(),
1203 values: Vec::new(),
1204 operations: Vec::new(),
1205 input_keys: Vec::new(),
1206 }
1207 }
1208
1209 fn visit(&mut self, key: &ValueKey<StdTensorOp>) -> usize {
1210 if let Some(&index) = self.val_map.get(key) {
1211 return index;
1212 }
1213 if let Some(target) = self.alias_target(key).cloned() {
1214 let index = self.visit(&target);
1215 let index = self.refine_alias_to_checkpoint_shape(key, index);
1216 self.val_map.insert(key.clone(), index);
1217 return index;
1218 }
1219
1220 let resolved = self.view.resolve_value(key);
1221 assert!(
1222 resolved.is_some(),
1223 "key not found in resolved view: {:?}",
1224 key
1225 );
1226 match resolved {
1227 Some(ValueDef::Input { .. }) => self.materialize_input(key),
1228 Some(ValueDef::Produced {
1229 operation,
1230 input_keys,
1231 role,
1232 output_slot,
1233 }) => self.materialize_produced(operation, input_keys, role, output_slot),
1234 None => unreachable!("asserted above"),
1235 }
1236 }
1237
1238 fn alias_target(&self, key: &ValueKey<StdTensorOp>) -> Option<&ValueKey<StdTensorOp>> {
1239 let ValueKey::Input(input_key) = key else {
1240 return None;
1241 };
1242 self.aliases.get(input_key)
1243 }
1244
1245 fn refine_alias_to_checkpoint_shape(
1246 &mut self,
1247 key: &ValueKey<StdTensorOp>,
1248 target_index: usize,
1249 ) -> usize {
1250 let ValueKey::Input(input_key) = key else {
1251 return target_index;
1252 };
1253 let Some(shape) = self.alias_shapes.get(input_key) else {
1254 return target_index;
1255 };
1256 let target_key = self.values[target_index].key.clone();
1257 let rank = shape.len();
1258 self.materialize_produced(
1259 StdTensorOp::Slice(SliceConfig {
1260 starts: vec![0; rank],
1261 limits: shape.clone(),
1262 strides: vec![1; rank],
1263 }),
1264 vec![target_key],
1265 computegraph::types::OperationRole::Primary,
1266 0,
1267 )
1268 }
1269
1270 fn materialize_input(&mut self, key: &ValueKey<StdTensorOp>) -> usize {
1271 let index = self.values.len();
1272 self.values.push(MaterializedValue {
1273 key: key.clone(),
1274 producer: None,
1275 });
1276 self.val_map.insert(key.clone(), index);
1277 self.input_keys.push(key.clone());
1278 index
1279 }
1280
1281 fn materialize_produced(
1282 &mut self,
1283 operation: StdTensorOp,
1284 input_keys: Vec<ValueKey<StdTensorOp>>,
1285 role: computegraph::types::OperationRole,
1286 output_slot: usize,
1287 ) -> usize {
1288 let op_key = Arc::new(OperationKey::new(
1289 operation.clone(),
1290 input_keys.clone(),
1291 role.clone(),
1292 ));
1293
1294 if self.op_map.contains_key(&op_key) {
1295 let output_key = ValueKey::Derived {
1296 operation: op_key,
1297 output_slot: output_slot as u8,
1298 };
1299 let val_index = self.val_map.get(&output_key).copied();
1300 assert!(
1301 val_index.is_some(),
1302 "materialized op {:?} is missing output slot {}",
1303 operation,
1304 output_slot
1305 );
1306 return match val_index {
1307 Some(index) => index,
1308 None => unreachable!("asserted above"),
1309 };
1310 }
1311
1312 let materialized_inputs = input_keys.iter().map(|input| self.visit(input)).collect();
1313 let op_index = self.operations.len();
1314 self.op_map.insert(Arc::clone(&op_key), op_index);
1315 self.operations.push(MaterializedOperation {
1316 operation: operation.clone(),
1317 inputs: materialized_inputs,
1318 outputs: Vec::with_capacity(operation.output_count()),
1319 role,
1320 });
1321
1322 for slot in 0..operation.output_count() {
1323 let output_key = ValueKey::Derived {
1324 operation: Arc::clone(&op_key),
1325 output_slot: slot as u8,
1326 };
1327 let val_index = self.values.len();
1328 self.values.push(MaterializedValue {
1329 key: output_key.clone(),
1330 producer: Some((op_index, slot)),
1331 });
1332 self.val_map.insert(output_key, val_index);
1333 self.operations[op_index].outputs.push(val_index);
1334 }
1335
1336 self.operations[op_index].outputs[output_slot]
1337 }
1338}
1339
1340fn materialize_merge_with_input_aliases(
1341 view: &ResolvedView<StdTensorOp>,
1342 outputs: &[ValueKey<StdTensorOp>],
1343 aliases: &HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1344 alias_shapes: &HashMap<TensorInputKey, Vec<usize>>,
1345) -> MaterializedGraph<StdTensorOp> {
1346 let mut materializer = AliasAwareMaterializer::new(view, aliases, alias_shapes);
1347 let mut materialized_outputs = Vec::with_capacity(outputs.len());
1348
1349 for output in outputs {
1350 let output_slot = materializer.visit(output);
1351 materialized_outputs.push(materializer.values[output_slot].key.clone());
1352 }
1353
1354 MaterializedGraph {
1355 values: materializer.values,
1356 operations: materializer.operations,
1357 inputs: materializer.input_keys,
1358 outputs: materialized_outputs,
1359 }
1360}
1361
1362fn validate_placeholder_spec(
1363 index: usize,
1364 placeholder: &TracedTensor,
1365 dtype: DType,
1366 shape: &[usize],
1367) -> Result<()> {
1368 if placeholder.data.is_some() {
1369 return Err(Error::UnexpectedBinding {
1370 binding_index: index,
1371 });
1372 }
1373 placeholder.input_key().ok_or(Error::UnexpectedBinding {
1374 binding_index: index,
1375 })?;
1376
1377 if placeholder.dtype != dtype {
1378 return Err(Error::PlaceholderDtypeMismatch {
1379 expected: placeholder.dtype,
1380 actual: dtype,
1381 });
1382 }
1383 validate_placeholder_shape(placeholder, shape)
1384}
1385
1386fn validate_placeholder_shape(placeholder: &TracedTensor, shape: &[usize]) -> Result<()> {
1387 match try_concrete_shape(placeholder) {
1388 Some(expected_shape) => {
1389 if expected_shape.as_slice() != shape {
1390 return Err(Error::PlaceholderShapeMismatch {
1391 expected: expected_shape,
1392 actual: shape.to_vec(),
1393 });
1394 }
1395 }
1396 None => {
1397 if placeholder.rank != shape.len() {
1398 return Err(Error::PlaceholderRankMismatch {
1399 expected: placeholder.rank,
1400 actual: shape.len(),
1401 });
1402 }
1403 }
1404 }
1405 Ok(())
1406}
1407
1408fn descriptor_for_input(
1409 key: &TensorInputKey,
1410 binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
1411 default_inputs: &HashMap<TensorInputKey, Arc<Tensor>>,
1412 allow_unbound_placeholders: bool,
1413) -> Result<InputDescriptor> {
1414 if let Some(tensor) = default_inputs.get(key) {
1415 return Ok(InputDescriptor {
1416 dtype: tensor.dtype(),
1417 shape: tensor.shape().to_vec(),
1418 extent_identity: default_input_extent_identity(key, tensor)?,
1419 default_tensor: Some(tensor.clone()),
1420 });
1421 }
1422 if let Some(spec) = binding_specs.get(key) {
1423 return Ok(spec.clone());
1424 }
1425 if allow_unbound_placeholders {
1426 return descriptor_for_unbound_input(key);
1427 }
1428 Err(Error::UnboundPlaceholder {
1429 input_key: format!("{:?}", key),
1430 })
1431}
1432
1433fn descriptor_for_unbound_input(key: &TensorInputKey) -> Result<InputDescriptor> {
1434 let metadata = registered_meta(&ValueKey::Input(key.clone()))?;
1435 if let Some(shape) = metadata
1436 .exact_shape()
1437 .as_deref()
1438 .and_then(concrete_shape_from_sym_dims)
1439 {
1440 return Ok(InputDescriptor {
1441 dtype: metadata.dtype,
1442 shape,
1443 extent_identity: InputExtentIdentity::Concrete,
1444 default_tensor: None,
1445 });
1446 }
1447 Ok(InputDescriptor {
1448 dtype: metadata.dtype,
1449 shape: vec![0; metadata.rank()],
1450 extent_identity: InputExtentIdentity::Symbolic,
1451 default_tensor: None,
1452 })
1453}
1454
1455fn concrete_shape_from_sym_dims(shape: &[SymDim]) -> Option<Vec<usize>> {
1456 shape.iter().map(SymDim::constant_value).collect()
1457}
1458
1459fn default_input_extent_identity(
1460 key: &TensorInputKey,
1461 tensor: &Tensor,
1462) -> Result<InputExtentIdentity> {
1463 let metadata = registered_meta(&ValueKey::Input(key.clone()))?;
1464 let exact_shape = metadata.exact_shape();
1465 if metadata.dtype == tensor.dtype()
1466 && exact_shape_matches_tensor_shape(exact_shape.as_deref(), tensor.shape())
1467 {
1468 Ok(InputExtentIdentity::Concrete)
1469 } else {
1470 Ok(InputExtentIdentity::Symbolic)
1471 }
1472}
1473
1474fn exact_shape_matches_tensor_shape(
1475 shape: Option<&[tenferro_ops::SymDim]>,
1476 tensor_shape: &[usize],
1477) -> bool {
1478 let Some(shape) = shape else {
1479 return false;
1480 };
1481 shape.len() == tensor_shape.len()
1482 && shape
1483 .iter()
1484 .zip(tensor_shape)
1485 .all(|(dim, &extent)| dim.constant_value() == Some(extent))
1486}
1487
1488fn prune_compiled_extension_outputs(prog: &mut CompiledProgram<StdTensorOp>) -> Result<()> {
1489 let mut live_slots = vec![false; prog.n_slots];
1490 for &slot in &prog.output_slots {
1491 let Some(live) = live_slots.get_mut(slot) else {
1492 return Err(invalid_compiled_graph(format!(
1493 "program output slot {slot} is outside slot table of length {}",
1494 prog.n_slots
1495 )));
1496 };
1497 *live = true;
1498 }
1499
1500 for instr in prog.instructions.iter_mut().rev() {
1501 let live_outputs = instr
1502 .outputs
1503 .iter()
1504 .map(|&slot| {
1505 live_slots.get(slot).copied().ok_or_else(|| {
1506 invalid_compiled_graph(format!(
1507 "instruction output slot {slot} is outside slot table of length {}",
1508 prog.n_slots
1509 ))
1510 })
1511 })
1512 .collect::<Result<Vec<_>>>()?;
1513
1514 if let StdTensorOp::Extension(ext) = &instr.operation {
1515 if let Some(pruned) = ext.prune_outputs(&live_outputs) {
1516 let kept_outputs = instr
1517 .outputs
1518 .iter()
1519 .zip(live_outputs.iter())
1520 .filter_map(|(&slot, &live)| live.then_some(slot))
1521 .collect::<Vec<_>>();
1522 if pruned.output_count() != kept_outputs.len() {
1523 return Err(invalid_compiled_graph(format!(
1524 "extension family_id={:?} pruned to {} outputs for {} live slots",
1525 ext.family_id(),
1526 pruned.output_count(),
1527 kept_outputs.len()
1528 )));
1529 }
1530 instr.operation = StdTensorOp::Extension(pruned);
1531 instr.outputs = kept_outputs;
1532 }
1533 }
1534
1535 if live_outputs.iter().any(|&live| live) {
1536 for &slot in &instr.inputs {
1537 let Some(live) = live_slots.get_mut(slot) else {
1538 return Err(invalid_compiled_graph(format!(
1539 "instruction input slot {slot} is outside slot table of length {}",
1540 prog.n_slots
1541 )));
1542 };
1543 *live = true;
1544 }
1545 }
1546 }
1547
1548 Ok(())
1549}
1550
1551fn invalid_compiled_graph(message: impl Into<String>) -> Error {
1552 Error::Internal(message.into())
1553}
1554
1555fn default_tensors_equivalent(lhs: &Arc<Tensor>, rhs: &Arc<Tensor>) -> bool {
1556 if Arc::ptr_eq(lhs, rhs) {
1557 return true;
1558 }
1559 if lhs.dtype() != rhs.dtype() || lhs.shape() != rhs.shape() {
1560 return false;
1561 }
1562 match lhs.dtype() {
1563 DType::F32 => default_slices_equivalent::<f32>(lhs, rhs),
1564 DType::F64 => default_slices_equivalent::<f64>(lhs, rhs),
1565 DType::I32 => default_slices_equivalent::<i32>(lhs, rhs),
1566 DType::I64 => default_slices_equivalent::<i64>(lhs, rhs),
1567 DType::Bool => default_slices_equivalent::<bool>(lhs, rhs),
1568 DType::C32 => default_slices_equivalent::<Complex32>(lhs, rhs),
1569 DType::C64 => default_slices_equivalent::<Complex64>(lhs, rhs),
1570 }
1571}
1572
1573fn default_slices_equivalent<T: TensorScalar + PartialEq>(lhs: &Tensor, rhs: &Tensor) -> bool {
1574 match (lhs.as_slice::<T>(), rhs.as_slice::<T>()) {
1575 (Ok(lhs), Ok(rhs)) => lhs == rhs,
1576 _ => false,
1579 }
1580}
1581
1582#[cfg(test)]
1583mod constraint_scope_tests;
1584
1585#[cfg(test)]
1586mod test_support;
1587
1588#[cfg(test)]
1589mod tests {
1590 use super::*;
1591 use std::any::Any;
1592 use std::hash::Hasher;
1593 use std::sync::Arc;
1594 use tenferro_ops::{
1595 ext_op::{ExtensionAliasDeclaration, ExtensionEffectDeclaration, ExtensionOp},
1596 SymDim,
1597 };
1598 use tenferro_tensor::{
1599 Buffer, BufferHandle, DeviceId, DeviceKind, GpuBackendKind, MemoryKind, Placement,
1600 TypedTensor,
1601 };
1602
1603 #[test]
1604 fn compile_publishes_semantic_program_and_separate_default_bindings() {
1605 let input = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1606 let output = input.neg().unwrap();
1607
1608 let program = GraphCompiler::new().compile(&output).unwrap();
1609
1610 assert_eq!(program.program().inputs().len(), 1);
1611 assert_eq!(program.program().outputs().len(), 1);
1612 assert_eq!(program.program().operations().count(), 1);
1613 assert_eq!(program.bindings().len(), 1);
1614 assert_eq!(
1615 program
1616 .program()
1617 .value_metadata(program.program().inputs()[0])
1618 .unwrap()
1619 .shape(),
1620 &[ShapeExtent::Exact(DimExpr::Const(2))]
1621 );
1622 assert_eq!(
1623 program
1624 .program()
1625 .value_metadata(program.program().outputs()[0])
1626 .unwrap()
1627 .dtype(),
1628 DType::F64
1629 );
1630 }
1631
1632 #[test]
1633 fn compile_preserves_symbolic_default_input_extent_identity() {
1634 let tensor = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1635 let input = TracedTensor::from_tensor_symbolic_shape(tensor).unwrap();
1636 let output = input.neg().unwrap();
1637
1638 let program = GraphCompiler::new().compile(&output).unwrap();
1639
1640 assert!(matches!(
1641 program
1642 .program()
1643 .value_metadata(program.program().inputs()[0])
1644 .unwrap()
1645 .shape(),
1646 [ShapeExtent::Exact(DimExpr::InputDim {
1647 input_idx: 0,
1648 axis: 0
1649 })]
1650 ));
1651 }
1652
1653 #[test]
1654 fn compile_many_rejects_conflicting_default_inputs_for_same_key() {
1655 let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
1656 let y1 = x.neg().unwrap();
1657 let mut y2 = x.neg().unwrap();
1658 let key = x.input_key().expect("concrete traced tensor has input key");
1659 let replacement = Arc::new(Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap());
1660 let mut inputs = (*y2.inputs_map).clone();
1661 inputs.insert(key.clone(), replacement);
1662 y2.inputs_map = Arc::new(inputs);
1663
1664 let err = GraphCompiler::new().compile_many(&[&y1, &y2]).unwrap_err();
1665
1666 assert!(matches!(
1667 err,
1668 Error::DuplicateBinding { ref input_key } if input_key.contains(&format!("{key:?}"))
1669 ));
1670 }
1671
1672 #[test]
1673 fn default_tensors_equivalent_rejects_distinct_backend_buffers() {
1674 let placement = Placement {
1675 memory_kind: MemoryKind::Device,
1676 device: Some(DeviceId {
1677 kind: DeviceKind::Gpu(GpuBackendKind::Cuda),
1678 ordinal: 0,
1679 }),
1680 cpu_affinity: None,
1681 };
1682 let lhs = Arc::new(Tensor::F64(
1683 TypedTensor::from_buffer_col_major(
1684 vec![2],
1685 Buffer::Backend(Arc::new(BufferHandle::<f64>::new_with_len(1, 2))),
1686 placement.clone(),
1687 )
1688 .unwrap(),
1689 ));
1690 let rhs = Arc::new(Tensor::F64(
1691 TypedTensor::from_buffer_col_major(
1692 vec![2],
1693 Buffer::Backend(Arc::new(BufferHandle::<f64>::new_with_len(2, 2))),
1694 placement,
1695 )
1696 .unwrap(),
1697 ));
1698
1699 assert!(
1700 !default_tensors_equivalent(&lhs, &rhs),
1701 "distinct backend-resident default tensors must not compare equal just because both are unreadable on host"
1702 );
1703 assert!(default_tensors_equivalent(&lhs, &lhs));
1704 }
1705
1706 #[test]
1707 fn compile_frozen_program_rejects_static_unbound_shape_guard_mismatch() {
1708 let mut builder = SemanticProgramBuilder::new();
1709 let lhs = builder
1710 .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
1711 .unwrap();
1712 let rhs = builder
1713 .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(3)]))
1714 .unwrap();
1715 let output = builder.add_op(CoreSemanticOp::Neg, &[lhs]).unwrap()[0];
1716 builder
1717 .add_shape_guards_to_output(
1718 output,
1719 [ProgramShapeGuard::new(
1720 ProgramShapeRelation::Equal,
1721 DimExpr::InputDim {
1722 input_idx: 0,
1723 axis: 0,
1724 },
1725 DimExpr::InputDim {
1726 input_idx: 1,
1727 axis: 0,
1728 },
1729 )],
1730 )
1731 .unwrap();
1732 let frozen = builder.finish(&[output]).unwrap();
1733
1734 let err = GraphCompiler::new()
1735 .compile_frozen_program(&frozen)
1736 .unwrap_err();
1737
1738 assert!(matches!(
1739 err,
1740 Error::ShapeConstraintViolation {
1741 lhs_value: 2,
1742 rhs_value: 3,
1743 ..
1744 }
1745 ));
1746 let _ = rhs;
1747 }
1748
1749 #[test]
1750 fn compile_frozen_program_defers_dynamic_unbound_shape_guard() {
1751 let mut builder = SemanticProgramBuilder::new();
1752 let lhs = builder
1753 .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
1754 .unwrap();
1755 let rhs = builder
1756 .input(ProgramInputSpec::from_metadata(
1757 ProgramValueMetadata::from_extents(DType::F64, [ShapeExtent::Unknown]),
1758 ))
1759 .unwrap();
1760 let output = builder.add_op(CoreSemanticOp::Neg, &[lhs]).unwrap()[0];
1761 builder
1762 .add_shape_guards_to_output(
1763 output,
1764 [ProgramShapeGuard::new(
1765 ProgramShapeRelation::Equal,
1766 DimExpr::InputDim {
1767 input_idx: 0,
1768 axis: 0,
1769 },
1770 DimExpr::InputDim {
1771 input_idx: 1,
1772 axis: 0,
1773 },
1774 )],
1775 )
1776 .unwrap();
1777 let frozen = builder.finish(&[output]).unwrap();
1778
1779 GraphCompiler::new()
1780 .compile_frozen_program(&frozen)
1781 .unwrap();
1782 let _ = rhs;
1783 }
1784
1785 #[derive(Clone, Debug, PartialEq, Eq)]
1786 struct PrunableTestOp {
1787 pruned: bool,
1788 }
1789
1790 impl ExtensionOp for PrunableTestOp {
1791 fn family_id(&self) -> &'static str {
1792 "tenferro-runtime.test-prunable.v1"
1793 }
1794
1795 fn payload_hash(&self, hasher: &mut dyn Hasher) {
1796 hasher.write_u8(u8::from(self.pruned));
1797 }
1798
1799 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
1800 other
1801 .as_any()
1802 .downcast_ref::<Self>()
1803 .is_some_and(|that| self == that)
1804 }
1805
1806 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
1807 Arc::new(self.clone())
1808 }
1809
1810 fn as_any(&self) -> &dyn Any {
1811 self
1812 }
1813
1814 fn input_count(&self) -> usize {
1815 1
1816 }
1817
1818 fn output_count(&self) -> usize {
1819 if self.pruned {
1820 1
1821 } else {
1822 3
1823 }
1824 }
1825
1826 fn semantic_effects(&self) -> ExtensionEffectDeclaration<'_> {
1827 ExtensionEffectDeclaration::Declared(&[])
1828 }
1829
1830 fn semantic_aliases(&self) -> ExtensionAliasDeclaration<'_> {
1831 ExtensionAliasDeclaration::AllFresh
1832 }
1833
1834 fn infer_output_meta(
1835 &self,
1836 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
1837 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1838 let dtype = ctx.input_dtype(0)?;
1839 let shape = ctx.input_shape(0)?.to_vec();
1840 Ok((0..self.output_count())
1841 .map(|_| (dtype, shape.clone()))
1842 .collect())
1843 }
1844
1845 fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
1846 (!self.pruned && live_outputs == [false, true, false])
1847 .then(|| Arc::new(Self { pruned: true }) as Arc<dyn ExtensionOp>)
1848 }
1849 }
1850
1851 #[test]
1852 fn compile_prunes_extension_outputs_with_replacement_op() {
1853 let input = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1854 let outputs =
1855 crate::extension::apply(Arc::new(PrunableTestOp { pruned: false }), &[&input]).unwrap();
1856
1857 let program = GraphCompiler::new().compile(&outputs[1]).unwrap();
1858 let staging =
1859 stage_semantic_program(program.program(), program.compiler_options()).unwrap();
1860 let pruned_instruction = staging
1861 .instructions
1862 .iter()
1863 .find_map(|inst| match &inst.op {
1864 crate::exec::ExecOp::Extension(op)
1865 if op.family_id() == "tenferro-runtime.test-prunable.v1" =>
1866 {
1867 Some((
1868 inst.output_slots.clone(),
1869 format!("{op:?}"),
1870 op.output_count(),
1871 ))
1872 }
1873 _ => None,
1874 })
1875 .expect("compiled program should contain the test extension");
1876
1877 assert_eq!(pruned_instruction.0.len(), 1);
1878 assert!(pruned_instruction.1.contains("pruned: true"));
1879 assert_eq!(pruned_instruction.2, 1);
1880 }
1881
1882 #[test]
1883 fn compiled_graph_input_keys_preserve_order_for_binary_graph() {
1884 let a = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1885 let b = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1886 let a_key = a.input_key().expect("concrete traced tensor has input key");
1887 let b_key = b.input_key().expect("concrete traced tensor has input key");
1888 let c = a.mul(&b).unwrap();
1889
1890 let program = GraphCompiler::new().compile_many(&[&c]).unwrap();
1891
1892 assert_eq!(program.input_count(), 2);
1893 assert_eq!(program.input_keys().len(), 2);
1894 let keys: Vec<_> = program.input_keys().to_vec();
1895 assert!(keys.contains(&a_key), "input_keys must contain a's key");
1896 assert!(keys.contains(&b_key), "input_keys must contain b's key");
1897 assert_eq!(
1899 program.input_key_index(&a_key),
1900 keys.iter().position(|k| k == &a_key)
1901 );
1902 assert_eq!(
1903 program.input_key_index(&b_key),
1904 keys.iter().position(|k| k == &b_key)
1905 );
1906 }
1907
1908 #[test]
1909 fn compile_with_input_specs_reorders_inputs_by_explicit_order() {
1910 let a = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
1911 let b = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
1912 let a_key = a.input_key().expect("symbolic traced tensor has input key");
1913 let b_key = b.input_key().expect("symbolic traced tensor has input key");
1914 let c = a.mul(&b).unwrap();
1915
1916 let program = GraphCompiler::new()
1918 .compile_with_input_specs(&c, &[(&b, DType::F64, &[2]), (&a, DType::F64, &[2])])
1919 .unwrap();
1920
1921 assert_eq!(program.input_count(), 2);
1922 assert_eq!(program.input_keys().len(), 2);
1923 assert_eq!(
1925 program.input_key_index(&b_key),
1926 Some(0),
1927 "b must be first in explicit order"
1928 );
1929 assert_eq!(
1930 program.input_key_index(&a_key),
1931 Some(1),
1932 "a must be second in explicit order"
1933 );
1934 }
1935}