1use std::collections::hash_map::DefaultHasher;
4use std::error::Error as StdError;
5use std::hash::{Hash, Hasher};
6use std::mem::size_of;
7use std::sync::{Arc, OnceLock};
8
9use computegraph::compile::{compile, CompiledProgram, Instruction};
10use computegraph::graph::GraphBuilder;
11use computegraph::materialize::materialize_merge;
12use computegraph::resolve::resolve;
13use computegraph::types::{ValueKey, ValueRef};
14use tenferro_ad::extension::{adopt_untracked_eager_value, apply_eager_with_extension_session};
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::{ErrorPhase, ExtensionCacheKey};
20use tenferro_tensor::{ErrorKind, ShapeMismatch, Tensor, ValidationError, ValidationKind};
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::ellipsis::resolve_einsum_notation;
29use crate::extension::EinsumExtensionOp;
30use crate::optimize::{
31 default_auto_options, hash_einsum_plan_spec, plan_specs_equal, resolve_plan_spec,
32 EinsumPlanSpec,
33};
34use crate::{
35 parse_einsum_notation, EinsumNotation, EinsumSubscripts, Error, Result, Subscripts,
36 TensorDotAxes,
37};
38
39pub trait EagerEinsumExt {
41 fn einsum(&self, subscripts: &str) -> Result<EagerTensor>;
50
51 fn einsum_notation(&self, notation: &EinsumNotation) -> Result<EagerTensor>;
65
66 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor>;
75}
76
77fn eager_cpu_extension_module(
78) -> tenferro_runtime::Result<Arc<dyn tenferro_runtime::ExtensionModule>> {
79 static MODULE: OnceLock<Arc<dyn tenferro_runtime::ExtensionModule>> = OnceLock::new();
80 if let Some(module) = MODULE.get() {
81 return Ok(Arc::clone(module));
82 }
83
84 let engine_id = tenferro_cpu::runtime_engine_id().map_err(|source| {
85 tenferro_runtime::Error::runtime_state_source(
86 "tenferro_einsum::eager_extension_module",
87 ErrorPhase::Execution,
88 source,
89 )
90 })?;
91 let module = crate::extension::extension_module::<tenferro_cpu::CpuBackend>(engine_id)
92 .map_err(|source| {
93 tenferro_runtime::Error::runtime_state_source(
94 "tenferro_einsum::eager_extension_module",
95 ErrorPhase::Execution,
96 source,
97 )
98 })?;
99 let _ = MODULE.set(Arc::clone(&module));
100 Ok(MODULE.get().cloned().unwrap_or(module))
101}
102
103impl EagerEinsumExt for [&EagerTensor] {
104 fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
105 einsum(self, subscripts)
106 }
107
108 fn einsum_notation(&self, notation: &EinsumNotation) -> Result<EagerTensor> {
109 einsum_notation(self, notation)
110 }
111
112 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
113 einsum_subscripts(self, subscripts)
114 }
115}
116
117impl<const N: usize> EagerEinsumExt for [&EagerTensor; N] {
118 fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
119 einsum(self.as_slice(), subscripts)
120 }
121
122 fn einsum_notation(&self, notation: &EinsumNotation) -> Result<EagerTensor> {
123 self.as_slice().einsum_notation(notation)
124 }
125
126 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
127 einsum_subscripts(self.as_slice(), subscripts)
128 }
129}
130
131pub trait EagerTensorEinsumExt {
133 fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor>;
141}
142
143impl EagerTensorEinsumExt for EagerTensor {
144 fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor> {
145 tensordot(self, rhs, axes)
146 }
147}
148
149pub fn einsum(inputs: &[&EagerTensor], subscripts: &str) -> Result<EagerTensor> {
180 let notation = parse_einsum_notation(subscripts)?;
181 einsum_notation(inputs, ¬ation)
182}
183
184pub fn einsum_notation(inputs: &[&EagerTensor], notation: &EinsumNotation) -> Result<EagerTensor> {
193 let shapes: Vec<&[usize]> = inputs.iter().map(|tensor| tensor.shape()).collect();
194 let subscripts = resolve_einsum_notation(notation, &shapes)?;
195 let subscripts = EinsumSubscripts::from(subscripts);
196 let allow_broadcast = notation
197 .inputs
198 .iter()
199 .chain(std::iter::once(¬ation.output))
200 .any(|term| term.contains(&crate::EinsumAxis::Ellipsis))
201 || requires_broadcast(inputs, &subscripts);
202 einsum_subscripts_with_broadcast(inputs, &subscripts, allow_broadcast)
203}
204
205pub fn einsum_subscripts(
236 inputs: &[&EagerTensor],
237 subscripts: &EinsumSubscripts,
238) -> Result<EagerTensor> {
239 einsum_subscripts_with_broadcast(inputs, subscripts, false)
240}
241
242fn einsum_subscripts_with_broadcast(
243 inputs: &[&EagerTensor],
244 subscripts: &EinsumSubscripts,
245 allow_broadcast: bool,
246) -> Result<EagerTensor> {
247 if let Some(result) = try_direct_binary_dot_general(inputs, subscripts) {
248 return result;
249 }
250
251 if let Some(result) = try_whole_program_untracked(inputs, subscripts)? {
252 return Ok(result);
253 }
254
255 let output_shape_hint = infer_eager_output_shape(subscripts, inputs)?;
256 if !requires_broadcast(inputs, subscripts) {
257 if let Some(result) = try_expand_eager_einsum(inputs, subscripts)? {
258 return Ok(result);
259 }
260 }
261
262 let plan_spec = EinsumPlanSpec::Auto(default_auto_options());
263 let op = Arc::new(if allow_broadcast {
264 EinsumExtensionOp::with_output_shape_hint_and_broadcast(
265 subscripts.clone(),
266 output_shape_hint,
267 plan_spec,
268 true,
269 )
270 } else {
271 EinsumExtensionOp::with_output_shape_hint(subscripts.clone(), output_shape_hint, plan_spec)
272 });
273 let mut outputs =
274 apply_eager_with_extension_session(op, inputs, eager_cpu_extension_module()?)?;
275 outputs.pop().ok_or_else(|| {
276 Error::Runtime(tenferro_runtime::Error::MissingInput(
277 "einsum extension produced no eager output".into(),
278 ))
279 })
280}
281
282fn try_direct_binary_dot_general(
283 inputs: &[&EagerTensor],
284 subscripts: &EinsumSubscripts,
285) -> Option<Result<EagerTensor>> {
286 if inputs.len() != 2 || subscripts.inputs.len() != 2 {
287 return None;
288 }
289
290 let lhs_labels = &subscripts.inputs[0];
291 let rhs_labels = &subscripts.inputs[1];
292 if lhs_labels.len() != inputs[0].shape().len() || rhs_labels.len() != inputs[1].shape().len() {
293 return None;
294 }
295
296 if let Some(plan) =
297 try_build_exact_output_binary_dot_plan(lhs_labels, rhs_labels, &subscripts.output)
298 {
299 let (lhs, rhs) = match plan.operand_order {
300 BinaryDotOperandOrder::Original => (inputs[0], inputs[1]),
301 BinaryDotOperandOrder::Swapped => (inputs[1], inputs[0]),
302 };
303 if !exact_dot_shapes(lhs.shape(), rhs.shape(), &plan.config) {
304 return None;
305 }
306 return Some(lhs.dot_general(rhs, plan.config).map_err(Error::Runtime));
307 }
308 None
309}
310
311fn requires_broadcast(inputs: &[&EagerTensor], subscripts: &EinsumSubscripts) -> bool {
312 let mut sizes = std::collections::HashMap::<u32, usize>::new();
313 for (tensor, labels) in inputs.iter().zip(&subscripts.inputs) {
314 for (&label, &size) in labels.iter().zip(tensor.shape()) {
315 if let Some(previous) = sizes.insert(label, size) {
316 if previous != size && (previous == 1 || size == 1) {
317 return true;
318 }
319 }
320 }
321 }
322 false
323}
324
325fn exact_dot_shapes(
326 lhs_shape: &[usize],
327 rhs_shape: &[usize],
328 config: &tenferro_tensor::DotGeneralConfig,
329) -> bool {
330 config
331 .lhs_contracting_dims
332 .iter()
333 .zip(&config.rhs_contracting_dims)
334 .all(|(&lhs, &rhs)| lhs_shape[lhs] == rhs_shape[rhs])
335 && config
336 .lhs_batch_dims
337 .iter()
338 .zip(&config.rhs_batch_dims)
339 .all(|(&lhs, &rhs)| lhs_shape[lhs] == rhs_shape[rhs])
340}
341
342fn whole_program_untracked_enabled() -> bool {
351 std::env::var_os("TENFERRO_EAGER_WHOLE_PROGRAM").is_some()
352}
353
354fn try_whole_program_untracked(
360 inputs: &[&EagerTensor],
361 subscripts: &EinsumSubscripts,
362) -> Result<Option<EagerTensor>> {
363 if !whole_program_untracked_enabled() {
364 return Ok(None);
365 }
366 let Some(first) = inputs.first() else {
367 return Ok(None);
368 };
369 if inputs.iter().any(|tensor| tensor.tracks_grad()) {
370 return Ok(None);
371 }
372 let runtime = first.runtime();
373 if inputs
374 .iter()
375 .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
376 {
377 return Ok(None);
378 }
379
380 let subs = Subscripts::from(subscripts);
381 let tensor_owners = inputs
382 .iter()
383 .map(|tensor| tensor.to_tensor().map_err(Error::Runtime))
384 .collect::<Result<Vec<Tensor>>>()?;
385 let tensors: Vec<_> = tensor_owners.iter().collect();
386 let result = runtime.with_execution_session(|backend| {
387 crate::eager::eager_einsum_subscripts_with_session(backend, &tensors, &subs)
388 })??;
389 Ok(Some(EagerTensor::from_tensor_in(result, runtime.clone())?))
390}
391
392#[cfg(test)]
425fn einsum_whole_program_untracked(
426 inputs: &[&EagerTensor],
427 tree: &crate::ContractionTree,
428) -> Result<EagerTensor> {
429 let first = inputs.first().ok_or_else(|| {
430 Error::invalid_argument(
431 "einsum",
432 "inputs",
433 "einsum requires at least one input tensor",
434 )
435 })?;
436 if inputs.iter().any(|tensor| tensor.tracks_grad()) {
437 return Err(Error::invalid_argument(
438 "einsum",
439 "inputs",
440 "whole-program eager einsum requires untracked inputs",
441 ));
442 }
443 let runtime = first.runtime();
444 if inputs
445 .iter()
446 .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
447 {
448 return Err(Error::invalid_argument(
449 "einsum",
450 "inputs",
451 "whole-program eager einsum requires inputs from one runtime",
452 ));
453 }
454 let tensor_owners = inputs
455 .iter()
456 .map(|tensor| tensor.to_tensor().map_err(Error::Runtime))
457 .collect::<Result<Vec<Tensor>>>()?;
458 let tensors: Vec<_> = tensor_owners.iter().collect();
459 let result = runtime.with_execution_session(|backend| {
460 crate::eager::eager_einsum_with_tree(backend, &tensors, tree)
461 })??;
462 EagerTensor::from_tensor_in(result, runtime.clone()).map_err(Error::Runtime)
463}
464
465fn try_expand_eager_einsum(
466 inputs: &[&EagerTensor],
467 subscripts: &EinsumSubscripts,
468) -> Result<Option<EagerTensor>> {
469 if inputs.len() <= 1 {
470 return Ok(None);
471 }
472
473 let shapes: Vec<Vec<usize>> = inputs
474 .iter()
475 .map(|tensor| tensor.shape().to_vec())
476 .collect();
477 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
478 let subs = Subscripts::from(subscripts);
479 let plan_spec = EinsumPlanSpec::Auto(default_auto_options());
480
481 let program = cached_expanded_eager_program(
482 inputs[0].runtime(),
483 subscripts,
484 &subs,
485 &plan_spec,
486 &shape_refs,
487 &shapes,
488 )?;
489 execute_eager_einsum_program(inputs, &program)
490}
491
492struct ExpandedEagerProgram {
493 compiled: CompiledProgram<StdTensorOp>,
494 input_slots: Vec<(usize, usize)>,
495}
496
497#[derive(Clone)]
498struct ExpandedEagerProgramCacheKeyData {
499 subscripts: EinsumSubscripts,
500 shapes: Vec<Vec<usize>>,
501 plan_spec: EinsumPlanSpec,
502}
503
504impl ExpandedEagerProgramCacheKeyData {
505 fn new(
506 subscripts: &EinsumSubscripts,
507 shapes: &[Vec<usize>],
508 plan_spec: &EinsumPlanSpec,
509 ) -> Self {
510 Self {
511 subscripts: subscripts.clone(),
512 shapes: shapes.to_vec(),
513 plan_spec: plan_spec.clone(),
514 }
515 }
516
517 fn matches_expanded_eager_program(
518 &self,
519 subscripts: &EinsumSubscripts,
520 shapes: &[Vec<usize>],
521 plan_spec: &EinsumPlanSpec,
522 ) -> bool {
523 self.subscripts == *subscripts
524 && self.shapes.as_slice() == shapes
525 && plan_specs_equal(&self.plan_spec, plan_spec)
526 }
527
528 fn retained_bytes(&self) -> usize {
529 saturating_sum([
530 crate::cache::einsum_subscripts_retained_bytes(&self.subscripts),
531 saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
532 plan_spec_retained_bytes(&self.plan_spec),
533 ])
534 }
535}
536
537struct CachedExpandedEagerProgram {
538 key_data: ExpandedEagerProgramCacheKeyData,
539 program: Arc<ExpandedEagerProgram>,
540}
541
542fn cached_expanded_eager_program(
543 runtime: &Arc<EagerRuntime>,
544 subscripts: &EinsumSubscripts,
545 subs: &Subscripts,
546 plan_spec: &EinsumPlanSpec,
547 shape_refs: &[&[usize]],
548 shapes: &[Vec<usize>],
549) -> Result<Arc<ExpandedEagerProgram>> {
550 runtime.with_extension_execution_context(|extension_ctx| {
551 let caches = extension_ctx.caches_mut();
552 let plan_hash = plan_spec_hash(plan_spec);
553 let key = expanded_eager_program_cache_key(subscripts, shapes, plan_hash);
554 if let Some(cached) = caches.get::<CachedExpandedEagerProgram>(&key) {
555 let key_data = &cached.key_data;
556 if key_data.matches_expanded_eager_program(subscripts, shapes, plan_spec) {
557 return Ok(Arc::clone(&cached.program));
558 }
559 }
560
561 let tree = resolve_plan_spec(plan_spec, subs, shape_refs)?;
562 let program = Arc::new(build_expanded_eager_program(&tree, shapes)?);
563 let key_data = ExpandedEagerProgramCacheKeyData::new(subscripts, shapes, plan_spec);
564 let retained_bytes = saturating_sum([
565 key_data.retained_bytes(),
566 expanded_eager_program_retained_bytes(&program),
567 ]);
568 caches.put(
569 key,
570 CachedExpandedEagerProgram {
571 key_data,
572 program: Arc::clone(&program),
573 },
574 retained_bytes,
575 );
576 Ok(program)
577 })?
578}
579
580fn expanded_eager_program_cache_key(
581 subscripts: &EinsumSubscripts,
582 shapes: &[Vec<usize>],
583 plan_hash: u64,
584) -> ExtensionCacheKey {
585 let mut hasher = DefaultHasher::new();
586 subscripts.hash(&mut hasher);
587 shapes.hash(&mut hasher);
588 plan_hash.hash(&mut hasher);
589 ExtensionCacheKey::new(
590 EINSUM_EXTENSION_FAMILY_ID,
591 EINSUM_EAGER_EXPANDED_PROGRAMS_CACHE,
592 hasher.finish(),
593 )
594}
595
596fn plan_spec_hash(plan_spec: &EinsumPlanSpec) -> u64 {
597 let mut hasher = DefaultHasher::new();
598 hash_einsum_plan_spec(plan_spec, &mut hasher);
599 hasher.finish()
600}
601
602fn plan_spec_retained_bytes(plan_spec: &EinsumPlanSpec) -> usize {
603 match plan_spec {
604 EinsumPlanSpec::Auto(options) => saturating_sum([
605 std::mem::size_of::<EinsumPlanSpec>(),
606 vec_retained_bytes(&options.betas),
607 ]),
608 EinsumPlanSpec::LeftToRight => std::mem::size_of::<EinsumPlanSpec>(),
609 EinsumPlanSpec::Path(path) | EinsumPlanSpec::FixedPairs(path) => saturating_sum([
610 std::mem::size_of::<EinsumPlanSpec>(),
611 vec_retained_bytes(path),
612 ]),
613 }
614}
615
616fn build_expanded_eager_program(
617 tree: &crate::ContractionTree,
618 shapes: &[Vec<usize>],
619) -> Result<ExpandedEagerProgram> {
620 let mut builder = GraphBuilder::<StdTensorOp>::new();
621 let mut input_vals = Vec::with_capacity(shapes.len());
622 for input_idx in 0..shapes.len() {
623 let local = builder.add_input(TensorInputKey::User {
624 id: input_idx as u64,
625 });
626 input_vals.push(ValueRef::Local(local));
627 }
628
629 let result_ref = build_einsum_graph(&mut builder, tree, &input_vals, shapes)?;
630 let ValueRef::Local(result_local) = result_ref else {
631 return Err(Error::Runtime(tenferro_runtime::Error::Internal(
632 "expanded eager einsum returned an external value".into(),
633 )));
634 };
635 builder.set_outputs(vec![result_local]);
636 let graph = Arc::new(builder.build());
637 let output_key = graph.values()[result_local].key.clone();
638 let view = resolve(vec![graph]);
639 let graph = materialize_merge(&view, &[output_key]);
640 let compiled = compile(&graph);
641 let input_slots = compiled
642 .input_slots
643 .iter()
644 .zip(graph.inputs.iter())
645 .map(|(&slot, key)| {
646 let ValueKey::Input(TensorInputKey::User { id }) = key else {
647 return Err(runtime_internal(format!(
648 "expanded eager einsum saw unexpected input key: {key:?}"
649 )));
650 };
651 Ok((slot, *id as usize))
652 })
653 .collect::<Result<_>>()?;
654
655 Ok(ExpandedEagerProgram {
656 compiled,
657 input_slots,
658 })
659}
660
661fn execute_eager_einsum_program(
662 inputs: &[&EagerTensor],
663 program: &ExpandedEagerProgram,
664) -> Result<Option<EagerTensor>> {
665 let mut slots: Vec<Option<EagerTensor>> = vec![None; program.compiled.n_slots];
666 for &(slot, input_idx) in &program.input_slots {
667 let tensor = inputs.get(input_idx).ok_or_else(|| {
668 runtime_missing(format!(
669 "expanded eager einsum input {input_idx} is missing"
670 ))
671 })?;
672 slots[slot] = Some((*tensor).clone());
673 }
674
675 let mut instruction_idx = 0;
676 while instruction_idx < program.compiled.instructions.len() {
677 if let Some((output_slot, output)) = try_execute_eager_broadcast_multiply_pattern(
678 &program.compiled.instructions,
679 instruction_idx,
680 &slots,
681 &program.compiled.output_slots,
682 )? {
683 slots[output_slot] = Some(output);
684 instruction_idx += 3;
685 continue;
686 }
687
688 let instr = &program.compiled.instructions[instruction_idx];
689 if instr.outputs.len() != 1 {
690 return Err(runtime_internal(format!(
691 "expanded eager einsum expected single-output op, got {} outputs",
692 instr.outputs.len()
693 )));
694 }
695 let input_refs: Vec<&EagerTensor> = instr
696 .inputs
697 .iter()
698 .map(|&slot| slot_tensor(&slots, slot))
699 .collect::<Result<_>>()?;
700 let output =
701 tenferro_ad::extension::apply_standard_op(instr.operation.clone(), &input_refs)?;
702 slots[instr.outputs[0]] = Some(output);
703 instruction_idx += 1;
704 }
705
706 let [output_slot] = program.compiled.output_slots.as_slice() else {
707 return Err(runtime_internal(format!(
708 "expanded eager einsum expected one graph output, got {}",
709 program.compiled.output_slots.len()
710 )));
711 };
712 slots
713 .get_mut(*output_slot)
714 .and_then(Option::take)
715 .map(Some)
716 .ok_or_else(|| runtime_missing("expanded eager einsum output slot is missing"))
717}
718
719fn expanded_eager_program_retained_bytes(program: &ExpandedEagerProgram) -> usize {
720 saturating_sum([
721 size_of::<ExpandedEagerProgram>(),
722 vec_retained_bytes(&program.input_slots),
723 compiled_program_retained_bytes(&program.compiled),
724 ])
725}
726
727fn compiled_program_retained_bytes(program: &CompiledProgram<StdTensorOp>) -> usize {
728 saturating_sum([
729 size_of::<CompiledProgram<StdTensorOp>>(),
730 vec_retained_bytes(&program.instructions),
731 vec_retained_bytes(&program.input_slots),
732 vec_retained_bytes(&program.output_slots),
733 saturating_sum(program.instructions.iter().map(instruction_retained_bytes)),
734 ])
735}
736
737fn instruction_retained_bytes(instruction: &Instruction<StdTensorOp>) -> usize {
738 saturating_sum([
739 size_of::<Instruction<StdTensorOp>>(),
740 std_tensor_op_retained_bytes(&instruction.operation),
741 vec_retained_bytes(&instruction.inputs),
742 vec_retained_bytes(&instruction.outputs),
743 ])
744}
745
746fn std_tensor_op_retained_bytes(op: &StdTensorOp) -> usize {
747 match op {
748 StdTensorOp::DotGeneral { config } => saturating_sum([
749 vec_retained_bytes(&config.lhs_contracting_dims),
750 vec_retained_bytes(&config.rhs_contracting_dims),
751 vec_retained_bytes(&config.lhs_batch_dims),
752 vec_retained_bytes(&config.rhs_batch_dims),
753 ]),
754 StdTensorOp::Transpose { perm } => vec_retained_bytes(perm),
755 StdTensorOp::Reshape { to_shape } => vec_retained_bytes(to_shape),
756 StdTensorOp::BroadcastInDim { shape, dims } => {
757 saturating_sum([vec_retained_bytes(shape), vec_retained_bytes(dims)])
758 }
759 StdTensorOp::Constant { bytes, .. } => vec_retained_bytes(bytes),
760 StdTensorOp::ReduceSum { axes }
761 | StdTensorOp::ReduceProd { axes }
762 | StdTensorOp::ReduceMax { axes }
763 | StdTensorOp::ReduceMin { axes }
764 | StdTensorOp::Reverse { axes } => vec_retained_bytes(axes),
765 StdTensorOp::DynamicSlice { slice_sizes } => vec_retained_bytes(slice_sizes),
766 StdTensorOp::GatherDynamicSliceSizes {
767 offset_dims,
768 collapsed_slice_dims,
769 start_index_map,
770 slice_sizes,
771 ..
772 } => saturating_sum([
773 vec_retained_bytes(offset_dims),
774 vec_retained_bytes(collapsed_slice_dims),
775 vec_retained_bytes(start_index_map),
776 vec_retained_bytes(slice_sizes),
777 ]),
778 _ => 0,
779 }
780}
781
782fn try_execute_eager_broadcast_multiply_pattern(
783 instructions: &[Instruction<StdTensorOp>],
784 instruction_idx: usize,
785 slots: &[Option<EagerTensor>],
786 output_slots: &[usize],
787) -> Result<Option<(usize, EagerTensor)>> {
788 if instruction_idx + 2 >= instructions.len() {
789 return Ok(None);
790 }
791 let lhs_bc = &instructions[instruction_idx];
792 let rhs_bc = &instructions[instruction_idx + 1];
793 let multiply = &instructions[instruction_idx + 2];
794
795 let StdTensorOp::BroadcastInDim {
796 shape: lhs_shape_exprs,
797 dims: lhs_dims,
798 } = &lhs_bc.operation
799 else {
800 return Ok(None);
801 };
802 let StdTensorOp::BroadcastInDim {
803 shape: rhs_shape_exprs,
804 dims: rhs_dims,
805 } = &rhs_bc.operation
806 else {
807 return Ok(None);
808 };
809 if !matches!(multiply.operation, StdTensorOp::Mul)
810 || lhs_bc.outputs.len() != 1
811 || rhs_bc.outputs.len() != 1
812 || multiply.outputs.len() != 1
813 || multiply.inputs.len() != 2
814 || lhs_bc.inputs.is_empty()
815 || rhs_bc.inputs.is_empty()
816 || multiply.inputs[0] != lhs_bc.outputs[0]
817 || multiply.inputs[1] != rhs_bc.outputs[0]
818 {
819 return Ok(None);
820 }
821
822 let lhs_bc_slot = lhs_bc.outputs[0];
823 let rhs_bc_slot = rhs_bc.outputs[0];
824 if output_slots.contains(&lhs_bc_slot)
825 || output_slots.contains(&rhs_bc_slot)
826 || instructions[instruction_idx + 3..]
827 .iter()
828 .any(|instr| instr.inputs.contains(&lhs_bc_slot) || instr.inputs.contains(&rhs_bc_slot))
829 {
830 return Ok(None);
831 }
832
833 let lhs = slot_tensor(slots, lhs_bc.inputs[0])?;
834 let rhs = slot_tensor(slots, rhs_bc.inputs[0])?;
835 let lhs_shape = eval_shape_exprs(slots, &lhs_bc.inputs, lhs_shape_exprs)?;
836 let rhs_shape = eval_shape_exprs(slots, &rhs_bc.inputs, rhs_shape_exprs)?;
837 let Some(output) =
838 backend_broadcast_multiply_untracked(lhs, &lhs_shape, lhs_dims, rhs, &rhs_shape, rhs_dims)?
839 else {
840 return Ok(None);
841 };
842
843 Ok(Some((multiply.outputs[0], output)))
844}
845
846#[allow(clippy::too_many_arguments)]
847fn backend_broadcast_multiply_untracked(
848 lhs: &EagerTensor,
849 lhs_shape: &[usize],
850 lhs_dims: &[usize],
851 rhs: &EagerTensor,
852 rhs_shape: &[usize],
853 rhs_dims: &[usize],
854) -> Result<Option<EagerTensor>> {
855 if !Arc::ptr_eq(lhs.runtime(), rhs.runtime()) {
856 return Err(tenferro_runtime::Error::ContextMismatch {
857 lhs: lhs.ctx_id(),
858 rhs: rhs.ctx_id(),
859 }
860 .into());
861 }
862 if lhs.tracks_grad() || rhs.tracks_grad() {
863 return Ok(None);
864 }
865
866 let runtime = lhs.runtime();
867 let value = runtime.with_execution_session(|backend| {
868 backend.execute_broadcast_multiply_value(
869 lhs.tensor_read(),
870 lhs_shape,
871 lhs_dims,
872 rhs.tensor_read(),
873 rhs_shape,
874 rhs_dims,
875 )
876 })??;
877
878 Ok(value
879 .map(|value| adopt_untracked_eager_value(runtime.clone(), value))
880 .transpose()?)
881}
882
883fn eval_shape_exprs(
884 slots: &[Option<EagerTensor>],
885 input_slots: &[usize],
886 shape: &[DimExpr],
887) -> Result<Vec<usize>> {
888 let inputs = input_slots
889 .iter()
890 .map(|&slot| slot_tensor(slots, slot))
891 .collect::<Result<Vec<_>>>()?;
892 let input_shapes = inputs
893 .iter()
894 .map(|tensor| tensor.shape())
895 .collect::<Vec<_>>();
896 DimExpr::eval_all(shape, &input_shapes).map_err(|error| {
897 runtime_extension_error(
898 "einsum",
899 ErrorKind::Validation(ValidationKind::InvalidArgument),
900 error,
901 )
902 })
903}
904
905fn slot_tensor(slots: &[Option<EagerTensor>], slot: usize) -> Result<&EagerTensor> {
906 slots.get(slot).and_then(Option::as_ref).ok_or_else(|| {
907 Error::Runtime(tenferro_runtime::Error::MissingInput(format!(
908 "expanded eager einsum missing value for slot {slot}"
909 )))
910 })
911}
912
913fn infer_eager_output_shape(
914 subscripts: &EinsumSubscripts,
915 inputs: &[&EagerTensor],
916) -> Result<Vec<tenferro_runtime::SymDim>> {
917 if inputs.is_empty() {
918 return Err(Error::invalid_argument(
919 "einsum",
920 "inputs",
921 "einsum requires at least one input tensor",
922 ));
923 }
924 if subscripts.inputs.len() != inputs.len() {
925 return Err(Error::invalid_argument(
926 "einsum",
927 "inputs",
928 format!(
929 "einsum subscripts expect {} inputs, got {}",
930 subscripts.inputs.len(),
931 inputs.len()
932 ),
933 ));
934 }
935
936 let mut label_dims = std::collections::HashMap::new();
937 for (labels, tensor) in subscripts.inputs.iter().zip(inputs.iter()) {
938 let shape = tensor.shape();
939 if labels.len() != shape.len() {
940 return Err(Error::validation(
941 "einsum",
942 ValidationError::RankMismatch {
943 expected: labels.len(),
944 actual: shape.len(),
945 },
946 ));
947 }
948 for (&label, &dim) in labels.iter().zip(shape.iter()) {
949 if let Some(existing) = label_dims.get_mut(&label) {
950 if *existing != dim && *existing != 1 && dim != 1 {
951 return Err(Error::validation(
952 "einsum",
953 ShapeMismatch::ExpectedActual {
954 expected: tenferro_tensor::ShapeVec::from_vec(vec![*existing]),
955 actual: tenferro_tensor::ShapeVec::from_vec(vec![dim]),
956 }
957 .into(),
958 ));
959 }
960 if *existing == 1 {
961 *existing = dim;
962 }
963 } else {
964 label_dims.insert(label, dim);
965 }
966 }
967 }
968
969 subscripts
970 .output
971 .iter()
972 .map(|label| {
973 label_dims
974 .get(label)
975 .copied()
976 .map(tenferro_runtime::SymDim::from)
977 .ok_or_else(|| {
978 Error::invalid_argument(
979 "einsum",
980 "output",
981 format!("einsum output label {label} is missing from input labels"),
982 )
983 })
984 })
985 .collect()
986}
987
988fn runtime_extension_error<E>(op: &'static str, kind: ErrorKind, source: E) -> Error
989where
990 E: StdError + Send + Sync + 'static,
991{
992 Error::Runtime(tenferro_runtime::Error::extension(
993 op,
994 ErrorPhase::Execution,
995 EINSUM_EXTENSION_FAMILY_ID,
996 kind,
997 source,
998 ))
999}
1000
1001fn runtime_internal(message: impl Into<String>) -> Error {
1002 Error::Runtime(tenferro_runtime::Error::Internal(message.into()))
1003}
1004
1005fn runtime_missing(message: impl Into<String>) -> Error {
1006 Error::Runtime(tenferro_runtime::Error::MissingInput(message.into()))
1007}
1008
1009pub fn tensordot(
1043 lhs: &EagerTensor,
1044 rhs: &EagerTensor,
1045 axes: TensorDotAxes<'_>,
1046) -> Result<EagerTensor> {
1047 let config = crate::tensordot::dot_general_config(axes, lhs.shape().len(), rhs.shape().len())?;
1048 crate::tensordot::validate_concrete_contract_dims(lhs.shape(), rhs.shape(), &config)?;
1049 lhs.dot_general(rhs, config).map_err(Error::Runtime)
1050}
1051
1052#[cfg(test)]
1053mod tests;