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