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