Skip to main content

tenferro_einsum/
eager_ad.rs

1//! EagerTensor einsum extension API.
2
3use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::mem::size_of;
6use std::sync::Arc;
7
8use computegraph::compile::{compile, CompiledProgram, Instruction};
9use computegraph::graph::GraphBuilder;
10use computegraph::materialize::materialize_merge;
11use computegraph::resolve::resolve;
12use computegraph::types::{ValueKey, ValueRef};
13use tenferro_ad::error::{Error, Result};
14use tenferro_ad::extension::{adopt_untracked_eager_value, apply_eager};
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::ExtensionCacheKey;
20use tenferro_tensor::TensorFusion;
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::{register_runtime, 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, Subscripts, TensorDotAxes};
34
35/// Eager einsum extension methods for slices or arrays of [`EagerTensor`] refs.
36pub trait EagerEinsumExt {
37    fn einsum(&self, subscripts: &str) -> Result<EagerTensor>;
38    fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor>;
39}
40
41impl EagerEinsumExt for [&EagerTensor] {
42    fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
43        einsum(self, subscripts)
44    }
45
46    fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
47        einsum_subscripts(self, subscripts)
48    }
49}
50
51impl<const N: usize> EagerEinsumExt for [&EagerTensor; N] {
52    fn einsum(&self, subscripts: &str) -> Result<EagerTensor> {
53        einsum(self.as_slice(), subscripts)
54    }
55
56    fn einsum_subscripts(&self, subscripts: &EinsumSubscripts) -> Result<EagerTensor> {
57        einsum_subscripts(self.as_slice(), subscripts)
58    }
59}
60
61/// Eager tensor contraction-sugar methods.
62pub trait EagerTensorEinsumExt {
63    fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor>;
64}
65
66impl EagerTensorEinsumExt for EagerTensor {
67    fn tensordot(&self, rhs: &EagerTensor, axes: TensorDotAxes<'_>) -> Result<EagerTensor> {
68        tensordot(self, rhs, axes)
69    }
70}
71
72/// Execute an einsum eagerly on [`EagerTensor`] values.
73///
74/// # Examples
75///
76/// ```
77/// use tenferro_ad::{EagerRuntime, EagerTensor};
78/// use tenferro_cpu::CpuBackend;
79/// use tenferro_einsum::EagerEinsumExt;
80/// use tenferro_tensor::Tensor;
81///
82/// let runtime = EagerRuntime::with_cpu_backend(CpuBackend::new());
83/// let a = EagerTensor::from_tensor_in(
84///     Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap(),
85///     runtime.clone(),
86/// ).unwrap();
87/// let b = EagerTensor::from_tensor_in(
88///     Tensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap(),
89///     runtime,
90/// ).unwrap();
91/// let out = [&a, &b].einsum("ij,jk->ik")?;
92/// assert_eq!(out.shape(), &[2, 4]);
93/// # Ok::<(), tenferro_ad::error::Error>(())
94/// ```
95pub fn einsum(inputs: &[&EagerTensor], subscripts: &str) -> Result<EagerTensor> {
96    let subscripts = parse_einsum_subscripts(subscripts)
97        .map_err(|err| Error::ContractionError(err.to_string()))?;
98    einsum_subscripts(inputs, &subscripts)
99}
100
101/// Execute an einsum eagerly from integer labels.
102///
103/// # Examples
104///
105/// ```
106/// use tenferro_ad::{EagerRuntime, EagerTensor};
107/// use tenferro_cpu::CpuBackend;
108/// use tenferro_einsum::{EagerEinsumExt, parse_einsum_subscripts};
109/// use tenferro_tensor::Tensor;
110///
111/// let runtime = EagerRuntime::with_cpu_backend(CpuBackend::new());
112/// let a = EagerTensor::from_tensor_in(
113///     Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap(),
114///     runtime.clone(),
115/// ).unwrap();
116/// let b = EagerTensor::from_tensor_in(
117///     Tensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap(),
118///     runtime,
119/// ).unwrap();
120/// let subscripts = parse_einsum_subscripts("ij,jk->ik").unwrap();
121/// let out = [&a, &b].einsum_subscripts(&subscripts)?;
122/// assert_eq!(out.shape(), &[2, 4]);
123/// # Ok::<(), tenferro_ad::error::Error>(())
124/// ```
125pub fn einsum_subscripts(
126    inputs: &[&EagerTensor],
127    subscripts: &EinsumSubscripts,
128) -> Result<EagerTensor> {
129    if let Some(result) = try_direct_binary_dot_general(inputs, subscripts) {
130        return result;
131    }
132
133    if let Some(result) = try_whole_program_untracked(inputs, subscripts)? {
134        return Ok(result);
135    }
136
137    let output_shape_hint = infer_eager_output_shape(subscripts, inputs)?;
138    if let Some(result) = try_expand_eager_einsum(inputs, subscripts)? {
139        return Ok(result);
140    }
141
142    if let Some(first) = inputs.first() {
143        first
144            .runtime()
145            .register_extension(register_runtime)
146            .map_err(|err| Error::Internal(err.to_string()))?;
147    }
148
149    let op = Arc::new(EinsumExtensionOp::with_output_shape_hint(
150        subscripts.clone(),
151        output_shape_hint,
152        EinsumPlanSpec::Auto(default_auto_options()),
153    ));
154    let mut outputs = apply_eager(op, inputs)?;
155    outputs
156        .pop()
157        .ok_or_else(|| Error::Internal("einsum extension produced no eager output".to_string()))
158}
159
160fn try_direct_binary_dot_general(
161    inputs: &[&EagerTensor],
162    subscripts: &EinsumSubscripts,
163) -> Option<Result<EagerTensor>> {
164    if inputs.len() != 2 || subscripts.inputs.len() != 2 {
165        return None;
166    }
167
168    let lhs_labels = &subscripts.inputs[0];
169    let rhs_labels = &subscripts.inputs[1];
170    if lhs_labels.len() != inputs[0].shape().len() || rhs_labels.len() != inputs[1].shape().len() {
171        return None;
172    }
173
174    if let Some(plan) =
175        try_build_exact_output_binary_dot_plan(lhs_labels, rhs_labels, &subscripts.output)
176    {
177        return Some(match plan.operand_order {
178            BinaryDotOperandOrder::Original => inputs[0].dot_general(inputs[1], plan.config),
179            BinaryDotOperandOrder::Swapped => inputs[1].dot_general(inputs[0], plan.config),
180        });
181    }
182    None
183}
184
185/// Whether the untracked whole-program eager einsum executor is enabled.
186///
187/// Prototype gate (issue #1060 follow-up): when set, untracked N-ary eager
188/// einsum runs the whole contraction in one backend session via
189/// [`crate::eager::eager_einsum_subscripts`] instead of executing the expanded
190/// program one standard op at a time. Tracked (`requires_grad`) inputs keep the
191/// existing per-op path so eager AD recording semantics are unchanged.
192fn whole_program_untracked_enabled() -> bool {
193    std::env::var_os("TENFERRO_EAGER_WHOLE_PROGRAM").is_some()
194}
195
196/// Run an untracked eager einsum as a single backend-session program.
197///
198/// Returns `None` (so the caller falls back to the per-op expanded path) when
199/// the gate is off, there are no inputs, any input tracks gradients, or the
200/// inputs do not all share one runtime.
201fn try_whole_program_untracked(
202    inputs: &[&EagerTensor],
203    subscripts: &EinsumSubscripts,
204) -> Result<Option<EagerTensor>> {
205    if !whole_program_untracked_enabled() {
206        return Ok(None);
207    }
208    let Some(first) = inputs.first() else {
209        return Ok(None);
210    };
211    if inputs.iter().any(|tensor| tensor.tracks_grad()) {
212        return Ok(None);
213    }
214    let runtime = first.runtime();
215    if inputs
216        .iter()
217        .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
218    {
219        return Ok(None);
220    }
221
222    let subs = Subscripts::from(subscripts);
223    let tensor_arcs = inputs
224        .iter()
225        .map(|tensor| tensor.materialized())
226        .collect::<Result<Vec<_>>>()?;
227    let tensors: Vec<_> = tensor_arcs.iter().map(|tensor| tensor.as_ref()).collect();
228    let result = runtime.with_backend_mut(|backend| {
229        crate::eager::eager_einsum_subscripts(backend, &tensors, &subs)
230    })??;
231    Ok(Some(EagerTensor::from_tensor_in(result, runtime.clone())?))
232}
233
234/// Run an untracked whole-program eager einsum on an explicit contraction tree.
235///
236/// Prototype/benchmark entry (issue #1060 follow-up). Executes the whole
237/// contraction in one backend session on the caller-provided path (e.g. an
238/// externally optimized `opt_flops` order via [`crate::ContractionTree::from_pairs`]),
239/// instead of one eager op per expanded step. All inputs must be untracked and
240/// share one runtime; tracked inputs should use the per-op path to keep eager
241/// AD semantics.
242///
243/// # Examples
244///
245/// ```
246/// use tenferro_ad::{EagerRuntime, EagerTensor};
247/// use tenferro_cpu::CpuBackend;
248/// use tenferro_einsum::{ContractionTree, Subscripts};
249/// use tenferro_tensor::Tensor;
250///
251/// let runtime = EagerRuntime::with_cpu_backend(CpuBackend::new());
252/// let a = EagerTensor::from_tensor_in(
253///     Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap(),
254///     runtime.clone(),
255/// ).unwrap();
256/// let b = EagerTensor::from_tensor_in(
257///     Tensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap(),
258///     runtime,
259/// ).unwrap();
260/// let subs = Subscripts::parse("ij,jk->ik").unwrap();
261/// let tree = ContractionTree::from_pairs(&subs, &[&[2, 3], &[3, 4]], &[(0, 1)]).unwrap();
262/// let out = einsum_whole_program_untracked(&[&a, &b], &tree)?;
263/// assert_eq!(out.shape(), &[2, 4]);
264/// # Ok::<(), tenferro_ad::error::Error>(())
265/// ```
266#[cfg(test)]
267fn einsum_whole_program_untracked(
268    inputs: &[&EagerTensor],
269    tree: &crate::ContractionTree,
270) -> Result<EagerTensor> {
271    let first = inputs.first().ok_or_else(|| {
272        Error::ContractionError("einsum requires at least one input tensor".into())
273    })?;
274    if inputs.iter().any(|tensor| tensor.tracks_grad()) {
275        return Err(Error::Internal(
276            "whole-program eager einsum requires untracked inputs".into(),
277        ));
278    }
279    let runtime = first.runtime();
280    if inputs
281        .iter()
282        .any(|tensor| !Arc::ptr_eq(tensor.runtime(), runtime))
283    {
284        return Err(Error::Internal(
285            "whole-program eager einsum requires inputs from one runtime".into(),
286        ));
287    }
288    let tensor_arcs = inputs
289        .iter()
290        .map(|tensor| tensor.materialized())
291        .collect::<Result<Vec<_>>>()?;
292    let tensors: Vec<_> = tensor_arcs.iter().map(|tensor| tensor.as_ref()).collect();
293    let result = runtime.with_backend_mut(|backend| {
294        crate::eager::eager_einsum_with_tree(backend, &tensors, tree)
295    })??;
296    EagerTensor::from_tensor_in(result, runtime.clone())
297}
298
299fn try_expand_eager_einsum(
300    inputs: &[&EagerTensor],
301    subscripts: &EinsumSubscripts,
302) -> Result<Option<EagerTensor>> {
303    if inputs.len() <= 1 {
304        return Ok(None);
305    }
306
307    let shapes: Vec<Vec<usize>> = inputs
308        .iter()
309        .map(|tensor| tensor.shape().to_vec())
310        .collect();
311    let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
312    let subs = Subscripts::from(subscripts);
313    let plan_spec = EinsumPlanSpec::Auto(default_auto_options());
314
315    let program = cached_expanded_eager_program(
316        inputs[0].runtime(),
317        subscripts,
318        &subs,
319        &plan_spec,
320        &shape_refs,
321        &shapes,
322    )?;
323    execute_eager_einsum_program(inputs, &program)
324}
325
326struct ExpandedEagerProgram {
327    compiled: CompiledProgram<StdTensorOp>,
328    input_slots: Vec<(usize, usize)>,
329}
330
331#[derive(Clone)]
332struct ExpandedEagerProgramCacheKeyData {
333    subscripts: EinsumSubscripts,
334    shapes: Vec<Vec<usize>>,
335    plan_spec: EinsumPlanSpec,
336}
337
338impl ExpandedEagerProgramCacheKeyData {
339    fn new(
340        subscripts: &EinsumSubscripts,
341        shapes: &[Vec<usize>],
342        plan_spec: &EinsumPlanSpec,
343    ) -> Self {
344        Self {
345            subscripts: subscripts.clone(),
346            shapes: shapes.to_vec(),
347            plan_spec: plan_spec.clone(),
348        }
349    }
350
351    fn matches_expanded_eager_program(
352        &self,
353        subscripts: &EinsumSubscripts,
354        shapes: &[Vec<usize>],
355        plan_spec: &EinsumPlanSpec,
356    ) -> bool {
357        self.subscripts == *subscripts
358            && self.shapes.as_slice() == shapes
359            && plan_specs_equal(&self.plan_spec, plan_spec)
360    }
361
362    fn retained_bytes(&self) -> usize {
363        saturating_sum([
364            crate::cache::einsum_subscripts_retained_bytes(&self.subscripts),
365            saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
366            plan_spec_retained_bytes(&self.plan_spec),
367        ])
368    }
369}
370
371struct CachedExpandedEagerProgram {
372    key_data: ExpandedEagerProgramCacheKeyData,
373    program: Arc<ExpandedEagerProgram>,
374}
375
376fn cached_expanded_eager_program(
377    runtime: &Arc<EagerRuntime>,
378    subscripts: &EinsumSubscripts,
379    subs: &Subscripts,
380    plan_spec: &EinsumPlanSpec,
381    shape_refs: &[&[usize]],
382    shapes: &[Vec<usize>],
383) -> Result<Arc<ExpandedEagerProgram>> {
384    runtime.with_extension_caches_mut(|caches| {
385        let plan_hash = plan_spec_hash(plan_spec);
386        let key = expanded_eager_program_cache_key(subscripts, shapes, plan_hash);
387        if let Some(cached) = caches.get::<CachedExpandedEagerProgram>(&key) {
388            let key_data = &cached.key_data;
389            if key_data.matches_expanded_eager_program(subscripts, shapes, plan_spec) {
390                return Ok(Arc::clone(&cached.program));
391            }
392        }
393
394        let tree = resolve_plan_spec(plan_spec, subs, shape_refs)
395            .map_err(|err| Error::ContractionError(err.to_string()))?;
396        let program = Arc::new(build_expanded_eager_program(&tree, shapes)?);
397        let key_data = ExpandedEagerProgramCacheKeyData::new(subscripts, shapes, plan_spec);
398        let retained_bytes = saturating_sum([
399            key_data.retained_bytes(),
400            expanded_eager_program_retained_bytes(&program),
401        ]);
402        caches.put(
403            key,
404            CachedExpandedEagerProgram {
405                key_data,
406                program: Arc::clone(&program),
407            },
408            retained_bytes,
409        );
410        Ok(program)
411    })?
412}
413
414fn expanded_eager_program_cache_key(
415    subscripts: &EinsumSubscripts,
416    shapes: &[Vec<usize>],
417    plan_hash: u64,
418) -> ExtensionCacheKey {
419    let mut hasher = DefaultHasher::new();
420    subscripts.hash(&mut hasher);
421    shapes.hash(&mut hasher);
422    plan_hash.hash(&mut hasher);
423    ExtensionCacheKey::new(
424        EINSUM_EXTENSION_FAMILY_ID,
425        EINSUM_EAGER_EXPANDED_PROGRAMS_CACHE,
426        hasher.finish(),
427    )
428}
429
430fn plan_spec_hash(plan_spec: &EinsumPlanSpec) -> u64 {
431    let mut hasher = DefaultHasher::new();
432    hash_einsum_plan_spec(plan_spec, &mut hasher);
433    hasher.finish()
434}
435
436fn plan_spec_retained_bytes(plan_spec: &EinsumPlanSpec) -> usize {
437    match plan_spec {
438        EinsumPlanSpec::Auto(options) => saturating_sum([
439            std::mem::size_of::<EinsumPlanSpec>(),
440            vec_retained_bytes(&options.betas),
441        ]),
442        EinsumPlanSpec::LeftToRight => std::mem::size_of::<EinsumPlanSpec>(),
443        EinsumPlanSpec::Path(path) | EinsumPlanSpec::FixedPairs(path) => saturating_sum([
444            std::mem::size_of::<EinsumPlanSpec>(),
445            vec_retained_bytes(path),
446        ]),
447    }
448}
449
450fn build_expanded_eager_program(
451    tree: &crate::ContractionTree,
452    shapes: &[Vec<usize>],
453) -> Result<ExpandedEagerProgram> {
454    let mut builder = GraphBuilder::<StdTensorOp>::new();
455    let mut input_vals = Vec::with_capacity(shapes.len());
456    for input_idx in 0..shapes.len() {
457        let local = builder.add_input(TensorInputKey::User {
458            id: input_idx as u64,
459        });
460        input_vals.push(ValueRef::Local(local));
461    }
462
463    let result_ref = build_einsum_graph(&mut builder, tree, &input_vals, shapes)
464        .map_err(|err| Error::ContractionError(err.to_string()))?;
465    let ValueRef::Local(result_local) = result_ref else {
466        return Err(Error::Internal(
467            "expanded eager einsum returned an external value".into(),
468        ));
469    };
470    builder.set_outputs(vec![result_local]);
471    let graph = Arc::new(builder.build());
472    let output_key = graph.values()[result_local].key.clone();
473    let view = resolve(vec![graph]);
474    let graph = materialize_merge(&view, &[output_key]);
475    let compiled = compile(&graph);
476    let input_slots = compiled
477        .input_slots
478        .iter()
479        .zip(graph.inputs.iter())
480        .map(|(&slot, key)| {
481            let ValueKey::Input(TensorInputKey::User { id }) = key else {
482                return Err(Error::Internal(format!(
483                    "expanded eager einsum saw unexpected input key: {key:?}"
484                )));
485            };
486            Ok((slot, *id as usize))
487        })
488        .collect::<Result<_>>()?;
489
490    Ok(ExpandedEagerProgram {
491        compiled,
492        input_slots,
493    })
494}
495
496fn execute_eager_einsum_program(
497    inputs: &[&EagerTensor],
498    program: &ExpandedEagerProgram,
499) -> Result<Option<EagerTensor>> {
500    let mut slots: Vec<Option<EagerTensor>> = vec![None; program.compiled.n_slots];
501    for &(slot, input_idx) in &program.input_slots {
502        let tensor = inputs.get(input_idx).ok_or_else(|| {
503            Error::Internal(format!(
504                "expanded eager einsum input {input_idx} is missing"
505            ))
506        })?;
507        slots[slot] = Some((*tensor).clone());
508    }
509
510    let mut instruction_idx = 0;
511    while instruction_idx < program.compiled.instructions.len() {
512        if let Some((output_slot, output)) = try_execute_eager_broadcast_multiply_pattern(
513            &program.compiled.instructions,
514            instruction_idx,
515            &slots,
516            &program.compiled.output_slots,
517        )? {
518            slots[output_slot] = Some(output);
519            instruction_idx += 3;
520            continue;
521        }
522
523        let instr = &program.compiled.instructions[instruction_idx];
524        if instr.outputs.len() != 1 {
525            return Err(Error::Internal(format!(
526                "expanded eager einsum expected single-output op, got {} outputs",
527                instr.outputs.len()
528            )));
529        }
530        let input_values: Vec<EagerTensor> = instr
531            .inputs
532            .iter()
533            .map(|&slot| {
534                slots
535                    .get(slot)
536                    .and_then(Option::as_ref)
537                    .cloned()
538                    .ok_or_else(|| {
539                        Error::Internal(format!(
540                            "expanded eager einsum missing value for slot {slot}"
541                        ))
542                    })
543            })
544            .collect::<Result<_>>()?;
545        let input_refs: Vec<&EagerTensor> = input_values.iter().collect();
546        let output =
547            tenferro_ad::extension::apply_standard_op(instr.operation.clone(), &input_refs)?;
548        slots[instr.outputs[0]] = Some(output);
549        instruction_idx += 1;
550    }
551
552    let [output_slot] = program.compiled.output_slots.as_slice() else {
553        return Err(Error::Internal(format!(
554            "expanded eager einsum expected one graph output, got {}",
555            program.compiled.output_slots.len()
556        )));
557    };
558    slots
559        .get_mut(*output_slot)
560        .and_then(Option::take)
561        .map(Some)
562        .ok_or_else(|| Error::Internal("expanded eager einsum output slot is missing".into()))
563}
564
565fn expanded_eager_program_retained_bytes(program: &ExpandedEagerProgram) -> usize {
566    saturating_sum([
567        size_of::<ExpandedEagerProgram>(),
568        vec_retained_bytes(&program.input_slots),
569        compiled_program_retained_bytes(&program.compiled),
570    ])
571}
572
573fn compiled_program_retained_bytes(program: &CompiledProgram<StdTensorOp>) -> usize {
574    saturating_sum([
575        size_of::<CompiledProgram<StdTensorOp>>(),
576        vec_retained_bytes(&program.instructions),
577        vec_retained_bytes(&program.input_slots),
578        vec_retained_bytes(&program.output_slots),
579        saturating_sum(program.instructions.iter().map(instruction_retained_bytes)),
580    ])
581}
582
583fn instruction_retained_bytes(instruction: &Instruction<StdTensorOp>) -> usize {
584    saturating_sum([
585        size_of::<Instruction<StdTensorOp>>(),
586        std_tensor_op_retained_bytes(&instruction.operation),
587        vec_retained_bytes(&instruction.inputs),
588        vec_retained_bytes(&instruction.outputs),
589    ])
590}
591
592fn std_tensor_op_retained_bytes(op: &StdTensorOp) -> usize {
593    match op {
594        StdTensorOp::DotGeneral { config } => saturating_sum([
595            vec_retained_bytes(&config.lhs_contracting_dims),
596            vec_retained_bytes(&config.rhs_contracting_dims),
597            vec_retained_bytes(&config.lhs_batch_dims),
598            vec_retained_bytes(&config.rhs_batch_dims),
599        ]),
600        StdTensorOp::Transpose { perm } => vec_retained_bytes(perm),
601        StdTensorOp::Reshape { to_shape } => vec_retained_bytes(to_shape),
602        StdTensorOp::BroadcastInDim { shape, dims } => {
603            saturating_sum([vec_retained_bytes(shape), vec_retained_bytes(dims)])
604        }
605        StdTensorOp::Constant { bytes, .. } => vec_retained_bytes(bytes),
606        StdTensorOp::ReduceSum { axes }
607        | StdTensorOp::ReduceProd { axes }
608        | StdTensorOp::ReduceMax { axes }
609        | StdTensorOp::ReduceMin { axes }
610        | StdTensorOp::Reverse { axes } => vec_retained_bytes(axes),
611        StdTensorOp::DynamicSlice { slice_sizes } => vec_retained_bytes(slice_sizes),
612        StdTensorOp::GatherDynamicSliceSizes {
613            offset_dims,
614            collapsed_slice_dims,
615            start_index_map,
616            slice_sizes,
617            ..
618        } => saturating_sum([
619            vec_retained_bytes(offset_dims),
620            vec_retained_bytes(collapsed_slice_dims),
621            vec_retained_bytes(start_index_map),
622            vec_retained_bytes(slice_sizes),
623        ]),
624        _ => 0,
625    }
626}
627
628fn try_execute_eager_broadcast_multiply_pattern(
629    instructions: &[Instruction<StdTensorOp>],
630    instruction_idx: usize,
631    slots: &[Option<EagerTensor>],
632    output_slots: &[usize],
633) -> Result<Option<(usize, EagerTensor)>> {
634    if instruction_idx + 2 >= instructions.len() {
635        return Ok(None);
636    }
637    let lhs_bc = &instructions[instruction_idx];
638    let rhs_bc = &instructions[instruction_idx + 1];
639    let multiply = &instructions[instruction_idx + 2];
640
641    let StdTensorOp::BroadcastInDim {
642        shape: lhs_shape_exprs,
643        dims: lhs_dims,
644    } = &lhs_bc.operation
645    else {
646        return Ok(None);
647    };
648    let StdTensorOp::BroadcastInDim {
649        shape: rhs_shape_exprs,
650        dims: rhs_dims,
651    } = &rhs_bc.operation
652    else {
653        return Ok(None);
654    };
655    if !matches!(multiply.operation, StdTensorOp::Mul)
656        || lhs_bc.outputs.len() != 1
657        || rhs_bc.outputs.len() != 1
658        || multiply.outputs.len() != 1
659        || multiply.inputs.len() != 2
660        || lhs_bc.inputs.is_empty()
661        || rhs_bc.inputs.is_empty()
662        || multiply.inputs[0] != lhs_bc.outputs[0]
663        || multiply.inputs[1] != rhs_bc.outputs[0]
664    {
665        return Ok(None);
666    }
667
668    let lhs_bc_slot = lhs_bc.outputs[0];
669    let rhs_bc_slot = rhs_bc.outputs[0];
670    if output_slots.contains(&lhs_bc_slot)
671        || output_slots.contains(&rhs_bc_slot)
672        || instructions[instruction_idx + 3..]
673            .iter()
674            .any(|instr| instr.inputs.contains(&lhs_bc_slot) || instr.inputs.contains(&rhs_bc_slot))
675    {
676        return Ok(None);
677    }
678
679    let lhs = slot_tensor(slots, lhs_bc.inputs[0])?;
680    let rhs = slot_tensor(slots, rhs_bc.inputs[0])?;
681    let lhs_shape = eval_shape_exprs(slots, &lhs_bc.inputs, lhs_shape_exprs)?;
682    let rhs_shape = eval_shape_exprs(slots, &rhs_bc.inputs, rhs_shape_exprs)?;
683    let Some(output) =
684        backend_broadcast_multiply_untracked(lhs, &lhs_shape, lhs_dims, rhs, &rhs_shape, rhs_dims)?
685    else {
686        return Ok(None);
687    };
688
689    Ok(Some((multiply.outputs[0], output)))
690}
691
692#[allow(clippy::too_many_arguments)]
693fn backend_broadcast_multiply_untracked(
694    lhs: &EagerTensor,
695    lhs_shape: &[usize],
696    lhs_dims: &[usize],
697    rhs: &EagerTensor,
698    rhs_shape: &[usize],
699    rhs_dims: &[usize],
700) -> Result<Option<EagerTensor>> {
701    if !Arc::ptr_eq(lhs.runtime(), rhs.runtime()) {
702        return Err(Error::ContextMismatch {
703            lhs: lhs.ctx_id(),
704            rhs: rhs.ctx_id(),
705        });
706    }
707    if lhs.tracks_grad() || rhs.tracks_grad() {
708        return Ok(None);
709    }
710
711    let runtime = lhs.runtime();
712    let value = runtime.with_backend_mut(|backend| {
713        backend.execute_broadcast_multiply_value(
714            lhs.tensor_read(),
715            lhs_shape,
716            lhs_dims,
717            rhs.tensor_read(),
718            rhs_shape,
719            rhs_dims,
720        )
721    })??;
722
723    Ok(value.map(|value| adopt_untracked_eager_value(runtime.clone(), value)))
724}
725
726fn eval_shape_exprs(
727    slots: &[Option<EagerTensor>],
728    input_slots: &[usize],
729    shape: &[DimExpr],
730) -> Result<Vec<usize>> {
731    let inputs = input_slots
732        .iter()
733        .map(|&slot| slot_tensor(slots, slot))
734        .collect::<Result<Vec<_>>>()?;
735    let input_shapes = inputs
736        .iter()
737        .map(|tensor| tensor.shape())
738        .collect::<Vec<_>>();
739    DimExpr::eval_all(shape, &input_shapes).map_err(|err| Error::InvalidCompiledGraph {
740        message: format!("invalid eager einsum shape expression: {err}"),
741    })
742}
743
744fn slot_tensor(slots: &[Option<EagerTensor>], slot: usize) -> Result<&EagerTensor> {
745    slots.get(slot).and_then(Option::as_ref).ok_or_else(|| {
746        Error::Internal(format!(
747            "expanded eager einsum missing value for slot {slot}"
748        ))
749    })
750}
751
752fn infer_eager_output_shape(
753    subscripts: &EinsumSubscripts,
754    inputs: &[&EagerTensor],
755) -> Result<Vec<tenferro_runtime::SymDim>> {
756    if inputs.is_empty() {
757        return Err(Error::ContractionError(
758            "einsum requires at least one input tensor".into(),
759        ));
760    }
761    if subscripts.inputs.len() != inputs.len() {
762        return Err(Error::ContractionError(format!(
763            "einsum subscripts expect {} inputs, got {}",
764            subscripts.inputs.len(),
765            inputs.len()
766        )));
767    }
768
769    let mut label_dims = std::collections::HashMap::new();
770    for (labels, tensor) in subscripts.inputs.iter().zip(inputs.iter()) {
771        let shape = tensor.shape();
772        if labels.len() != shape.len() {
773            return Err(Error::ContractionError(format!(
774                "einsum input rank mismatch: labels={}, shape={}",
775                labels.len(),
776                shape.len()
777            )));
778        }
779        for (&label, &dim) in labels.iter().zip(shape.iter()) {
780            if let Some(existing) = label_dims.insert(label, dim) {
781                if existing != dim {
782                    return Err(Error::ContractionError(format!(
783                        "einsum label {label} has inconsistent dimensions {existing} and {dim}"
784                    )));
785                }
786            }
787        }
788    }
789
790    subscripts
791        .output
792        .iter()
793        .map(|label| {
794            label_dims
795                .get(label)
796                .copied()
797                .map(tenferro_runtime::SymDim::from)
798                .ok_or_else(|| {
799                    Error::ContractionError(format!(
800                        "einsum output label {label} is missing from input labels"
801                    ))
802                })
803        })
804        .collect()
805}
806
807/// Execute a NumPy-style tensor contraction on [`EagerTensor`] values.
808///
809/// This helper lives in the einsum extension trait surface because it is
810/// contraction sugar over `dot_general`, not a linear algebra facade.
811///
812/// # Examples
813///
814/// ```
815/// use tenferro_tensor::Tensor;
816/// use tenferro_cpu::CpuBackend;
817/// use tenferro_ad::{EagerRuntime, EagerTensor};
818/// use tenferro_einsum::{EagerTensorEinsumExt, TensorDotAxes};
819///
820/// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
821/// let lhs = EagerTensor::from_tensor_in(
822///     Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap(),
823///     ctx.clone(),
824/// ).unwrap();
825/// let rhs = EagerTensor::from_tensor_in(
826///     Tensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap(),
827///     ctx,
828/// ).unwrap();
829/// let out = lhs.tensordot(&rhs, TensorDotAxes::Count(1)).unwrap();
830///
831/// assert_eq!(out.shape(), &[2, 4]);
832/// ```
833pub fn tensordot(
834    lhs: &EagerTensor,
835    rhs: &EagerTensor,
836    axes: TensorDotAxes<'_>,
837) -> Result<EagerTensor> {
838    let config = crate::tensordot::dot_general_config(axes, lhs.shape().len(), rhs.shape().len())?;
839    crate::tensordot::validate_concrete_contract_dims(lhs.shape(), rhs.shape(), &config)?;
840    lhs.dot_general(rhs, config)
841}
842
843#[cfg(test)]
844mod tests;