1use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::mem::size_of;
6use std::sync::Arc;
7
8use computegraph::compile::{compile, CompiledProgram, Instruction};
9use computegraph::graph::GraphBuilder;
10use computegraph::materialize::materialize_merge;
11use computegraph::resolve::resolve;
12use computegraph::types::{ValueKey, ValueRef};
13use tenferro_ad::error::{Error, Result};
14use tenferro_ad::extension::{adopt_untracked_eager_value, apply_eager};
15use tenferro_ad::{EagerRuntime, EagerTensor};
16use tenferro_ops::dim_expr::DimExpr;
17use tenferro_ops::input_key::TensorInputKey;
18use tenferro_ops::std_tensor_op::StdTensorOp;
19use tenferro_runtime::ExtensionCacheKey;
20use tenferro_tensor::TensorFusion;
21
22use crate::binary_dot::{try_build_exact_output_binary_dot_plan, BinaryDotOperandOrder};
23use crate::builder::build_einsum_graph;
24use crate::cache::{
25 saturating_sum, vec_retained_bytes, EINSUM_EAGER_EXPANDED_PROGRAMS_CACHE,
26 EINSUM_EXTENSION_FAMILY_ID,
27};
28use crate::extension::{register_runtime, EinsumExtensionOp};
29use crate::optimize::{
30 default_auto_options, hash_einsum_plan_spec, plan_specs_equal, resolve_plan_spec,
31 EinsumPlanSpec,
32};
33use crate::{parse_einsum_subscripts, EinsumSubscripts, Subscripts, TensorDotAxes};
34
35pub trait EagerEinsumExt {
37 fn einsum(&self, subscripts: &str) -> Result<EagerTensor>;
38 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor>;
39}
40
41impl EagerEinsumExt for [&EagerTensor] {
42 fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
43 einsum(self, subscripts)
44 }
45
46 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
47 einsum_subscripts(self, subscripts)
48 }
49}
50
51impl<const N: usize> EagerEinsumExt for [&EagerTensor; N] {
52 fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
53 einsum(self.as_slice(), subscripts)
54 }
55
56 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
57 einsum_subscripts(self.as_slice(), subscripts)
58 }
59}
60
61pub trait EagerTensorEinsumExt {
63 fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor>;
64}
65
66impl EagerTensorEinsumExt for EagerTensor {
67 fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor> {
68 tensordot(self, rhs, axes)
69 }
70}
71
72pub fn einsum(inputs: &[&EagerTensor], subscripts: &str) -> Result<EagerTensor> {
96 let subscripts = parse_einsum_subscripts(subscripts)
97 .map_err(|err| Error::ContractionError(err.to_string()))?;
98 einsum_subscripts(inputs, &subscripts)
99}
100
101pub fn einsum_subscripts(
126 inputs: &[&EagerTensor],
127 subscripts: &EinsumSubscripts,
128) -> Result<EagerTensor> {
129 if let Some(result) = try_direct_binary_dot_general(inputs, subscripts) {
130 return result;
131 }
132
133 if let Some(result) = try_whole_program_untracked(inputs, subscripts)? {
134 return Ok(result);
135 }
136
137 let output_shape_hint = infer_eager_output_shape(subscripts, inputs)?;
138 if let Some(result) = try_expand_eager_einsum(inputs, subscripts)? {
139 return Ok(result);
140 }
141
142 if let Some(first) = inputs.first() {
143 first
144 .runtime()
145 .register_extension(register_runtime)
146 .map_err(|err| Error::Internal(err.to_string()))?;
147 }
148
149 let op = Arc::new(EinsumExtensionOp::with_output_shape_hint(
150 subscripts.clone(),
151 output_shape_hint,
152 EinsumPlanSpec::Auto(default_auto_options()),
153 ));
154 let mut outputs = apply_eager(op, inputs)?;
155 outputs
156 .pop()
157 .ok_or_else(|| Error::Internal("einsum extension produced no eager output".to_string()))
158}
159
160fn try_direct_binary_dot_general(
161 inputs: &[&EagerTensor],
162 subscripts: &EinsumSubscripts,
163) -> Option<Result<EagerTensor>> {
164 if inputs.len() != 2 || subscripts.inputs.len() != 2 {
165 return None;
166 }
167
168 let lhs_labels = &subscripts.inputs[0];
169 let rhs_labels = &subscripts.inputs[1];
170 if lhs_labels.len() != inputs[0].shape().len() || rhs_labels.len() != inputs[1].shape().len() {
171 return None;
172 }
173
174 if let Some(plan) =
175 try_build_exact_output_binary_dot_plan(lhs_labels, rhs_labels, &subscripts.output)
176 {
177 return Some(match plan.operand_order {
178 BinaryDotOperandOrder::Original => inputs[0].dot_general(inputs[1], plan.config),
179 BinaryDotOperandOrder::Swapped => inputs[1].dot_general(inputs[0], plan.config),
180 });
181 }
182 None
183}
184
185fn whole_program_untracked_enabled() -> bool {
193 std::env::var_os("TENFERRO_EAGER_WHOLE_PROGRAM").is_some()
194}
195
196fn try_whole_program_untracked(
202 inputs: &[&EagerTensor],
203 subscripts: &EinsumSubscripts,
204) -> Result<Option<EagerTensor>> {
205 if !whole_program_untracked_enabled() {
206 return Ok(None);
207 }
208 let Some(first) = inputs.first() else {
209 return Ok(None);
210 };
211 if inputs.iter().any(|tensor| tensor.tracks_grad()) {
212 return Ok(None);
213 }
214 let runtime = first.runtime();
215 if inputs
216 .iter()
217 .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
218 {
219 return Ok(None);
220 }
221
222 let subs = Subscripts::from(subscripts);
223 let tensor_arcs = inputs
224 .iter()
225 .map(|tensor| tensor.materialized())
226 .collect::<Result<Vec<_>>>()?;
227 let tensors: Vec<_> = tensor_arcs.iter().map(|tensor| tensor.as_ref()).collect();
228 let result = runtime.with_backend_mut(|backend| {
229 crate::eager::eager_einsum_subscripts(backend, &tensors, &subs)
230 })??;
231 Ok(Some(EagerTensor::from_tensor_in(result, runtime.clone())?))
232}
233
234#[cfg(test)]
267fn einsum_whole_program_untracked(
268 inputs: &[&EagerTensor],
269 tree: &crate::ContractionTree,
270) -> Result<EagerTensor> {
271 let first = inputs.first().ok_or_else(|| {
272 Error::ContractionError("einsum requires at least one input tensor".into())
273 })?;
274 if inputs.iter().any(|tensor| tensor.tracks_grad()) {
275 return Err(Error::Internal(
276 "whole-program eager einsum requires untracked inputs".into(),
277 ));
278 }
279 let runtime = first.runtime();
280 if inputs
281 .iter()
282 .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
283 {
284 return Err(Error::Internal(
285 "whole-program eager einsum requires inputs from one runtime".into(),
286 ));
287 }
288 let tensor_arcs = inputs
289 .iter()
290 .map(|tensor| tensor.materialized())
291 .collect::<Result<Vec<_>>>()?;
292 let tensors: Vec<_> = tensor_arcs.iter().map(|tensor| tensor.as_ref()).collect();
293 let result = runtime.with_backend_mut(|backend| {
294 crate::eager::eager_einsum_with_tree(backend, &tensors, tree)
295 })??;
296 EagerTensor::from_tensor_in(result, runtime.clone())
297}
298
299fn try_expand_eager_einsum(
300 inputs: &[&EagerTensor],
301 subscripts: &EinsumSubscripts,
302) -> Result<Option<EagerTensor>> {
303 if inputs.len() <= 1 {
304 return Ok(None);
305 }
306
307 let shapes: Vec<Vec<usize>> = inputs
308 .iter()
309 .map(|tensor| tensor.shape().to_vec())
310 .collect();
311 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
312 let subs = Subscripts::from(subscripts);
313 let plan_spec = EinsumPlanSpec::Auto(default_auto_options());
314
315 let program = cached_expanded_eager_program(
316 inputs[0].runtime(),
317 subscripts,
318 &subs,
319 &plan_spec,
320 &shape_refs,
321 &shapes,
322 )?;
323 execute_eager_einsum_program(inputs, &program)
324}
325
326struct ExpandedEagerProgram {
327 compiled: CompiledProgram<StdTensorOp>,
328 input_slots: Vec<(usize, usize)>,
329}
330
331#[derive(Clone)]
332struct ExpandedEagerProgramCacheKeyData {
333 subscripts: EinsumSubscripts,
334 shapes: Vec<Vec<usize>>,
335 plan_spec: EinsumPlanSpec,
336}
337
338impl ExpandedEagerProgramCacheKeyData {
339 fn new(
340 subscripts: &EinsumSubscripts,
341 shapes: &[Vec<usize>],
342 plan_spec: &EinsumPlanSpec,
343 ) -> Self {
344 Self {
345 subscripts: subscripts.clone(),
346 shapes: shapes.to_vec(),
347 plan_spec: plan_spec.clone(),
348 }
349 }
350
351 fn matches_expanded_eager_program(
352 &self,
353 subscripts: &EinsumSubscripts,
354 shapes: &[Vec<usize>],
355 plan_spec: &EinsumPlanSpec,
356 ) -> bool {
357 self.subscripts == *subscripts
358 && self.shapes.as_slice() == shapes
359 && plan_specs_equal(&self.plan_spec, plan_spec)
360 }
361
362 fn retained_bytes(&self) -> usize {
363 saturating_sum([
364 crate::cache::einsum_subscripts_retained_bytes(&self.subscripts),
365 saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
366 plan_spec_retained_bytes(&self.plan_spec),
367 ])
368 }
369}
370
371struct CachedExpandedEagerProgram {
372 key_data: ExpandedEagerProgramCacheKeyData,
373 program: Arc<ExpandedEagerProgram>,
374}
375
376fn cached_expanded_eager_program(
377 runtime: &Arc<EagerRuntime>,
378 subscripts: &EinsumSubscripts,
379 subs: &Subscripts,
380 plan_spec: &EinsumPlanSpec,
381 shape_refs: &[&[usize]],
382 shapes: &[Vec<usize>],
383) -> Result<Arc<ExpandedEagerProgram>> {
384 runtime.with_extension_caches_mut(|caches| {
385 let plan_hash = plan_spec_hash(plan_spec);
386 let key = expanded_eager_program_cache_key(subscripts, shapes, plan_hash);
387 if let Some(cached) = caches.get::<CachedExpandedEagerProgram>(&key) {
388 let key_data = &cached.key_data;
389 if key_data.matches_expanded_eager_program(subscripts, shapes, plan_spec) {
390 return Ok(Arc::clone(&cached.program));
391 }
392 }
393
394 let tree = resolve_plan_spec(plan_spec, subs, shape_refs)
395 .map_err(|err| Error::ContractionError(err.to_string()))?;
396 let program = Arc::new(build_expanded_eager_program(&tree, shapes)?);
397 let key_data = ExpandedEagerProgramCacheKeyData::new(subscripts, shapes, plan_spec);
398 let retained_bytes = saturating_sum([
399 key_data.retained_bytes(),
400 expanded_eager_program_retained_bytes(&program),
401 ]);
402 caches.put(
403 key,
404 CachedExpandedEagerProgram {
405 key_data,
406 program: Arc::clone(&program),
407 },
408 retained_bytes,
409 );
410 Ok(program)
411 })?
412}
413
414fn expanded_eager_program_cache_key(
415 subscripts: &EinsumSubscripts,
416 shapes: &[Vec<usize>],
417 plan_hash: u64,
418) -> ExtensionCacheKey {
419 let mut hasher = DefaultHasher::new();
420 subscripts.hash(&mut hasher);
421 shapes.hash(&mut hasher);
422 plan_hash.hash(&mut hasher);
423 ExtensionCacheKey::new(
424 EINSUM_EXTENSION_FAMILY_ID,
425 EINSUM_EAGER_EXPANDED_PROGRAMS_CACHE,
426 hasher.finish(),
427 )
428}
429
430fn plan_spec_hash(plan_spec: &EinsumPlanSpec) -> u64 {
431 let mut hasher = DefaultHasher::new();
432 hash_einsum_plan_spec(plan_spec, &mut hasher);
433 hasher.finish()
434}
435
436fn plan_spec_retained_bytes(plan_spec: &EinsumPlanSpec) -> usize {
437 match plan_spec {
438 EinsumPlanSpec::Auto(options) => saturating_sum([
439 std::mem::size_of::<EinsumPlanSpec>(),
440 vec_retained_bytes(&options.betas),
441 ]),
442 EinsumPlanSpec::LeftToRight => std::mem::size_of::<EinsumPlanSpec>(),
443 EinsumPlanSpec::Path(path) | EinsumPlanSpec::FixedPairs(path) => saturating_sum([
444 std::mem::size_of::<EinsumPlanSpec>(),
445 vec_retained_bytes(path),
446 ]),
447 }
448}
449
450fn build_expanded_eager_program(
451 tree: &crate::ContractionTree,
452 shapes: &[Vec<usize>],
453) -> Result<ExpandedEagerProgram> {
454 let mut builder = GraphBuilder::<StdTensorOp>::new();
455 let mut input_vals = Vec::with_capacity(shapes.len());
456 for input_idx in 0..shapes.len() {
457 let local = builder.add_input(TensorInputKey::User {
458 id: input_idx as u64,
459 });
460 input_vals.push(ValueRef::Local(local));
461 }
462
463 let result_ref = build_einsum_graph(&mut builder, tree, &input_vals, shapes)
464 .map_err(|err| Error::ContractionError(err.to_string()))?;
465 let ValueRef::Local(result_local) = result_ref else {
466 return Err(Error::Internal(
467 "expanded eager einsum returned an external value".into(),
468 ));
469 };
470 builder.set_outputs(vec![result_local]);
471 let graph = Arc::new(builder.build());
472 let output_key = graph.values()[result_local].key.clone();
473 let view = resolve(vec![graph]);
474 let graph = materialize_merge(&view, &[output_key]);
475 let compiled = compile(&graph);
476 let input_slots = compiled
477 .input_slots
478 .iter()
479 .zip(graph.inputs.iter())
480 .map(|(&slot, key)| {
481 let ValueKey::Input(TensorInputKey::User { id }) = key else {
482 return Err(Error::Internal(format!(
483 "expanded eager einsum saw unexpected input key: {key:?}"
484 )));
485 };
486 Ok((slot, *id as usize))
487 })
488 .collect::<Result<_>>()?;
489
490 Ok(ExpandedEagerProgram {
491 compiled,
492 input_slots,
493 })
494}
495
496fn execute_eager_einsum_program(
497 inputs: &[&EagerTensor],
498 program: &ExpandedEagerProgram,
499) -> Result<Option<EagerTensor>> {
500 let mut slots: Vec<Option<EagerTensor>> = vec![None; program.compiled.n_slots];
501 for &(slot, input_idx) in &program.input_slots {
502 let tensor = inputs.get(input_idx).ok_or_else(|| {
503 Error::Internal(format!(
504 "expanded eager einsum input {input_idx} is missing"
505 ))
506 })?;
507 slots[slot] = Some((*tensor).clone());
508 }
509
510 let mut instruction_idx = 0;
511 while instruction_idx < program.compiled.instructions.len() {
512 if let Some((output_slot, output)) = try_execute_eager_broadcast_multiply_pattern(
513 &program.compiled.instructions,
514 instruction_idx,
515 &slots,
516 &program.compiled.output_slots,
517 )? {
518 slots[output_slot] = Some(output);
519 instruction_idx += 3;
520 continue;
521 }
522
523 let instr = &program.compiled.instructions[instruction_idx];
524 if instr.outputs.len() != 1 {
525 return Err(Error::Internal(format!(
526 "expanded eager einsum expected single-output op, got {} outputs",
527 instr.outputs.len()
528 )));
529 }
530 let input_values: Vec<EagerTensor> = instr
531 .inputs
532 .iter()
533 .map(|&slot| {
534 slots
535 .get(slot)
536 .and_then(Option::as_ref)
537 .cloned()
538 .ok_or_else(|| {
539 Error::Internal(format!(
540 "expanded eager einsum missing value for slot {slot}"
541 ))
542 })
543 })
544 .collect::<Result<_>>()?;
545 let input_refs: Vec<&EagerTensor> = input_values.iter().collect();
546 let output =
547 tenferro_ad::extension::apply_standard_op(instr.operation.clone(), &input_refs)?;
548 slots[instr.outputs[0]] = Some(output);
549 instruction_idx += 1;
550 }
551
552 let [output_slot] = program.compiled.output_slots.as_slice() else {
553 return Err(Error::Internal(format!(
554 "expanded eager einsum expected one graph output, got {}",
555 program.compiled.output_slots.len()
556 )));
557 };
558 slots
559 .get_mut(*output_slot)
560 .and_then(Option::take)
561 .map(Some)
562 .ok_or_else(|| Error::Internal("expanded eager einsum output slot is missing".into()))
563}
564
565fn expanded_eager_program_retained_bytes(program: &ExpandedEagerProgram) -> usize {
566 saturating_sum([
567 size_of::<ExpandedEagerProgram>(),
568 vec_retained_bytes(&program.input_slots),
569 compiled_program_retained_bytes(&program.compiled),
570 ])
571}
572
573fn compiled_program_retained_bytes(program: &CompiledProgram<StdTensorOp>) -> usize {
574 saturating_sum([
575 size_of::<CompiledProgram<StdTensorOp>>(),
576 vec_retained_bytes(&program.instructions),
577 vec_retained_bytes(&program.input_slots),
578 vec_retained_bytes(&program.output_slots),
579 saturating_sum(program.instructions.iter().map(instruction_retained_bytes)),
580 ])
581}
582
583fn instruction_retained_bytes(instruction: &Instruction<StdTensorOp>) -> usize {
584 saturating_sum([
585 size_of::<Instruction<StdTensorOp>>(),
586 std_tensor_op_retained_bytes(&instruction.operation),
587 vec_retained_bytes(&instruction.inputs),
588 vec_retained_bytes(&instruction.outputs),
589 ])
590}
591
592fn std_tensor_op_retained_bytes(op: &StdTensorOp) -> usize {
593 match op {
594 StdTensorOp::DotGeneral { config } => saturating_sum([
595 vec_retained_bytes(&config.lhs_contracting_dims),
596 vec_retained_bytes(&config.rhs_contracting_dims),
597 vec_retained_bytes(&config.lhs_batch_dims),
598 vec_retained_bytes(&config.rhs_batch_dims),
599 ]),
600 StdTensorOp::Transpose { perm } => vec_retained_bytes(perm),
601 StdTensorOp::Reshape { to_shape } => vec_retained_bytes(to_shape),
602 StdTensorOp::BroadcastInDim { shape, dims } => {
603 saturating_sum([vec_retained_bytes(shape), vec_retained_bytes(dims)])
604 }
605 StdTensorOp::Constant { bytes, .. } => vec_retained_bytes(bytes),
606 StdTensorOp::ReduceSum { axes }
607 | StdTensorOp::ReduceProd { axes }
608 | StdTensorOp::ReduceMax { axes }
609 | StdTensorOp::ReduceMin { axes }
610 | StdTensorOp::Reverse { axes } => vec_retained_bytes(axes),
611 StdTensorOp::DynamicSlice { slice_sizes } => vec_retained_bytes(slice_sizes),
612 StdTensorOp::GatherDynamicSliceSizes {
613 offset_dims,
614 collapsed_slice_dims,
615 start_index_map,
616 slice_sizes,
617 ..
618 } => saturating_sum([
619 vec_retained_bytes(offset_dims),
620 vec_retained_bytes(collapsed_slice_dims),
621 vec_retained_bytes(start_index_map),
622 vec_retained_bytes(slice_sizes),
623 ]),
624 _ => 0,
625 }
626}
627
628fn try_execute_eager_broadcast_multiply_pattern(
629 instructions: &[Instruction<StdTensorOp>],
630 instruction_idx: usize,
631 slots: &[Option<EagerTensor>],
632 output_slots: &[usize],
633) -> Result<Option<(usize, EagerTensor)>> {
634 if instruction_idx + 2 >= instructions.len() {
635 return Ok(None);
636 }
637 let lhs_bc = &instructions[instruction_idx];
638 let rhs_bc = &instructions[instruction_idx + 1];
639 let multiply = &instructions[instruction_idx + 2];
640
641 let StdTensorOp::BroadcastInDim {
642 shape: lhs_shape_exprs,
643 dims: lhs_dims,
644 } = &lhs_bc.operation
645 else {
646 return Ok(None);
647 };
648 let StdTensorOp::BroadcastInDim {
649 shape: rhs_shape_exprs,
650 dims: rhs_dims,
651 } = &rhs_bc.operation
652 else {
653 return Ok(None);
654 };
655 if !matches!(multiply.operation, StdTensorOp::Mul)
656 || lhs_bc.outputs.len() != 1
657 || rhs_bc.outputs.len() != 1
658 || multiply.outputs.len() != 1
659 || multiply.inputs.len() != 2
660 || lhs_bc.inputs.is_empty()
661 || rhs_bc.inputs.is_empty()
662 || multiply.inputs[0] != lhs_bc.outputs[0]
663 || multiply.inputs[1] != rhs_bc.outputs[0]
664 {
665 return Ok(None);
666 }
667
668 let lhs_bc_slot = lhs_bc.outputs[0];
669 let rhs_bc_slot = rhs_bc.outputs[0];
670 if output_slots.contains(&lhs_bc_slot)
671 || output_slots.contains(&rhs_bc_slot)
672 || instructions[instruction_idx + 3..]
673 .iter()
674 .any(|instr| instr.inputs.contains(&lhs_bc_slot) || instr.inputs.contains(&rhs_bc_slot))
675 {
676 return Ok(None);
677 }
678
679 let lhs = slot_tensor(slots, lhs_bc.inputs[0])?;
680 let rhs = slot_tensor(slots, rhs_bc.inputs[0])?;
681 let lhs_shape = eval_shape_exprs(slots, &lhs_bc.inputs, lhs_shape_exprs)?;
682 let rhs_shape = eval_shape_exprs(slots, &rhs_bc.inputs, rhs_shape_exprs)?;
683 let Some(output) =
684 backend_broadcast_multiply_untracked(lhs, &lhs_shape, lhs_dims, rhs, &rhs_shape, rhs_dims)?
685 else {
686 return Ok(None);
687 };
688
689 Ok(Some((multiply.outputs[0], output)))
690}
691
692#[allow(clippy::too_many_arguments)]
693fn backend_broadcast_multiply_untracked(
694 lhs: &EagerTensor,
695 lhs_shape: &[usize],
696 lhs_dims: &[usize],
697 rhs: &EagerTensor,
698 rhs_shape: &[usize],
699 rhs_dims: &[usize],
700) -> Result<Option<EagerTensor>> {
701 if !Arc::ptr_eq(lhs.runtime(), rhs.runtime()) {
702 return Err(Error::ContextMismatch {
703 lhs: lhs.ctx_id(),
704 rhs: rhs.ctx_id(),
705 });
706 }
707 if lhs.tracks_grad() || rhs.tracks_grad() {
708 return Ok(None);
709 }
710
711 let runtime = lhs.runtime();
712 let value = runtime.with_backend_mut(|backend| {
713 backend.execute_broadcast_multiply_value(
714 lhs.tensor_read(),
715 lhs_shape,
716 lhs_dims,
717 rhs.tensor_read(),
718 rhs_shape,
719 rhs_dims,
720 )
721 })??;
722
723 Ok(value.map(|value| adopt_untracked_eager_value(runtime.clone(), value)))
724}
725
726fn eval_shape_exprs(
727 slots: &[Option<EagerTensor>],
728 input_slots: &[usize],
729 shape: &[DimExpr],
730) -> Result<Vec<usize>> {
731 let inputs = input_slots
732 .iter()
733 .map(|&slot| slot_tensor(slots, slot))
734 .collect::<Result<Vec<_>>>()?;
735 let input_shapes = inputs
736 .iter()
737 .map(|tensor| tensor.shape())
738 .collect::<Vec<_>>();
739 DimExpr::eval_all(shape, &input_shapes).map_err(|err| Error::InvalidCompiledGraph {
740 message: format!("invalid eager einsum shape expression: {err}"),
741 })
742}
743
744fn slot_tensor(slots: &[Option<EagerTensor>], slot: usize) -> Result<&EagerTensor> {
745 slots.get(slot).and_then(Option::as_ref).ok_or_else(|| {
746 Error::Internal(format!(
747 "expanded eager einsum missing value for slot {slot}"
748 ))
749 })
750}
751
752fn infer_eager_output_shape(
753 subscripts: &EinsumSubscripts,
754 inputs: &[&EagerTensor],
755) -> Result<Vec<tenferro_runtime::SymDim>> {
756 if inputs.is_empty() {
757 return Err(Error::ContractionError(
758 "einsum requires at least one input tensor".into(),
759 ));
760 }
761 if subscripts.inputs.len() != inputs.len() {
762 return Err(Error::ContractionError(format!(
763 "einsum subscripts expect {} inputs, got {}",
764 subscripts.inputs.len(),
765 inputs.len()
766 )));
767 }
768
769 let mut label_dims = std::collections::HashMap::new();
770 for (labels, tensor) in subscripts.inputs.iter().zip(inputs.iter()) {
771 let shape = tensor.shape();
772 if labels.len() != shape.len() {
773 return Err(Error::ContractionError(format!(
774 "einsum input rank mismatch: labels={}, shape={}",
775 labels.len(),
776 shape.len()
777 )));
778 }
779 for (&label, &dim) in labels.iter().zip(shape.iter()) {
780 if let Some(existing) = label_dims.insert(label, dim) {
781 if existing != dim {
782 return Err(Error::ContractionError(format!(
783 "einsum label {label} has inconsistent dimensions {existing} and {dim}"
784 )));
785 }
786 }
787 }
788 }
789
790 subscripts
791 .output
792 .iter()
793 .map(|label| {
794 label_dims
795 .get(label)
796 .copied()
797 .map(tenferro_runtime::SymDim::from)
798 .ok_or_else(|| {
799 Error::ContractionError(format!(
800 "einsum output label {label} is missing from input labels"
801 ))
802 })
803 })
804 .collect()
805}
806
807pub fn tensordot(
834 lhs: &EagerTensor,
835 rhs: &EagerTensor,
836 axes: TensorDotAxes<'_>,
837) -> Result<EagerTensor> {
838 let config = crate::tensordot::dot_general_config(axes, lhs.shape().len(), rhs.shape().len())?;
839 crate::tensordot::validate_concrete_contract_dims(lhs.shape(), rhs.shape(), &config)?;
840 lhs.dot_general(rhs, config)
841}
842
843#[cfg(test)]
844mod tests;