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_in_session,
16 EagerExtensionBackendKind, EagerExtensionTarget,
17};
18use tenferro_ad::{EagerSession, EagerTensor};
19use tenferro_cpu::CpuEngine;
20#[cfg(feature = "cuda")]
21use tenferro_gpu::cuda::CudaEngine;
22use tenferro_ops::dim_expr::DimExpr;
23use tenferro_ops::input_key::TensorInputKey;
24use tenferro_ops::std_tensor_op::StdTensorOp;
25use tenferro_runtime::{ErrorPhase, ExtensionCacheKey, ExtensionModule};
26use tenferro_tensor::{ErrorKind, ShapeMismatch, ValidationError, ValidationKind};
27
28use crate::binary_dot::{try_build_exact_output_binary_dot_plan, BinaryDotOperandOrder};
29use crate::builder::build_einsum_graph;
30use crate::cache::{
31 saturating_sum, vec_retained_bytes, EINSUM_EAGER_EXPANDED_PROGRAMS_CACHE,
32 EINSUM_EXTENSION_FAMILY_ID,
33};
34use crate::ellipsis::resolve_einsum_notation;
35use crate::extension::EinsumExtensionOp;
36use crate::optimize::{
37 default_auto_options, hash_einsum_plan_spec, plan_specs_equal, resolve_plan_spec,
38 EinsumPlanSpec,
39};
40use crate::{
41 parse_einsum_notation, EinsumNotation, EinsumSubscripts, Error, Result, Subscripts,
42 TensorDotAxes,
43};
44
45#[cfg_attr(docsrs, doc(cfg(feature = "autodiff")))]
69pub trait EagerSessionEinsumExt {
70 fn einsum(&mut self, inputs: &[&EagerTensor], subscripts: &str) -> Result<EagerTensor>;
92
93 fn einsum_notation(
115 &mut self,
116 inputs: &[&EagerTensor],
117 notation: &EinsumNotation,
118 ) -> Result<EagerTensor>;
119
120 fn einsum_subscripts(
143 &mut self,
144 inputs: &[&EagerTensor],
145 subscripts: &EinsumSubscripts,
146 ) -> Result<EagerTensor>;
147
148 fn tensordot(
169 &mut self,
170 lhs: &EagerTensor,
171 rhs: &EagerTensor,
172 axes: TensorDotAxes<'_>,
173 ) -> Result<EagerTensor>;
174}
175
176impl EagerSessionEinsumExt for EagerSession<'_> {
177 fn einsum(&mut self, inputs: &[&EagerTensor], subscripts: &str) -> Result<EagerTensor> {
178 einsum(self, inputs, subscripts)
179 }
180
181 fn einsum_notation(
182 &mut self,
183 inputs: &[&EagerTensor],
184 notation: &EinsumNotation,
185 ) -> Result<EagerTensor> {
186 einsum_notation(self, inputs, notation)
187 }
188
189 fn einsum_subscripts(
190 &mut self,
191 inputs: &[&EagerTensor],
192 subscripts: &EinsumSubscripts,
193 ) -> Result<EagerTensor> {
194 einsum_subscripts_with_broadcast(self, inputs, subscripts, false)
195 }
196
197 fn tensordot(
198 &mut self,
199 lhs: &EagerTensor,
200 rhs: &EagerTensor,
201 axes: TensorDotAxes<'_>,
202 ) -> Result<EagerTensor> {
203 tensordot(self, lhs, rhs, axes)
204 }
205}
206
207fn eager_extension_module(
208 target: EagerExtensionTarget,
209) -> tenferro_runtime::Result<Arc<dyn ExtensionModule>> {
210 let EagerExtensionTarget {
211 engine_id,
212 backend_kind,
213 } = target;
214 match backend_kind {
215 EagerExtensionBackendKind::Cpu => {
216 crate::extension::extension_module::<CpuEngine>(engine_id)
217 .map_err(eager_runtime_config_error)
218 }
219 #[cfg(feature = "cuda")]
220 EagerExtensionBackendKind::Cuda => {
221 crate::extension::extension_module::<CudaEngine>(engine_id)
222 .map_err(eager_runtime_config_error)
223 }
224 }
225}
226
227fn eager_runtime_config_error(
228 source: tenferro_runtime::RuntimeConfigError,
229) -> tenferro_runtime::Error {
230 tenferro_runtime::Error::runtime_state_source(
231 "tenferro_einsum::eager_extension_module",
232 ErrorPhase::Execution,
233 source,
234 )
235}
236
237fn einsum(
238 session: &mut EagerSession<'_>,
239 inputs: &[&EagerTensor],
240 subscripts: &str,
241) -> Result<EagerTensor> {
242 let notation = parse_einsum_notation(subscripts)?;
243 einsum_notation(session, inputs, ¬ation)
244}
245
246fn einsum_notation(
247 session: &mut EagerSession<'_>,
248 inputs: &[&EagerTensor],
249 notation: &EinsumNotation,
250) -> Result<EagerTensor> {
251 let shapes: Vec<&[usize]> = inputs.iter().map(|tensor| tensor.shape()).collect();
252 let subscripts = resolve_einsum_notation(notation, &shapes)?;
253 let subscripts = EinsumSubscripts::from(subscripts);
254 let allow_broadcast = notation
255 .inputs
256 .iter()
257 .chain(std::iter::once(¬ation.output))
258 .any(|term| term.contains(&crate::EinsumAxis::Ellipsis))
259 || requires_broadcast(inputs, &subscripts);
260 einsum_subscripts_with_broadcast(session, inputs, &subscripts, allow_broadcast)
261}
262
263fn einsum_subscripts_with_broadcast(
264 session: &mut EagerSession<'_>,
265 inputs: &[&EagerTensor],
266 subscripts: &EinsumSubscripts,
267 allow_broadcast: bool,
268) -> Result<EagerTensor> {
269 if let Some(result) = try_direct_binary_dot_general(session, inputs, subscripts) {
270 return result;
271 }
272
273 let output_shape_hint = infer_eager_output_shape(subscripts, inputs)?;
274 if !requires_broadcast(inputs, subscripts) {
275 if let Some(result) = try_expand_eager_einsum(session, inputs, subscripts)? {
276 return Ok(result);
277 }
278 }
279
280 let plan_spec = EinsumPlanSpec::Auto(default_auto_options());
281 let op = Arc::new(if allow_broadcast {
282 EinsumExtensionOp::with_output_shape_hint_and_broadcast(
283 subscripts.clone(),
284 output_shape_hint,
285 plan_spec,
286 true,
287 )
288 } else {
289 EinsumExtensionOp::with_output_shape_hint(subscripts.clone(), output_shape_hint, plan_spec)
290 });
291 let mut outputs = apply_eager_with_targeted_extension_in_session(
292 session,
293 op,
294 inputs,
295 eager_extension_module,
296 )?;
297 outputs.pop().ok_or_else(|| {
298 Error::Runtime(tenferro_runtime::Error::MissingInput(
299 "einsum extension produced no eager output".into(),
300 ))
301 })
302}
303
304fn try_direct_binary_dot_general(
305 session: &mut EagerSession<'_>,
306 inputs: &[&EagerTensor],
307 subscripts: &EinsumSubscripts,
308) -> Option<Result<EagerTensor>> {
309 if inputs.len() != 2 || subscripts.inputs.len() != 2 {
310 return None;
311 }
312
313 let lhs_labels = &subscripts.inputs[0];
314 let rhs_labels = &subscripts.inputs[1];
315 if lhs_labels.len() != inputs[0].shape().len() || rhs_labels.len() != inputs[1].shape().len() {
316 return None;
317 }
318
319 if let Some(plan) =
320 try_build_exact_output_binary_dot_plan(lhs_labels, rhs_labels, &subscripts.output)
321 {
322 let (lhs, rhs) = match plan.operand_order {
323 BinaryDotOperandOrder::Original => (inputs[0], inputs[1]),
324 BinaryDotOperandOrder::Swapped => (inputs[1], inputs[0]),
325 };
326 if !exact_dot_shapes(lhs.shape(), rhs.shape(), &plan.config) {
327 return None;
328 }
329 return Some(
330 session
331 .dot_general(lhs, rhs, plan.config)
332 .map_err(Error::Runtime),
333 );
334 }
335 None
336}
337
338fn requires_broadcast(inputs: &[&EagerTensor], subscripts: &EinsumSubscripts) -> bool {
339 let mut sizes = std::collections::HashMap::<u32, usize>::new();
340 for (tensor, labels) in inputs.iter().zip(&subscripts.inputs) {
341 for (&label, &size) in labels.iter().zip(tensor.shape()) {
342 if let Some(previous) = sizes.insert(label, size) {
343 if previous != size && (previous == 1 || size == 1) {
344 return true;
345 }
346 }
347 }
348 }
349 false
350}
351
352fn exact_dot_shapes(
353 lhs_shape: &[usize],
354 rhs_shape: &[usize],
355 config: &tenferro_tensor::DotGeneralConfig,
356) -> bool {
357 config
358 .lhs_contracting_dims
359 .iter()
360 .zip(&config.rhs_contracting_dims)
361 .all(|(&lhs, &rhs)| lhs_shape[lhs] == rhs_shape[rhs])
362 && config
363 .lhs_batch_dims
364 .iter()
365 .zip(&config.rhs_batch_dims)
366 .all(|(&lhs, &rhs)| lhs_shape[lhs] == rhs_shape[rhs])
367}
368
369fn try_expand_eager_einsum(
370 session: &mut EagerSession<'_>,
371 inputs: &[&EagerTensor],
372 subscripts: &EinsumSubscripts,
373) -> Result<Option<EagerTensor>> {
374 if inputs.len() <= 1 {
375 return Ok(None);
376 }
377
378 let shapes: Vec<Vec<usize>> = inputs
379 .iter()
380 .map(|tensor| tensor.shape().to_vec())
381 .collect();
382 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
383 let subs = Subscripts::from(subscripts);
384 let plan_spec = EinsumPlanSpec::Auto(default_auto_options());
385
386 let program = cached_expanded_eager_program(
387 session,
388 subscripts,
389 &subs,
390 &plan_spec,
391 &shape_refs,
392 &shapes,
393 )?;
394 execute_eager_einsum_program_in_session(session, inputs, &program)
395}
396
397struct ExpandedEagerProgram {
398 compiled: CompiledProgram<StdTensorOp>,
399 input_slots: Vec<(usize, usize)>,
400}
401
402#[derive(Clone)]
403struct ExpandedEagerProgramCacheKeyData {
404 subscripts: EinsumSubscripts,
405 shapes: Vec<Vec<usize>>,
406 plan_spec: EinsumPlanSpec,
407}
408
409impl ExpandedEagerProgramCacheKeyData {
410 fn new(
411 subscripts: &EinsumSubscripts,
412 shapes: &[Vec<usize>],
413 plan_spec: &EinsumPlanSpec,
414 ) -> Self {
415 Self {
416 subscripts: subscripts.clone(),
417 shapes: shapes.to_vec(),
418 plan_spec: plan_spec.clone(),
419 }
420 }
421
422 fn matches_expanded_eager_program(
423 &self,
424 subscripts: &EinsumSubscripts,
425 shapes: &[Vec<usize>],
426 plan_spec: &EinsumPlanSpec,
427 ) -> bool {
428 self.subscripts == *subscripts
429 && self.shapes.as_slice() == shapes
430 && plan_specs_equal(&self.plan_spec, plan_spec)
431 }
432
433 fn retained_bytes(&self) -> usize {
434 saturating_sum([
435 crate::cache::einsum_subscripts_retained_bytes(&self.subscripts),
436 saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
437 plan_spec_retained_bytes(&self.plan_spec),
438 ])
439 }
440}
441
442struct CachedExpandedEagerProgram {
443 key_data: ExpandedEagerProgramCacheKeyData,
444 program: Arc<ExpandedEagerProgram>,
445}
446
447fn cached_expanded_eager_program(
448 session: &mut EagerSession<'_>,
449 subscripts: &EinsumSubscripts,
450 subs: &Subscripts,
451 plan_spec: &EinsumPlanSpec,
452 shape_refs: &[&[usize]],
453 shapes: &[Vec<usize>],
454) -> Result<Arc<ExpandedEagerProgram>> {
455 session.with_extension_caches(|caches| {
456 let plan_hash = plan_spec_hash(plan_spec);
457 let key = expanded_eager_program_cache_key(subscripts, shapes, plan_hash);
458 if let Some(cached) = caches.get::<CachedExpandedEagerProgram>(&key) {
459 let key_data = &cached.key_data;
460 if key_data.matches_expanded_eager_program(subscripts, shapes, plan_spec) {
461 return Ok(Arc::clone(&cached.program));
462 }
463 }
464
465 let tree = resolve_plan_spec(plan_spec, subs, shape_refs)?;
466 let program = Arc::new(build_expanded_eager_program(&tree, shapes)?);
467 let key_data = ExpandedEagerProgramCacheKeyData::new(subscripts, shapes, plan_spec);
468 let retained_bytes = saturating_sum([
469 key_data.retained_bytes(),
470 expanded_eager_program_retained_bytes(&program),
471 ]);
472 caches.put(
473 key,
474 CachedExpandedEagerProgram {
475 key_data,
476 program: Arc::clone(&program),
477 },
478 retained_bytes,
479 );
480 Ok(program)
481 })?
482}
483
484fn expanded_eager_program_cache_key(
485 subscripts: &EinsumSubscripts,
486 shapes: &[Vec<usize>],
487 plan_hash: u64,
488) -> ExtensionCacheKey {
489 let mut hasher = DefaultHasher::new();
490 subscripts.hash(&mut hasher);
491 shapes.hash(&mut hasher);
492 plan_hash.hash(&mut hasher);
493 ExtensionCacheKey::new(
494 EINSUM_EXTENSION_FAMILY_ID,
495 EINSUM_EAGER_EXPANDED_PROGRAMS_CACHE,
496 hasher.finish(),
497 )
498}
499
500fn plan_spec_hash(plan_spec: &EinsumPlanSpec) -> u64 {
501 let mut hasher = DefaultHasher::new();
502 hash_einsum_plan_spec(plan_spec, &mut hasher);
503 hasher.finish()
504}
505
506fn plan_spec_retained_bytes(plan_spec: &EinsumPlanSpec) -> usize {
507 match plan_spec {
508 EinsumPlanSpec::Auto(options) => saturating_sum([
509 std::mem::size_of::<EinsumPlanSpec>(),
510 vec_retained_bytes(&options.betas),
511 ]),
512 EinsumPlanSpec::LeftToRight => std::mem::size_of::<EinsumPlanSpec>(),
513 EinsumPlanSpec::Path(path) | EinsumPlanSpec::FixedPairs(path) => saturating_sum([
514 std::mem::size_of::<EinsumPlanSpec>(),
515 vec_retained_bytes(path),
516 ]),
517 }
518}
519
520fn build_expanded_eager_program(
521 tree: &crate::ContractionTree,
522 shapes: &[Vec<usize>],
523) -> Result<ExpandedEagerProgram> {
524 let mut builder = GraphBuilder::<StdTensorOp>::new();
525 let mut input_vals = Vec::with_capacity(shapes.len());
526 for input_idx in 0..shapes.len() {
527 let local = builder.add_input(TensorInputKey::User {
528 id: input_idx as u64,
529 });
530 input_vals.push(ValueRef::Local(local));
531 }
532
533 let result_ref = build_einsum_graph(&mut builder, tree, &input_vals, shapes)?;
534 let ValueRef::Local(result_local) = result_ref else {
535 return Err(Error::Runtime(tenferro_runtime::Error::Internal(
536 "expanded eager einsum returned an external value".into(),
537 )));
538 };
539 builder.set_outputs(vec![result_local]);
540 let graph = Arc::new(builder.build());
541 let output_key = graph.values()[result_local].key.clone();
542 let view = resolve(vec![graph]);
543 let graph = materialize_merge(&view, &[output_key]);
544 let compiled = compile(&graph);
545 let input_slots = compiled
546 .input_slots
547 .iter()
548 .zip(graph.inputs.iter())
549 .map(|(&slot, key)| {
550 let ValueKey::Input(TensorInputKey::User { id }) = key else {
551 return Err(runtime_internal(format!(
552 "expanded eager einsum saw unexpected input key: {key:?}"
553 )));
554 };
555 Ok((slot, *id as usize))
556 })
557 .collect::<Result<_>>()?;
558
559 Ok(ExpandedEagerProgram {
560 compiled,
561 input_slots,
562 })
563}
564
565fn execute_eager_einsum_program_in_session(
566 session: &mut EagerSession<'_>,
567 inputs: &[&EagerTensor],
568 program: &ExpandedEagerProgram,
569) -> Result<Option<EagerTensor>> {
570 let mut slots: Vec<Option<EagerTensor>> = vec![None; program.compiled.n_slots];
571 for &(slot, input_idx) in &program.input_slots {
572 let tensor = inputs.get(input_idx).ok_or_else(|| {
573 runtime_missing(format!(
574 "expanded eager einsum input {input_idx} is missing"
575 ))
576 })?;
577 slots[slot] = Some((*tensor).clone());
578 }
579
580 let mut instruction_idx = 0;
581 while instruction_idx < program.compiled.instructions.len() {
582 if let Some((output_slot, output)) = try_execute_eager_broadcast_multiply_pattern(
583 session,
584 &program.compiled.instructions,
585 instruction_idx,
586 &slots,
587 &program.compiled.output_slots,
588 )? {
589 slots[output_slot] = Some(output);
590 instruction_idx += 3;
591 continue;
592 }
593
594 let instr = &program.compiled.instructions[instruction_idx];
595 if instr.outputs.len() != 1 {
596 return Err(runtime_internal(format!(
597 "expanded eager einsum expected single-output op, got {} outputs",
598 instr.outputs.len()
599 )));
600 }
601 let input_refs: Vec<&EagerTensor> = instr
602 .inputs
603 .iter()
604 .map(|&slot| slot_tensor(&slots, slot))
605 .collect::<Result<_>>()?;
606 let output = session
607 .apply_standard_op(instr.operation.clone(), &input_refs)
608 .map_err(Error::Runtime)?;
609 slots[instr.outputs[0]] = Some(output);
610 instruction_idx += 1;
611 }
612
613 let [output_slot] = program.compiled.output_slots.as_slice() else {
614 return Err(runtime_internal(format!(
615 "expanded eager einsum expected one graph output, got {}",
616 program.compiled.output_slots.len()
617 )));
618 };
619 slots
620 .get_mut(*output_slot)
621 .and_then(Option::take)
622 .map(Some)
623 .ok_or_else(|| runtime_missing("expanded eager einsum output slot is missing"))
624}
625
626fn expanded_eager_program_retained_bytes(program: &ExpandedEagerProgram) -> usize {
627 saturating_sum([
628 size_of::<ExpandedEagerProgram>(),
629 vec_retained_bytes(&program.input_slots),
630 compiled_program_retained_bytes(&program.compiled),
631 ])
632}
633
634fn compiled_program_retained_bytes(program: &CompiledProgram<StdTensorOp>) -> usize {
635 saturating_sum([
636 size_of::<CompiledProgram<StdTensorOp>>(),
637 vec_retained_bytes(&program.instructions),
638 vec_retained_bytes(&program.input_slots),
639 vec_retained_bytes(&program.output_slots),
640 saturating_sum(program.instructions.iter().map(instruction_retained_bytes)),
641 ])
642}
643
644fn instruction_retained_bytes(instruction: &Instruction<StdTensorOp>) -> usize {
645 saturating_sum([
646 size_of::<Instruction<StdTensorOp>>(),
647 std_tensor_op_retained_bytes(&instruction.operation),
648 vec_retained_bytes(&instruction.inputs),
649 vec_retained_bytes(&instruction.outputs),
650 ])
651}
652
653fn std_tensor_op_retained_bytes(op: &StdTensorOp) -> usize {
654 match op {
655 StdTensorOp::DotGeneral { config } => saturating_sum(
657 [
658 &config.lhs_contracting_dims,
659 &config.rhs_contracting_dims,
660 &config.lhs_batch_dims,
661 &config.rhs_batch_dims,
662 ]
663 .into_iter()
664 .filter(|axes| axes.is_heap())
665 .map(|axes| axes.capacity().saturating_mul(size_of::<usize>())),
666 ),
667 StdTensorOp::Transpose { perm } => vec_retained_bytes(perm),
668 StdTensorOp::Reshape { to_shape } => vec_retained_bytes(to_shape),
669 StdTensorOp::BroadcastInDim { shape, dims } => {
670 saturating_sum([vec_retained_bytes(shape), vec_retained_bytes(dims)])
671 }
672 StdTensorOp::Constant { bytes, .. } => vec_retained_bytes(bytes),
673 StdTensorOp::ReduceSum { axes }
674 | StdTensorOp::ReduceProd { axes }
675 | StdTensorOp::ReduceMax { axes }
676 | StdTensorOp::ReduceMin { axes }
677 | StdTensorOp::Reverse { axes } => vec_retained_bytes(axes),
678 StdTensorOp::DynamicSlice { slice_sizes } => vec_retained_bytes(slice_sizes),
679 StdTensorOp::GatherDynamicSliceSizes {
680 offset_dims,
681 collapsed_slice_dims,
682 start_index_map,
683 slice_sizes,
684 ..
685 } => saturating_sum([
686 vec_retained_bytes(offset_dims),
687 vec_retained_bytes(collapsed_slice_dims),
688 vec_retained_bytes(start_index_map),
689 vec_retained_bytes(slice_sizes),
690 ]),
691 _ => 0,
692 }
693}
694
695fn try_execute_eager_broadcast_multiply_pattern(
696 session: &mut EagerSession<'_>,
697 instructions: &[Instruction<StdTensorOp>],
698 instruction_idx: usize,
699 slots: &[Option<EagerTensor>],
700 output_slots: &[usize],
701) -> Result<Option<(usize, EagerTensor)>> {
702 if instruction_idx + 2 >= instructions.len() {
703 return Ok(None);
704 }
705 let lhs_bc = &instructions[instruction_idx];
706 let rhs_bc = &instructions[instruction_idx + 1];
707 let multiply = &instructions[instruction_idx + 2];
708
709 let StdTensorOp::BroadcastInDim {
710 shape: lhs_shape_exprs,
711 dims: lhs_dims,
712 } = &lhs_bc.operation
713 else {
714 return Ok(None);
715 };
716 let StdTensorOp::BroadcastInDim {
717 shape: rhs_shape_exprs,
718 dims: rhs_dims,
719 } = &rhs_bc.operation
720 else {
721 return Ok(None);
722 };
723 if !matches!(multiply.operation, StdTensorOp::Mul)
724 || lhs_bc.outputs.len() != 1
725 || rhs_bc.outputs.len() != 1
726 || multiply.outputs.len() != 1
727 || multiply.inputs.len() != 2
728 || lhs_bc.inputs.is_empty()
729 || rhs_bc.inputs.is_empty()
730 || multiply.inputs[0] != lhs_bc.outputs[0]
731 || multiply.inputs[1] != rhs_bc.outputs[0]
732 {
733 return Ok(None);
734 }
735
736 let lhs_bc_slot = lhs_bc.outputs[0];
737 let rhs_bc_slot = rhs_bc.outputs[0];
738 if output_slots.contains(&lhs_bc_slot)
739 || output_slots.contains(&rhs_bc_slot)
740 || instructions[instruction_idx + 3..]
741 .iter()
742 .any(|instr| instr.inputs.contains(&lhs_bc_slot) || instr.inputs.contains(&rhs_bc_slot))
743 {
744 return Ok(None);
745 }
746
747 let lhs = slot_tensor(slots, lhs_bc.inputs[0])?;
748 let rhs = slot_tensor(slots, rhs_bc.inputs[0])?;
749 let lhs_shape = eval_shape_exprs(slots, &lhs_bc.inputs, lhs_shape_exprs)?;
750 let rhs_shape = eval_shape_exprs(slots, &rhs_bc.inputs, rhs_shape_exprs)?;
751 let Some(output) = backend_broadcast_multiply_untracked(
752 session, lhs, &lhs_shape, lhs_dims, rhs, &rhs_shape, rhs_dims,
753 )?
754 else {
755 return Ok(None);
756 };
757
758 Ok(Some((multiply.outputs[0], output)))
759}
760
761#[allow(clippy::too_many_arguments)]
762fn backend_broadcast_multiply_untracked(
763 session: &mut EagerSession<'_>,
764 lhs: &EagerTensor,
765 lhs_shape: &[usize],
766 lhs_dims: &[usize],
767 rhs: &EagerTensor,
768 rhs_shape: &[usize],
769 rhs_dims: &[usize],
770) -> Result<Option<EagerTensor>> {
771 if !Arc::ptr_eq(lhs.runtime(), rhs.runtime()) {
772 return Err(tenferro_runtime::Error::ContextMismatch {
773 lhs: lhs.ctx_id(),
774 rhs: rhs.ctx_id(),
775 }
776 .into());
777 }
778 if lhs.tracks_grad() || rhs.tracks_grad() {
779 return Ok(None);
780 }
781
782 let runtime = lhs.runtime();
783 let value = session.backend_session().execute_broadcast_multiply_value(
784 lhs.tensor_read(),
785 lhs_shape,
786 lhs_dims,
787 rhs.tensor_read(),
788 rhs_shape,
789 rhs_dims,
790 )?;
791
792 Ok(value
793 .map(|value| adopt_untracked_eager_value(runtime.clone(), value))
794 .transpose()?)
795}
796
797fn eval_shape_exprs(
798 slots: &[Option<EagerTensor>],
799 input_slots: &[usize],
800 shape: &[DimExpr],
801) -> Result<Vec<usize>> {
802 let inputs = input_slots
803 .iter()
804 .map(|&slot| slot_tensor(slots, slot))
805 .collect::<Result<Vec<_>>>()?;
806 let input_shapes = inputs
807 .iter()
808 .map(|tensor| tensor.shape())
809 .collect::<Vec<_>>();
810 DimExpr::eval_all(shape, &input_shapes).map_err(|error| {
811 runtime_extension_error(
812 "einsum",
813 ErrorKind::Validation(ValidationKind::InvalidArgument),
814 error,
815 )
816 })
817}
818
819fn slot_tensor(slots: &[Option<EagerTensor>], slot: usize) -> Result<&EagerTensor> {
820 slots.get(slot).and_then(Option::as_ref).ok_or_else(|| {
821 Error::Runtime(tenferro_runtime::Error::MissingInput(format!(
822 "expanded eager einsum missing value for slot {slot}"
823 )))
824 })
825}
826
827fn infer_eager_output_shape(
828 subscripts: &EinsumSubscripts,
829 inputs: &[&EagerTensor],
830) -> Result<Vec<tenferro_runtime::SymDim>> {
831 if inputs.is_empty() {
832 return Err(Error::invalid_argument(
833 "einsum",
834 "inputs",
835 "einsum requires at least one input tensor",
836 ));
837 }
838 if subscripts.inputs.len() != inputs.len() {
839 return Err(Error::invalid_argument(
840 "einsum",
841 "inputs",
842 format!(
843 "einsum subscripts expect {} inputs, got {}",
844 subscripts.inputs.len(),
845 inputs.len()
846 ),
847 ));
848 }
849
850 let mut label_dims = std::collections::HashMap::new();
851 for (labels, tensor) in subscripts.inputs.iter().zip(inputs.iter()) {
852 let shape = tensor.shape();
853 if labels.len() != shape.len() {
854 return Err(Error::validation(
855 "einsum",
856 ValidationError::RankMismatch {
857 expected: labels.len(),
858 actual: shape.len(),
859 },
860 ));
861 }
862 for (&label, &dim) in labels.iter().zip(shape.iter()) {
863 if let Some(existing) = label_dims.get_mut(&label) {
864 if *existing != dim && *existing != 1 && dim != 1 {
865 return Err(Error::validation(
866 "einsum",
867 ShapeMismatch::ExpectedActual {
868 expected: vec![*existing].into_iter().collect(),
869 actual: vec![dim].into_iter().collect(),
870 }
871 .into(),
872 ));
873 }
874 if *existing == 1 {
875 *existing = dim;
876 }
877 } else {
878 label_dims.insert(label, dim);
879 }
880 }
881 }
882
883 subscripts
884 .output
885 .iter()
886 .map(|label| {
887 label_dims
888 .get(label)
889 .copied()
890 .map(tenferro_runtime::SymDim::from)
891 .ok_or_else(|| {
892 Error::invalid_argument(
893 "einsum",
894 "output",
895 format!("einsum output label {label} is missing from input labels"),
896 )
897 })
898 })
899 .collect()
900}
901
902fn runtime_extension_error<E>(op: &'static str, kind: ErrorKind, source: E) -> Error
903where
904 E: StdError + Send + Sync + 'static,
905{
906 Error::Runtime(tenferro_runtime::Error::extension(
907 op,
908 ErrorPhase::Execution,
909 EINSUM_EXTENSION_FAMILY_ID,
910 kind,
911 source,
912 ))
913}
914
915fn runtime_internal(message: impl Into<String>) -> Error {
916 Error::Runtime(tenferro_runtime::Error::Internal(message.into()))
917}
918
919fn runtime_missing(message: impl Into<String>) -> Error {
920 Error::Runtime(tenferro_runtime::Error::MissingInput(message.into()))
921}
922
923fn tensordot(
924 session: &mut EagerSession<'_>,
925 lhs: &EagerTensor,
926 rhs: &EagerTensor,
927 axes: TensorDotAxes<'_>,
928) -> Result<EagerTensor> {
929 let config = crate::tensordot::dot_general_config(axes, lhs.shape().len(), rhs.shape().len())?;
930 crate::tensordot::validate_concrete_contract_dims(lhs.shape(), rhs.shape(), &config)?;
931 session
932 .dot_general(lhs, rhs, config)
933 .map_err(Error::Runtime)
934}
935
936#[cfg(test)]
937mod tests;