1use std::cell::RefCell;
23use std::cmp::Reverse;
24use std::collections::{HashMap, HashSet};
25use std::env;
26use std::time::{Duration, Instant};
27
28use super::eager_session::{with_eager_session, with_engine};
29use anyhow::Result;
30use petgraph::algo::connected_components;
31use petgraph::prelude::*;
32use tenferro::TensorValue;
33use tenferro_einsum::{EagerSessionEinsumExt, EinsumSubscripts};
34use tensor4all_tensorbackend::{
35 einsum_native_tensor_reads, einsum_native_tensors_owned, NativeTensorReadInput,
36};
37
38#[cfg(test)]
39use crate::defaults::DynId;
40use crate::defaults::IdxTensorError;
41use crate::defaults::{DynIndex, IdxTensor};
42
43use crate::index_like::IndexLike;
44#[derive(Debug, Clone, Hash, PartialEq, Eq)]
45struct ContractOperandSignature {
46 dims: Vec<usize>,
47 ids: Vec<usize>,
48 is_diag: bool,
49}
50
51#[derive(Debug, Clone, Hash, PartialEq, Eq)]
52struct ContractSignature {
53 operands: Vec<ContractOperandSignature>,
54 output_ids: Vec<usize>,
55 output_dims: Vec<usize>,
56}
57
58#[derive(Debug, Default, Clone)]
59struct ContractProfileEntry {
60 calls: usize,
61 total_time: Duration,
62}
63
64thread_local! {
65 static CONTRACT_PROFILE_STATE: RefCell<HashMap<ContractSignature, ContractProfileEntry>> =
66 RefCell::new(HashMap::new());
67}
68
69fn contract_profile_enabled() -> bool {
70 env::var("T4A_PROFILE_CONTRACT").is_ok()
71}
72
73fn record_contract_profile(signature: ContractSignature, elapsed: Duration) {
74 if !contract_profile_enabled() {
75 return;
76 }
77 CONTRACT_PROFILE_STATE.with(|state| {
78 let mut state = state.borrow_mut();
79 let entry = state.entry(signature).or_default();
80 entry.calls += 1;
81 entry.total_time += elapsed;
82 });
83}
84
85pub fn reset_contract_profile() {
87 CONTRACT_PROFILE_STATE.with(|state| state.borrow_mut().clear());
88}
89
90pub fn print_and_reset_contract_profile() {
92 if !contract_profile_enabled() {
93 return;
94 }
95 CONTRACT_PROFILE_STATE.with(|state| {
96 let mut entries: Vec<_> = state
97 .borrow()
98 .iter()
99 .map(|(k, v)| (k.clone(), v.clone()))
100 .collect();
101 state.borrow_mut().clear();
102 entries.sort_by_key(|(_, entry)| Reverse(entry.total_time));
103
104 eprintln!("=== contract Profile ===");
105 for (idx, (signature, entry)) in entries.into_iter().take(20).enumerate() {
106 let operands = signature
107 .operands
108 .iter()
109 .map(|operand| {
110 format!(
111 "dims={:?} ids={:?}{}",
112 operand.dims,
113 operand.ids,
114 if operand.is_diag { " diag" } else { "" }
115 )
116 })
117 .collect::<Vec<_>>()
118 .join(" ; ");
119 eprintln!(
120 "#{idx:02} calls={} total={:.3}s per_call={:.3}us output_dims={:?} output_ids={:?}",
121 entry.calls,
122 entry.total_time.as_secs_f64(),
123 entry.total_time.as_secs_f64() * 1e6 / entry.calls as f64,
124 signature.output_dims,
125 signature.output_ids,
126 );
127 eprintln!(" {operands}");
128 }
129 });
130}
131
132#[derive(Clone, Copy, Debug)]
153pub struct ContractionOptions<'a> {
154 pub retain_indices: &'a [DynIndex],
156}
157
158impl<'a> ContractionOptions<'a> {
159 pub fn new() -> Self {
161 Self {
162 retain_indices: &[],
163 }
164 }
165
166 pub fn with_retain_indices(mut self, retain_indices: &'a [DynIndex]) -> Self {
168 self.retain_indices = retain_indices;
169 self
170 }
171}
172
173impl Default for ContractionOptions<'_> {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179#[derive(Clone)]
206pub struct PreparedContraction {
207 expected_indices: Vec<Vec<DynIndex>>,
208 expected_dims: Vec<Vec<usize>>,
209 expected_axis_classes: Vec<Vec<usize>>,
210 plan: ContractionPlan,
211 has_retained_indices: bool,
212}
213
214impl std::fmt::Debug for PreparedContraction {
215 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 formatter
217 .debug_struct("PreparedContraction")
218 .field("operand_count", &self.expected_indices.len())
219 .field("result_rank", &self.plan.result_indices.len())
220 .field("has_retained_indices", &self.has_retained_indices)
221 .finish_non_exhaustive()
222 }
223}
224
225impl PreparedContraction {
226 pub fn new(
262 tensors: &[&IdxTensor],
263 options: ContractionOptions<'_>,
264 ) -> std::result::Result<Self, IdxTensorError> {
265 Self::new_impl(tensors, options).map_err(IdxTensorError::from)
266 }
267
268 fn new_impl(tensors: &[&IdxTensor], options: ContractionOptions<'_>) -> Result<Self> {
269 if tensors.is_empty() {
270 return Err(anyhow::anyhow!("No tensors to contract"));
271 }
272 validate_retained_indices_exist(tensors, options.retain_indices)?;
273 if tensors.len() > 1 {
274 let components =
275 find_tensor_connected_components_with_retained(tensors, options.retain_indices);
276 if components.len() > 1 {
277 return Err(anyhow::anyhow!(
278 "Disconnected tensor network: {} components found",
279 components.len()
280 ));
281 }
282 }
283 let plan = build_contraction_plan(tensors, options)?;
284 Ok(Self {
285 expected_indices: tensors
286 .iter()
287 .map(|tensor| tensor.indices().to_vec())
288 .collect(),
289 expected_dims: tensors.iter().map(|tensor| tensor.dims()).collect(),
290 expected_axis_classes: tensors
291 .iter()
292 .map(|tensor| tensor.axis_classes().to_vec())
293 .collect(),
294 plan,
295 has_retained_indices: !options.retain_indices.is_empty(),
296 })
297 }
298
299 pub fn execute(
334 &self,
335 tensors: &[&IdxTensor],
336 ) -> std::result::Result<IdxTensor, IdxTensorError> {
337 self.validate_operands(tensors)?;
338 self.execute_impl(tensors).map_err(IdxTensorError::from)
339 }
340
341 fn validate_operands(&self, tensors: &[&IdxTensor]) -> std::result::Result<(), IdxTensorError> {
342 if tensors.len() != self.expected_indices.len() {
343 return Err(IdxTensorError::ShapeMismatch {
344 operation: "prepared contraction",
345 expected: format!("{} operands", self.expected_indices.len()),
346 actual: format!("{} operands", tensors.len()),
347 });
348 }
349 for (operand, tensor) in tensors.iter().enumerate() {
350 let dims = tensor.dims();
351 if dims != self.expected_dims[operand] {
352 return Err(IdxTensorError::ShapeMismatch {
353 operation: "prepared contraction",
354 expected: format!(
355 "operand {operand} dimensions {:?}",
356 self.expected_dims[operand]
357 ),
358 actual: format!("operand {operand} dimensions {dims:?}"),
359 });
360 }
361 if tensor.indices() != self.expected_indices[operand] {
362 return Err(IdxTensorError::ShapeMismatch {
363 operation: "prepared contraction",
364 expected: format!(
365 "operand {operand} indices {:?}",
366 self.expected_indices[operand]
367 ),
368 actual: format!("operand {operand} indices {:?}", tensor.indices()),
369 });
370 }
371 if tensor.axis_classes() != self.expected_axis_classes[operand] {
372 return Err(IdxTensorError::ShapeMismatch {
373 operation: "prepared contraction",
374 expected: format!(
375 "operand {operand} axis classes {:?}",
376 self.expected_axis_classes[operand]
377 ),
378 actual: format!("operand {operand} axis classes {:?}", tensor.axis_classes()),
379 });
380 }
381 }
382 Ok(())
383 }
384
385 fn execute_impl(&self, tensors: &[&IdxTensor]) -> Result<IdxTensor> {
386 if tensors.len() == 1 {
387 return Ok((*tensors[0]).clone());
388 }
389 if tensors.len() == 2 && !self.has_retained_indices {
390 return tensors[0].try_contract_pairwise_default_with_options(
391 tensors[1],
392 PairwiseContractionOptions::new(),
393 );
394 }
395 let has_structured_storage = tensors
396 .iter()
397 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
398 .collect::<Result<Vec<_>>>()?
399 .into_iter()
400 .any(|structured| structured);
401 let has_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
402 if has_structured_storage || has_grad {
403 return IdxTensor::contract_structured_payloads_nary(
404 tensors,
405 self.plan.result_indices.clone(),
406 self.plan.input_ids.clone(),
407 self.plan.output_ids.clone(),
408 );
409 }
410 execute_contraction_plan(tensors, &self.plan, self.has_retained_indices)
411 }
412}
413
414#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
446pub struct PairwiseContractionOptions {
447 pub lhs_conj: bool,
449 pub rhs_conj: bool,
451}
452
453impl PairwiseContractionOptions {
454 pub fn new() -> Self {
466 Self::default()
467 }
468
469 pub fn with_lhs_conj(mut self, lhs_conj: bool) -> Self {
481 self.lhs_conj = lhs_conj;
482 self
483 }
484
485 pub fn with_rhs_conj(mut self, rhs_conj: bool) -> Self {
497 self.rhs_conj = rhs_conj;
498 self
499 }
500
501 pub(crate) fn has_conj(self) -> bool {
502 self.lhs_conj || self.rhs_conj
503 }
504}
505
506pub fn contract(tensors: &[&IdxTensor]) -> std::result::Result<IdxTensor, IdxTensorError> {
521 contract_with_options(tensors, ContractionOptions::new())
522}
523
524pub fn contract_with_options(
531 tensors: &[&IdxTensor],
532 options: ContractionOptions<'_>,
533) -> std::result::Result<IdxTensor, IdxTensorError> {
534 contract_with_options_impl(tensors, options).map_err(IdxTensorError::from)
535}
536
537pub fn contract_owned(tensors: Vec<IdxTensor>) -> std::result::Result<IdxTensor, IdxTensorError> {
544 contract_owned_with_options(tensors, ContractionOptions::new())
545}
546
547pub fn contract_owned_with_options(
554 tensors: Vec<IdxTensor>,
555 options: ContractionOptions<'_>,
556) -> std::result::Result<IdxTensor, IdxTensorError> {
557 let tensor_refs = tensors.iter().collect::<Vec<_>>();
558 let components =
559 find_tensor_connected_components_with_retained(&tensor_refs, options.retain_indices);
560 if components.len() > 1 {
561 return Err(IdxTensorError::from(anyhow::anyhow!(
562 "Tensors form disconnected components; use explicit outer_product operations for an intentional disconnected product"
563 )));
564 }
565 drop(tensor_refs);
566 contract_owned_with_options_impl(tensors, options).map_err(IdxTensorError::from)
567}
568
569pub fn contract_pair(
580 lhs: &IdxTensor,
581 rhs: &IdxTensor,
582) -> std::result::Result<IdxTensor, IdxTensorError> {
583 lhs.try_contract_pairwise_default_with_options(rhs, PairwiseContractionOptions::new())
584 .map_err(IdxTensorError::from)
585}
586
587pub fn contract_pair_with_operand_options(
628 lhs: &IdxTensor,
629 rhs: &IdxTensor,
630 options: PairwiseContractionOptions,
631) -> std::result::Result<IdxTensor, IdxTensorError> {
632 lhs.try_contract_pairwise_default_with_options(rhs, options)
633 .map_err(IdxTensorError::from)
634}
635
636pub fn contract_pair_with_options(
643 lhs: &IdxTensor,
644 rhs: &IdxTensor,
645 options: ContractionOptions<'_>,
646) -> std::result::Result<IdxTensor, IdxTensorError> {
647 contract_with_options(&[lhs, rhs], options)
648}
649
650pub fn tensordot(
657 lhs: &IdxTensor,
658 rhs: &IdxTensor,
659 pairs: &[(DynIndex, DynIndex)],
660) -> std::result::Result<IdxTensor, IdxTensorError> {
661 lhs.try_tensordot_pairwise_explicit(rhs, pairs)
662 .map_err(IdxTensorError::from)
663}
664
665pub fn outer_product(
675 lhs: &IdxTensor,
676 rhs: &IdxTensor,
677) -> std::result::Result<IdxTensor, IdxTensorError> {
678 lhs.try_outer_product_pairwise(rhs)
679 .map_err(IdxTensorError::from)
680}
681
682fn contract_owned_with_options_impl(
691 tensors: Vec<IdxTensor>,
692 options: ContractionOptions<'_>,
693) -> Result<IdxTensor> {
694 match tensors.len() {
695 0 => Err(anyhow::anyhow!("No tensors to contract")),
696 _ => {
697 let tensor_refs = tensors.iter().collect::<Vec<_>>();
698 validate_retained_indices_exist(&tensor_refs, options.retain_indices)?;
699
700 if tensors.len() == 1 {
701 drop(tensor_refs);
702 let Some(tensor) = tensors.into_iter().next() else {
703 return Err(anyhow::anyhow!("No tensors to contract"));
704 };
705 return Ok(tensor);
706 }
707
708 let has_structured_storage = tensor_refs
709 .iter()
710 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
711 .collect::<Result<Vec<_>>>()?
712 .into_iter()
713 .any(|structured| structured);
714 let requires_borrowed_path =
715 tensor_refs.iter().any(|tensor| tensor.tracks_grad()) || has_structured_storage;
716 if requires_borrowed_path {
717 return contract_with_options(&tensor_refs, options).map_err(anyhow::Error::from);
718 }
719
720 let components = find_tensor_connected_components_with_retained(
721 &tensor_refs,
722 options.retain_indices,
723 );
724 if components.len() > 1 {
725 return Err(anyhow::anyhow!(
726 "Tensors form disconnected components; use explicit outer_product operations for an intentional disconnected product"
727 ));
728 }
729
730 let plan = build_contraction_plan(&tensor_refs, options)?;
731 drop(tensor_refs);
732 let native_operands = tensors
733 .into_iter()
734 .enumerate()
735 .map(|(tensor_idx, tensor)| {
736 Ok((
737 {
738 let inner = tensor.as_inner()?;
739 with_engine(inner.runtime(), None, |engine| {
740 inner.duplicate_value(engine)
741 })?
742 },
743 plan.input_ids[tensor_idx].clone(),
744 ))
745 })
746 .collect::<Result<Vec<_>>>()?;
747 let result_native = einsum_native_tensors_owned(native_operands, &plan.output_ids)?;
748 IdxTensor::from_untracked_native_with_axis_classes(
749 plan.result_indices,
750 result_native,
751 plan.result_axis_classes,
752 )
753 }
754 }
755}
756
757fn has_dense_axis_classes(tensor: &IdxTensor) -> Result<bool> {
758 Ok(tensor
759 .axis_classes()
760 .iter()
761 .copied()
762 .eq(0..tensor.indices().len()))
763}
764
765fn contract_with_options_impl(
766 tensors: &[&IdxTensor],
767 options: ContractionOptions<'_>,
768) -> Result<IdxTensor> {
769 match tensors.len() {
770 0 => Err(anyhow::anyhow!("No tensors to contract")),
771 _ => {
772 validate_retained_indices_exist(tensors, options.retain_indices)?;
773 if tensors.len() == 1 {
774 return Ok((*tensors[0]).clone());
775 }
776
777 let components =
779 find_tensor_connected_components_with_retained(tensors, options.retain_indices);
780 if components.len() > 1 {
781 return Err(anyhow::anyhow!(
782 "Disconnected tensor network: {} components found",
783 components.len()
784 ));
785 }
786
787 if tensors.len() == 2 && options.retain_indices.is_empty() {
788 return tensors[0].try_contract_pairwise_default_with_options(
789 tensors[1],
790 PairwiseContractionOptions::new(),
791 );
792 }
793
794 let has_structured_storage = tensors
795 .iter()
796 .map(|tensor| has_dense_axis_classes(tensor).map(|dense| !dense))
797 .collect::<Result<Vec<_>>>()?
798 .into_iter()
799 .any(|structured| structured);
800 let has_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
801 if has_structured_storage || has_grad {
802 let plan = build_contraction_plan(tensors, options)?;
803 return IdxTensor::contract_structured_payloads_nary(
804 tensors,
805 plan.result_indices,
806 plan.input_ids,
807 plan.output_ids,
808 );
809 }
810
811 contract_impl(tensors, options)
813 }
814 }
815}
816
817#[derive(Debug, Clone)]
826#[cfg(test)]
827pub(crate) struct AxisUnionFind {
828 parent: HashMap<DynId, DynId>,
830 rank: HashMap<DynId, usize>,
832}
833
834#[cfg(test)]
835impl AxisUnionFind {
836 pub fn new() -> Self {
838 Self {
839 parent: HashMap::new(),
840 rank: HashMap::new(),
841 }
842 }
843
844 pub fn make_set(&mut self, id: DynId) {
846 use std::collections::hash_map::Entry;
847 if let Entry::Vacant(e) = self.parent.entry(id) {
848 e.insert(id);
849 self.rank.insert(id, 0);
850 }
851 }
852
853 pub fn find(&mut self, id: DynId) -> DynId {
856 self.make_set(id);
857 if self.parent[&id] != id {
858 let root = self.find(self.parent[&id]);
859 self.parent.insert(id, root);
860 }
861 self.parent[&id]
862 }
863
864 pub fn union(&mut self, a: DynId, b: DynId) {
867 let root_a = self.find(a);
868 let root_b = self.find(b);
869
870 if root_a == root_b {
871 return;
872 }
873
874 let rank_a = self.rank[&root_a];
875 let rank_b = self.rank[&root_b];
876
877 if rank_a < rank_b {
878 self.parent.insert(root_a, root_b);
879 } else if rank_a > rank_b {
880 self.parent.insert(root_b, root_a);
881 } else {
882 self.parent.insert(root_b, root_a);
883 if let Some(rank) = self.rank.get_mut(&root_a) {
884 *rank += 1;
885 }
886 }
887 }
888
889 pub fn remap(&mut self, id: DynId) -> DynId {
891 self.find(id)
892 }
893
894 pub fn remap_ids(&mut self, ids: &[DynId]) -> Vec<DynId> {
896 ids.iter().map(|id| self.find(*id)).collect()
897 }
898}
899
900#[cfg(test)]
901impl Default for AxisUnionFind {
902 fn default() -> Self {
903 Self::new()
904 }
905}
906
907#[cfg(test)]
916pub(crate) fn remap_tensor_ids(tensors: &[&IdxTensor], uf: &mut AxisUnionFind) -> Vec<Vec<DynId>> {
917 tensors
918 .iter()
919 .map(|t| t.indices.iter().map(|idx| uf.find(*idx.id())).collect())
920 .collect()
921}
922
923#[cfg(test)]
925pub(crate) fn remap_output_ids(output: &[DynIndex], uf: &mut AxisUnionFind) -> Vec<DynId> {
926 output.iter().map(|idx| uf.find(*idx.id())).collect()
927}
928
929#[cfg(test)]
934pub(crate) fn collect_sizes(
935 tensors: &[&IdxTensor],
936 uf: &mut AxisUnionFind,
937) -> HashMap<DynId, usize> {
938 let mut sizes = HashMap::new();
939
940 for tensor in tensors {
941 let dims = tensor.dims();
942 for (idx, &dim) in tensor.indices.iter().zip(dims.iter()) {
943 let rep = uf.find(*idx.id());
944 sizes.entry(rep).or_insert(dim);
945 }
946 }
947
948 sizes
949}
950
951fn contract_impl(tensors: &[&IdxTensor], options: ContractionOptions<'_>) -> Result<IdxTensor> {
964 let plan = build_contraction_plan(tensors, options)?;
966
967 let mut sizes: HashMap<usize, usize> = HashMap::new();
972 for (tensor_idx, tensor) in tensors.iter().enumerate() {
973 let dims = tensor.dims();
974 for (pos, &dim) in dims.iter().enumerate() {
975 let internal_id = plan.input_ids[tensor_idx][pos];
976 match sizes.entry(internal_id) {
977 std::collections::hash_map::Entry::Vacant(entry) => {
978 entry.insert(dim);
979 }
980 std::collections::hash_map::Entry::Occupied(entry) => {
981 if *entry.get() != dim {
982 return Err(anyhow::anyhow!(
983 "Internal label shape mismatch: label {} has dimensions {} and {}",
984 internal_id,
985 entry.get(),
986 dim
987 ));
988 }
989 }
990 }
991 }
992 }
993
994 let profile_signature = contract_profile_enabled().then(|| ContractSignature {
995 operands: tensors
996 .iter()
997 .enumerate()
998 .map(|(tensor_idx, tensor)| ContractOperandSignature {
999 dims: tensor.dims().to_vec(),
1000 ids: plan.input_ids[tensor_idx].clone(),
1001 is_diag: tensor.is_diag(),
1002 })
1003 .collect(),
1004 output_ids: plan.output_ids.clone(),
1005 output_dims: plan.output_ids.iter().map(|id| sizes[id]).collect(),
1006 });
1007 let profile_started = contract_profile_enabled().then(Instant::now);
1008
1009 let result = execute_contraction_plan(tensors, &plan, !options.retain_indices.is_empty())?;
1010 if let (Some(signature), Some(started)) = (profile_signature, profile_started) {
1011 record_contract_profile(signature, started.elapsed());
1012 }
1013 Ok(result)
1014}
1015
1016#[cfg(feature = "tenferro-cuda")]
1023pub(crate) fn contract_pair_via_plan(
1024 lhs: &IdxTensor,
1025 rhs: &IdxTensor,
1026 options: PairwiseContractionOptions,
1027) -> Result<IdxTensor> {
1028 let lhs_owned;
1029 let rhs_owned;
1030 let (lhs, rhs) = match (options.lhs_conj, options.rhs_conj) {
1031 (false, false) => (lhs, rhs),
1032 (true, false) => {
1033 lhs_owned = lhs.conj();
1034 (&lhs_owned, rhs)
1035 }
1036 (false, true) => {
1037 rhs_owned = rhs.conj();
1038 (lhs, &rhs_owned)
1039 }
1040 (true, true) => {
1041 lhs_owned = lhs.conj();
1042 rhs_owned = rhs.conj();
1043 (&lhs_owned, &rhs_owned)
1044 }
1045 };
1046 let tensors = [lhs, rhs];
1047 let plan = build_contraction_plan(&tensors, ContractionOptions::new())?;
1048 execute_contraction_plan(&tensors, &plan, false)
1049}
1050
1051fn execute_contraction_plan(
1052 tensors: &[&IdxTensor],
1053 plan: &ContractionPlan,
1054 has_retained_indices: bool,
1055) -> Result<IdxTensor> {
1056 #[cfg(feature = "tenferro-cuda")]
1062 if !tensors.is_empty() && tensors.iter().all(|tensor| tensor.is_cuda_resident()) {
1063 let operands = tensors
1064 .iter()
1065 .map(|tensor| tensor.as_inner())
1066 .collect::<Result<Vec<_>>>()?;
1067 let subscripts = build_einsum_subscripts_from_usize_ids(&plan.input_ids, &plan.output_ids)?;
1068 let runtime = operands
1069 .first()
1070 .map(|operand| operand.runtime())
1071 .ok_or_else(|| anyhow::anyhow!("No tensors to contract"))?;
1072 let result = with_eager_session(runtime, None, |session| {
1073 session.einsum_subscripts(&operands, &subscripts)
1074 })?;
1075 return IdxTensor::from_inner_with_axis_classes(
1076 plan.result_indices.clone(),
1077 result,
1078 plan.result_axis_classes.clone(),
1079 );
1080 }
1081 let any_grad = tensors.iter().any(|tensor| tensor.tracks_grad());
1082 if any_grad {
1083 let first_dtype = tensors[0].as_inner()?.dtype();
1084 let same_dtype = tensors
1085 .iter()
1086 .map(|tensor| Ok(tensor.as_inner()?.dtype() == first_dtype))
1087 .collect::<Result<Vec<_>>>()?
1088 .into_iter()
1089 .all(|same| same);
1090 let has_non_dense_axis_classes = tensors.iter().any(|tensor| {
1091 tensor
1092 .axis_classes()
1093 .iter()
1094 .copied()
1095 .enumerate()
1096 .any(|(axis, class)| axis != class)
1097 });
1098
1099 if same_dtype && has_non_dense_axis_classes && !has_retained_indices {
1100 let mut iter = tensors.iter();
1103 let Some(first) = iter.next() else {
1104 return Err(anyhow::anyhow!("No tensors to contract"));
1105 };
1106 let mut result = (*first).clone();
1107 for tensor in iter {
1108 result = contract_pair(&result, tensor)?;
1109 }
1110 return Ok(result);
1111 }
1112
1113 let operands = tensors
1114 .iter()
1115 .map(|tensor| tensor.as_inner())
1116 .collect::<Result<Vec<_>>>()?;
1117 let subscripts = build_einsum_subscripts_from_usize_ids(&plan.input_ids, &plan.output_ids)?;
1118 let runtime = operands
1119 .first()
1120 .map(|operand| operand.runtime())
1121 .ok_or_else(|| anyhow::anyhow!("No tensors to contract"))?;
1122 let result = with_eager_session(runtime, None, |session| {
1123 session.einsum_subscripts(&operands, &subscripts)
1124 })?;
1125 return IdxTensor::from_inner_with_axis_classes(
1126 plan.result_indices.clone(),
1127 result,
1128 plan.result_axis_classes.clone(),
1129 );
1130 }
1131
1132 let native_operands = tensors
1133 .iter()
1134 .enumerate()
1135 .map(|(tensor_idx, tensor)| {
1136 Ok((
1137 NativeTensorReadInput::Borrowed(tensor.as_inner()?.tensor_read()),
1138 plan.input_ids[tensor_idx].as_slice(),
1139 ))
1140 })
1141 .collect::<Result<Vec<_>>>()?;
1142 let operand_refs = native_operands
1143 .iter()
1144 .map(|(tensor, ids)| (tensor, *ids))
1145 .collect::<Vec<_>>();
1146 let result_native = einsum_native_tensor_reads(&operand_refs, &plan.output_ids)?;
1147 let mut common: Option<std::sync::Arc<tenferro_ad::EagerRuntime>> = None;
1152 let mut mixed = false;
1153 for tensor in tensors {
1154 if let Some(runtime) = tensor.eager_runtime() {
1155 match &common {
1156 None => common = Some(runtime),
1157 Some(first) => {
1158 if first.id() != runtime.id() {
1159 mixed = true;
1160 }
1161 }
1162 }
1163 }
1164 }
1165 match (common, mixed) {
1166 (Some(runtime), false) => {
1167 let inner = tenferro_ad::extension::adopt_untracked_eager_value(
1168 runtime,
1169 TensorValue::from_tensor(result_native),
1170 )?;
1171 IdxTensor::from_inner_with_axis_classes(
1172 plan.result_indices.clone(),
1173 inner,
1174 plan.result_axis_classes.clone(),
1175 )
1176 }
1177 _ => IdxTensor::from_untracked_native_with_axis_classes(
1178 plan.result_indices.clone(),
1179 result_native,
1180 plan.result_axis_classes.clone(),
1181 ),
1182 }
1183}
1184
1185fn build_einsum_subscripts_from_usize_ids(
1186 input_ids: &[Vec<usize>],
1187 output_ids: &[usize],
1188) -> Result<EinsumSubscripts> {
1189 let inputs = input_ids
1190 .iter()
1191 .map(|ids| {
1192 ids.iter()
1193 .map(|&id| {
1194 u32::try_from(id)
1195 .map_err(|_| anyhow::anyhow!("einsum label {id} exceeds u32 range"))
1196 })
1197 .collect::<Result<Vec<_>>>()
1198 })
1199 .collect::<Result<Vec<_>>>()?;
1200 let output = output_ids
1201 .iter()
1202 .map(|&id| {
1203 u32::try_from(id).map_err(|_| anyhow::anyhow!("einsum label {id} exceeds u32 range"))
1204 })
1205 .collect::<Result<Vec<_>>>()?;
1206 let input_refs = inputs.iter().map(Vec::as_slice).collect::<Vec<_>>();
1207 Ok(EinsumSubscripts::new(&input_refs, &output))
1208}
1209
1210#[derive(Debug, Clone)]
1212pub(crate) struct ContractionPlan {
1213 pub(crate) input_ids: Vec<Vec<usize>>,
1214 pub(crate) output_ids: Vec<usize>,
1215 pub(crate) result_indices: Vec<DynIndex>,
1216 pub(crate) result_axis_classes: Vec<usize>,
1217}
1218
1219pub(crate) fn session_contraction_plan(
1227 tensors: &[&IdxTensor],
1228 options: ContractionOptions<'_>,
1229) -> Result<Option<ContractionPlan>> {
1230 if tensors.is_empty() {
1231 return Err(anyhow::anyhow!("No tensors to contract"));
1232 }
1233 validate_retained_indices_exist(tensors, options.retain_indices)?;
1234 if tensors.len() == 1 {
1235 return Ok(None);
1236 }
1237 let components =
1238 find_tensor_connected_components_with_retained(tensors, options.retain_indices);
1239 if components.len() > 1 {
1240 return Err(anyhow::anyhow!(
1241 "Disconnected tensor network: {} components found",
1242 components.len()
1243 ));
1244 }
1245 build_contraction_plan(tensors, options).map(Some)
1246}
1247
1248pub(crate) fn build_contraction_plan(
1249 tensors: &[&IdxTensor],
1250 options: ContractionOptions<'_>,
1251) -> Result<ContractionPlan> {
1252 let retained_indices: HashSet<DynIndex> = options.retain_indices.iter().cloned().collect();
1253 let (input_ids, internal_id_to_original) = build_internal_ids(tensors, &retained_indices)?;
1254
1255 let mut counts: HashMap<usize, usize> = HashMap::new();
1256 for ids in &input_ids {
1257 for &internal_id in ids {
1258 *counts.entry(internal_id).or_insert(0) += 1;
1259 }
1260 }
1261 let mut output_ids = Vec::new();
1262 let mut seen_output = HashSet::new();
1263 let mut found_retained = HashSet::new();
1264
1265 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1266 for (axis, idx) in tensor.indices.iter().enumerate() {
1267 let internal_id = input_ids[tensor_idx][axis];
1268 let should_output = counts[&internal_id] == 1 || retained_indices.contains(idx);
1269 if should_output && seen_output.insert(internal_id) {
1270 output_ids.push(internal_id);
1271 }
1272 if retained_indices.contains(idx) {
1273 found_retained.insert(idx.clone());
1274 }
1275 }
1276 }
1277
1278 for retained in retained_indices {
1279 if !found_retained.contains(&retained) {
1280 return Err(anyhow::anyhow!(
1281 "Retained index {:?} does not appear in the input tensors",
1282 retained
1283 ));
1284 }
1285 }
1286
1287 let result_indices: Vec<DynIndex> = output_ids
1288 .iter()
1289 .map(|&internal_id| {
1290 let (tensor_idx, pos) = internal_id_to_original[&internal_id];
1291 tensors[tensor_idx].indices[pos].clone()
1292 })
1293 .collect();
1294 validate_unique_output_indices(&result_indices)?;
1295 let result_axis_classes =
1296 output_axis_classes(tensors, &input_ids, &output_ids, &internal_id_to_original)?;
1297
1298 Ok(ContractionPlan {
1299 input_ids,
1300 output_ids,
1301 result_indices,
1302 result_axis_classes,
1303 })
1304}
1305
1306fn validate_retained_indices_exist(
1307 tensors: &[&IdxTensor],
1308 retain_indices: &[DynIndex],
1309) -> Result<()> {
1310 for retain in retain_indices {
1311 let found = tensors
1312 .iter()
1313 .any(|tensor| tensor.indices().iter().any(|idx| idx == retain));
1314 if !found {
1315 return Err(anyhow::anyhow!(
1316 "Retained index {:?} does not appear in the input tensors",
1317 retain
1318 ));
1319 }
1320 }
1321 Ok(())
1322}
1323
1324fn validate_unique_output_indices(indices: &[DynIndex]) -> Result<()> {
1325 let mut seen = HashSet::new();
1326 for idx in indices {
1327 if !seen.insert(idx.clone()) {
1328 return Err(anyhow::anyhow!(
1329 "Contraction result would contain duplicate output indices"
1330 ));
1331 }
1332 }
1333 Ok(())
1334}
1335
1336fn output_axis_classes(
1337 tensors: &[&IdxTensor],
1338 ixs: &[Vec<usize>],
1339 output: &[usize],
1340 internal_id_to_original: &HashMap<usize, (usize, usize)>,
1341) -> Result<Vec<usize>> {
1342 fn find(parent: &mut [usize], value: usize) -> usize {
1343 if parent[value] != value {
1344 parent[value] = find(parent, parent[value]);
1345 }
1346 parent[value]
1347 }
1348
1349 fn union(parent: &mut [usize], lhs: usize, rhs: usize) {
1350 let lhs_root = find(parent, lhs);
1351 let rhs_root = find(parent, rhs);
1352 if lhs_root != rhs_root {
1353 parent[rhs_root] = lhs_root;
1354 }
1355 }
1356
1357 let mut class_offsets = Vec::with_capacity(tensors.len());
1358 let mut next_node = 0usize;
1359 for tensor in tensors {
1360 class_offsets.push(next_node);
1361 let payload_rank = tensor
1362 .axis_classes()
1363 .iter()
1364 .copied()
1365 .max()
1366 .map(|value| value + 1)
1367 .unwrap_or(0);
1368 next_node += payload_rank;
1369 }
1370 let mut parent: Vec<usize> = (0..next_node).collect();
1371 let mut axes_by_internal_id: HashMap<usize, Vec<usize>> = HashMap::new();
1372
1373 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1374 for (axis, &internal_id) in ixs[tensor_idx].iter().enumerate() {
1375 let class_id = tensor.axis_classes()[axis];
1376 let node = class_offsets[tensor_idx] + class_id;
1377 axes_by_internal_id
1378 .entry(internal_id)
1379 .or_default()
1380 .push(node);
1381 }
1382 }
1383
1384 for nodes in axes_by_internal_id.values() {
1385 if let Some((&first, rest)) = nodes.split_first() {
1386 for &node in rest {
1387 union(&mut parent, first, node);
1388 }
1389 }
1390 }
1391
1392 let mut root_to_class = HashMap::new();
1393 let mut next_class = 0usize;
1394 output
1395 .iter()
1396 .map(|internal_id| {
1397 let (tensor_idx, axis) = internal_id_to_original[internal_id];
1398 let class_id = tensors[tensor_idx].axis_classes()[axis];
1399 let node = class_offsets[tensor_idx] + class_id;
1400 let root = find(&mut parent, node);
1401 Ok(*root_to_class.entry(root).or_insert_with(|| {
1402 let class = next_class;
1403 next_class += 1;
1404 class
1405 }))
1406 })
1407 .collect::<Result<Vec<_>>>()
1408}
1409
1410#[allow(clippy::type_complexity)]
1418fn build_internal_ids(
1419 tensors: &[&IdxTensor],
1420 retained_indices: &HashSet<DynIndex>,
1421) -> Result<(Vec<Vec<usize>>, HashMap<usize, (usize, usize)>)> {
1422 let mut next_id = 0usize;
1423 let mut index_to_internal: HashMap<DynIndex, usize> = HashMap::new();
1424 let mut retained_index_to_internal: HashMap<DynIndex, usize> = HashMap::new();
1425 let mut assigned: HashMap<(usize, usize), usize> = HashMap::new();
1426 let mut internal_id_to_original: HashMap<usize, (usize, usize)> = HashMap::new();
1427
1428 for ti in 0..tensors.len() {
1429 for tj in (ti + 1)..tensors.len() {
1430 for (pi, idx_i) in tensors[ti].indices.iter().enumerate() {
1431 for (pj, idx_j) in tensors[tj].indices.iter().enumerate() {
1432 if idx_i.is_contractable(idx_j) {
1433 let key_i = (ti, pi);
1434 let key_j = (tj, pj);
1435
1436 match (assigned.get(&key_i).copied(), assigned.get(&key_j).copied()) {
1437 (None, None) => {
1438 let internal_id = if let Some(&id) = index_to_internal.get(idx_i) {
1439 id
1440 } else {
1441 let id = next_id;
1442 next_id += 1;
1443 index_to_internal.insert(idx_i.clone(), id);
1444 internal_id_to_original.insert(id, key_i);
1445 id
1446 };
1447 assigned.insert(key_i, internal_id);
1448 assigned.insert(key_j, internal_id);
1449 if idx_i != idx_j {
1450 index_to_internal.insert(idx_j.clone(), internal_id);
1451 }
1452 }
1453 (Some(id), None) => {
1454 assigned.insert(key_j, id);
1455 index_to_internal.insert(idx_j.clone(), id);
1456 }
1457 (None, Some(id)) => {
1458 assigned.insert(key_i, id);
1459 index_to_internal.insert(idx_i.clone(), id);
1460 }
1461 (Some(_id_i), Some(_id_j)) => {
1462 }
1464 }
1465 }
1466 }
1467 }
1468 }
1469 }
1470
1471 for (tensor_idx, tensor) in tensors.iter().enumerate() {
1473 for (pos, idx) in tensor.indices.iter().enumerate() {
1474 let key = (tensor_idx, pos);
1475 if let std::collections::hash_map::Entry::Vacant(e) = assigned.entry(key) {
1476 let internal_id = if retained_indices.contains(idx) {
1477 if let Some(&id) = retained_index_to_internal.get(idx) {
1478 id
1479 } else {
1480 let id = next_id;
1481 next_id += 1;
1482 retained_index_to_internal.insert(idx.clone(), id);
1483 internal_id_to_original.insert(id, key);
1484 id
1485 }
1486 } else {
1487 let id = next_id;
1488 next_id += 1;
1489 internal_id_to_original.insert(id, key);
1490 id
1491 };
1492 e.insert(internal_id);
1493 }
1494 }
1495 }
1496
1497 let ixs: Vec<Vec<usize>> = tensors
1499 .iter()
1500 .enumerate()
1501 .map(|(tensor_idx, tensor)| {
1502 (0..tensor.indices.len())
1503 .map(|pos| assigned[&(tensor_idx, pos)])
1504 .collect()
1505 })
1506 .collect();
1507
1508 Ok((ixs, internal_id_to_original))
1509}
1510
1511fn has_contractable_indices(a: &IdxTensor, b: &IdxTensor) -> bool {
1517 a.indices
1518 .iter()
1519 .any(|idx_a| b.indices.iter().any(|idx_b| idx_a.is_contractable(idx_b)))
1520}
1521
1522#[cfg(test)]
1526fn find_tensor_connected_components(tensors: &[&IdxTensor]) -> Vec<Vec<usize>> {
1527 find_tensor_connected_components_with_retained(tensors, &[])
1528}
1529
1530fn find_tensor_connected_components_with_retained(
1531 tensors: &[&IdxTensor],
1532 retain_indices: &[DynIndex],
1533) -> Vec<Vec<usize>> {
1534 let n = tensors.len();
1535 if n == 0 {
1536 return vec![];
1537 }
1538 if n == 1 {
1539 return vec![vec![0]];
1540 }
1541
1542 let mut graph = UnGraph::<(), ()>::new_undirected();
1544 let nodes: Vec<_> = (0..n).map(|_| graph.add_node(())).collect();
1545
1546 for i in 0..n {
1547 for j in (i + 1)..n {
1548 if has_contractable_indices(tensors[i], tensors[j]) {
1549 graph.add_edge(nodes[i], nodes[j], ());
1550 }
1551 }
1552 }
1553
1554 if !retain_indices.is_empty() {
1555 for i in 0..n {
1556 for j in (i + 1)..n {
1557 if shares_retained_index(tensors[i], tensors[j], retain_indices) {
1558 graph.add_edge(nodes[i], nodes[j], ());
1559 }
1560 }
1561 }
1562 }
1563
1564 let num_components = connected_components(&graph);
1566
1567 if num_components == 1 {
1568 return vec![(0..n).collect()];
1569 }
1570
1571 use petgraph::visit::Dfs;
1573 let mut visited = vec![false; n];
1574 let mut components = Vec::new();
1575
1576 for start in 0..n {
1577 if !visited[start] {
1578 let mut component = Vec::new();
1579 let mut dfs = Dfs::new(&graph, nodes[start]);
1580 while let Some(node) = dfs.next(&graph) {
1581 let idx = node.index();
1582 if !visited[idx] {
1583 visited[idx] = true;
1584 component.push(idx);
1585 }
1586 }
1587 component.sort();
1588 components.push(component);
1589 }
1590 }
1591
1592 components.sort_by_key(|c| c[0]);
1593 components
1594}
1595
1596fn shares_retained_index(a: &IdxTensor, b: &IdxTensor, retain_indices: &[DynIndex]) -> bool {
1597 retain_indices.iter().any(|retain| {
1598 a.indices().iter().any(|idx_a| idx_a == retain)
1599 && b.indices().iter().any(|idx_b| idx_b == retain)
1600 })
1601}
1602
1603#[cfg(test)]
1604mod tests;