Skip to main content

tenferro_einsum/
eager_ad.rs

1//! Eager einsum and tensordot on a borrowed [`EagerSession`].
2
3use 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/// Eager einsum and tensordot on a runtime-bound borrowed eager session.
46///
47/// Every operation runs inside the caller's session, so it composes with other
48/// eager operations in the same [`tenferro_ad::EagerRuntime::with_eager_session`]
49/// callback and never reopens the runtime. The calling thread's `no_grad` and
50/// `capture_trace` modes govern it like any other eager operation.
51///
52/// # Examples
53///
54/// ```rust
55/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
56/// use tenferro_einsum::EagerSessionEinsumExt;
57///
58/// let ctx = EagerRuntime::new()?;
59/// let a = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6])?, ctx.clone())?;
60/// let b = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12])?, ctx.clone())?;
61/// let product = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |session| {
62///     let c = session.einsum(&[&a, &b], "ij,jk->ik")?;
63///     session.einsum(&[&c], "ij->")
64/// })?;
65/// assert_eq!(product.value()?.as_slice::<f64>()?, &[24.0]);
66/// # Ok::<(), Box<dyn std::error::Error>>(())
67/// ```
68#[cfg_attr(docsrs, doc(cfg(feature = "autodiff")))]
69pub trait EagerSessionEinsumExt {
70    /// Execute an einsum from string notation.
71    ///
72    /// # Examples
73    ///
74    /// ```rust
75    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
76    /// use tenferro_einsum::EagerSessionEinsumExt;
77    ///
78    /// let ctx = EagerRuntime::new()?;
79    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0])?, ctx.clone())?;
80    /// let dot = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |session| session.einsum(&[&x, &x], "i,i->"))?;
81    /// assert_eq!(dot.value()?.as_slice::<f64>()?, &[13.0]);
82    /// # Ok::<(), Box<dyn std::error::Error>>(())
83    /// ```
84    ///
85    /// # Errors
86    ///
87    /// Returns [`Error::InvalidSubscripts`] for malformed notation,
88    /// [`Error::Validation`] for rank/shape/dtype mismatches, or
89    /// [`Error::Planning`] / [`Error::Runtime`] for contraction planning and
90    /// execution failures, including inputs owned by another runtime.
91    fn einsum(&mut self, inputs: &[&EagerTensor], subscripts: &str) -> Result<EagerTensor>;
92
93    /// Execute an einsum from rank-unresolved notation.
94    ///
95    /// # Examples
96    ///
97    /// ```rust
98    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
99    /// use tenferro_einsum::{EagerSessionEinsumExt, EinsumAxis, EinsumNotation};
100    ///
101    /// let ctx = EagerRuntime::new()?;
102    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0])?, ctx.clone())?;
103    /// // `...->...` keeps every axis the ellipsis covers.
104    /// let notation = EinsumNotation::new(&[&[EinsumAxis::Ellipsis]], &[EinsumAxis::Ellipsis]);
105    /// let same = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |session| session.einsum_notation(&[&x], &notation))?;
106    /// assert_eq!(same.value()?.as_slice::<f64>()?, &[2.0, 3.0]);
107    /// # Ok::<(), Box<dyn std::error::Error>>(())
108    /// ```
109    ///
110    /// # Errors
111    ///
112    /// Returns a typed validation, planning, or runtime error when notation or
113    /// execution is invalid.
114    fn einsum_notation(
115        &mut self,
116        inputs: &[&EagerTensor],
117        notation: &EinsumNotation,
118    ) -> Result<EagerTensor>;
119
120    /// Execute an einsum from parsed integer labels.
121    ///
122    /// # Examples
123    ///
124    /// ```rust
125    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
126    /// use tenferro_einsum::{EagerSessionEinsumExt, EinsumSubscripts};
127    ///
128    /// let ctx = EagerRuntime::new()?;
129    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0])?, ctx.clone())?;
130    /// let subscripts = EinsumSubscripts::new(&[&[0], &[0]], &[]);
131    /// let dot = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |session| session.einsum_subscripts(&[&x, &x], &subscripts))?;
132    /// assert_eq!(dot.value()?.as_slice::<f64>()?, &[13.0]);
133    /// # Ok::<(), Box<dyn std::error::Error>>(())
134    /// ```
135    ///
136    /// # Errors
137    ///
138    /// Returns [`Error::Validation`] for rank/shape/dtype mismatches,
139    /// [`Error::Planning`] for an invalid contraction plan, or
140    /// [`Error::Runtime`] for extension registration or backend execution
141    /// failures.
142    fn einsum_subscripts(
143        &mut self,
144        inputs: &[&EagerTensor],
145        subscripts: &EinsumSubscripts,
146    ) -> Result<EagerTensor>;
147
148    /// Contract two eager tensors over the requested axes.
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
154    /// use tenferro_einsum::{EagerSessionEinsumExt, TensorDotAxes};
155    ///
156    /// let ctx = EagerRuntime::new()?;
157    /// let a = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6])?, ctx.clone())?;
158    /// let b = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12])?, ctx.clone())?;
159    /// let c = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |session| session.tensordot(&a, &b, TensorDotAxes::Count(1)))?;
160    /// assert_eq!(c.shape(), &[2, 4]);
161    /// # Ok::<(), Box<dyn std::error::Error>>(())
162    /// ```
163    ///
164    /// # Errors
165    ///
166    /// Returns [`Error::Validation`] for invalid axes or mismatched contracted
167    /// extents, or [`Error::Runtime`] for execution failures.
168    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, &notation)
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(&notation.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        // Inline axes are already counted in Instruction<StdTensorOp>.
656        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;