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, 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::extension::{execute_einsum_extension_session_reads, 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, Error, Result, Subscripts, TensorDotAxes};
34
35pub trait EagerEinsumExt {
37 fn einsum(&self, subscripts: &str) -> Result<EagerTensor>;
46
47 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor>;
56}
57
58fn eager_cpu_extension_module() -> Result<Arc<dyn tenferro_runtime::ExtensionModule>> {
59 static MODULE: OnceLock<Arc<dyn tenferro_runtime::ExtensionModule>> = OnceLock::new();
60 if let Some(module) = MODULE.get() {
61 return Ok(Arc::clone(module));
62 }
63
64 let engine_id = tenferro_cpu::runtime_engine_id().map_err(eager_runtime_config_error)?;
65 let module = crate::extension::extension_module::<tenferro_cpu::CpuBackend>(engine_id)
66 .map_err(eager_runtime_config_error)?;
67 let _ = MODULE.set(Arc::clone(&module));
68 Ok(MODULE.get().cloned().unwrap_or(module))
69}
70
71fn eager_runtime_config_error(source: tenferro_runtime::RuntimeConfigError) -> Error {
72 Error::Runtime(tenferro_runtime::Error::runtime_state_source(
73 "tenferro_einsum::eager_extension_module",
74 ErrorPhase::Execution,
75 source,
76 ))
77}
78
79impl EagerEinsumExt for [&EagerTensor] {
80 fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
81 einsum(self, subscripts)
82 }
83
84 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
85 einsum_subscripts(self, subscripts)
86 }
87}
88
89impl<const N: usize> EagerEinsumExt for [&EagerTensor; N] {
90 fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
91 einsum(self.as_slice(), subscripts)
92 }
93
94 fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
95 einsum_subscripts(self.as_slice(), subscripts)
96 }
97}
98
99pub trait EagerTensorEinsumExt {
101 fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor>;
109}
110
111impl EagerTensorEinsumExt for EagerTensor {
112 fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor> {
113 tensordot(self, rhs, axes)
114 }
115}
116
117pub fn einsum(inputs: &[&EagerTensor], subscripts: &str) -> Result<EagerTensor> {
148 let subscripts = parse_einsum_subscripts(subscripts)?;
149 einsum_subscripts(inputs, &subscripts)
150}
151
152pub fn einsum_subscripts(
183 inputs: &[&EagerTensor],
184 subscripts: &EinsumSubscripts,
185) -> Result<EagerTensor> {
186 if let Some(result) = try_direct_binary_dot_general(inputs, subscripts) {
187 return result;
188 }
189
190 if let Some(result) = try_whole_program_untracked(inputs, subscripts)? {
191 return Ok(result);
192 }
193
194 let output_shape_hint = infer_eager_output_shape(subscripts, inputs)?;
195 if let Some(result) = try_expand_eager_einsum(inputs, subscripts)? {
196 return Ok(result);
197 }
198
199 let op = Arc::new(EinsumExtensionOp::with_output_shape_hint(
200 subscripts.clone(),
201 output_shape_hint,
202 EinsumPlanSpec::Auto(default_auto_options()),
203 ));
204 let execute_op = Arc::clone(&op);
205 let module = eager_cpu_extension_module()?;
206 let mut outputs =
207 apply_eager_with_extension_session(op, inputs, module, move |_op, input_reads, ctx| {
208 execute_einsum_extension_session_reads(&execute_op, input_reads, ctx)
209 })?;
210 outputs.pop().ok_or_else(|| {
211 Error::Runtime(tenferro_runtime::Error::MissingInput(
212 "einsum extension produced no eager output".into(),
213 ))
214 })
215}
216
217fn try_direct_binary_dot_general(
218 inputs: &[&EagerTensor],
219 subscripts: &EinsumSubscripts,
220) -> Option<Result<EagerTensor>> {
221 if inputs.len() != 2 || subscripts.inputs.len() != 2 {
222 return None;
223 }
224
225 let lhs_labels = &subscripts.inputs[0];
226 let rhs_labels = &subscripts.inputs[1];
227 if lhs_labels.len() != inputs[0].shape().len() || rhs_labels.len() != inputs[1].shape().len() {
228 return None;
229 }
230
231 if let Some(plan) =
232 try_build_exact_output_binary_dot_plan(lhs_labels, rhs_labels, &subscripts.output)
233 {
234 return Some(match plan.operand_order {
235 BinaryDotOperandOrder::Original => inputs[0]
236 .dot_general(inputs[1], plan.config)
237 .map_err(Error::Runtime),
238 BinaryDotOperandOrder::Swapped => inputs[1]
239 .dot_general(inputs[0], plan.config)
240 .map_err(Error::Runtime),
241 });
242 }
243 None
244}
245
246fn whole_program_untracked_enabled() -> bool {
254 std::env::var_os("TENFERRO_EAGER_WHOLE_PROGRAM").is_some()
255}
256
257fn try_whole_program_untracked(
263 inputs: &[&EagerTensor],
264 subscripts: &EinsumSubscripts,
265) -> Result<Option<EagerTensor>> {
266 if !whole_program_untracked_enabled() {
267 return Ok(None);
268 }
269 let Some(first) = inputs.first() else {
270 return Ok(None);
271 };
272 if inputs.iter().any(|tensor| tensor.tracks_grad()) {
273 return Ok(None);
274 }
275 let runtime = first.runtime();
276 if inputs
277 .iter()
278 .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
279 {
280 return Ok(None);
281 }
282
283 let subs = Subscripts::from(subscripts);
284 let tensor_arcs = inputs
285 .iter()
286 .map(|tensor| tensor.materialized().map_err(Error::Runtime))
287 .collect::<Result<Vec<_>>>()?;
288 let tensors: Vec<_> = tensor_arcs.iter().map(|tensor| tensor.as_ref()).collect();
289 let result = runtime.with_execution_session(|backend| {
290 crate::eager::eager_einsum_subscripts_with_session(backend, &tensors, &subs)
291 })??;
292 Ok(Some(EagerTensor::from_tensor_in(result, runtime.clone())?))
293}
294
295#[cfg(test)]
328fn einsum_whole_program_untracked(
329 inputs: &[&EagerTensor],
330 tree: &crate::ContractionTree,
331) -> Result<EagerTensor> {
332 let first = inputs.first().ok_or_else(|| {
333 Error::invalid_argument(
334 "einsum",
335 "inputs",
336 "einsum requires at least one input tensor",
337 )
338 })?;
339 if inputs.iter().any(|tensor| tensor.tracks_grad()) {
340 return Err(Error::invalid_argument(
341 "einsum",
342 "inputs",
343 "whole-program eager einsum requires untracked inputs",
344 ));
345 }
346 let runtime = first.runtime();
347 if inputs
348 .iter()
349 .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
350 {
351 return Err(Error::invalid_argument(
352 "einsum",
353 "inputs",
354 "whole-program eager einsum requires inputs from one runtime",
355 ));
356 }
357 let tensor_arcs = inputs
358 .iter()
359 .map(|tensor| tensor.materialized().map_err(Error::Runtime))
360 .collect::<Result<Vec<_>>>()?;
361 let tensors: Vec<_> = tensor_arcs.iter().map(|tensor| tensor.as_ref()).collect();
362 let result = runtime.with_execution_session(|backend| {
363 crate::eager::eager_einsum_with_tree(backend, &tensors, tree)
364 })??;
365 EagerTensor::from_tensor_in(result, runtime.clone()).map_err(Error::Runtime)
366}
367
368fn try_expand_eager_einsum(
369 inputs: &[&EagerTensor],
370 subscripts: &EinsumSubscripts,
371) -> Result<Option<EagerTensor>> {
372 if inputs.len() <= 1 {
373 return Ok(None);
374 }
375
376 let shapes: Vec<Vec<usize>> = inputs
377 .iter()
378 .map(|tensor| tensor.shape().to_vec())
379 .collect();
380 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
381 let subs = Subscripts::from(subscripts);
382 let plan_spec = EinsumPlanSpec::Auto(default_auto_options());
383
384 let program = cached_expanded_eager_program(
385 inputs[0].runtime(),
386 subscripts,
387 &subs,
388 &plan_spec,
389 &shape_refs,
390 &shapes,
391 )?;
392 execute_eager_einsum_program(inputs, &program)
393}
394
395struct ExpandedEagerProgram {
396 compiled: CompiledProgram<StdTensorOp>,
397 input_slots: Vec<(usize, usize)>,
398}
399
400#[derive(Clone)]
401struct ExpandedEagerProgramCacheKeyData {
402 subscripts: EinsumSubscripts,
403 shapes: Vec<Vec<usize>>,
404 plan_spec: EinsumPlanSpec,
405}
406
407impl ExpandedEagerProgramCacheKeyData {
408 fn new(
409 subscripts: &EinsumSubscripts,
410 shapes: &[Vec<usize>],
411 plan_spec: &EinsumPlanSpec,
412 ) -> Self {
413 Self {
414 subscripts: subscripts.clone(),
415 shapes: shapes.to_vec(),
416 plan_spec: plan_spec.clone(),
417 }
418 }
419
420 fn matches_expanded_eager_program(
421 &self,
422 subscripts: &EinsumSubscripts,
423 shapes: &[Vec<usize>],
424 plan_spec: &EinsumPlanSpec,
425 ) -> bool {
426 self.subscripts == *subscripts
427 && self.shapes.as_slice() == shapes
428 && plan_specs_equal(&self.plan_spec, plan_spec)
429 }
430
431 fn retained_bytes(&self) -> usize {
432 saturating_sum([
433 crate::cache::einsum_subscripts_retained_bytes(&self.subscripts),
434 saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
435 plan_spec_retained_bytes(&self.plan_spec),
436 ])
437 }
438}
439
440struct CachedExpandedEagerProgram {
441 key_data: ExpandedEagerProgramCacheKeyData,
442 program: Arc<ExpandedEagerProgram>,
443}
444
445fn cached_expanded_eager_program(
446 runtime: &Arc<EagerRuntime>,
447 subscripts: &EinsumSubscripts,
448 subs: &Subscripts,
449 plan_spec: &EinsumPlanSpec,
450 shape_refs: &[&[usize]],
451 shapes: &[Vec<usize>],
452) -> Result<Arc<ExpandedEagerProgram>> {
453 runtime.with_extension_execution_context(|extension_ctx| {
454 let caches = extension_ctx.caches_mut();
455 let plan_hash = plan_spec_hash(plan_spec);
456 let key = expanded_eager_program_cache_key(subscripts, shapes, plan_hash);
457 if let Some(cached) = caches.get::<CachedExpandedEagerProgram>(&key) {
458 let key_data = &cached.key_data;
459 if key_data.matches_expanded_eager_program(subscripts, shapes, plan_spec) {
460 return Ok(Arc::clone(&cached.program));
461 }
462 }
463
464 let tree = resolve_plan_spec(plan_spec, subs, shape_refs)?;
465 let program = Arc::new(build_expanded_eager_program(&tree, shapes)?);
466 let key_data = ExpandedEagerProgramCacheKeyData::new(subscripts, shapes, plan_spec);
467 let retained_bytes = saturating_sum([
468 key_data.retained_bytes(),
469 expanded_eager_program_retained_bytes(&program),
470 ]);
471 caches.put(
472 key,
473 CachedExpandedEagerProgram {
474 key_data,
475 program: Arc::clone(&program),
476 },
477 retained_bytes,
478 );
479 Ok(program)
480 })?
481}
482
483fn expanded_eager_program_cache_key(
484 subscripts: &EinsumSubscripts,
485 shapes: &[Vec<usize>],
486 plan_hash: u64,
487) -> ExtensionCacheKey {
488 let mut hasher = DefaultHasher::new();
489 subscripts.hash(&mut hasher);
490 shapes.hash(&mut hasher);
491 plan_hash.hash(&mut hasher);
492 ExtensionCacheKey::new(
493 EINSUM_EXTENSION_FAMILY_ID,
494 EINSUM_EAGER_EXPANDED_PROGRAMS_CACHE,
495 hasher.finish(),
496 )
497}
498
499fn plan_spec_hash(plan_spec: &EinsumPlanSpec) -> u64 {
500 let mut hasher = DefaultHasher::new();
501 hash_einsum_plan_spec(plan_spec, &mut hasher);
502 hasher.finish()
503}
504
505fn plan_spec_retained_bytes(plan_spec: &EinsumPlanSpec) -> usize {
506 match plan_spec {
507 EinsumPlanSpec::Auto(options) => saturating_sum([
508 std::mem::size_of::<EinsumPlanSpec>(),
509 vec_retained_bytes(&options.betas),
510 ]),
511 EinsumPlanSpec::LeftToRight => std::mem::size_of::<EinsumPlanSpec>(),
512 EinsumPlanSpec::Path(path) | EinsumPlanSpec::FixedPairs(path) => saturating_sum([
513 std::mem::size_of::<EinsumPlanSpec>(),
514 vec_retained_bytes(path),
515 ]),
516 }
517}
518
519fn build_expanded_eager_program(
520 tree: &crate::ContractionTree,
521 shapes: &[Vec<usize>],
522) -> Result<ExpandedEagerProgram> {
523 let mut builder = GraphBuilder::<StdTensorOp>::new();
524 let mut input_vals = Vec::with_capacity(shapes.len());
525 for input_idx in 0..shapes.len() {
526 let local = builder.add_input(TensorInputKey::User {
527 id: input_idx as u64,
528 });
529 input_vals.push(ValueRef::Local(local));
530 }
531
532 let result_ref = build_einsum_graph(&mut builder, tree, &input_vals, shapes)?;
533 let ValueRef::Local(result_local) = result_ref else {
534 return Err(Error::Runtime(tenferro_runtime::Error::Internal(
535 "expanded eager einsum returned an external value".into(),
536 )));
537 };
538 builder.set_outputs(vec![result_local]);
539 let graph = Arc::new(builder.build());
540 let output_key = graph.values()[result_local].key.clone();
541 let view = resolve(vec![graph]);
542 let graph = materialize_merge(&view, &[output_key]);
543 let compiled = compile(&graph);
544 let input_slots = compiled
545 .input_slots
546 .iter()
547 .zip(graph.inputs.iter())
548 .map(|(&slot, key)| {
549 let ValueKey::Input(TensorInputKey::User { id }) = key else {
550 return Err(runtime_internal(format!(
551 "expanded eager einsum saw unexpected input key: {key:?}"
552 )));
553 };
554 Ok((slot, *id as usize))
555 })
556 .collect::<Result<_>>()?;
557
558 Ok(ExpandedEagerProgram {
559 compiled,
560 input_slots,
561 })
562}
563
564fn execute_eager_einsum_program(
565 inputs: &[&EagerTensor],
566 program: &ExpandedEagerProgram,
567) -> Result<Option<EagerTensor>> {
568 let mut slots: Vec<Option<EagerTensor>> = vec![None; program.compiled.n_slots];
569 for &(slot, input_idx) in &program.input_slots {
570 let tensor = inputs.get(input_idx).ok_or_else(|| {
571 runtime_missing(format!(
572 "expanded eager einsum input {input_idx} is missing"
573 ))
574 })?;
575 slots[slot] = Some((*tensor).clone());
576 }
577
578 let mut instruction_idx = 0;
579 while instruction_idx < program.compiled.instructions.len() {
580 if let Some((output_slot, output)) = try_execute_eager_broadcast_multiply_pattern(
581 &program.compiled.instructions,
582 instruction_idx,
583 &slots,
584 &program.compiled.output_slots,
585 )? {
586 slots[output_slot] = Some(output);
587 instruction_idx += 3;
588 continue;
589 }
590
591 let instr = &program.compiled.instructions[instruction_idx];
592 if instr.outputs.len() != 1 {
593 return Err(runtime_internal(format!(
594 "expanded eager einsum expected single-output op, got {} outputs",
595 instr.outputs.len()
596 )));
597 }
598 let input_refs: Vec<&EagerTensor> = instr
599 .inputs
600 .iter()
601 .map(|&slot| slot_tensor(&slots, slot))
602 .collect::<Result<_>>()?;
603 let output =
604 tenferro_ad::extension::apply_standard_op(instr.operation.clone(), &input_refs)?;
605 slots[instr.outputs[0]] = Some(output);
606 instruction_idx += 1;
607 }
608
609 let [output_slot] = program.compiled.output_slots.as_slice() else {
610 return Err(runtime_internal(format!(
611 "expanded eager einsum expected one graph output, got {}",
612 program.compiled.output_slots.len()
613 )));
614 };
615 slots
616 .get_mut(*output_slot)
617 .and_then(Option::take)
618 .map(Some)
619 .ok_or_else(|| runtime_missing("expanded eager einsum output slot is missing"))
620}
621
622fn expanded_eager_program_retained_bytes(program: &ExpandedEagerProgram) -> usize {
623 saturating_sum([
624 size_of::<ExpandedEagerProgram>(),
625 vec_retained_bytes(&program.input_slots),
626 compiled_program_retained_bytes(&program.compiled),
627 ])
628}
629
630fn compiled_program_retained_bytes(program: &CompiledProgram<StdTensorOp>) -> usize {
631 saturating_sum([
632 size_of::<CompiledProgram<StdTensorOp>>(),
633 vec_retained_bytes(&program.instructions),
634 vec_retained_bytes(&program.input_slots),
635 vec_retained_bytes(&program.output_slots),
636 saturating_sum(program.instructions.iter().map(instruction_retained_bytes)),
637 ])
638}
639
640fn instruction_retained_bytes(instruction: &Instruction<StdTensorOp>) -> usize {
641 saturating_sum([
642 size_of::<Instruction<StdTensorOp>>(),
643 std_tensor_op_retained_bytes(&instruction.operation),
644 vec_retained_bytes(&instruction.inputs),
645 vec_retained_bytes(&instruction.outputs),
646 ])
647}
648
649fn std_tensor_op_retained_bytes(op: &StdTensorOp) -> usize {
650 match op {
651 StdTensorOp::DotGeneral { config } => saturating_sum([
652 vec_retained_bytes(&config.lhs_contracting_dims),
653 vec_retained_bytes(&config.rhs_contracting_dims),
654 vec_retained_bytes(&config.lhs_batch_dims),
655 vec_retained_bytes(&config.rhs_batch_dims),
656 ]),
657 StdTensorOp::Transpose { perm } => vec_retained_bytes(perm),
658 StdTensorOp::Reshape { to_shape } => vec_retained_bytes(to_shape),
659 StdTensorOp::BroadcastInDim { shape, dims } => {
660 saturating_sum([vec_retained_bytes(shape), vec_retained_bytes(dims)])
661 }
662 StdTensorOp::Constant { bytes, .. } => vec_retained_bytes(bytes),
663 StdTensorOp::ReduceSum { axes }
664 | StdTensorOp::ReduceProd { axes }
665 | StdTensorOp::ReduceMax { axes }
666 | StdTensorOp::ReduceMin { axes }
667 | StdTensorOp::Reverse { axes } => vec_retained_bytes(axes),
668 StdTensorOp::DynamicSlice { slice_sizes } => vec_retained_bytes(slice_sizes),
669 StdTensorOp::GatherDynamicSliceSizes {
670 offset_dims,
671 collapsed_slice_dims,
672 start_index_map,
673 slice_sizes,
674 ..
675 } => saturating_sum([
676 vec_retained_bytes(offset_dims),
677 vec_retained_bytes(collapsed_slice_dims),
678 vec_retained_bytes(start_index_map),
679 vec_retained_bytes(slice_sizes),
680 ]),
681 _ => 0,
682 }
683}
684
685fn try_execute_eager_broadcast_multiply_pattern(
686 instructions: &[Instruction<StdTensorOp>],
687 instruction_idx: usize,
688 slots: &[Option<EagerTensor>],
689 output_slots: &[usize],
690) -> Result<Option<(usize, EagerTensor)>> {
691 if instruction_idx + 2 >= instructions.len() {
692 return Ok(None);
693 }
694 let lhs_bc = &instructions[instruction_idx];
695 let rhs_bc = &instructions[instruction_idx + 1];
696 let multiply = &instructions[instruction_idx + 2];
697
698 let StdTensorOp::BroadcastInDim {
699 shape: lhs_shape_exprs,
700 dims: lhs_dims,
701 } = &lhs_bc.operation
702 else {
703 return Ok(None);
704 };
705 let StdTensorOp::BroadcastInDim {
706 shape: rhs_shape_exprs,
707 dims: rhs_dims,
708 } = &rhs_bc.operation
709 else {
710 return Ok(None);
711 };
712 if !matches!(multiply.operation, StdTensorOp::Mul)
713 || lhs_bc.outputs.len() != 1
714 || rhs_bc.outputs.len() != 1
715 || multiply.outputs.len() != 1
716 || multiply.inputs.len() != 2
717 || lhs_bc.inputs.is_empty()
718 || rhs_bc.inputs.is_empty()
719 || multiply.inputs[0] != lhs_bc.outputs[0]
720 || multiply.inputs[1] != rhs_bc.outputs[0]
721 {
722 return Ok(None);
723 }
724
725 let lhs_bc_slot = lhs_bc.outputs[0];
726 let rhs_bc_slot = rhs_bc.outputs[0];
727 if output_slots.contains(&lhs_bc_slot)
728 || output_slots.contains(&rhs_bc_slot)
729 || instructions[instruction_idx + 3..]
730 .iter()
731 .any(|instr| instr.inputs.contains(&lhs_bc_slot) || instr.inputs.contains(&rhs_bc_slot))
732 {
733 return Ok(None);
734 }
735
736 let lhs = slot_tensor(slots, lhs_bc.inputs[0])?;
737 let rhs = slot_tensor(slots, rhs_bc.inputs[0])?;
738 let lhs_shape = eval_shape_exprs(slots, &lhs_bc.inputs, lhs_shape_exprs)?;
739 let rhs_shape = eval_shape_exprs(slots, &rhs_bc.inputs, rhs_shape_exprs)?;
740 let Some(output) =
741 backend_broadcast_multiply_untracked(lhs, &lhs_shape, lhs_dims, rhs, &rhs_shape, rhs_dims)?
742 else {
743 return Ok(None);
744 };
745
746 Ok(Some((multiply.outputs[0], output)))
747}
748
749#[allow(clippy::too_many_arguments)]
750fn backend_broadcast_multiply_untracked(
751 lhs: &EagerTensor,
752 lhs_shape: &[usize],
753 lhs_dims: &[usize],
754 rhs: &EagerTensor,
755 rhs_shape: &[usize],
756 rhs_dims: &[usize],
757) -> Result<Option<EagerTensor>> {
758 if !Arc::ptr_eq(lhs.runtime(), rhs.runtime()) {
759 return Err(tenferro_runtime::Error::ContextMismatch {
760 lhs: lhs.ctx_id(),
761 rhs: rhs.ctx_id(),
762 }
763 .into());
764 }
765 if lhs.tracks_grad() || rhs.tracks_grad() {
766 return Ok(None);
767 }
768
769 let runtime = lhs.runtime();
770 let value = runtime.with_execution_session(|backend| {
771 backend.execute_broadcast_multiply_value(
772 lhs.tensor_read(),
773 lhs_shape,
774 lhs_dims,
775 rhs.tensor_read(),
776 rhs_shape,
777 rhs_dims,
778 )
779 })??;
780
781 Ok(value.map(|value| adopt_untracked_eager_value(runtime.clone(), value)))
782}
783
784fn eval_shape_exprs(
785 slots: &[Option<EagerTensor>],
786 input_slots: &[usize],
787 shape: &[DimExpr],
788) -> Result<Vec<usize>> {
789 let inputs = input_slots
790 .iter()
791 .map(|&slot| slot_tensor(slots, slot))
792 .collect::<Result<Vec<_>>>()?;
793 let input_shapes = inputs
794 .iter()
795 .map(|tensor| tensor.shape())
796 .collect::<Vec<_>>();
797 DimExpr::eval_all(shape, &input_shapes).map_err(|error| {
798 runtime_extension_error(
799 "einsum",
800 ErrorKind::Validation(ValidationKind::InvalidArgument),
801 error,
802 )
803 })
804}
805
806fn slot_tensor(slots: &[Option<EagerTensor>], slot: usize) -> Result<&EagerTensor> {
807 slots.get(slot).and_then(Option::as_ref).ok_or_else(|| {
808 Error::Runtime(tenferro_runtime::Error::MissingInput(format!(
809 "expanded eager einsum missing value for slot {slot}"
810 )))
811 })
812}
813
814fn infer_eager_output_shape(
815 subscripts: &EinsumSubscripts,
816 inputs: &[&EagerTensor],
817) -> Result<Vec<tenferro_runtime::SymDim>> {
818 if inputs.is_empty() {
819 return Err(Error::invalid_argument(
820 "einsum",
821 "inputs",
822 "einsum requires at least one input tensor",
823 ));
824 }
825 if subscripts.inputs.len() != inputs.len() {
826 return Err(Error::invalid_argument(
827 "einsum",
828 "inputs",
829 format!(
830 "einsum subscripts expect {} inputs, got {}",
831 subscripts.inputs.len(),
832 inputs.len()
833 ),
834 ));
835 }
836
837 let mut label_dims = std::collections::HashMap::new();
838 for (labels, tensor) in subscripts.inputs.iter().zip(inputs.iter()) {
839 let shape = tensor.shape();
840 if labels.len() != shape.len() {
841 return Err(Error::validation(
842 "einsum",
843 ValidationError::RankMismatch {
844 expected: labels.len(),
845 actual: shape.len(),
846 },
847 ));
848 }
849 for (&label, &dim) in labels.iter().zip(shape.iter()) {
850 if let Some(existing) = label_dims.insert(label, dim) {
851 if existing != dim {
852 return Err(Error::validation(
853 "einsum",
854 ShapeMismatch::ExpectedActual {
855 expected: tenferro_tensor::ShapeVec::from_vec(vec![existing]),
856 actual: tenferro_tensor::ShapeVec::from_vec(vec![dim]),
857 }
858 .into(),
859 ));
860 }
861 }
862 }
863 }
864
865 subscripts
866 .output
867 .iter()
868 .map(|label| {
869 label_dims
870 .get(label)
871 .copied()
872 .map(tenferro_runtime::SymDim::from)
873 .ok_or_else(|| {
874 Error::invalid_argument(
875 "einsum",
876 "output",
877 format!("einsum output label {label} is missing from input labels"),
878 )
879 })
880 })
881 .collect()
882}
883
884fn runtime_extension_error<E>(op: &'static str, kind: ErrorKind, source: E) -> Error
885where
886 E: StdError + Send + Sync + 'static,
887{
888 Error::Runtime(tenferro_runtime::Error::extension(
889 op,
890 ErrorPhase::Execution,
891 EINSUM_EXTENSION_FAMILY_ID,
892 kind,
893 source,
894 ))
895}
896
897fn runtime_internal(message: impl Into<String>) -> Error {
898 Error::Runtime(tenferro_runtime::Error::Internal(message.into()))
899}
900
901fn runtime_missing(message: impl Into<String>) -> Error {
902 Error::Runtime(tenferro_runtime::Error::MissingInput(message.into()))
903}
904
905pub fn tensordot(
939 lhs: &EagerTensor,
940 rhs: &EagerTensor,
941 axes: TensorDotAxes<'_>,
942) -> Result<EagerTensor> {
943 let config = crate::tensordot::dot_general_config(axes, lhs.shape().len(), rhs.shape().len())?;
944 crate::tensordot::validate_concrete_contract_dims(lhs.shape(), rhs.shape(), &config)?;
945 lhs.dot_general(rhs, config).map_err(Error::Runtime)
946}
947
948#[cfg(test)]
949mod tests;