Skip to main content

tensor4all_tensorbackend/
tenferro_bridge.rs

1//! Bridge helpers between tensor4all storage snapshots and tenferro tensors.
2
3use std::cell::RefCell;
4use std::cmp::Reverse;
5use std::collections::{HashMap, HashSet};
6use std::env;
7use std::time::{Duration, Instant};
8
9use anyhow::{anyhow, ensure, Result};
10use num_complex::{Complex32, Complex64};
11use omeco::ScoreFunction;
12use tenferro::{
13    program::CoreSemanticOp, DType, GraphCompiler, Runtime, ScopedExecutionOutcome,
14    ScopedReadInputs, Tensor as NativeTensor, TensorRead, TensorScalar, TensorSessionOpsExt,
15    TensorView, TraceContext,
16};
17use tenferro_einsum::{
18    ContractionOptimizerOptions, ContractionTree, EinsumSubscripts, Subscripts,
19    TraceContextEinsumExt,
20};
21use tenferro_linalg::TensorLinalgExt;
22
23use crate::any_scalar::promote_scalar_native;
24/// Error returned by the storage/tensor bridge helpers.
25///
26/// Wraps the underlying tensor-element or backend diagnostic, preserving its
27/// source chain.
28///
29/// # Remedies
30/// - Dtype mismatch: extract with the element type matching the native tensor
31///   dtype, or promote explicitly before the call.
32/// - Shape mismatch: validate the native tensor shape against the payload
33///   contract.
34/// - Backend failure: the wrapped source chain identifies the failing stage.
35#[derive(Debug, thiserror::Error)]
36#[error("native tensor bridge operation failed: {source}")]
37pub struct BridgeError {
38    /// Original tensor-element or backend diagnostic.
39    #[source]
40    pub source: anyhow::Error,
41}
42
43impl From<anyhow::Error> for BridgeError {
44    fn from(source: anyhow::Error) -> Self {
45        Self { source }
46    }
47}
48
49fn native_tensor_from_vec<T: TensorScalar>(
50    shape: Vec<usize>,
51    values: Vec<T>,
52) -> std::result::Result<NativeTensor, BridgeError> {
53    NativeTensor::from_vec_col_major(shape, values)
54        .map_err(|source| BridgeError::from(anyhow::Error::new(source)))
55}
56
57use crate::context::{
58    default_engine_buffer_pool_stats, reset_default_engine, reset_default_engine_buffer_pool,
59    with_default_graph_runtime, with_default_session,
60};
61use crate::memory::release_process_allocator_cached_memory;
62use crate::storage::Storage;
63#[cfg(test)]
64use crate::storage::StorageRepr;
65use crate::tensor_element::TensorElement;
66use crate::BackendScalar;
67
68/// Read-only native tensor input that can either borrow external payload data
69/// or own a temporary materialized tensor.
70// Both tenferro handle variants are large; boxing the slightly larger one would
71// add an allocation to the eager einsum path for little enum-size reduction.
72#[allow(clippy::large_enum_variant)]
73pub enum NativeTensorReadInput<'a> {
74    /// Borrowed read-only tensor input.
75    Borrowed(TensorRead<'a>),
76    /// Owned temporary tensor input.
77    Owned(NativeTensor),
78}
79
80impl<'a> NativeTensorReadInput<'a> {
81    /// Return this input as a read-only tenferro tensor input.
82    pub fn as_read(&'a self) -> TensorRead<'a> {
83        match self {
84            Self::Borrowed(read) => read.clone(),
85            Self::Owned(tensor) => TensorRead::from_tensor(tensor),
86        }
87    }
88
89    /// Return the scalar dtype of this input.
90    pub fn dtype(&self) -> DType {
91        match self {
92            Self::Borrowed(read) => read.dtype(),
93            Self::Owned(tensor) => tensor.dtype(),
94        }
95    }
96
97    /// Return the tensor shape of this input.
98    pub fn shape(&self) -> &[usize] {
99        match self {
100            Self::Borrowed(read) => read.shape(),
101            Self::Owned(tensor) => tensor.shape(),
102        }
103    }
104}
105
106#[cfg(test)]
107use std::cell::Cell;
108
109#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
110enum NativeEinsumPath {
111    Owned,
112    Borrowed,
113    BorrowedWithConversions,
114}
115
116#[derive(Debug, Clone, Hash, PartialEq, Eq)]
117struct NativeOperandSignature {
118    shape: Vec<usize>,
119    ids: Vec<u32>,
120    dtype: DType,
121}
122
123#[derive(Debug, Clone, Hash, PartialEq, Eq)]
124struct NativeEinsumSignature {
125    path: NativeEinsumPath,
126    operands: Vec<NativeOperandSignature>,
127    output_ids: Vec<u32>,
128}
129
130#[derive(Debug, Default, Clone)]
131struct NativeEinsumProfileEntry {
132    calls: usize,
133    total_time: Duration,
134}
135
136thread_local! {
137    static NATIVE_EINSUM_PROFILE_STATE: RefCell<HashMap<NativeEinsumSignature, NativeEinsumProfileEntry>> =
138        RefCell::new(HashMap::new());
139    static NATIVE_EINSUM_TRACE_STATE: RefCell<HashSet<NativeEinsumSignature>> =
140        RefCell::new(HashSet::new());
141}
142
143#[cfg(test)]
144thread_local! {
145    static FORCE_NATIVE_EINSUM_PROFILE: Cell<bool> = const { Cell::new(false) };
146}
147
148fn native_einsum_profile_enabled() -> bool {
149    #[cfg(test)]
150    if FORCE_NATIVE_EINSUM_PROFILE.with(Cell::get) {
151        return true;
152    }
153    env::var("T4A_PROFILE_NATIVE_EINSUM").is_ok()
154}
155
156fn native_einsum_path_trace_enabled() -> bool {
157    env::var("T4A_TRACE_NATIVE_EINSUM_PATHS").is_ok()
158}
159
160fn native_einsum_path_trace_min_bytes() -> usize {
161    env::var("T4A_TRACE_NATIVE_EINSUM_MIN_BYTES")
162        .ok()
163        .and_then(|value| value.parse().ok())
164        .unwrap_or(0)
165}
166
167fn native_einsum_path_trace_max_signatures() -> usize {
168    env::var("T4A_TRACE_NATIVE_EINSUM_MAX_SIGNATURES")
169        .ok()
170        .and_then(|value| value.parse().ok())
171        .unwrap_or(64)
172}
173
174fn native_einsum_pool_trace_enabled() -> bool {
175    env::var("T4A_TRACE_NATIVE_EINSUM_POOL").is_ok()
176}
177
178fn native_einsum_pool_trace_min_output_bytes() -> usize {
179    env::var("T4A_TRACE_NATIVE_EINSUM_POOL_MIN_OUTPUT_BYTES")
180        .ok()
181        .and_then(|value| value.parse().ok())
182        .unwrap_or(0)
183}
184
185fn native_einsum_pool_trace_min_retained_bytes() -> usize {
186    env::var("T4A_TRACE_NATIVE_EINSUM_POOL_MIN_RETAINED_BYTES")
187        .ok()
188        .and_then(|value| value.parse().ok())
189        .unwrap_or(0)
190}
191
192fn reset_native_einsum_engine_after_call() -> bool {
193    env::var("T4A_RESET_NATIVE_EINSUM_ENGINE_AFTER_CALL").is_ok()
194}
195
196fn reset_native_einsum_buffer_pool_after_call() -> bool {
197    env::var("T4A_RESET_NATIVE_EINSUM_BUFFER_POOL_AFTER_CALL").is_ok()
198}
199
200fn release_allocator_after_native_einsum_call() -> bool {
201    env::var("T4A_RELEASE_ALLOCATOR_AFTER_NATIVE_EINSUM_CALL").is_ok()
202}
203
204#[cfg(test)]
205pub(crate) fn set_native_einsum_profile_enabled_for_tests(enabled: bool) {
206    FORCE_NATIVE_EINSUM_PROFILE.with(|slot| slot.set(enabled));
207}
208
209fn checked_native_einsum_labels(labels: &[usize]) -> Result<Vec<u32>> {
210    labels
211        .iter()
212        .copied()
213        .map(|label| {
214            u32::try_from(label)
215                .map_err(|_| anyhow!("native einsum label {label} exceeds the supported u32 range"))
216        })
217        .collect()
218}
219
220fn native_einsum_signature(
221    path: NativeEinsumPath,
222    operands: &[(&NativeTensor, &[u32])],
223    output_ids: &[u32],
224) -> NativeEinsumSignature {
225    NativeEinsumSignature {
226        path,
227        operands: operands
228            .iter()
229            .map(|(tensor, ids)| NativeOperandSignature {
230                shape: tensor.shape().to_vec(),
231                ids: ids.to_vec(),
232                dtype: tensor.dtype(),
233            })
234            .collect(),
235        output_ids: output_ids.to_vec(),
236    }
237}
238
239fn record_native_einsum_profile(
240    path: NativeEinsumPath,
241    operands: &[(&NativeTensor, &[u32])],
242    output_ids: &[u32],
243    elapsed: Duration,
244) {
245    if !native_einsum_profile_enabled() {
246        return;
247    }
248    let signature = native_einsum_signature(path, operands, output_ids);
249    NATIVE_EINSUM_PROFILE_STATE.with(|state| {
250        let mut state = state.borrow_mut();
251        let entry = state.entry(signature).or_default();
252        entry.calls += 1;
253        entry.total_time += elapsed;
254    });
255}
256
257fn native_slice<'a, T: TensorScalar>(
258    tensor: &'a NativeTensor,
259    label: &'static str,
260) -> Result<&'a [T]> {
261    tensor
262        .as_slice::<T>()
263        .map_err(|error| anyhow!("{label}: {error}"))
264}
265
266fn dtype_size_bytes(dtype: DType) -> usize {
267    match dtype {
268        DType::F32 => 4,
269        DType::F64 => 8,
270        DType::C32 => 8,
271        DType::C64 => 16,
272        DType::I32 => 4,
273        DType::I64 => 8,
274        DType::Bool => 1,
275    }
276}
277
278fn native_tensor_bytes(tensor: &NativeTensor) -> usize {
279    tensor
280        .shape()
281        .iter()
282        .copied()
283        .fold(1usize, usize::saturating_mul)
284        .saturating_mul(dtype_size_bytes(tensor.dtype()))
285}
286
287fn format_label(label: u32) -> String {
288    char::from_u32(label).map_or_else(|| label.to_string(), |label| label.to_string())
289}
290
291fn format_labels(labels: &[u32]) -> String {
292    if labels.is_empty() {
293        "scalar".to_string()
294    } else {
295        labels
296            .iter()
297            .map(|&label| format_label(label))
298            .collect::<Vec<_>>()
299            .join("")
300    }
301}
302
303fn label_dims(subscripts: &Subscripts, shapes: &[Vec<usize>]) -> Result<HashMap<u32, usize>> {
304    let mut dims = HashMap::new();
305    for (labels, shape) in subscripts.inputs.iter().zip(shapes.iter()) {
306        ensure!(
307            labels.len() == shape.len(),
308            "einsum labels {:?} do not match shape {:?}",
309            labels,
310            shape
311        );
312        for (&label, &dim) in labels.iter().zip(shape.iter()) {
313            if let Some(previous) = dims.insert(label, dim) {
314                ensure!(
315                    previous == dim,
316                    "inconsistent dimension for einsum label {}: {} vs {}",
317                    format_label(label),
318                    previous,
319                    dim
320                );
321            }
322        }
323    }
324    Ok(dims)
325}
326
327fn labels_size(labels: &[u32], dims: &HashMap<u32, usize>) -> usize {
328    labels.iter().fold(1usize, |size, label| {
329        size.saturating_mul(dims.get(label).copied().unwrap_or(1))
330    })
331}
332
333fn union_labels(lhs: &[u32], rhs: &[u32]) -> Vec<u32> {
334    let mut seen = HashSet::new();
335    let mut labels = Vec::new();
336    for &label in lhs.iter().chain(rhs.iter()) {
337        if seen.insert(label) {
338            labels.push(label);
339        }
340    }
341    labels
342}
343
344#[derive(Debug)]
345struct NativeEinsumPlanReport {
346    lines: Vec<String>,
347    peak_intermediate_bytes: usize,
348}
349
350fn time_optimized_contraction_options() -> ContractionOptimizerOptions {
351    ContractionOptimizerOptions {
352        score: ScoreFunction::time_optimized(),
353        ..ContractionOptimizerOptions::default()
354    }
355}
356
357fn native_einsum_plan_report_with_options(
358    signature: &NativeEinsumSignature,
359    optimizer_name: &'static str,
360    options: &ContractionOptimizerOptions,
361) -> Result<NativeEinsumPlanReport> {
362    let input_ids = signature
363        .operands
364        .iter()
365        .map(|operand| operand.ids.as_slice())
366        .collect::<Vec<_>>();
367    let subscripts_string = build_einsum_subscripts(&input_ids, &signature.output_ids)?;
368    let subscripts = Subscripts {
369        inputs: input_ids.iter().map(|ids| ids.to_vec()).collect(),
370        output: signature.output_ids.clone(),
371    };
372    let shapes = signature
373        .operands
374        .iter()
375        .map(|operand| operand.shape.clone())
376        .collect::<Vec<_>>();
377    let shape_refs = shapes.iter().map(Vec::as_slice).collect::<Vec<_>>();
378    let tree = ContractionTree::optimize_with_options(&subscripts, &shape_refs, options)
379        .map_err(|e| anyhow!("failed to optimize native einsum path: {e}"))?;
380    let dims = label_dims(&subscripts, &shapes)?;
381    let dtype = signature
382        .operands
383        .first()
384        .map(|operand| operand.dtype)
385        .unwrap_or(DType::F64);
386    let dtype_size = dtype_size_bytes(dtype);
387
388    let mut lines = Vec::new();
389    lines.push(format!(
390        "optimizer={optimizer_name} subscripts={subscripts_string} dtype={dtype:?} steps={}",
391        tree.step_count()
392    ));
393    let mut peak_intermediate_elems = 1usize;
394    for step in 0..tree.step_count() {
395        let Some((left, right)) = tree.step_pair(step) else {
396            continue;
397        };
398        let Some((lhs, rhs, out)) = tree.step_subscripts(step) else {
399            continue;
400        };
401        let lhs_elems = labels_size(lhs, &dims);
402        let rhs_elems = labels_size(rhs, &dims);
403        let out_elems = labels_size(out, &dims);
404        let flop_index_elems = labels_size(&union_labels(lhs, rhs), &dims);
405        peak_intermediate_elems = peak_intermediate_elems.max(out_elems);
406        lines.push(format!(
407            "  step {step:02}: pair=({left},{right}) {}[{}] x {}[{}] -> {}[{}]  flop_index={}  intermediate={} elems ({:.3} MiB)",
408            format_labels(lhs),
409            lhs_elems,
410            format_labels(rhs),
411            rhs_elems,
412            format_labels(out),
413            out_elems,
414            flop_index_elems,
415            out_elems,
416            out_elems as f64 * dtype_size as f64 / (1024.0 * 1024.0),
417        ));
418    }
419    let peak_intermediate_bytes = peak_intermediate_elems.saturating_mul(dtype_size);
420    lines.push(format!(
421        "  peak_intermediate={} elems ({:.3} MiB)",
422        peak_intermediate_elems,
423        peak_intermediate_bytes as f64 / (1024.0 * 1024.0)
424    ));
425
426    Ok(NativeEinsumPlanReport {
427        lines,
428        peak_intermediate_bytes,
429    })
430}
431
432fn native_einsum_time_optimized_plan_report(
433    signature: &NativeEinsumSignature,
434) -> Result<NativeEinsumPlanReport> {
435    native_einsum_plan_report_with_options(
436        signature,
437        "time_optimized",
438        &time_optimized_contraction_options(),
439    )
440}
441
442fn native_einsum_balanced_plan_report(
443    signature: &NativeEinsumSignature,
444) -> Result<NativeEinsumPlanReport> {
445    native_einsum_plan_report_with_options(
446        signature,
447        "balanced_default",
448        &ContractionOptimizerOptions::default(),
449    )
450}
451
452fn maybe_trace_native_einsum_path(
453    path: NativeEinsumPath,
454    operands: &[(&NativeTensor, &[u32])],
455    output_ids: &[u32],
456) {
457    if !native_einsum_path_trace_enabled() {
458        return;
459    }
460    let signature = native_einsum_signature(path, operands, output_ids);
461    let report = match native_einsum_time_optimized_plan_report(&signature) {
462        Ok(report) if report.peak_intermediate_bytes >= native_einsum_path_trace_min_bytes() => {
463            report
464        }
465        Ok(_) => return,
466        Err(err) => {
467            eprintln!("native_einsum path trace failed: {err:#}");
468            return;
469        }
470    };
471
472    let max_signatures = native_einsum_path_trace_max_signatures();
473    let should_trace = NATIVE_EINSUM_TRACE_STATE.with(|state| {
474        let mut state = state.borrow_mut();
475        if state.len() >= max_signatures || state.contains(&signature) {
476            false
477        } else {
478            state.insert(signature.clone());
479            true
480        }
481    });
482    if !should_trace {
483        return;
484    }
485
486    eprintln!("=== native_einsum Path Trace ===");
487    eprintln!(
488        "path={:?} output_ids={:?}",
489        signature.path, signature.output_ids
490    );
491    for operand in &signature.operands {
492        eprintln!(
493            "  operand shape={:?} ids={:?} dtype={:?}",
494            operand.shape, operand.ids, operand.dtype
495        );
496    }
497    for line in report.lines {
498        eprintln!("{line}");
499    }
500    if env::var("T4A_TRACE_NATIVE_EINSUM_COMPARE_BALANCED").is_ok() {
501        match native_einsum_balanced_plan_report(&signature) {
502            Ok(balanced) => {
503                for line in balanced.lines {
504                    eprintln!("{line}");
505                }
506            }
507            Err(err) => eprintln!("balanced native_einsum path trace failed: {err:#}"),
508        }
509    }
510}
511
512/// Reset the aggregated native einsum profile.
513pub fn reset_native_einsum_profile() {
514    NATIVE_EINSUM_PROFILE_STATE.with(|state| state.borrow_mut().clear());
515    NATIVE_EINSUM_TRACE_STATE.with(|state| state.borrow_mut().clear());
516}
517
518/// Print and clear the aggregated native einsum profile.
519pub fn print_and_reset_native_einsum_profile() {
520    if !native_einsum_profile_enabled() {
521        return;
522    }
523    NATIVE_EINSUM_PROFILE_STATE.with(|state| {
524        let mut entries: Vec<_> = state
525            .borrow()
526            .iter()
527            .map(|(k, v)| (k.clone(), v.clone()))
528            .collect();
529        state.borrow_mut().clear();
530        entries.sort_by_key(|(_, entry)| Reverse(entry.total_time));
531
532        eprintln!("=== native_einsum Profile ===");
533        for (idx, (signature, entry)) in entries.into_iter().take(20).enumerate() {
534            eprintln!(
535                "#{idx:02} path={:?} calls={} total={:.3}s per_call={:.3}us output_ids={:?}",
536                signature.path,
537                entry.calls,
538                entry.total_time.as_secs_f64(),
539                entry.total_time.as_secs_f64() * 1e6 / entry.calls as f64,
540                signature.output_ids,
541            );
542            for operand in &signature.operands {
543                eprintln!(
544                    "     shape={:?} ids={:?} dtype={:?}",
545                    operand.shape, operand.ids, operand.dtype
546                );
547            }
548            match native_einsum_time_optimized_plan_report(&signature) {
549                Ok(report) => {
550                    for line in report.lines {
551                        eprintln!("     {line}");
552                    }
553                }
554                Err(err) => eprintln!("     path report failed: {err:#}"),
555            }
556        }
557    });
558}
559
560fn common_dtype(dtypes: &[DType]) -> DType {
561    let has_f64 = dtypes.contains(&DType::F64);
562    let has_c64 = dtypes.contains(&DType::C64);
563    let has_c32 = dtypes.contains(&DType::C32);
564    let has_i32 = dtypes.contains(&DType::I32);
565    let has_i64 = dtypes.contains(&DType::I64);
566    let has_bool = dtypes.contains(&DType::Bool);
567    let has_complex = has_c64 || has_c32;
568    if has_c64 || (has_f64 && has_complex) {
569        DType::C64
570    } else if has_c32 {
571        DType::C32
572    } else if has_f64 || has_i64 || has_i32 {
573        DType::F64
574    } else if has_bool {
575        DType::Bool
576    } else {
577        DType::F32
578    }
579}
580
581fn convert_tensor(tensor: &NativeTensor, to: DType) -> Result<NativeTensor> {
582    if tensor.dtype() == to {
583        return tensor
584            .duplicate()
585            .map_err(|e| anyhow!("tensor duplication failed: {e}"));
586    }
587    with_default_session(|session| tensor.convert(to, session))
588        .map_err(|e| anyhow!("tensor conversion to {to:?} failed: {e}"))
589}
590
591fn ids_to_subscript(ids: &[u32]) -> Result<String> {
592    const LETTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
593    let mut out = String::with_capacity(ids.len());
594    for &id in ids {
595        let idx = usize::try_from(id).unwrap_or(usize::MAX);
596        let letter = LETTERS
597            .get(idx)
598            .ok_or_else(|| anyhow!("einsum label {id} exceeds supported label range"))?;
599        out.push(char::from(*letter));
600    }
601    Ok(out)
602}
603
604fn build_einsum_subscripts(operands: &[&[u32]], output_ids: &[u32]) -> Result<String> {
605    let inputs = operands
606        .iter()
607        .map(|ids| ids_to_subscript(ids))
608        .collect::<Result<Vec<_>>>()?;
609    Ok(format!(
610        "{}->{}",
611        inputs.join(","),
612        ids_to_subscript(output_ids)?
613    ))
614}
615
616fn compile_native_einsum_program(
617    compiler: &mut GraphCompiler,
618    input_specs: impl IntoIterator<Item = tenferro::program::ProgramInputSpec>,
619    subscripts: &EinsumSubscripts,
620) -> Result<tenferro::CompiledGraph> {
621    let input_specs = input_specs.into_iter().collect::<Vec<_>>();
622    let target = common_dtype(
623        input_specs
624            .iter()
625            .map(|spec| spec.metadata().dtype())
626            .collect::<Vec<_>>()
627            .as_slice(),
628    );
629    let mut trace = TraceContext::new();
630    let trace_inputs = input_specs
631        .into_iter()
632        .map(|spec| {
633            let dtype = spec.metadata().dtype();
634            let input = trace
635                .input(spec)
636                .map_err(|e| anyhow!("native einsum input tracing failed: {e}"))?;
637            if dtype == target {
638                return Ok(input);
639            }
640            trace
641                .add_op(
642                    CoreSemanticOp::Convert {
643                        from: dtype,
644                        to: target,
645                    },
646                    &[input],
647                )
648                .map_err(|e| anyhow!("native einsum promotion tracing failed: {e}"))?
649                .first()
650                .copied()
651                .ok_or_else(|| anyhow!("native einsum promotion returned no output"))
652        })
653        .collect::<Result<Vec<_>>>()?;
654    let output = trace
655        .einsum_subscripts(&trace_inputs, subscripts)
656        .map_err(|e| anyhow!("native einsum tracing failed: {e}"))?;
657    let graph = trace
658        .finish(&[output])
659        .map_err(|e| anyhow!("native einsum graph finalization failed: {e}"))?;
660    compiler
661        .compile_traced_graph(&graph)
662        .map_err(|e| anyhow!("native einsum graph compilation failed: {e}"))
663}
664
665fn run_cached_native_einsum(
666    subscripts: &EinsumSubscripts,
667    execute: impl FnOnce(&mut GraphCompiler, &Runtime) -> Result<NativeTensor>,
668) -> Result<NativeTensor> {
669    let trace_pool = native_einsum_pool_trace_enabled();
670    let pool_before = trace_pool
671        .then(default_engine_buffer_pool_stats)
672        .transpose()?;
673    let result =
674        with_default_graph_runtime(|compiler, runtime, _backend| execute(compiler, runtime))??;
675    if trace_pool {
676        let pool_after = default_engine_buffer_pool_stats()?;
677        let output_bytes = native_tensor_bytes(&result);
678        let retained_threshold = native_einsum_pool_trace_min_retained_bytes();
679        if pool_after != pool_before.unwrap_or_default()
680            && pool_after.capacity_bytes >= retained_threshold
681            || output_bytes >= native_einsum_pool_trace_min_output_bytes()
682        {
683            let before = pool_before.unwrap_or_default();
684            eprintln!(
685                "native_einsum pool subscripts={subscripts:?} before_buffers={} before_capacity={:.3} MiB after_buffers={} after_capacity={:.3} MiB output_shape={:?} output_bytes={:.3} MiB",
686                before.buffers,
687                before.capacity_bytes as f64 / (1024.0 * 1024.0),
688                pool_after.buffers,
689                pool_after.capacity_bytes as f64 / (1024.0 * 1024.0),
690                result.shape(),
691                output_bytes as f64 / (1024.0 * 1024.0),
692            );
693        }
694    }
695    if reset_native_einsum_engine_after_call() {
696        let before_reset = trace_pool
697            .then(default_engine_buffer_pool_stats)
698            .transpose()?;
699        reset_default_engine()?;
700        if trace_pool
701            && before_reset.unwrap_or_default().capacity_bytes
702                >= native_einsum_pool_trace_min_retained_bytes()
703        {
704            let before = before_reset.unwrap_or_default();
705            let after = default_engine_buffer_pool_stats()?;
706            eprintln!(
707                "native_einsum engine_reset before_buffers={} before_capacity={:.3} MiB after_buffers={} after_capacity={:.3} MiB",
708                before.buffers,
709                before.capacity_bytes as f64 / (1024.0 * 1024.0),
710                after.buffers,
711                after.capacity_bytes as f64 / (1024.0 * 1024.0),
712            );
713        }
714    } else if reset_native_einsum_buffer_pool_after_call() {
715        let before_clear = trace_pool
716            .then(default_engine_buffer_pool_stats)
717            .transpose()?;
718        reset_default_engine_buffer_pool()?;
719        if trace_pool
720            && before_clear.unwrap_or_default().capacity_bytes
721                >= native_einsum_pool_trace_min_retained_bytes()
722        {
723            let before = before_clear.unwrap_or_default();
724            let after = default_engine_buffer_pool_stats()?;
725            eprintln!(
726                "native_einsum pool_reset before_buffers={} before_capacity={:.3} MiB after_buffers={} after_capacity={:.3} MiB",
727                before.buffers,
728                before.capacity_bytes as f64 / (1024.0 * 1024.0),
729                after.buffers,
730                after.capacity_bytes as f64 / (1024.0 * 1024.0),
731            );
732        }
733    }
734    if release_allocator_after_native_einsum_call() {
735        let report = release_process_allocator_cached_memory();
736        if trace_pool && (report.released_bytes.unwrap_or(0) > 0 || report.success == Some(true)) {
737            eprintln!(
738                "native_einsum allocator_pressure_relief supported={} released_bytes={:?} success={:?}",
739                report.supported,
740                report.released_bytes,
741                report.success,
742            );
743        }
744    }
745    Ok(result)
746}
747
748fn cached_einsum_native_tensors(
749    inputs: &[&NativeTensor],
750    subscripts: &EinsumSubscripts,
751) -> Result<NativeTensor> {
752    run_cached_native_einsum(subscripts, |compiler, runtime| {
753        let program = compile_native_einsum_program(
754            compiler,
755            inputs.iter().map(|tensor| {
756                tenferro::program::ProgramInputSpec::new(
757                    tensor.dtype(),
758                    tensor.shape().iter().copied().map(Into::into),
759                )
760            }),
761            subscripts,
762        )?;
763        let mut outputs = runtime
764            .run_compiled(&program, inputs)
765            .map_err(|e| anyhow!("native einsum failed: {e}"))?;
766        if outputs.len() != 1 {
767            return Err(anyhow!(
768                "native einsum returned {} outputs instead of one",
769                outputs.len()
770            ));
771        }
772        outputs
773            .pop()
774            .ok_or_else(|| anyhow!("native einsum returned no output"))
775    })
776}
777
778fn cached_einsum_native_reads(
779    inputs: &[TensorRead<'_>],
780    subscripts: &Subscripts,
781) -> Result<NativeTensor> {
782    let views = inputs
783        .iter()
784        .map(|input| input.clone().tensor_view())
785        .collect::<Vec<_>>();
786    let einsum_subscripts = EinsumSubscripts::from(subscripts);
787    run_cached_native_einsum(&einsum_subscripts, |compiler, runtime| {
788        let program = compile_native_einsum_program(
789            compiler,
790            views.iter().map(|tensor| {
791                tenferro::program::ProgramInputSpec::new(
792                    tensor.dtype(),
793                    tensor.shape().iter().copied().map(Into::into),
794                )
795            }),
796            &einsum_subscripts,
797        )?;
798        let outcome = runtime
799            .execute_scoped_read_only(&program, ScopedReadInputs::new(views))
800            .map_err(|rejected| {
801                let (error, _inputs) = rejected.into_parts();
802                anyhow!("native einsum submission rejected: {error}")
803            })?;
804        let bundle = match outcome {
805            ScopedExecutionOutcome::Completed(bundle) => bundle,
806            ScopedExecutionOutcome::RetiredFailed { error, .. } => {
807                return Err(anyhow!("native einsum failed: {error}"));
808            }
809        };
810        bundle
811            .into_owned_output(0)
812            .map_err(|(_, error)| anyhow!("native einsum output extraction failed: {error}"))
813    })
814    .map_err(|e| anyhow!("native read einsum failed: {e}"))
815}
816
817/// Build native einsum ids for a binary contraction.
818pub(crate) fn build_binary_einsum_ids(
819    lhs_rank: usize,
820    axes_a: &[usize],
821    rhs_rank: usize,
822    axes_b: &[usize],
823) -> Result<(Vec<u32>, Vec<u32>, Vec<u32>)> {
824    ensure!(
825        axes_a.len() == axes_b.len(),
826        "contract axis length mismatch: lhs {:?}, rhs {:?}",
827        axes_a,
828        axes_b
829    );
830
831    let mut lhs_ids = vec![u32::MAX; lhs_rank];
832    let mut rhs_ids = vec![u32::MAX; rhs_rank];
833    let mut next_id = 0u32;
834
835    let mut seen_lhs = vec![false; lhs_rank];
836    let mut seen_rhs = vec![false; rhs_rank];
837
838    for (&lhs_axis, &rhs_axis) in axes_a.iter().zip(axes_b.iter()) {
839        ensure!(
840            lhs_axis < lhs_rank,
841            "lhs contract axis {lhs_axis} out of range"
842        );
843        ensure!(
844            rhs_axis < rhs_rank,
845            "rhs contract axis {rhs_axis} out of range"
846        );
847        ensure!(
848            !seen_lhs[lhs_axis],
849            "duplicate lhs contract axis {lhs_axis}"
850        );
851        ensure!(
852            !seen_rhs[rhs_axis],
853            "duplicate rhs contract axis {rhs_axis}"
854        );
855        seen_lhs[lhs_axis] = true;
856        seen_rhs[rhs_axis] = true;
857        lhs_ids[lhs_axis] = next_id;
858        rhs_ids[rhs_axis] = next_id;
859        next_id += 1;
860    }
861
862    let mut output_ids = Vec::with_capacity(lhs_rank + rhs_rank - 2 * axes_a.len());
863    for (axis, slot) in lhs_ids.iter_mut().enumerate() {
864        if *slot == u32::MAX {
865            *slot = next_id;
866            output_ids.push(next_id);
867            next_id += 1;
868        } else {
869            let _ = axis;
870        }
871    }
872    for slot in &mut rhs_ids {
873        if *slot == u32::MAX {
874            *slot = next_id;
875            output_ids.push(next_id);
876            next_id += 1;
877        }
878    }
879
880    Ok((lhs_ids, rhs_ids, output_ids))
881}
882
883/// Build a dense native tensor from column-major data.
884/// # Errors
885///
886/// Returns an error when the data length does not match the logical dimension product (a shape mismatch) or the backend conversion fails.
887pub fn dense_native_tensor_from_col_major<T: TensorElement>(
888    data: &[T],
889    logical_dims: &[usize],
890) -> Result<NativeTensor> {
891    T::dense_native_tensor_from_col_major(data, logical_dims)
892}
893
894/// Build a dense native tensor whose logical values are diagonal.
895/// # Errors
896///
897/// Returns an error when the diagonal payload is incompatible with the logical rank (a shape mismatch) or the backend conversion fails.
898pub fn diag_native_tensor_from_col_major<T: TensorElement>(
899    data: &[T],
900    logical_rank: usize,
901) -> Result<NativeTensor> {
902    T::diag_native_tensor_from_col_major(data, logical_rank)
903}
904
905/// Convert storage to a dense native tensor.
906/// # Errors
907///
908/// Returns an error when the storage cannot be converted to a native tensor (a scalar-kind mismatch or backend failure).
909pub fn storage_to_native_tensor(
910    storage: &Storage,
911    logical_dims: &[usize],
912) -> std::result::Result<NativeTensor, BridgeError> {
913    if storage.is_c64() {
914        dense_native_tensor_from_col_major(
915            &storage
916                .to_dense_c64_col_major_vec(logical_dims)
917                .map_err(|e| anyhow!("dense c64 materialization failed: {e}"))?,
918            logical_dims,
919        )
920        .map_err(BridgeError::from)
921    } else {
922        dense_native_tensor_from_col_major(
923            &storage
924                .to_dense_f64_col_major_vec(logical_dims)
925                .map_err(|e| anyhow!("dense f64 materialization failed: {e}"))?,
926            logical_dims,
927        )
928        .map_err(BridgeError::from)
929    }
930}
931
932/// Build a read-only native tensor input over the compact storage payload.
933///
934/// Contiguous payloads are borrowed without copying. Non-contiguous payloads
935/// are materialized into an owned native tensor.
936/// # Errors
937///
938/// Returns an error when the storage payload cannot be read into a native buffer (a scalar-kind mismatch or backend failure).
939pub fn storage_payload_native_read_input(
940    storage: &Storage,
941) -> std::result::Result<NativeTensorReadInput<'_>, BridgeError> {
942    if storage.is_f64() {
943        if let Some(view) = storage
944            .payload_f64_col_major_view_if_contiguous()
945            .map_err(anyhow::Error::msg)?
946        {
947            return Ok(NativeTensorReadInput::Borrowed(TensorRead::from_view(
948                TensorView::f64(storage.payload_dims(), view)
949                    .map_err(|e| BridgeError::from(anyhow::Error::new(e)))?,
950            )));
951        }
952        native_tensor_from_vec(
953            storage.payload_dims().to_vec(),
954            storage
955                .payload_f64_col_major_vec()
956                .map_err(anyhow::Error::msg)?,
957        )
958        .map(NativeTensorReadInput::Owned)
959    } else if storage.is_c64() {
960        if let Some(view) = storage
961            .payload_c64_col_major_view_if_contiguous()
962            .map_err(anyhow::Error::msg)?
963        {
964            return Ok(NativeTensorReadInput::Borrowed(TensorRead::from_view(
965                TensorView::c64(storage.payload_dims(), view)
966                    .map_err(|e| BridgeError::from(anyhow::Error::new(e)))?,
967            )));
968        }
969        native_tensor_from_vec(
970            storage.payload_dims().to_vec(),
971            storage
972                .payload_c64_col_major_vec()
973                .map_err(anyhow::Error::msg)?,
974        )
975        .map(NativeTensorReadInput::Owned)
976    } else {
977        Err(anyhow!("unsupported storage scalar type").into())
978    }
979}
980
981/// Materialize a native tensor into dense storage.
982/// # Errors
983///
984/// Returns an error when the native tensor cannot be converted to storage (a scalar-kind mismatch or backend failure).
985pub fn native_tensor_primal_to_storage(
986    tensor: &NativeTensor,
987) -> std::result::Result<Storage, BridgeError> {
988    match tensor.dtype() {
989        DType::F32 => Storage::from_dense_col_major(
990            native_slice::<f32>(tensor, "failed to read f32 native tensor")?
991                .iter()
992                .map(|&value| value as f64)
993                .collect::<Vec<_>>(),
994            tensor.shape(),
995        )
996        .map_err(|e| {
997            BridgeError::from(anyhow!(
998                "native tensor snapshot materialization failed: {e}"
999            ))
1000        }),
1001        DType::F64 => Storage::from_dense_col_major(
1002            native_slice::<f64>(tensor, "failed to read f64 native tensor")?.to_vec(),
1003            tensor.shape(),
1004        )
1005        .map_err(|e| {
1006            BridgeError::from(anyhow!(
1007                "native tensor snapshot materialization failed: {e}"
1008            ))
1009        }),
1010        DType::I32 => Storage::from_dense_col_major(
1011            native_slice::<i32>(tensor, "failed to read i32 native tensor")?
1012                .iter()
1013                .map(|&value| value as f64)
1014                .collect::<Vec<_>>(),
1015            tensor.shape(),
1016        )
1017        .map_err(|e| {
1018            BridgeError::from(anyhow!(
1019                "native tensor snapshot materialization failed: {e}"
1020            ))
1021        }),
1022        DType::I64 => Storage::from_dense_col_major(
1023            native_slice::<i64>(tensor, "failed to read i64 native tensor")?
1024                .iter()
1025                .map(|&value| value as f64)
1026                .collect::<Vec<_>>(),
1027            tensor.shape(),
1028        )
1029        .map_err(|e| {
1030            BridgeError::from(anyhow!(
1031                "native tensor snapshot materialization failed: {e}"
1032            ))
1033        }),
1034        DType::Bool => Storage::from_dense_col_major(
1035            native_slice::<bool>(tensor, "failed to read bool native tensor")?
1036                .iter()
1037                .map(|&value| if value { 1.0 } else { 0.0 })
1038                .collect::<Vec<_>>(),
1039            tensor.shape(),
1040        )
1041        .map_err(|e| {
1042            BridgeError::from(anyhow!(
1043                "native tensor snapshot materialization failed: {e}"
1044            ))
1045        }),
1046        DType::C32 => Storage::from_dense_col_major(
1047            native_slice::<Complex32>(tensor, "failed to read c32 native tensor")?
1048                .iter()
1049                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
1050                .collect::<Vec<_>>(),
1051            tensor.shape(),
1052        )
1053        .map_err(|e| {
1054            BridgeError::from(anyhow!(
1055                "native tensor snapshot materialization failed: {e}"
1056            ))
1057        }),
1058        DType::C64 => Storage::from_dense_col_major(
1059            native_slice::<Complex64>(tensor, "failed to read c64 native tensor")?.to_vec(),
1060            tensor.shape(),
1061        )
1062        .map_err(|e| {
1063            BridgeError::from(anyhow!(
1064                "native tensor snapshot materialization failed: {e}"
1065            ))
1066        }),
1067    }
1068}
1069
1070/// Materialize dense column-major values from a native tensor.
1071/// # Errors
1072///
1073/// Returns an error when the native tensor cannot be materialized as a dense
1074/// column-major buffer (a dtype mismatch or backend failure).
1075pub fn native_tensor_primal_to_dense_col_major<T: TensorElement>(
1076    tensor: &NativeTensor,
1077) -> std::result::Result<Vec<T>, BridgeError> {
1078    let target = <T as TensorScalar>::dtype();
1079    let tensor_is_real = matches!(
1080        tensor.dtype(),
1081        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1082    );
1083    let target_is_real = matches!(
1084        target,
1085        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1086    );
1087    if tensor_is_real != target_is_real {
1088        return Err(anyhow!(
1089            "expected {} native tensor, got dtype {:?}",
1090            if target_is_real { "real" } else { "complex" },
1091            tensor.dtype()
1092        )
1093        .into());
1094    }
1095    <T as TensorElement>::dense_values_from_native_col_major(tensor).map_err(BridgeError::from)
1096}
1097
1098/// Materialize diagonal values from a native tensor, promoting to the
1099/// matching real (`f64`) or complex (`Complex64`) dtype.
1100///
1101/// Real native tensors (`f32`, `f64`, `i32`, `i64`, `bool`) are promoted to
1102/// `f64`; complex tensors (`c32`, `c64`) are promoted to `Complex64`. The
1103/// scalar type `T` selects the promoted target.
1104/// # Errors
1105///
1106/// Returns an error when the native tensor is not compatible with the scalar
1107/// target (a dtype mismatch) or the materialization fails.
1108///
1109/// # Examples
1110/// ```
1111/// use tenferro::Tensor as NativeTensor;
1112/// use tensor4all_tensorbackend::native_tensor_primal_to_diag;
1113/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1114/// let native = NativeTensor::from_vec_col_major(vec![3, 3], vec![1.0_f64, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0])?;
1115/// let diag = native_tensor_primal_to_diag::<f64>(&native)?;
1116/// assert_eq!(diag, vec![1.0, 2.0, 3.0]);
1117/// # Ok(())
1118/// # }
1119/// ```
1120pub fn native_tensor_primal_to_diag<T: TensorElement>(
1121    tensor: &NativeTensor,
1122) -> std::result::Result<Vec<T>, BridgeError> {
1123    let promote_to = <T as TensorScalar>::dtype();
1124    let tensor_is_real = matches!(
1125        tensor.dtype(),
1126        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1127    );
1128    let target_is_real = matches!(
1129        promote_to,
1130        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1131    );
1132    if tensor_is_real != target_is_real {
1133        return Err(anyhow!(
1134            "expected {} native tensor, got dtype {:?}",
1135            if target_is_real { "real" } else { "complex" },
1136            tensor.dtype()
1137        )
1138        .into());
1139    }
1140    let promoted = convert_tensor(tensor, promote_to)?;
1141    <T as TensorElement>::diag_values_from_native_temp(&promoted).map_err(BridgeError::from)
1142}
1143
1144/// Reshape a native tensor without changing its column-major linearization.
1145/// # Errors
1146///
1147/// Returns an error when the native tensor cannot be reshaped to the requested dimensions (a shape mismatch) or the backend fails.
1148pub fn reshape_col_major_native_tensor(
1149    tensor: &NativeTensor,
1150    logical_dims: &[usize],
1151) -> Result<NativeTensor> {
1152    with_default_session(|session| tensor.reshape(logical_dims, session))
1153        .map_err(|e| anyhow!("native reshape failed: {e}"))
1154}
1155
1156/// Compute a QR decomposition on a native tensor.
1157/// # Errors
1158///
1159/// Returns an error when the QR factorization fails (a backend or non-convergence failure).
1160pub fn qr_native_tensor(
1161    tensor: &NativeTensor,
1162) -> std::result::Result<(NativeTensor, NativeTensor), BridgeError> {
1163    let (q, r) = with_default_session(|session| tensor.qr(session))
1164        .map_err(|e| anyhow!("native QR failed: {e}"))?;
1165    Ok((q, r))
1166}
1167
1168/// Compute an SVD on a native tensor.
1169/// # Errors
1170///
1171/// Returns an error when the SVD factorization fails (a backend or non-convergence failure).
1172pub fn svd_native_tensor(
1173    tensor: &NativeTensor,
1174) -> Result<(NativeTensor, NativeTensor, NativeTensor)> {
1175    let (u, s, vt) = with_default_session(|session| tensor.svd(session))
1176        .map_err(|e| anyhow!("native SVD failed: {e}"))?;
1177    Ok((u, s, vt))
1178}
1179
1180/// Sum all elements of a native tensor, returning a dynamic scalar.
1181/// # Errors
1182///
1183/// Returns an error when the native reduction fails (a backend or dtype mismatch failure).
1184pub fn sum_native_tensor(tensor: &NativeTensor) -> std::result::Result<BackendScalar, BridgeError> {
1185    let reduced = if tensor.shape().is_empty() {
1186        tensor
1187            .duplicate()
1188            .map_err(|e| anyhow!("native scalar duplication failed: {e}"))?
1189    } else {
1190        let axes: Vec<usize> = (0..tensor.shape().len()).collect();
1191        with_default_session(|session| tensor.reduce_sum(&axes, session))
1192            .map_err(|e| anyhow!("native sum failed: {e}"))?
1193    };
1194    Ok(BackendScalar::from_native(reduced)?)
1195}
1196
1197/// Return the tangent tensor when present.
1198///
1199/// Plain `Tensor` values do not carry tangent storage, so this bridge returns
1200/// `None`.
1201pub fn tangent_native_tensor(_tensor: &NativeTensor) -> Option<NativeTensor> {
1202    None
1203}
1204
1205/// Multiply a native tensor by a dynamic scalar.
1206/// # Errors
1207///
1208/// Returns an error when the native scaling fails (a backend or dtype mismatch failure).
1209pub fn scale_native_tensor(
1210    tensor: &NativeTensor,
1211    scalar: &BackendScalar,
1212) -> std::result::Result<NativeTensor, BridgeError> {
1213    let target = common_dtype(&[tensor.dtype(), scalar.as_native().dtype()]);
1214    let tensor = convert_tensor(tensor, target)?;
1215    let scalar = promote_scalar_native(scalar.as_native(), target)?;
1216
1217    match target {
1218        DType::F32 => {
1219            let factor = native_slice::<f32>(&scalar, "failed to read promoted f32 scalar")?
1220                .first()
1221                .copied()
1222                .ok_or_else(|| anyhow!("failed to read promoted f32 scalar"))?;
1223            let values = native_slice::<f32>(&tensor, "failed to read promoted f32 tensor")?
1224                .iter()
1225                .map(|&value| value * factor)
1226                .collect::<Vec<_>>();
1227            native_tensor_from_vec(tensor.shape().to_vec(), values)
1228        }
1229        DType::F64 => {
1230            let factor = native_slice::<f64>(&scalar, "failed to read promoted f64 scalar")?
1231                .first()
1232                .copied()
1233                .ok_or_else(|| anyhow!("failed to read promoted f64 scalar"))?;
1234            let values = native_slice::<f64>(&tensor, "failed to read promoted f64 tensor")?
1235                .iter()
1236                .map(|&value| value * factor)
1237                .collect::<Vec<_>>();
1238            native_tensor_from_vec(tensor.shape().to_vec(), values)
1239        }
1240        DType::C32 => {
1241            let factor = native_slice::<Complex32>(&scalar, "failed to read promoted c32 scalar")?
1242                .first()
1243                .copied()
1244                .ok_or_else(|| anyhow!("failed to read promoted c32 scalar"))?;
1245            let values = native_slice::<Complex32>(&tensor, "failed to read promoted c32 tensor")?
1246                .iter()
1247                .map(|&value| value * factor)
1248                .collect::<Vec<_>>();
1249            native_tensor_from_vec(tensor.shape().to_vec(), values)
1250        }
1251        DType::C64 => {
1252            let factor = native_slice::<Complex64>(&scalar, "failed to read promoted c64 scalar")?
1253                .first()
1254                .copied()
1255                .ok_or_else(|| anyhow!("failed to read promoted c64 scalar"))?;
1256            let values = native_slice::<Complex64>(&tensor, "failed to read promoted c64 tensor")?
1257                .iter()
1258                .map(|&value| value * factor)
1259                .collect::<Vec<_>>();
1260            native_tensor_from_vec(tensor.shape().to_vec(), values)
1261        }
1262        DType::I32 | DType::I64 | DType::Bool => {
1263            Err(anyhow!("scale_native_tensor does not support integer/bool tensors").into())
1264        }
1265    }
1266}
1267
1268/// Compute `a * lhs + b * rhs`.
1269/// # Errors
1270///
1271/// Returns an error when the native axpby fails (a shape or dtype mismatch, or a backend failure).
1272pub fn axpby_native_tensor(
1273    lhs: &NativeTensor,
1274    a: &BackendScalar,
1275    rhs: &NativeTensor,
1276    b: &BackendScalar,
1277) -> std::result::Result<NativeTensor, BridgeError> {
1278    if lhs.shape() != rhs.shape() {
1279        return Err(BridgeError::from(anyhow!(
1280            "axpby requires matching tensor shapes, got lhs {:?} and rhs {:?}",
1281            lhs.shape(),
1282            rhs.shape()
1283        )));
1284    }
1285
1286    let target = common_dtype(&[
1287        lhs.dtype(),
1288        rhs.dtype(),
1289        a.as_native().dtype(),
1290        b.as_native().dtype(),
1291    ]);
1292    let lhs = convert_tensor(lhs, target)?;
1293    let rhs = convert_tensor(rhs, target)?;
1294    let a = promote_scalar_native(a.as_native(), target)?;
1295    let b = promote_scalar_native(b.as_native(), target)?;
1296
1297    match target {
1298        DType::F32 => {
1299            let a = native_slice::<f32>(&a, "failed to read promoted f32 scalar a")?
1300                .first()
1301                .copied()
1302                .ok_or_else(|| anyhow!("failed to read promoted f32 scalar a"))?;
1303            let b = native_slice::<f32>(&b, "failed to read promoted f32 scalar b")?
1304                .first()
1305                .copied()
1306                .ok_or_else(|| anyhow!("failed to read promoted f32 scalar b"))?;
1307            let lhs_values = native_slice::<f32>(&lhs, "failed to read promoted f32 lhs")?;
1308            let rhs_values = native_slice::<f32>(&rhs, "failed to read promoted f32 rhs")?;
1309            let values = lhs_values
1310                .iter()
1311                .zip(rhs_values.iter())
1312                .map(|(&x, &y)| a * x + b * y)
1313                .collect::<Vec<_>>();
1314            native_tensor_from_vec(lhs.shape().to_vec(), values)
1315        }
1316        DType::F64 => {
1317            let a = native_slice::<f64>(&a, "failed to read promoted f64 scalar a")?
1318                .first()
1319                .copied()
1320                .ok_or_else(|| anyhow!("failed to read promoted f64 scalar a"))?;
1321            let b = native_slice::<f64>(&b, "failed to read promoted f64 scalar b")?
1322                .first()
1323                .copied()
1324                .ok_or_else(|| anyhow!("failed to read promoted f64 scalar b"))?;
1325            let lhs_values = native_slice::<f64>(&lhs, "failed to read promoted f64 lhs")?;
1326            let rhs_values = native_slice::<f64>(&rhs, "failed to read promoted f64 rhs")?;
1327            let values = lhs_values
1328                .iter()
1329                .zip(rhs_values.iter())
1330                .map(|(&x, &y)| a * x + b * y)
1331                .collect::<Vec<_>>();
1332            native_tensor_from_vec(lhs.shape().to_vec(), values)
1333        }
1334        DType::C32 => {
1335            let a = native_slice::<Complex32>(&a, "failed to read promoted c32 scalar a")?
1336                .first()
1337                .copied()
1338                .ok_or_else(|| anyhow!("failed to read promoted c32 scalar a"))?;
1339            let b = native_slice::<Complex32>(&b, "failed to read promoted c32 scalar b")?
1340                .first()
1341                .copied()
1342                .ok_or_else(|| anyhow!("failed to read promoted c32 scalar b"))?;
1343            let lhs_values = native_slice::<Complex32>(&lhs, "failed to read promoted c32 lhs")?;
1344            let rhs_values = native_slice::<Complex32>(&rhs, "failed to read promoted c32 rhs")?;
1345            let values = lhs_values
1346                .iter()
1347                .zip(rhs_values.iter())
1348                .map(|(&x, &y)| a * x + b * y)
1349                .collect::<Vec<_>>();
1350            native_tensor_from_vec(lhs.shape().to_vec(), values)
1351        }
1352        DType::C64 => {
1353            let a = native_slice::<Complex64>(&a, "failed to read promoted c64 scalar a")?
1354                .first()
1355                .copied()
1356                .ok_or_else(|| anyhow!("failed to read promoted c64 scalar a"))?;
1357            let b = native_slice::<Complex64>(&b, "failed to read promoted c64 scalar b")?
1358                .first()
1359                .copied()
1360                .ok_or_else(|| anyhow!("failed to read promoted c64 scalar b"))?;
1361            let lhs_values = native_slice::<Complex64>(&lhs, "failed to read promoted c64 lhs")?;
1362            let rhs_values = native_slice::<Complex64>(&rhs, "failed to read promoted c64 rhs")?;
1363            let values = lhs_values
1364                .iter()
1365                .zip(rhs_values.iter())
1366                .map(|(&x, &y)| a * x + b * y)
1367                .collect::<Vec<_>>();
1368            native_tensor_from_vec(lhs.shape().to_vec(), values)
1369        }
1370        DType::I32 | DType::I64 | DType::Bool => {
1371            Err(anyhow!("axpby_native_tensor does not support integer/bool tensors").into())
1372        }
1373    }
1374}
1375
1376/// Execute a cached einsum over owned native tensors.
1377///
1378/// This is the consuming bridge used by higher-level owned contraction APIs.
1379/// Inputs are promoted to a common dtype before tenferro evaluates the
1380/// contraction. Repeated calls with the same equation and shapes reuse
1381/// tenferro's process-global contraction path cache.
1382///
1383/// # Arguments
1384/// * `operands` - Native tensors paired with numeric einsum labels for each axis.
1385/// * `output_ids` - Numeric labels to keep in the result, in output axis order.
1386///
1387/// # Returns
1388/// The contracted native tensor in the promoted common dtype.
1389///
1390/// # Errors
1391/// Returns an error if the operand list is empty, any label list length does
1392/// not match its tensor rank, label generation exceeds the supported range, or
1393/// the backend contraction fails.
1394///
1395/// # Examples
1396/// ```
1397/// use tensor4all_tensorbackend::einsum_native_tensors_owned;
1398/// use tenferro::Tensor as NativeTensor;
1399/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1400///
1401/// let lhs = NativeTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6])?;
1402/// let rhs = NativeTensor::from_vec_col_major(vec![3, 2], vec![1.0_f64; 6])?;
1403/// let result = einsum_native_tensors_owned(vec![(lhs, vec![0, 1]), (rhs, vec![1, 2])], &[0, 2])?;
1404///
1405/// assert_eq!(result.shape(), &[2, 2]);
1406/// assert_eq!(result.as_slice::<f64>()?, &[3.0, 3.0, 3.0, 3.0]);
1407/// # Ok(())
1408/// # }
1409/// ```
1410pub fn einsum_native_tensors_owned(
1411    operands: Vec<(NativeTensor, Vec<usize>)>,
1412    output_ids: &[usize],
1413) -> Result<NativeTensor> {
1414    ensure!(
1415        !operands.is_empty(),
1416        "native einsum requires at least one operand"
1417    );
1418
1419    let target = common_dtype(
1420        &operands
1421            .iter()
1422            .map(|(tensor, _)| tensor.dtype())
1423            .collect::<Vec<_>>(),
1424    );
1425
1426    let output_ids_u32 = checked_native_einsum_labels(output_ids)?;
1427    let mut converted = Vec::with_capacity(operands.len());
1428    let mut input_ids = Vec::with_capacity(operands.len());
1429    for (tensor, ids) in operands {
1430        ensure!(
1431            tensor.shape().len() == ids.len(),
1432            "einsum id list {:?} does not match tensor shape {:?}",
1433            ids,
1434            tensor.shape()
1435        );
1436        let checked_ids = checked_native_einsum_labels(&ids)?;
1437        let tensor = if tensor.dtype() == target {
1438            tensor
1439        } else {
1440            convert_tensor(&tensor, target)?
1441        };
1442        input_ids.push(checked_ids);
1443        converted.push(tensor);
1444    }
1445
1446    let input_slices = input_ids.iter().map(Vec::as_slice).collect::<Vec<_>>();
1447    let subscripts = EinsumSubscripts::new(&input_slices, &output_ids_u32);
1448
1449    let input_refs = converted.iter().collect::<Vec<_>>();
1450    let trace_operands = input_refs
1451        .iter()
1452        .zip(input_ids.iter())
1453        .map(|(tensor, ids)| (*tensor, ids.as_slice()))
1454        .collect::<Vec<_>>();
1455    maybe_trace_native_einsum_path(NativeEinsumPath::Owned, &trace_operands, &output_ids_u32);
1456    let started = Instant::now();
1457    let result = cached_einsum_native_tensors(&input_refs, &subscripts)?;
1458    record_native_einsum_profile(
1459        NativeEinsumPath::Owned,
1460        &trace_operands,
1461        &output_ids_u32,
1462        started.elapsed(),
1463    );
1464    Ok(result)
1465}
1466
1467/// Execute a cached einsum over borrowed native tensors.
1468///
1469/// Inputs are promoted to a common dtype before contraction. Operands that
1470/// already have the target dtype are passed to the backend by reference;
1471/// operands with another dtype are converted into temporary native tensors and
1472/// then borrowed for the contraction. Repeated calls with the same equation
1473/// and shapes reuse tenferro's process-global contraction path cache.
1474///
1475/// # Arguments
1476/// * `operands` - Native tensors paired with numeric einsum labels for each axis.
1477///
1478///   Each label slice must have the same length as the corresponding tensor rank.
1479/// * `output_ids` - Numeric labels to keep in the result, in output axis order.
1480///
1481/// # Returns
1482/// The contracted native tensor in the promoted common dtype.
1483///
1484/// # Errors
1485/// Returns an error if the operand list is empty, any label list length does
1486/// not match its tensor rank, label generation exceeds the supported range,
1487/// dtype conversion fails, or the backend contraction fails.
1488///
1489/// # Examples
1490/// ```
1491/// use tensor4all_tensorbackend::einsum_native_tensors;
1492/// use tenferro::Tensor as NativeTensor;
1493/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1494///
1495/// let lhs = NativeTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6])?;
1496/// let rhs = NativeTensor::from_vec_col_major(vec![3, 2], vec![1.0_f64; 6])?;
1497/// let result = einsum_native_tensors(&[(&lhs, &[0, 1]), (&rhs, &[1, 2])], &[0, 2])?;
1498///
1499/// assert_eq!(result.shape(), &[2, 2]);
1500/// assert_eq!(result.as_slice::<f64>()?, &[3.0, 3.0, 3.0, 3.0]);
1501/// # Ok(())
1502/// # }
1503/// ```
1504pub fn einsum_native_tensors(
1505    operands: &[(&NativeTensor, &[usize])],
1506    output_ids: &[usize],
1507) -> Result<NativeTensor> {
1508    ensure!(
1509        !operands.is_empty(),
1510        "native einsum requires at least one operand"
1511    );
1512
1513    let target = common_dtype(
1514        &operands
1515            .iter()
1516            .map(|(tensor, _)| tensor.dtype())
1517            .collect::<Vec<_>>(),
1518    );
1519    let output_ids_u32 = checked_native_einsum_labels(output_ids)?;
1520    let mut converted = Vec::with_capacity(operands.len());
1521    let mut input_ids = Vec::with_capacity(operands.len());
1522    let mut has_conversions = false;
1523    let started = Instant::now();
1524
1525    for (tensor, ids) in operands {
1526        ensure!(
1527            tensor.shape().len() == ids.len(),
1528            "einsum id list {:?} does not match tensor shape {:?}",
1529            ids,
1530            tensor.shape()
1531        );
1532        input_ids.push(checked_native_einsum_labels(ids)?);
1533        if tensor.dtype() == target {
1534            converted.push(None);
1535        } else {
1536            converted.push(Some(convert_tensor(tensor, target)?));
1537            has_conversions = true;
1538        }
1539    }
1540
1541    let input_slices = input_ids.iter().map(Vec::as_slice).collect::<Vec<_>>();
1542    let subscripts = EinsumSubscripts::new(&input_slices, &output_ids_u32);
1543    let input_refs = operands
1544        .iter()
1545        .zip(converted.iter())
1546        .map(|((tensor, _), converted)| converted.as_ref().unwrap_or(*tensor))
1547        .collect::<Vec<_>>();
1548    let trace_path = if has_conversions {
1549        NativeEinsumPath::BorrowedWithConversions
1550    } else {
1551        NativeEinsumPath::Borrowed
1552    };
1553    let trace_operands = input_refs
1554        .iter()
1555        .zip(input_ids.iter())
1556        .map(|(tensor, ids)| (*tensor, ids.as_slice()))
1557        .collect::<Vec<_>>();
1558    maybe_trace_native_einsum_path(trace_path, &trace_operands, &output_ids_u32);
1559    let result = cached_einsum_native_tensors(&input_refs, &subscripts)?;
1560    record_native_einsum_profile(
1561        trace_path,
1562        &trace_operands,
1563        &output_ids_u32,
1564        started.elapsed(),
1565    );
1566    Ok(result)
1567}
1568
1569/// Execute a cached einsum over read-only native tensor inputs.
1570///
1571/// Backends consume borrowed host views inside their execution session. Mixed
1572/// dtypes are promoted by `Convert` nodes in the compiled einsum graph, so
1573/// non-contiguous operands remain borrowed until runtime execution.
1574/// # Errors
1575///
1576/// Returns an error when the native einsum fails (a shape or dtype mismatch, or a backend failure).
1577pub fn einsum_native_tensor_reads(
1578    operands: &[(&NativeTensorReadInput<'_>, &[usize])],
1579    output_ids: &[usize],
1580) -> Result<NativeTensor> {
1581    ensure!(
1582        !operands.is_empty(),
1583        "native einsum requires at least one operand"
1584    );
1585
1586    let output_ids_u32 = checked_native_einsum_labels(output_ids)?;
1587    let mut input_ids = Vec::with_capacity(operands.len());
1588    let mut read_inputs = Vec::with_capacity(operands.len());
1589
1590    for (tensor, ids) in operands {
1591        ensure!(
1592            tensor.shape().len() == ids.len(),
1593            "einsum id list {:?} does not match tensor shape {:?}",
1594            ids,
1595            tensor.shape()
1596        );
1597        input_ids.push(checked_native_einsum_labels(ids)?);
1598        read_inputs.push(tensor.as_read());
1599    }
1600
1601    let subscripts = Subscripts {
1602        inputs: input_ids,
1603        output: output_ids_u32,
1604    };
1605    cached_einsum_native_reads(&read_inputs, &subscripts)
1606}
1607
1608/// Permute axes of a native tensor.
1609/// # Errors
1610///
1611/// Returns an error when the native permutation fails (a shape or index mismatch, or a backend failure).
1612pub fn permute_native_tensor(
1613    tensor: &NativeTensor,
1614    perm: &[usize],
1615) -> std::result::Result<NativeTensor, BridgeError> {
1616    with_default_session(|session| tensor.transpose(perm, session))
1617        .map_err(|e| anyhow!("native permute failed: {e}"))
1618        .map_err(BridgeError::from)
1619}
1620
1621/// Contract two native tensors along matching axes.
1622/// # Errors
1623///
1624/// Returns an error when the native contraction fails (a shape or index mismatch, or a backend failure).
1625pub fn contract_native_tensor(
1626    lhs: &NativeTensor,
1627    axes_a: &[usize],
1628    rhs: &NativeTensor,
1629    axes_b: &[usize],
1630) -> Result<NativeTensor> {
1631    let (lhs_ids, rhs_ids, output_ids) =
1632        build_binary_einsum_ids(lhs.shape().len(), axes_a, rhs.shape().len(), axes_b)?;
1633    let lhs_ids_usize = lhs_ids.iter().map(|&id| id as usize).collect::<Vec<_>>();
1634    let rhs_ids_usize = rhs_ids.iter().map(|&id| id as usize).collect::<Vec<_>>();
1635    let output_ids_usize = output_ids.iter().map(|&id| id as usize).collect::<Vec<_>>();
1636    let operands = [
1637        (lhs, lhs_ids_usize.as_slice()),
1638        (rhs, rhs_ids_usize.as_slice()),
1639    ];
1640    einsum_native_tensors(&operands, &output_ids_usize)
1641}
1642
1643/// Compute the outer product of two native tensors.
1644/// # Errors
1645///
1646/// Returns an error when the native outer product fails (a shared-index or shape mismatch, or a backend failure).
1647pub fn outer_product_native_tensor(
1648    lhs: &NativeTensor,
1649    rhs: &NativeTensor,
1650) -> std::result::Result<NativeTensor, BridgeError> {
1651    contract_native_tensor(lhs, &[], rhs, &[]).map_err(BridgeError::from)
1652}
1653
1654/// Conjugate a native tensor.
1655/// # Errors
1656///
1657/// Returns an error when the native conjugation fails (a dtype mismatch or backend failure).
1658pub fn conj_native_tensor(tensor: &NativeTensor) -> std::result::Result<NativeTensor, BridgeError> {
1659    match tensor.dtype() {
1660        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => tensor
1661            .duplicate()
1662            .map_err(|e| anyhow!("native tensor duplication failed: {e}"))
1663            .map_err(BridgeError::from),
1664        DType::C32 => native_tensor_from_vec(
1665            tensor.shape().to_vec(),
1666            native_slice::<Complex32>(tensor, "failed to read c32 native tensor")?
1667                .iter()
1668                .map(|&value| value.conj())
1669                .collect::<Vec<_>>(),
1670        ),
1671        DType::C64 => native_tensor_from_vec(
1672            tensor.shape().to_vec(),
1673            native_slice::<Complex64>(tensor, "failed to read c64 native tensor")?
1674                .iter()
1675                .map(|&value| value.conj())
1676                .collect::<Vec<_>>(),
1677        ),
1678    }
1679}
1680
1681/// Permute storage by round-tripping through native tensors.
1682/// # Errors
1683///
1684/// Returns an error when the native storage permutation fails (a shape or index mismatch, or a backend failure).
1685pub fn permute_storage_native(
1686    storage: &Storage,
1687    logical_dims: &[usize],
1688    perm: &[usize],
1689) -> std::result::Result<Storage, BridgeError> {
1690    let native = storage_to_native_tensor(storage, logical_dims)?;
1691    let permuted = permute_native_tensor(&native, perm)?;
1692    native_tensor_primal_to_storage(&permuted)
1693}
1694
1695/// Contract storages via native tensors.
1696/// # Errors
1697///
1698/// Returns an error when the native storage contraction fails (a shape or index mismatch, or a backend failure).
1699pub fn contract_storage_native(
1700    storage_a: &Storage,
1701    dims_a: &[usize],
1702    axes_a: &[usize],
1703    storage_b: &Storage,
1704    dims_b: &[usize],
1705    axes_b: &[usize],
1706    _result_dims: &[usize],
1707) -> std::result::Result<Storage, BridgeError> {
1708    let lhs = storage_to_native_tensor(storage_a, dims_a)?;
1709    let rhs = storage_to_native_tensor(storage_b, dims_b)?;
1710    let result = contract_native_tensor(&lhs, axes_a, &rhs, axes_b)?;
1711    native_tensor_primal_to_storage(&result)
1712}
1713
1714/// Outer-product storages via native tensors.
1715/// # Errors
1716///
1717/// Returns an error when the native storage outer product fails (a shared-index or shape mismatch, or a backend failure).
1718pub fn outer_product_storage_native(
1719    lhs: &Storage,
1720    lhs_dims: &[usize],
1721    rhs: &Storage,
1722    rhs_dims: &[usize],
1723    _result_dims: &[usize],
1724) -> std::result::Result<Storage, BridgeError> {
1725    let lhs = storage_to_native_tensor(lhs, lhs_dims)?;
1726    let rhs = storage_to_native_tensor(rhs, rhs_dims)?;
1727    let result = outer_product_native_tensor(&lhs, &rhs)?;
1728    native_tensor_primal_to_storage(&result)
1729}
1730
1731/// Scale storage by a scalar via native tensors.
1732/// # Errors
1733///
1734/// Returns an error when the native storage scaling fails (a dtype mismatch or backend failure).
1735pub fn scale_storage_native(
1736    storage: &Storage,
1737    logical_dims: &[usize],
1738    scalar: &BackendScalar,
1739) -> std::result::Result<Storage, BridgeError> {
1740    let native = storage_to_native_tensor(storage, logical_dims)?;
1741    let scaled = scale_native_tensor(&native, scalar)?;
1742    native_tensor_primal_to_storage(&scaled)
1743}
1744
1745/// Compute `a * lhs + b * rhs` over storages via native tensors.
1746/// # Errors
1747///
1748/// Returns an error when the native storage axpby fails (a shape or dtype mismatch, or a backend failure).
1749pub fn axpby_storage_native(
1750    lhs: &Storage,
1751    lhs_dims: &[usize],
1752    a: &BackendScalar,
1753    rhs: &Storage,
1754    rhs_dims: &[usize],
1755    b: &BackendScalar,
1756) -> std::result::Result<Storage, BridgeError> {
1757    let lhs = storage_to_native_tensor(lhs, lhs_dims)?;
1758    let rhs = storage_to_native_tensor(rhs, rhs_dims)?;
1759    let combined = axpby_native_tensor(&lhs, a, &rhs, b)?;
1760    native_tensor_primal_to_storage(&combined)
1761}
1762
1763#[cfg(test)]
1764mod tests;