1use 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#[derive(Debug, thiserror::Error)]
36#[error("native tensor bridge operation failed: {source}")]
37pub struct BridgeError {
38 #[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#[allow(clippy::large_enum_variant)]
73pub enum NativeTensorReadInput<'a> {
74 Borrowed(TensorRead<'a>),
76 Owned(NativeTensor),
78}
79
80impl<'a> NativeTensorReadInput<'a> {
81 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 pub fn dtype(&self) -> DType {
91 match self {
92 Self::Borrowed(read) => read.dtype(),
93 Self::Owned(tensor) => tensor.dtype(),
94 }
95 }
96
97 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
512pub 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
518pub 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
817pub(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
883pub 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
894pub 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
905pub 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
932pub 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
981pub 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
1070pub 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
1098pub 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
1144pub 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
1156pub 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
1168pub 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
1180pub 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
1197pub fn tangent_native_tensor(_tensor: &NativeTensor) -> Option<NativeTensor> {
1202 None
1203}
1204
1205pub 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
1268pub 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
1376pub 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
1467pub 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
1569pub 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
1608pub 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
1621pub 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
1643pub 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
1654pub 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
1681pub 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
1695pub 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
1714pub 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
1731pub 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
1745pub 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;