tenferro_runtime/traced.rs
1use std::collections::HashMap;
2use std::error::Error as StdError;
3use std::fmt;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Arc;
6
7use computegraph::graph::{Graph, GraphBuilder};
8use computegraph::types::{OperationRole, ValueKey, ValueRef};
9use computegraph::LocalValueId;
10use num_complex::{Complex32, Complex64};
11use tenferro_ops::ad::context::GlobalMetadataScope;
12use tenferro_ops::broadcast::{
13 broadcast_error_to_validation, broadcast_in_dim_extent_error, broadcast_input_plan,
14 broadcast_shape, broadcast_shapes, BroadcastError,
15};
16use tenferro_ops::dim_expr::DimExpr;
17use tenferro_ops::input_key::TensorInputKey;
18use tenferro_ops::std_tensor_op::StdTensorOp;
19use tenferro_tensor::{
20 CompareDir, DType, DotGeneralConfig, Error as TensorError, GatherConfig, IntoShapeVec,
21 PadConfig, ScatterConfig, ShapeMismatch, SliceConfig, Tensor, TensorScalar, TensorValue,
22 ValidationError,
23};
24
25use super::error::{Error, ErrorPhase, Result};
26use super::sym_dim::SymDim;
27use crate::checkpoint::{CheckpointNode, RetainedInputMap, RetainedValue};
28use crate::metadata::{
29 concrete_tensor_meta, register_scoped_graph_metadata, register_scoped_value_metadata,
30 symbolic_input_meta, tensor_meta, MetadataScopeChain,
31};
32use crate::scalar_semantics::{bool_from_real_for_op, round_real_to_i32_for_op, round_real_to_i64};
33use crate::shape_constraint::ConstraintScopeChain;
34
35static NEXT_INPUT_ID: AtomicU64 = AtomicU64::new(0);
36static NEXT_TRACED_ID: AtomicU64 = AtomicU64::new(0);
37
38pub type TracedTensorId = u64;
39
40pub(crate) fn next_input_key() -> TensorInputKey {
41 TensorInputKey::User {
42 id: NEXT_INPUT_ID.fetch_add(1, Ordering::Relaxed),
43 }
44}
45
46pub(crate) fn next_traced_id() -> TracedTensorId {
47 NEXT_TRACED_ID.fetch_add(1, Ordering::Relaxed)
48}
49
50type TracedInputMap = RetainedInputMap;
51
52#[derive(Clone)]
53pub struct TracedTensor {
54 pub id: TracedTensorId,
55 pub rank: usize,
56 pub dtype: DType,
57 pub(crate) graph: Arc<Graph<StdTensorOp>>,
58 pub val: LocalValueId,
59 pub(crate) data: Option<Arc<RetainedValue>>,
60 pub(crate) shape_hint: Option<Vec<SymDim>>,
61 pub(crate) inputs_map: Arc<TracedInputMap>,
62 pub(crate) extra_roots: Vec<Arc<Graph<StdTensorOp>>>,
63 pub(crate) checkpoint_chain: Option<Arc<CheckpointNode>>,
64 pub(crate) metadata_scopes: MetadataScopeChain,
65 pub(crate) constraint_scopes: ConstraintScopeChain,
66}
67
68impl fmt::Debug for TracedTensor {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.debug_struct("TracedTensor")
71 .field("id", &self.id)
72 .field("rank", &self.rank)
73 .field("dtype", &self.dtype)
74 .field("val", &self.val)
75 .field("shape_hint", &self.shape_hint)
76 .field("has_data", &self.data.is_some())
77 .finish_non_exhaustive()
78 }
79}
80
81pub(crate) fn merge_traced_inputs_map<'a>(
82 inputs: impl IntoIterator<Item = &'a TracedTensor>,
83) -> Arc<TracedInputMap> {
84 let maps: Vec<_> = inputs
85 .into_iter()
86 .map(|input| &input.inputs_map)
87 .filter(|map| !map.is_empty())
88 .collect();
89 match maps.as_slice() {
90 [] => return Arc::new(HashMap::new()),
91 [single] => return Arc::clone(*single),
92 _ => {}
93 }
94
95 for &candidate in &maps {
96 if input_map_matches_ordered_merge(candidate.as_ref(), &maps) {
97 return Arc::clone(candidate);
98 }
99 }
100
101 let mut merged = (**maps[0]).clone();
102 for map in maps.iter().skip(1) {
103 merged.extend(
104 map.iter()
105 .map(|(key, tensor)| (key.clone(), tensor.clone())),
106 );
107 }
108 Arc::new(merged)
109}
110
111fn input_map_matches_ordered_merge(
112 candidate: &TracedInputMap,
113 maps: &[&Arc<TracedInputMap>],
114) -> bool {
115 for map in maps {
116 for key in map.keys() {
117 let Some(final_tensor) = maps.iter().rev().find_map(|source| source.get(key)) else {
118 return false;
119 };
120 let Some(candidate_tensor) = candidate.get(key) else {
121 return false;
122 };
123 if !Arc::ptr_eq(candidate_tensor, final_tensor) {
124 return false;
125 }
126 }
127 }
128 true
129}
130
131pub(crate) fn try_concrete_shape(tensor: &TracedTensor) -> Option<Vec<usize>> {
132 tensor
133 .shape_hint
134 .as_ref()?
135 .iter()
136 .map(SymDim::constant_value)
137 .collect()
138}
139
140fn graph_validation(op: &'static str, source: impl Into<ValidationError>) -> Error {
141 Error::validation(op, ErrorPhase::GraphBuild, source.into())
142}
143
144fn graph_invalid_argument(
145 op: &'static str,
146 argument: &'static str,
147 message: impl Into<String>,
148) -> Error {
149 graph_validation(
150 op,
151 ValidationError::InvalidArgument {
152 argument,
153 message: message.into(),
154 },
155 )
156}
157
158fn graph_broadcast_error(op: &'static str, error: BroadcastError) -> Error {
159 graph_validation(op, broadcast_error_to_validation(error))
160}
161
162fn graph_tensor_error(op: &'static str, error: TensorError) -> Error {
163 match error {
164 TensorError::Validation { source, .. } => graph_validation(op, source),
165 other => Error::TensorRuntime(other),
166 }
167}
168
169fn graph_error_with_context(op: &'static str, error: Error) -> Error {
170 match error {
171 Error::Validation { source, .. } => graph_validation(op, source),
172 other => other,
173 }
174}
175
176pub(crate) fn concrete_shape(tensor: &TracedTensor) -> Result<Vec<usize>> {
177 tensor
178 .shape_hint
179 .as_ref()
180 .ok_or_else(|| {
181 graph_invalid_argument(
182 "TracedTensor::concrete_shape",
183 "shape",
184 format!("missing shape hint for traced tensor {}", tensor.id),
185 )
186 })?
187 .iter()
188 .map(|dim| {
189 dim.constant_value().ok_or_else(|| {
190 graph_invalid_argument(
191 "TracedTensor::concrete_shape",
192 "shape",
193 format!("symbolic dimension in shape hint for tensor {}", tensor.id),
194 )
195 })
196 })
197 .collect()
198}
199
200/// Broadcast a traced tensor to `target_shape`.
201///
202/// Expanding singleton axes are first reshaped away so the existing
203/// `BroadcastInDim` transpose rule reduces them correctly during VJP.
204pub(crate) fn broadcast_to(tensor: &TracedTensor, target_shape: &[usize]) -> Result<TracedTensor> {
205 let tensor_shape = concrete_shape(tensor)?;
206 if tensor_shape == target_shape {
207 return Ok(tensor.clone());
208 }
209
210 let plan = broadcast_input_plan(&tensor_shape, target_shape)
211 .map_err(|err| graph_broadcast_error("broadcast_to", err))?;
212
213 let source = if plan.source_shape == tensor_shape {
214 tensor.clone()
215 } else {
216 tensor.reshape(&plan.source_shape)?
217 };
218 source.broadcast_in_dim(target_shape, &plan.dims)
219}
220
221/// Broadcast two tensors to a common shape.
222pub(crate) fn broadcast_binary(
223 a: &TracedTensor,
224 b: &TracedTensor,
225) -> Result<(TracedTensor, TracedTensor)> {
226 if a.shape_hint == b.shape_hint && a.rank == b.rank {
227 return Ok((a.clone(), b.clone()));
228 }
229 if (try_concrete_shape(a).is_none() || try_concrete_shape(b).is_none()) && a.rank == b.rank {
230 return Ok((a.clone(), b.clone()));
231 }
232 let a_shape = concrete_shape(a)?;
233 let b_shape = concrete_shape(b)?;
234 let target = broadcast_shape(&a_shape, &b_shape)
235 .map_err(|err| graph_broadcast_error("broadcast_binary", err))?;
236 Ok((broadcast_to(a, &target)?, broadcast_to(b, &target)?))
237}
238
239pub(crate) fn broadcast_ternary(
240 a: &TracedTensor,
241 b: &TracedTensor,
242 c: &TracedTensor,
243) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
244 let a_shape = concrete_shape(a)?;
245 let b_shape = concrete_shape(b)?;
246 let c_shape = concrete_shape(c)?;
247 let target = broadcast_shapes([a_shape.as_slice(), b_shape.as_slice(), c_shape.as_slice()])
248 .map_err(|err| graph_broadcast_error("broadcast_ternary", err))?;
249 Ok((
250 broadcast_to(a, &target)?,
251 broadcast_to(b, &target)?,
252 broadcast_to(c, &target)?,
253 ))
254}
255
256fn scale_with_constant(input: &TracedTensor, op: StdTensorOp) -> Result<TracedTensor> {
257 let scalar = apply_nullary(op, 0, input.dtype, Some(vec![]))?;
258 apply_binary(
259 StdTensorOp::Mul,
260 input,
261 &scalar,
262 input.rank,
263 input.shape_hint.clone(),
264 )
265}
266
267fn try_inferred_output_dtype(
268 op: &StdTensorOp,
269 inputs: &[DType],
270 context: &'static str,
271) -> Result<DType> {
272 crate::shape_infer::infer_output_dtype_at(op, inputs, ErrorPhase::GraphBuild)
273 .map_err(|err| graph_error_with_context(context, err))
274}
275
276fn checked_shape_product_for_graph_build(
277 shape: &[usize],
278 context: &'static str,
279 _role: &'static str,
280) -> Result<usize> {
281 shape.iter().copied().try_fold(1usize, |acc, dim| {
282 acc.checked_mul(dim)
283 .ok_or_else(|| graph_validation(context, ValidationError::IntegerOverflow))
284 })
285}
286
287fn validate_concrete_reshape_shape(input: &TracedTensor, shape: &[usize]) -> Result<()> {
288 let to = checked_shape_product_for_graph_build(shape, "TracedTensor::reshape", "target")?;
289 let Some(input_shape) = try_concrete_shape(input) else {
290 return Ok(());
291 };
292 let from =
293 checked_shape_product_for_graph_build(&input_shape, "TracedTensor::reshape", "input")?;
294 if from != to {
295 return Err(graph_validation(
296 "TracedTensor::reshape",
297 ShapeMismatch::ReshapeElementCount { from, to },
298 ));
299 }
300 Ok(())
301}
302
303fn traced_input_shape_exprs(input_idx: usize, tensor: &TracedTensor) -> Vec<DimExpr> {
304 match tensor.shape_hint.as_ref() {
305 Some(shape) => shape
306 .iter()
307 .enumerate()
308 .map(|(axis, dim)| {
309 dim.constant_value()
310 .map_or(DimExpr::InputDim { input_idx, axis }, DimExpr::Const)
311 })
312 .collect(),
313 None => (0..tensor.rank)
314 .map(|axis| DimExpr::InputDim { input_idx, axis })
315 .collect(),
316 }
317}
318
319fn traced_input_sym_shape(tensor: &TracedTensor) -> Vec<SymDim> {
320 tensor.shape_hint.clone().unwrap_or_else(|| {
321 (0..tensor.rank)
322 .map(|axis| SymDim::tensor_axis(tensor.id, axis))
323 .collect()
324 })
325}
326
327pub(crate) fn infer_traced_single_output_shape(
328 op_name: &'static str,
329 op: &StdTensorOp,
330 inputs: &[&TracedTensor],
331) -> Result<(usize, Option<Vec<SymDim>>)> {
332 let input_shape_exprs: Vec<Vec<DimExpr>> = inputs
333 .iter()
334 .enumerate()
335 .map(|(input_idx, tensor)| traced_input_shape_exprs(input_idx, tensor))
336 .collect();
337 let input_shape_refs: Vec<&[DimExpr]> = input_shape_exprs.iter().map(Vec::as_slice).collect();
338 let output_shapes = crate::shape_infer::infer_output_shapes(op, &input_shape_refs)
339 .map_err(|err| graph_error_with_context(op_name, err))?;
340 let output_shape = output_shapes.first().ok_or_else(|| {
341 Error::Internal(format!("{op_name}: shape inference returned no outputs"))
342 })?;
343 if output_shapes.len() != 1 {
344 return Err(Error::Internal(format!(
345 "{op_name}: expected single-output shape inference, got {} outputs",
346 output_shapes.len()
347 )));
348 }
349
350 let input_sym_shapes: Vec<Vec<SymDim>> = inputs
351 .iter()
352 .map(|tensor| traced_input_sym_shape(tensor))
353 .collect();
354 let input_sym_refs: Vec<&[SymDim]> = input_sym_shapes.iter().map(Vec::as_slice).collect();
355 let out_shape_hint = output_shape
356 .iter()
357 .map(|dim| SymDim::from_dim_expr(dim, &input_sym_refs))
358 .collect();
359 Ok((output_shape.len(), Some(out_shape_hint)))
360}
361
362pub(crate) fn register_metadata_or_runtime_state<E>(
363 result: std::result::Result<GlobalMetadataScope, E>,
364) -> Result<GlobalMetadataScope>
365where
366 E: StdError + Send + Sync + 'static,
367{
368 result.map_err(|err| Error::runtime_state_source("metadata", ErrorPhase::Compile, err))
369}
370
371fn reduction_output_meta(
372 tensor: &TracedTensor,
373 axes: &[usize],
374 op: &'static str,
375) -> Result<(usize, Option<Vec<SymDim>>)> {
376 let mut seen = vec![false; tensor.rank];
377 for &axis in axes {
378 if axis >= tensor.rank {
379 return Err(graph_validation(
380 op,
381 ValidationError::AxisOutOfBounds {
382 axis,
383 rank: tensor.rank,
384 },
385 ));
386 }
387 if seen[axis] {
388 return Err(graph_validation(
389 op,
390 ValidationError::DuplicateAxis {
391 axis,
392 role: "reduction",
393 },
394 ));
395 }
396 seen[axis] = true;
397 }
398
399 let out_shape_hint = tensor.shape_hint.as_ref().map(|shape| {
400 (0..shape.len())
401 .filter(|d| !axes.contains(d))
402 .map(|d| shape[d].clone())
403 .collect()
404 });
405 Ok((tensor.rank - axes.len(), out_shape_hint))
406}
407
408fn validate_traced_axis(tensor: &TracedTensor, axis: usize, op: &'static str) -> Result<()> {
409 if axis >= tensor.rank {
410 return Err(graph_validation(
411 op,
412 ValidationError::AxisOutOfBounds {
413 axis,
414 rank: tensor.rank,
415 },
416 ));
417 }
418 Ok(())
419}
420
421fn validate_traced_axes(rank: usize, axes: &[usize], op: &'static str) -> Result<()> {
422 let mut seen = vec![false; rank];
423 for &axis in axes {
424 if axis >= rank {
425 return Err(graph_validation(
426 op,
427 ValidationError::AxisOutOfBounds { axis, rank },
428 ));
429 }
430 if seen[axis] {
431 return Err(graph_validation(
432 op,
433 ValidationError::DuplicateAxis { axis, role: "axis" },
434 ));
435 }
436 seen[axis] = true;
437 }
438 Ok(())
439}
440
441fn validate_traced_insert_axis(rank: usize, axis: usize, op: &'static str) -> Result<()> {
442 if axis > rank {
443 return Err(graph_invalid_argument(
444 op,
445 "axis",
446 format!("axis {axis} out of bounds for rank {rank} insertion"),
447 ));
448 }
449 Ok(())
450}
451
452fn validate_traced_perm(rank: usize, perm: &[usize], op: &'static str) -> Result<()> {
453 if perm.len() != rank {
454 return Err(graph_validation(
455 op,
456 ValidationError::InvalidPermutationLength {
457 expected: rank,
458 actual: perm.len(),
459 },
460 ));
461 }
462 let mut seen = vec![false; rank];
463 for &axis in perm {
464 if axis >= rank {
465 return Err(graph_validation(
466 op,
467 ValidationError::AxisOutOfBounds { axis, rank },
468 ));
469 }
470 if seen[axis] {
471 return Err(graph_validation(
472 op,
473 ValidationError::DuplicateAxis {
474 axis,
475 role: "permutation",
476 },
477 ));
478 }
479 seen[axis] = true;
480 }
481 Ok(())
482}
483
484fn validate_broadcast_in_dim_args(
485 input: &TracedTensor,
486 output_shape: &[SymDim],
487 dims: &[usize],
488 op: &'static str,
489) -> Result<()> {
490 if dims.len() != input.rank {
491 return Err(graph_validation(
492 op,
493 ValidationError::RankMismatch {
494 expected: input.rank,
495 actual: dims.len(),
496 },
497 ));
498 }
499
500 let concrete_input_shape: Option<Vec<usize>> = input
501 .shape_hint
502 .as_ref()
503 .and_then(|shape| shape.iter().map(SymDim::constant_value).collect());
504 let concrete_output_shape = output_shape
505 .iter()
506 .map(SymDim::constant_value)
507 .collect::<Option<Vec<_>>>();
508 if let (Some(input_shape), Some(output_shape)) = (
509 concrete_input_shape.as_deref(),
510 concrete_output_shape.as_deref(),
511 ) {
512 if let Some(error) = broadcast_in_dim_extent_error(input_shape, output_shape, dims) {
513 return Err(graph_broadcast_error(op, error));
514 }
515 }
516
517 let mut seen = vec![false; output_shape.len()];
518 for &dim in dims {
519 if dim >= output_shape.len() {
520 return Err(graph_validation(
521 op,
522 ValidationError::AxisOutOfBounds {
523 axis: dim,
524 rank: output_shape.len(),
525 },
526 ));
527 }
528 if seen[dim] {
529 return Err(graph_validation(
530 op,
531 ValidationError::DuplicateAxis {
532 axis: dim,
533 role: "broadcast",
534 },
535 ));
536 }
537 seen[dim] = true;
538 }
539
540 Ok(())
541}
542
543impl std::ops::Add for &TracedTensor {
544 type Output = Result<TracedTensor>;
545
546 fn add(self, rhs: &TracedTensor) -> Result<TracedTensor> {
547 TracedTensor::add(self, rhs)
548 }
549}
550
551impl std::ops::Sub for &TracedTensor {
552 type Output = Result<TracedTensor>;
553
554 fn sub(self, rhs: &TracedTensor) -> Result<TracedTensor> {
555 TracedTensor::sub(self, rhs)
556 }
557}
558
559impl std::ops::Mul for &TracedTensor {
560 type Output = Result<TracedTensor>;
561
562 fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
563 TracedTensor::mul(self, rhs)
564 }
565}
566
567impl std::ops::Mul<f64> for &TracedTensor {
568 type Output = Result<TracedTensor>;
569
570 fn mul(self, rhs: f64) -> Result<TracedTensor> {
571 self.scale_real(rhs)
572 }
573}
574
575impl std::ops::Mul<&TracedTensor> for f64 {
576 type Output = Result<TracedTensor>;
577
578 fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
579 rhs.scale_real(self)
580 }
581}
582
583impl std::ops::Neg for &TracedTensor {
584 type Output = Result<TracedTensor>;
585
586 fn neg(self) -> Self::Output {
587 TracedTensor::neg(self)
588 }
589}
590
591impl std::ops::Div for &TracedTensor {
592 type Output = Result<TracedTensor>;
593
594 fn div(self, rhs: &TracedTensor) -> Result<TracedTensor> {
595 TracedTensor::div(self, rhs)
596 }
597}
598
599impl std::ops::Rem for &TracedTensor {
600 type Output = Result<TracedTensor>;
601
602 fn rem(self, rhs: &TracedTensor) -> Result<TracedTensor> {
603 TracedTensor::rem(self, rhs)
604 }
605}
606
607impl TracedTensor {
608 /// Return the graph that owns this traced tensor's current value.
609 ///
610 /// # Examples
611 ///
612 /// ```
613 /// use tenferro_runtime::TracedTensor;
614 ///
615 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
616 /// let _graph = x.graph();
617 /// ```
618 pub fn graph(&self) -> &Arc<Graph<StdTensorOp>> {
619 &self.graph
620 }
621
622 /// Return the concrete tensor data attached to this traced value, if any.
623 ///
624 /// Placeholder tensors created with `input_concrete_shape` or
625 /// `input_symbolic_shape` have no attached data until execution bindings
626 /// provide it.
627 ///
628 /// # Examples
629 ///
630 /// ```
631 /// use tenferro_runtime::{DType, TracedTensor};
632 ///
633 /// let concrete = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
634 /// assert!(concrete.attached_value().is_some());
635 ///
636 /// let placeholder = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
637 /// assert!(placeholder.attached_value().is_none());
638 /// ```
639 pub fn attached_value(&self) -> Option<&Arc<RetainedValue>> {
640 self.data.as_ref()
641 }
642
643 /// Build a [`TracedTensor`] leaf from a concrete [`Tensor`], keeping its
644 /// shape as a concrete `shape_hint`.
645 ///
646 /// This is the common constructor when you have concrete tensor data that
647 /// you want to use both for graph building and for evaluation. The
648 /// resulting tensor is treated as a concrete-shape leaf by downstream
649 /// passes (binary einsum decomposition, build-time reshape folding, etc.).
650 ///
651 /// # Examples
652 ///
653 /// ```
654 /// use tenferro_runtime::{Tensor, TracedTensor};
655 ///
656 /// let a = TracedTensor::from_tensor_concrete_shape(
657 /// Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(),
658 /// )
659 /// .unwrap();
660 /// assert_eq!(a.rank, 2);
661 /// assert!(a.is_concrete_shape());
662 /// ```
663 ///
664 /// # Errors
665 ///
666 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
667 /// cannot retain the concrete tensor's shape or dtype.
668 pub fn from_tensor_concrete_shape(tensor: Tensor) -> Result<Self> {
669 let shape = tensor.shape().to_vec();
670 let rank = shape.len();
671 let dtype = tensor.dtype();
672 let key = next_input_key();
673 let id = next_traced_id();
674 let data = Arc::new(RetainedValue::from_tensor(tensor));
675
676 let mut builder = GraphBuilder::new();
677 let val = builder.add_input(key.clone());
678 builder.set_outputs(vec![val]);
679 let graph = Arc::new(builder.build());
680 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
681 graph.values()[val].key.clone(),
682 concrete_tensor_meta(dtype, &shape),
683 ))?;
684
685 let mut map = HashMap::new();
686 map.insert(key, Arc::clone(&data));
687
688 Ok(Self {
689 id,
690 rank,
691 dtype,
692 graph,
693 val,
694 data: Some(data),
695 shape_hint: Some(shape.into_iter().map(SymDim::from).collect()),
696 inputs_map: Arc::new(map),
697 extra_roots: Vec::new(),
698 checkpoint_chain: None,
699 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
700 constraint_scopes: ConstraintScopeChain::empty(),
701 })
702 }
703
704 /// Build a [`TracedTensor`] leaf from a concrete [`Tensor`] but advertise
705 /// a symbolic shape during graph construction.
706 ///
707 /// The tensor data is still attached (so plain `eval` works without
708 /// bindings), but graph passes see the leaf as shape-symbolic. This is
709 /// useful for building a single traced program that should not bake in
710 /// shape-specific optimizations.
711 ///
712 /// # Examples
713 ///
714 /// ```
715 /// use tenferro_runtime::{Tensor, TracedTensor};
716 ///
717 /// let t = TracedTensor::from_tensor_symbolic_shape(
718 /// Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(),
719 /// )
720 /// .unwrap();
721 /// assert_eq!(t.rank, 2);
722 /// assert!(!t.is_concrete_shape());
723 /// ```
724 ///
725 /// # Errors
726 ///
727 /// Returns [`Error::RuntimeStateSource`] when symbolic graph metadata
728 /// registration is unavailable or its registry state is poisoned.
729 pub fn from_tensor_symbolic_shape(tensor: Tensor) -> Result<Self> {
730 Self::from_tensor_value_symbolic_shape(TensorValue::from_tensor(tensor))
731 }
732
733 /// Build a data-attached symbolic-shape traced leaf from shared tensor data.
734 ///
735 /// This is an internal crate-boundary helper for eager AD recording. It
736 /// preserves the same tensor allocation used by the eager value while graph
737 /// passes see symbolic input extents.
738 ///
739 /// # Examples
740 ///
741 /// ```
742 /// use std::sync::Arc;
743 /// use tenferro_runtime::{Tensor, TracedTensor};
744 ///
745 /// let tensor = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
746 /// let traced = TracedTensor::from_tensor_symbolic_shape(tensor)?;
747 /// assert_eq!(traced.rank, 1);
748 /// assert!(!traced.is_concrete_shape());
749 /// # Ok::<(), tenferro_runtime::Error>(())
750 /// ```
751 ///
752 /// # Errors
753 ///
754 /// Returns [`Error::RuntimeStateSource`] when symbolic graph metadata
755 /// registration is unavailable or its registry state is poisoned.
756 #[doc(hidden)]
757 pub fn from_tensor_value_symbolic_shape(value: TensorValue) -> Result<Self> {
758 let data = Arc::new(RetainedValue::from_tensor_value(value)?);
759 Self::from_shared_tensor_value_symbolic_shape(data)
760 }
761
762 #[doc(hidden)]
763 pub fn from_shared_tensor_value_symbolic_shape(data: Arc<RetainedValue>) -> Result<Self> {
764 let rank = data.shape().len();
765 let dtype = data.dtype();
766 let key = next_input_key();
767 let id = next_traced_id();
768
769 let mut builder = GraphBuilder::new();
770 let val = builder.add_input(key.clone());
771 builder.set_outputs(vec![val]);
772 let graph = Arc::new(builder.build());
773 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
774 graph.values()[val].key.clone(),
775 symbolic_input_meta(dtype, id, rank),
776 ))?;
777
778 let mut map = HashMap::new();
779 map.insert(key, Arc::clone(&data));
780
781 Ok(Self {
782 id,
783 rank,
784 dtype,
785 graph,
786 val,
787 data: Some(data),
788 shape_hint: None,
789 inputs_map: Arc::new(map),
790 extra_roots: Vec::new(),
791 checkpoint_chain: None,
792 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
793 constraint_scopes: ConstraintScopeChain::empty(),
794 })
795 }
796
797 /// Build a data-less placeholder leaf with a fixed (concrete) shape.
798 ///
799 /// Must be passed as an input to [`crate::Runtime::run_compiled`] before evaluation.
800 /// Use this when you know the exact shape of the input but want to build
801 /// the graph once and feed different concrete tensors at execution time.
802 ///
803 /// # Examples
804 ///
805 /// ```
806 /// use tenferro_tensor::DType;
807 /// use tenferro_runtime::TracedTensor;
808 ///
809 /// let x = TracedTensor::input_concrete_shape(DType::F64, &[2, 3]).unwrap();
810 /// assert_eq!(x.rank, 2);
811 /// assert!(x.is_concrete_shape());
812 /// ```
813 ///
814 /// # Errors
815 ///
816 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
817 /// fails or the registry state is poisoned. `dtype` and `shape` are
818 /// metadata values and are not revalidated by this constructor.
819 pub fn input_concrete_shape(dtype: DType, shape: &[usize]) -> Result<Self> {
820 let shape = shape.to_vec();
821 let rank = shape.len();
822 let key = next_input_key();
823 let id = next_traced_id();
824
825 let mut builder = GraphBuilder::new();
826 let val = builder.add_input(key.clone());
827 builder.set_outputs(vec![val]);
828 let graph = Arc::new(builder.build());
829 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
830 graph.values()[val].key.clone(),
831 concrete_tensor_meta(dtype, &shape),
832 ))?;
833
834 Ok(Self {
835 id,
836 rank,
837 dtype,
838 graph,
839 val,
840 data: None,
841 shape_hint: Some(shape.into_iter().map(SymDim::from).collect()),
842 inputs_map: Arc::new(HashMap::new()),
843 extra_roots: Vec::new(),
844 checkpoint_chain: None,
845 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
846 constraint_scopes: ConstraintScopeChain::empty(),
847 })
848 }
849
850 /// Build a data-less placeholder leaf with the given rank but fully
851 /// symbolic shape (every dim is a distinct `SymDim::TensorAxis`).
852 ///
853 /// Must be passed as an input to [`crate::Runtime::run_compiled`] before
854 /// evaluation. Use this to build shape-agnostic graphs.
855 ///
856 /// # Examples
857 ///
858 /// ```
859 /// use tenferro_tensor::DType;
860 /// use tenferro_runtime::TracedTensor;
861 ///
862 /// let x = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
863 /// assert_eq!(x.rank, 2);
864 /// assert!(!x.is_concrete_shape());
865 /// ```
866 ///
867 /// # Errors
868 ///
869 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
870 /// fails or the registry state is poisoned. `rank` is recorded as the
871 /// symbolic placeholder rank and is not otherwise rejected here.
872 pub fn input_symbolic_shape(dtype: DType, rank: usize) -> Result<Self> {
873 let key = next_input_key();
874 let id = next_traced_id();
875
876 let mut builder = GraphBuilder::new();
877 let val = builder.add_input(key.clone());
878 builder.set_outputs(vec![val]);
879 let graph = Arc::new(builder.build());
880 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
881 graph.values()[val].key.clone(),
882 symbolic_input_meta(dtype, id, rank),
883 ))?;
884
885 Ok(Self {
886 id,
887 rank,
888 dtype,
889 graph,
890 val,
891 data: None,
892 shape_hint: None,
893 inputs_map: Arc::new(HashMap::new()),
894 extra_roots: Vec::new(),
895 checkpoint_chain: None,
896 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
897 constraint_scopes: ConstraintScopeChain::empty(),
898 })
899 }
900
901 /// Build a concrete-shape [`TracedTensor`] leaf from column-major typed
902 /// `Vec<T>` data.
903 ///
904 /// The data must already be in tenferro's physical column-major order.
905 ///
906 /// # Examples
907 ///
908 /// ```
909 /// use tenferro_runtime::TracedTensor;
910 ///
911 /// let a = TracedTensor::from_vec_col_major(
912 /// vec![2, 3],
913 /// vec![1.0_f64, 4.0, 2.0, 5.0, 3.0, 6.0],
914 /// )?;
915 /// assert_eq!(a.rank, 2);
916 /// # Ok::<(), tenferro_runtime::Error>(())
917 /// ```
918 ///
919 /// # Errors
920 ///
921 /// Returns [`Error::TensorRuntime`] containing
922 /// `ValidationError::ShapeDataLengthMismatch` when the shape product does
923 /// not equal `data.len()`, or `ValidationError::IntegerOverflow` when the
924 /// shape product cannot be represented by `usize`.
925 pub fn from_vec_col_major<T: TensorScalar>(
926 shape: impl IntoShapeVec,
927 data: Vec<T>,
928 ) -> Result<Self> {
929 Self::from_tensor_concrete_shape(Tensor::from_vec_col_major(shape, data)?)
930 }
931
932 /// Return the tensor element dtype recorded for this traced value.
933 pub fn dtype(&self) -> DType {
934 self.dtype
935 }
936
937 /// Returns `true` iff every dim of this tensor's `shape_hint` is a
938 /// constant `SymDim` (i.e. the shape is fully known at graph-build time).
939 ///
940 /// # Examples
941 ///
942 /// ```
943 /// use tenferro_tensor::DType;
944 /// use tenferro_runtime::TracedTensor;
945 ///
946 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
947 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
948 /// assert!(a.is_concrete_shape());
949 /// assert!(!b.is_concrete_shape());
950 /// ```
951 pub fn is_concrete_shape(&self) -> bool {
952 try_concrete_shape(self).is_some()
953 }
954
955 /// Return the fully-concrete shape of this tensor, if every dim of
956 /// its shape-hint is a constant `SymDim`. Returns `None` if any
957 /// dimension is symbolic.
958 ///
959 /// This is the counterpart to [`Self::is_concrete_shape`] for callers
960 /// that need to *use* the concrete shape (e.g. external composition
961 /// wrappers building `broadcast_in_dim` payloads from known shapes).
962 ///
963 /// # Examples
964 ///
965 /// ```
966 /// use tenferro_tensor::DType;
967 /// use tenferro_runtime::TracedTensor;
968 ///
969 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
970 /// assert_eq!(a.try_concrete_shape(), Some(vec![2, 3]));
971 ///
972 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
973 /// assert!(b.try_concrete_shape().is_none());
974 /// ```
975 pub fn try_concrete_shape(&self) -> Option<Vec<usize>> {
976 try_concrete_shape(self)
977 }
978
979 /// Return the concrete tensor shape.
980 ///
981 /// Returns an error when a shape hint is missing or any dimension is
982 /// symbolic. Composite traced ops that require concrete sizes should
983 /// propagate this error instead of panicking.
984 ///
985 /// # Errors
986 ///
987 /// Returns [`Error::Validation`] with `InvalidArgument` when this tensor
988 /// has no shape hint or any dimension is symbolic.
989 pub fn concrete_shape(&self) -> Result<Vec<usize>> {
990 concrete_shape(self)
991 }
992
993 /// If this `TracedTensor` is a leaf (single-node input graph),
994 /// return its input key. Computed tensors return `None`.
995 pub fn input_key(&self) -> Option<TensorInputKey> {
996 match &self.graph.values()[self.val].key {
997 ValueKey::Input(key) => Some(key.clone()),
998 _ => None,
999 }
1000 }
1001
1002 /// Return whether this traced graph carries default data for `key`.
1003 #[doc(hidden)]
1004 pub fn has_attached_input_key(&self, key: &TensorInputKey) -> bool {
1005 self.inputs_map.contains_key(key)
1006 }
1007
1008 /// Elementwise addition with NumPy-style broadcasting.
1009 ///
1010 /// Prefer using the `+` operator when it reads naturally.
1011 ///
1012 /// A longer expression such as `a + b + c` does not compose because the
1013 /// first `+` returns `Result<TracedTensor, Error>`, so the second `+`
1014 /// would receive a result rather than a tensor. Use `?` at each step or
1015 /// the explicit fallible method chain shown below when the operation
1016 /// sequence is more important than notation:
1017 ///
1018 /// # Examples
1019 ///
1020 /// ```rust
1021 /// # use tenferro_runtime::{Error, TracedTensor};
1022 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1023 /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1024 /// let y = x.add(&z);
1025 /// let y2 = &x + &z;
1026 /// # fn add_three(
1027 /// # a: &TracedTensor,
1028 /// # b: &TracedTensor,
1029 /// # c: &TracedTensor,
1030 /// # ) -> Result<TracedTensor, Error> {
1031 /// let ab = (a + b)?;
1032 /// let sum = (&ab + c)?;
1033 /// let method_chain = a.add(b)?.add(c)?;
1034 /// let _ = method_chain;
1035 /// # Ok(sum)
1036 /// # }
1037 /// ```
1038 ///
1039 /// Tenferro prioritizes robust error handling over the conciseness of
1040 /// chained operator notation; the explicit fallible methods are the
1041 /// canonical form for longer sequences.
1042 ///
1043 /// # Errors
1044 ///
1045 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1046 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1047 /// metadata registration fails.
1048 ///
1049 /// # Deferred errors
1050 ///
1051 /// If symbolic dimensions prevent shape comparison during graph
1052 /// construction, the same `ShapeMismatch` can be reported during
1053 /// compilation or execution, with the corresponding [`ErrorPhase`].
1054 pub fn add(&self, other: &TracedTensor) -> Result<TracedTensor> {
1055 let (lhs, rhs) = broadcast_binary(self, other)?;
1056 apply_binary(
1057 StdTensorOp::Add,
1058 &lhs,
1059 &rhs,
1060 lhs.rank,
1061 lhs.shape_hint.clone(),
1062 )
1063 }
1064
1065 /// Elementwise subtraction with NumPy-style broadcasting.
1066 ///
1067 /// Prefer using the `-` operator when it reads naturally.
1068 ///
1069 /// # Errors
1070 ///
1071 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1072 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1073 /// metadata registration fails.
1074 ///
1075 /// # Deferred errors
1076 ///
1077 /// If symbolic dimensions prevent shape comparison during graph
1078 /// construction, the same `ShapeMismatch` can be reported during
1079 /// compilation or execution, with the corresponding [`ErrorPhase`].
1080 pub fn sub(&self, other: &TracedTensor) -> Result<TracedTensor> {
1081 let (lhs, rhs) = broadcast_binary(self, other)?;
1082 apply_binary(
1083 StdTensorOp::Sub,
1084 &lhs,
1085 &rhs,
1086 lhs.rank,
1087 lhs.shape_hint.clone(),
1088 )
1089 }
1090
1091 /// Elementwise multiplication with NumPy-style broadcasting.
1092 ///
1093 /// Prefer using the `*` operator when it reads naturally.
1094 ///
1095 /// # Examples
1096 ///
1097 /// ```rust
1098 /// # use tenferro_runtime::TracedTensor;
1099 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1100 /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1101 /// let y = x.mul(&z);
1102 /// let y2 = &x * &z;
1103 /// ```
1104 ///
1105 /// # Errors
1106 ///
1107 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1108 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1109 /// metadata registration fails.
1110 ///
1111 /// # Deferred errors
1112 ///
1113 /// If symbolic ranks prevent shape comparison during graph construction,
1114 /// the same `ShapeMismatch` can be reported during compilation or
1115 /// execution, with the corresponding [`ErrorPhase`].
1116 pub fn mul(&self, other: &TracedTensor) -> Result<TracedTensor> {
1117 let (lhs, rhs) = broadcast_binary(self, other)?;
1118 apply_binary(
1119 StdTensorOp::Mul,
1120 &lhs,
1121 &rhs,
1122 lhs.rank,
1123 lhs.shape_hint.clone(),
1124 )
1125 }
1126
1127 /// Elementwise division with NumPy-style broadcasting.
1128 ///
1129 /// Prefer using the `/` operator when it reads naturally.
1130 ///
1131 /// # Examples
1132 ///
1133 /// ```rust
1134 /// # use tenferro_runtime::TracedTensor;
1135 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1136 /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1137 /// let y = x.div(&z);
1138 /// let y2 = &x / &z;
1139 /// ```
1140 ///
1141 /// # Errors
1142 ///
1143 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1144 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1145 /// metadata registration fails.
1146 ///
1147 /// # Deferred errors
1148 ///
1149 /// If symbolic ranks prevent shape comparison during graph construction,
1150 /// the same `ShapeMismatch` can be reported during compilation or
1151 /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
1152 /// a zero divisor is reported during execution as
1153 /// [`Error::TensorRuntime`] containing a
1154 /// [`tenferro_tensor::Error::Extension`] classified as
1155 /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
1156 /// backend source; floating-point and complex zero divisors follow their
1157 /// numeric semantics instead.
1158 pub fn div(&self, other: &TracedTensor) -> Result<TracedTensor> {
1159 let (lhs, rhs) = broadcast_binary(self, other)?;
1160 apply_binary(
1161 StdTensorOp::Div,
1162 &lhs,
1163 &rhs,
1164 lhs.rank,
1165 lhs.shape_hint.clone(),
1166 )
1167 }
1168
1169 /// Elementwise remainder with NumPy-style broadcasting.
1170 ///
1171 /// Prefer using the `%` operator when it reads naturally.
1172 ///
1173 /// # Errors
1174 ///
1175 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1176 /// cannot be broadcast, [`Error::Unsupported`] at
1177 /// [`ErrorPhase::GraphBuild`] when either operand has a complex dtype, or
1178 /// [`Error::RuntimeStateSource`] when graph metadata registration fails.
1179 ///
1180 /// # Deferred errors
1181 ///
1182 /// If symbolic ranks prevent shape comparison during graph construction,
1183 /// the same `ShapeMismatch` can be reported during compilation or
1184 /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
1185 /// a zero divisor is reported during execution as
1186 /// [`Error::TensorRuntime`] containing a
1187 /// [`tenferro_tensor::Error::Extension`] classified as
1188 /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
1189 /// backend source; floating-point zero divisors follow their numeric
1190 /// semantics.
1191 pub fn rem(&self, other: &TracedTensor) -> Result<TracedTensor> {
1192 let (lhs, rhs) = broadcast_binary(self, other)?;
1193 apply_binary(
1194 StdTensorOp::Rem,
1195 &lhs,
1196 &rhs,
1197 lhs.rank,
1198 lhs.shape_hint.clone(),
1199 )
1200 }
1201
1202 /// Elementwise comparison with NumPy-style broadcasting.
1203 ///
1204 /// # Errors
1205 ///
1206 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1207 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1208 /// comparison rejects a complex dtype, or [`Error::RuntimeStateSource`]
1209 /// when result metadata cannot be registered.
1210 ///
1211 /// # Deferred errors
1212 ///
1213 /// With same-rank symbolic operands, shape compatibility is retained as a
1214 /// graph constraint. A concrete mismatch is reported later as
1215 /// [`Error::TensorRuntime`] containing a typed validation source, with the
1216 /// failure phase identifying compilation or execution.
1217 pub fn compare(&self, other: &TracedTensor, dir: CompareDir) -> Result<TracedTensor> {
1218 apply_broadcast_binary_op(StdTensorOp::Compare(dir), self, other)
1219 }
1220
1221 /// Elementwise maximum with NumPy-style broadcasting.
1222 ///
1223 /// # Errors
1224 ///
1225 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1226 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1227 /// maximum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
1228 /// result metadata cannot be registered.
1229 ///
1230 /// # Deferred errors
1231 ///
1232 /// With same-rank symbolic operands, the broadcast constraint may fail at
1233 /// compile or execution and is returned as [`Error::TensorRuntime`] with
1234 /// its typed validation source.
1235 pub fn maximum(&self, other: &TracedTensor) -> Result<TracedTensor> {
1236 apply_broadcast_binary_op(StdTensorOp::Maximum, self, other)
1237 }
1238
1239 /// Elementwise minimum with NumPy-style broadcasting.
1240 ///
1241 /// # Errors
1242 ///
1243 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1244 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1245 /// minimum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
1246 /// result metadata cannot be registered.
1247 ///
1248 /// # Deferred errors
1249 ///
1250 /// With same-rank symbolic operands, the broadcast constraint may fail at
1251 /// compile or execution and is returned as [`Error::TensorRuntime`] with
1252 /// its typed validation source.
1253 pub fn minimum(&self, other: &TracedTensor) -> Result<TracedTensor> {
1254 apply_broadcast_binary_op(StdTensorOp::Minimum, self, other)
1255 }
1256
1257 /// Select values from `on_true` or `on_false` using `condition`.
1258 ///
1259 /// # Errors
1260 ///
1261 /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
1262 /// lacks concrete shape metadata, or `ShapeMismatch` when the concrete
1263 /// condition and branches cannot share a broadcast shape. Dtype promotion
1264 /// failures are returned as [`Error::TensorRuntime`] with the typed
1265 /// `UnsupportedDTypeConversion` source; metadata failures retain
1266 /// [`Error::RuntimeStateSource`].
1267 pub fn where_select(
1268 condition: &TracedTensor,
1269 on_true: &TracedTensor,
1270 on_false: &TracedTensor,
1271 ) -> Result<TracedTensor> {
1272 apply_broadcast_ternary_op(StdTensorOp::Select, condition, on_true, on_false)
1273 }
1274
1275 /// Alias for [`Self::where_select`].
1276 ///
1277 /// # Errors
1278 ///
1279 /// Returns the same concrete failures as [`Self::where_select`]:
1280 /// [`Error::Validation`] with `InvalidArgument`/`ShapeMismatch` for shape
1281 /// metadata or broadcasting, [`Error::TensorRuntime`] with
1282 /// `UnsupportedDTypeConversion` for failed promotion, and
1283 /// [`Error::RuntimeStateSource`] for metadata registration.
1284 pub fn select(
1285 condition: &TracedTensor,
1286 on_true: &TracedTensor,
1287 on_false: &TracedTensor,
1288 ) -> Result<TracedTensor> {
1289 Self::where_select(condition, on_true, on_false)
1290 }
1291
1292 /// Clamp values elementwise between lower and upper bounds.
1293 ///
1294 /// # Errors
1295 ///
1296 /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
1297 /// lacks concrete shape metadata, `ShapeMismatch` when bounds cannot be
1298 /// broadcast with the input, [`Error::Unsupported`] for an ordered
1299 /// complex dtype, or [`Error::RuntimeStateSource`] when metadata cannot be
1300 /// registered.
1301 pub fn clamp(&self, lower: &TracedTensor, upper: &TracedTensor) -> Result<TracedTensor> {
1302 apply_broadcast_ternary_op(StdTensorOp::Clamp, self, lower, upper)
1303 }
1304
1305 fn apply_same_shape_unary(&self, op: StdTensorOp) -> Result<TracedTensor> {
1306 apply_unary(op, self, self.rank, self.shape_hint.clone())
1307 }
1308
1309 /// Elementwise negation.
1310 ///
1311 /// Prefer using the unary `-` operator when it reads naturally.
1312 ///
1313 /// # Examples
1314 ///
1315 /// ```rust
1316 /// # use tenferro_runtime::TracedTensor;
1317 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1318 /// let y = x.neg().unwrap();
1319 /// let y2 = (-&x).unwrap();
1320 /// ```
1321 ///
1322 /// # Errors
1323 ///
1324 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1325 /// is unavailable or poisoned while recording the unary result.
1326 pub fn neg(&self) -> Result<TracedTensor> {
1327 self.apply_same_shape_unary(StdTensorOp::Neg)
1328 }
1329
1330 /// Elementwise complex conjugate.
1331 ///
1332 /// # Examples
1333 ///
1334 /// ```rust
1335 /// # use num_complex::Complex64;
1336 /// # use tenferro_runtime::TracedTensor;
1337 /// # let x = TracedTensor::from_vec_col_major(
1338 /// # vec![2],
1339 /// # vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)],
1340 /// # )
1341 /// # .unwrap();
1342 /// let y = x.conj().unwrap();
1343 /// ```
1344 ///
1345 /// # Errors
1346 ///
1347 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1348 /// is unavailable or poisoned while recording the unary result.
1349 pub fn conj(&self) -> Result<TracedTensor> {
1350 self.apply_same_shape_unary(StdTensorOp::Conj)
1351 }
1352
1353 /// Elementwise absolute value.
1354 ///
1355 /// Complex inputs return real magnitudes (`C32 -> F32`, `C64 -> F64`).
1356 ///
1357 /// # Examples
1358 ///
1359 /// ```rust
1360 /// # use tenferro_runtime::TracedTensor;
1361 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1362 /// let y = x.abs().unwrap();
1363 /// ```
1364 ///
1365 /// # Errors
1366 ///
1367 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1368 /// is unavailable or poisoned while recording the unary result.
1369 pub fn abs(&self) -> Result<TracedTensor> {
1370 self.apply_same_shape_unary(StdTensorOp::Abs)
1371 }
1372
1373 /// Elementwise sign.
1374 ///
1375 /// # Examples
1376 ///
1377 /// ```rust
1378 /// # use tenferro_runtime::TracedTensor;
1379 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1380 /// let y = x.sign().unwrap();
1381 /// ```
1382 ///
1383 /// # Errors
1384 ///
1385 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1386 /// is unavailable or poisoned while recording the unary result.
1387 pub fn sign(&self) -> Result<TracedTensor> {
1388 self.apply_same_shape_unary(StdTensorOp::Sign)
1389 }
1390
1391 /// Scale by a real scalar: `y = factor * x`.
1392 ///
1393 /// # Examples
1394 ///
1395 /// ```rust
1396 /// # use tenferro_runtime::TracedTensor;
1397 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1398 /// let y = x.scale_real(2.0)?;
1399 /// # Ok::<(), tenferro_runtime::Error>(())
1400 /// ```
1401 ///
1402 /// # Errors
1403 ///
1404 /// Returns [`Error::Validation`] with `InvalidArgument` when an integer or
1405 /// boolean factor is non-finite or out of range for the input dtype, or
1406 /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1407 pub fn scale_real(&self, factor: f64) -> Result<TracedTensor> {
1408 let op = match self.dtype {
1409 DType::F64 => StdTensorOp::constant(factor),
1410 DType::F32 => StdTensorOp::constant(factor as f32),
1411 DType::I32 => StdTensorOp::constant(round_real_to_i32_for_op("scale_real", factor)?),
1412 DType::I64 => StdTensorOp::constant(round_real_to_i64(factor)?),
1413 DType::Bool => StdTensorOp::constant(bool_from_real_for_op("scale_real", factor)?),
1414 DType::C64 => StdTensorOp::constant(Complex64::new(factor, 0.0)),
1415 DType::C32 => StdTensorOp::constant(Complex32::new(factor as f32, 0.0)),
1416 };
1417 scale_with_constant(self, op)
1418 }
1419
1420 /// Scale by a complex scalar: `y = factor * x`.
1421 ///
1422 /// Only complex tensors support complex scaling. For a real scalar factor
1423 /// that should preserve the input dtype, prefer [`scale_real`](Self::scale_real).
1424 ///
1425 /// # Examples
1426 ///
1427 /// ```rust
1428 /// use num_complex::Complex64;
1429 /// # use tenferro_runtime::TracedTensor;
1430 /// # let x = TracedTensor::from_vec_col_major(
1431 /// # vec![2],
1432 /// # vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)],
1433 /// # )
1434 /// # .unwrap();
1435 /// let y = x.scale_complex(Complex64::new(0.0, 1.0)).unwrap(); // multiply by i
1436 /// ```
1437 ///
1438 /// # Errors
1439 ///
1440 /// Returns [`Error::Validation`] with `InvalidArgument` when a complex
1441 /// factor is applied to a non-complex dtype, or
1442 /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1443 pub fn scale_complex(&self, factor: Complex64) -> Result<TracedTensor> {
1444 match self.dtype {
1445 DType::C64 => scale_with_constant(self, StdTensorOp::constant(factor)),
1446 DType::C32 => scale_with_constant(
1447 self,
1448 StdTensorOp::constant(Complex32::new(factor.re as f32, factor.im as f32)),
1449 ),
1450 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1451 Err(graph_invalid_argument(
1452 "scale_complex",
1453 "dtype",
1454 format!("requires complex tensor dtype, got {:?}", self.dtype),
1455 ))
1456 }
1457 }
1458 }
1459
1460 /// Elementwise exponential.
1461 ///
1462 /// # Examples
1463 ///
1464 /// ```rust
1465 /// # use tenferro_runtime::TracedTensor;
1466 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1467 /// let y = x.exp().unwrap();
1468 /// ```
1469 ///
1470 /// # Errors
1471 ///
1472 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1473 /// is unavailable or poisoned while recording the unary result.
1474 pub fn exp(&self) -> Result<TracedTensor> {
1475 self.apply_same_shape_unary(StdTensorOp::Exp)
1476 }
1477
1478 /// Elementwise natural logarithm.
1479 ///
1480 /// # Examples
1481 ///
1482 /// ```rust
1483 /// # use tenferro_runtime::TracedTensor;
1484 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1485 /// let y = x.log().unwrap();
1486 /// ```
1487 ///
1488 /// # Errors
1489 ///
1490 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1491 /// is unavailable or poisoned while recording the unary result.
1492 pub fn log(&self) -> Result<TracedTensor> {
1493 self.apply_same_shape_unary(StdTensorOp::Log)
1494 }
1495
1496 /// Elementwise sine.
1497 ///
1498 /// # Examples
1499 ///
1500 /// ```rust
1501 /// # use tenferro_runtime::TracedTensor;
1502 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1503 /// let y = x.sin().unwrap();
1504 /// ```
1505 ///
1506 /// # Errors
1507 ///
1508 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1509 /// is unavailable or poisoned while recording the unary result.
1510 pub fn sin(&self) -> Result<TracedTensor> {
1511 self.apply_same_shape_unary(StdTensorOp::Sin)
1512 }
1513
1514 /// Elementwise cosine.
1515 ///
1516 /// # Examples
1517 ///
1518 /// ```rust
1519 /// # use tenferro_runtime::TracedTensor;
1520 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1521 /// let y = x.cos().unwrap();
1522 /// ```
1523 ///
1524 /// # Errors
1525 ///
1526 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1527 /// is unavailable or poisoned while recording the unary result.
1528 pub fn cos(&self) -> Result<TracedTensor> {
1529 self.apply_same_shape_unary(StdTensorOp::Cos)
1530 }
1531
1532 /// Elementwise hyperbolic tangent.
1533 ///
1534 /// # Examples
1535 ///
1536 /// ```rust
1537 /// # use tenferro_runtime::TracedTensor;
1538 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1539 /// let y = x.tanh().unwrap();
1540 /// ```
1541 ///
1542 /// # Errors
1543 ///
1544 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1545 /// is unavailable or poisoned while recording the unary result.
1546 pub fn tanh(&self) -> Result<TracedTensor> {
1547 self.apply_same_shape_unary(StdTensorOp::Tanh)
1548 }
1549
1550 /// Elementwise square root.
1551 ///
1552 /// # Examples
1553 ///
1554 /// ```rust
1555 /// # use tenferro_runtime::TracedTensor;
1556 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1557 /// let y = x.sqrt().unwrap();
1558 /// ```
1559 ///
1560 /// # Errors
1561 ///
1562 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1563 /// is unavailable or poisoned while recording the unary result.
1564 pub fn sqrt(&self) -> Result<TracedTensor> {
1565 self.apply_same_shape_unary(StdTensorOp::Sqrt)
1566 }
1567
1568 /// Elementwise reciprocal square root.
1569 ///
1570 /// # Examples
1571 ///
1572 /// ```rust
1573 /// # use tenferro_runtime::TracedTensor;
1574 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1575 /// let y = x.rsqrt().unwrap();
1576 /// ```
1577 ///
1578 /// # Errors
1579 ///
1580 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1581 /// is unavailable or poisoned while recording the unary result.
1582 pub fn rsqrt(&self) -> Result<TracedTensor> {
1583 self.apply_same_shape_unary(StdTensorOp::Rsqrt)
1584 }
1585
1586 /// Elementwise power with NumPy-style broadcasting.
1587 ///
1588 /// # Examples
1589 ///
1590 /// ```rust
1591 /// # use tenferro_runtime::TracedTensor;
1592 /// # let base = TracedTensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0]).unwrap();
1593 /// # let exp = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 2.0]).unwrap();
1594 /// let y = base.pow(&exp);
1595 /// ```
1596 ///
1597 /// # Errors
1598 ///
1599 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1600 /// operands cannot be broadcast, or [`Error::RuntimeStateSource`] when
1601 /// result metadata cannot be registered.
1602 ///
1603 /// # Deferred errors
1604 ///
1605 /// A symbolic broadcast mismatch or integer negative exponent is
1606 /// discovered at compile or execution and is returned as
1607 /// [`Error::TensorRuntime`] with a typed `ShapeMismatch` or
1608 /// `NegativeIntegerExponent` numerical source and the corresponding
1609 /// [`ErrorPhase`].
1610 pub fn pow(&self, other: &TracedTensor) -> Result<TracedTensor> {
1611 let (lhs, rhs) = broadcast_binary(self, other)?;
1612 apply_binary(
1613 StdTensorOp::Pow,
1614 &lhs,
1615 &rhs,
1616 lhs.rank,
1617 lhs.shape_hint.clone(),
1618 )
1619 }
1620
1621 /// Elementwise `exp(x) - 1`.
1622 ///
1623 /// # Examples
1624 ///
1625 /// ```rust
1626 /// # use tenferro_runtime::TracedTensor;
1627 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1628 /// let y = x.expm1().unwrap();
1629 /// ```
1630 ///
1631 /// # Errors
1632 ///
1633 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1634 /// is unavailable or poisoned while recording the unary result.
1635 pub fn expm1(&self) -> Result<TracedTensor> {
1636 self.apply_same_shape_unary(StdTensorOp::Expm1)
1637 }
1638
1639 /// Elementwise `log(1 + x)`.
1640 ///
1641 /// # Examples
1642 ///
1643 /// ```rust
1644 /// # use tenferro_runtime::TracedTensor;
1645 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1646 /// let y = x.log1p().unwrap();
1647 /// ```
1648 ///
1649 /// # Errors
1650 ///
1651 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1652 /// is unavailable or poisoned while recording the unary result.
1653 pub fn log1p(&self) -> Result<TracedTensor> {
1654 self.apply_same_shape_unary(StdTensorOp::Log1p)
1655 }
1656
1657 /// Convert the tensor to a different dtype using checked conversion.
1658 ///
1659 /// Use [`cast`](Self::cast) when a lossy dtype projection is intended.
1660 ///
1661 /// # Examples
1662 ///
1663 /// ```rust
1664 /// use tenferro_runtime::DType;
1665 /// # use tenferro_runtime::TracedTensor;
1666 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1667 ///
1668 /// let y = x.convert(DType::C64)?;
1669 /// # Ok::<(), tenferro_runtime::Error>(())
1670 /// ```
1671 ///
1672 /// # Errors
1673 ///
1674 /// Returns [`tenferro_tensor::Error::UnsupportedDTypeConversion`] when the
1675 /// requested pair is outside tenferro's checked dtype-promotion lattice,
1676 /// or [`Error::Validation`] when graph metadata rejects the conversion.
1677 /// Use [`cast`](Self::cast) for explicit lossy dtype projection.
1678 pub fn convert(&self, to: DType) -> Result<TracedTensor> {
1679 tenferro_tensor::validate::validate_convert_dtype("TracedTensor::convert", self.dtype, to)?;
1680 self.cast(to)
1681 }
1682
1683 /// Cast the tensor to a different dtype using explicit dtype projection.
1684 ///
1685 /// `cast` may truncate, narrow precision, project complex values to their
1686 /// real component, or use boolean truthiness where the backend supports the
1687 /// requested projection.
1688 ///
1689 /// # Examples
1690 ///
1691 /// ```rust
1692 /// use tenferro_runtime::DType;
1693 /// # use tenferro_runtime::TracedTensor;
1694 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.2_f64, -2.8]).unwrap();
1695 ///
1696 /// let y = x.cast(DType::I32).unwrap();
1697 /// ```
1698 ///
1699 /// # Errors
1700 ///
1701 /// Returns [`Error::TensorRuntime`] containing
1702 /// `UnsupportedDTypeConversion` when the requested input-to-target
1703 /// projection is not supported, or [`Error::RuntimeStateSource`] when
1704 /// converted-output metadata cannot be registered.
1705 pub fn cast(&self, to: DType) -> Result<TracedTensor> {
1706 if self.dtype == to {
1707 return Ok(self.clone());
1708 }
1709
1710 apply_unary_with_dtype(
1711 StdTensorOp::Convert {
1712 from: self.dtype,
1713 to,
1714 },
1715 self,
1716 self.rank,
1717 self.shape_hint.clone(),
1718 to,
1719 )
1720 }
1721
1722 /// Generalized tensor contraction.
1723 ///
1724 /// # Examples
1725 ///
1726 /// ```rust
1727 /// # use tenferro_runtime::{DotGeneralConfig, TracedTensor};
1728 /// # let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1729 /// # let b = TracedTensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
1730 /// # let config = DotGeneralConfig {
1731 /// # lhs_contracting_dims: vec![1],
1732 /// # rhs_contracting_dims: vec![0],
1733 /// # lhs_batch_dims: vec![],
1734 /// # rhs_batch_dims: vec![],
1735 /// # };
1736 /// let y = a.dot_general(&b, config)?;
1737 /// # Ok::<(), tenferro_runtime::Error>(())
1738 /// ```
1739 ///
1740 /// # Errors
1741 ///
1742 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
1743 /// `DuplicateAxis`, or `AxisRoleConflict` when dimension numbers are
1744 /// invalid for the operand ranks, and [`Error::RuntimeStateSource`] when
1745 /// output metadata cannot be registered.
1746 ///
1747 /// # Deferred errors
1748 ///
1749 /// Contracting or batch dimensions whose sizes are symbolic are checked
1750 /// when concrete inputs reach compilation or execution. A mismatch is
1751 /// returned as [`Error::TensorRuntime`] with a typed `ShapeMismatch`
1752 /// source and its corresponding [`ErrorPhase`].
1753 pub fn dot_general(
1754 &self,
1755 other: &TracedTensor,
1756 config: DotGeneralConfig,
1757 ) -> Result<TracedTensor> {
1758 config
1759 .validate_dims_with_ranks(self.rank, other.rank)
1760 .map_err(|err| graph_tensor_error("dot_general", err))?;
1761 let lhs_free: Vec<usize> = (0..self.rank)
1762 .filter(|d| {
1763 !config.lhs_contracting_dims.contains(d) && !config.lhs_batch_dims.contains(d)
1764 })
1765 .collect();
1766 let rhs_free: Vec<usize> = (0..other.rank)
1767 .filter(|d| {
1768 !config.rhs_contracting_dims.contains(d) && !config.rhs_batch_dims.contains(d)
1769 })
1770 .collect();
1771 let out_rank = config.lhs_batch_dims.len() + lhs_free.len() + rhs_free.len();
1772 let out_shape_hint = match (&self.shape_hint, &other.shape_hint) {
1773 (Some(lhs_shape), Some(rhs_shape)) => {
1774 let mut out_shape = Vec::with_capacity(out_rank);
1775 for &d in &lhs_free {
1776 out_shape.push(lhs_shape[d].clone());
1777 }
1778 for &d in &rhs_free {
1779 out_shape.push(rhs_shape[d].clone());
1780 }
1781 for &d in &config.lhs_batch_dims {
1782 out_shape.push(lhs_shape[d].clone());
1783 }
1784 Some(out_shape)
1785 }
1786 _ => None,
1787 };
1788
1789 apply_binary(
1790 StdTensorOp::DotGeneral { config },
1791 self,
1792 other,
1793 out_rank,
1794 out_shape_hint,
1795 )
1796 }
1797
1798 /// Matrix multiplication for rank-2 tensors.
1799 ///
1800 /// # Errors
1801 ///
1802 /// Returns [`Error::Validation`] with `RankMismatch` when either operand is
1803 /// not rank 2, `ShapeMismatch::ContractedDimensions` when known matrix
1804 /// dimensions differ, or [`Error::RuntimeStateSource`] when output
1805 /// metadata cannot be registered.
1806 ///
1807 /// # Deferred errors
1808 ///
1809 /// If either contracted dimension is symbolic, the mismatch is discovered
1810 /// at compilation or execution and returned as [`Error::TensorRuntime`]
1811 /// with its typed `ShapeMismatch` source.
1812 pub fn matmul(&self, other: &TracedTensor) -> Result<TracedTensor> {
1813 if self.rank != 2 {
1814 return Err(graph_validation(
1815 "TracedTensor::matmul",
1816 ValidationError::RankMismatch {
1817 expected: 2,
1818 actual: self.rank,
1819 },
1820 ));
1821 }
1822 if other.rank != 2 {
1823 return Err(graph_validation(
1824 "TracedTensor::matmul",
1825 ValidationError::RankMismatch {
1826 expected: 2,
1827 actual: other.rank,
1828 },
1829 ));
1830 }
1831 if let (Some(lhs_shape), Some(rhs_shape)) = (&self.shape_hint, &other.shape_hint) {
1832 if let (Some(lhs_cols), Some(rhs_rows)) =
1833 (lhs_shape[1].constant_value(), rhs_shape[0].constant_value())
1834 {
1835 if lhs_cols != rhs_rows {
1836 return Err(graph_validation(
1837 "TracedTensor::matmul",
1838 ShapeMismatch::ContractedDimensions {
1839 lhs_axis: 1,
1840 lhs_size: lhs_cols,
1841 rhs_axis: 0,
1842 rhs_size: rhs_rows,
1843 },
1844 ));
1845 }
1846 }
1847 }
1848 self.dot_general(
1849 other,
1850 DotGeneralConfig {
1851 lhs_contracting_dims: vec![1],
1852 rhs_contracting_dims: vec![0],
1853 lhs_batch_dims: vec![],
1854 rhs_batch_dims: vec![],
1855 },
1856 )
1857 }
1858
1859 /// Sum over the given axes.
1860 ///
1861 /// # Examples
1862 ///
1863 /// ```rust
1864 /// # use tenferro_runtime::TracedTensor;
1865 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1866 /// let total = x.reduce_sum(None)?;
1867 /// let rows = x.reduce_sum(Some(&[1]))?;
1868 /// let identity = x.reduce_sum(Some(&[]))?;
1869 /// assert_eq!(total.rank, 0);
1870 /// assert_eq!(rows.rank, 1);
1871 /// assert_eq!(identity.rank, 2);
1872 /// # Ok::<(), tenferro_runtime::Error>(())
1873 /// ```
1874 ///
1875 /// # Errors
1876 ///
1877 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1878 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1879 /// or [`Error::RuntimeStateSource`] when output metadata cannot be
1880 /// registered.
1881 pub fn reduce_sum(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1882 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1883 let (out_rank, out_shape_hint) =
1884 reduction_output_meta(self, &axes, "TracedTensor::reduce_sum")?;
1885 apply_unary(
1886 StdTensorOp::ReduceSum { axes },
1887 self,
1888 out_rank,
1889 out_shape_hint,
1890 )
1891 }
1892
1893 /// Sum elementwise squares over the requested axes.
1894 ///
1895 /// Each value is squared in its input dtype before reduction. The initial
1896 /// supported dtypes are `f32` and `f64`; other dtypes return a typed
1897 /// unsupported error during execution. Passing an empty axis slice returns
1898 /// the elementwise square without reducing rank.
1899 ///
1900 /// This operation is useful when the squared sum is needed directly. Use
1901 /// the linalg norm APIs when a square root or complex magnitude semantics
1902 /// are required.
1903 ///
1904 /// # Errors
1905 ///
1906 /// Returns a typed validation error for invalid axes or a typed
1907 /// runtime-state error while registering output metadata.
1908 ///
1909 /// # Deferred errors
1910 ///
1911 /// Unsupported dtypes and backend execution failures are reported when the
1912 /// compiled graph is executed.
1913 /// # Examples
1914 ///
1915 /// ```rust
1916 /// # use tenferro_runtime::TracedTensor;
1917 /// # let x = TracedTensor::from_vec_col_major(
1918 /// # vec![2, 2],
1919 /// # vec![1.0_f64, 2.0, 3.0, 4.0],
1920 /// # )?;
1921 /// let squares = x.reduce_sum_squares(&[1])?;
1922 /// assert_eq!(squares.rank, 1);
1923 /// # Ok::<(), tenferro_runtime::Error>(())
1924 /// ```
1925 pub fn reduce_sum_squares(&self, axes: &[usize]) -> Result<TracedTensor> {
1926 let (out_rank, out_shape_hint) =
1927 reduction_output_meta(self, axes, "TracedTensor::reduce_sum_squares")?;
1928 apply_unary(
1929 StdTensorOp::ReduceSumSquares {
1930 axes: axes.to_vec(),
1931 },
1932 self,
1933 out_rank,
1934 out_shape_hint,
1935 )
1936 }
1937
1938 /// Reduce by taking the maximum along the given axes.
1939 ///
1940 /// Used by tropical (max-plus) compositions: a max-plus reduction over
1941 /// an axis is `ReduceMax` on that axis.
1942 ///
1943 /// # Examples
1944 ///
1945 /// ```rust
1946 /// # use tenferro_runtime::TracedTensor;
1947 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1948 /// let y = x.reduce_max(Some(&[0]))?;
1949 /// # Ok::<(), tenferro_runtime::Error>(())
1950 /// ```
1951 ///
1952 /// # Errors
1953 ///
1954 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1955 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1956 /// [`Error::Unsupported`] when a non-empty maximum reduction receives a
1957 /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
1958 /// cannot be registered.
1959 pub fn reduce_max(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1960 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1961 let (out_rank, out_shape_hint) =
1962 reduction_output_meta(self, &axes, "TracedTensor::reduce_max")?;
1963 try_apply_unary(
1964 StdTensorOp::ReduceMax { axes },
1965 self,
1966 out_rank,
1967 out_shape_hint,
1968 "TracedTensor::reduce_max",
1969 )
1970 }
1971
1972 /// Reduce by taking the minimum along the given axes.
1973 ///
1974 /// Used by tropical (min-plus) compositions: a min-plus reduction over
1975 /// an axis is `ReduceMin` on that axis.
1976 ///
1977 /// # Examples
1978 ///
1979 /// ```rust
1980 /// # use tenferro_runtime::TracedTensor;
1981 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
1982 /// let y = x.reduce_min(Some(&[0]))?;
1983 /// # Ok::<(), tenferro_runtime::Error>(())
1984 /// ```
1985 ///
1986 /// # Errors
1987 ///
1988 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
1989 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
1990 /// [`Error::Unsupported`] when a non-empty minimum reduction receives a
1991 /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
1992 /// cannot be registered.
1993 pub fn reduce_min(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
1994 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
1995 let (out_rank, out_shape_hint) =
1996 reduction_output_meta(self, &axes, "TracedTensor::reduce_min")?;
1997 try_apply_unary(
1998 StdTensorOp::ReduceMin { axes },
1999 self,
2000 out_rank,
2001 out_shape_hint,
2002 "TracedTensor::reduce_min",
2003 )
2004 }
2005
2006 /// Reduce by taking the product along the given axes.
2007 ///
2008 /// # Examples
2009 ///
2010 /// ```rust
2011 /// # use tenferro_runtime::TracedTensor;
2012 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2013 /// let y = x.reduce_prod(Some(&[0]))?;
2014 /// # Ok::<(), tenferro_runtime::Error>(())
2015 /// ```
2016 ///
2017 /// # Errors
2018 ///
2019 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2020 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2021 /// or [`Error::RuntimeStateSource`] when output metadata cannot be
2022 /// registered.
2023 pub fn reduce_prod(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2024 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2025 let (out_rank, out_shape_hint) =
2026 reduction_output_meta(self, &axes, "TracedTensor::reduce_prod")?;
2027 apply_unary(
2028 StdTensorOp::ReduceProd { axes },
2029 self,
2030 out_rank,
2031 out_shape_hint,
2032 )
2033 }
2034
2035 /// Reshape without changing element order.
2036 ///
2037 /// # Examples
2038 ///
2039 /// ```rust
2040 /// # use tenferro_runtime::TracedTensor;
2041 /// # let x = TracedTensor::from_vec_col_major(vec![4], vec![1.0_f64; 4]).unwrap();
2042 /// let y = x.reshape(&[2, 2])?;
2043 /// # Ok::<(), tenferro_runtime::Error>(())
2044 /// ```
2045 ///
2046 /// # Errors
2047 ///
2048 /// Returns [`Error::Validation`] with `ShapeMismatch::ReshapeElementCount`
2049 /// when a concrete input has a different element count, or
2050 /// `IntegerOverflow` when the target shape product overflows `usize`.
2051 pub fn reshape(&self, shape: &[usize]) -> Result<TracedTensor> {
2052 validate_concrete_reshape_shape(self, shape)?;
2053 apply_unary_with_dtype(
2054 StdTensorOp::Reshape {
2055 to_shape: DimExpr::from_concrete(shape),
2056 },
2057 self,
2058 shape.len(),
2059 Some(shape.iter().copied().map(SymDim::from).collect()),
2060 self.dtype,
2061 )
2062 }
2063
2064 /// Return a symbolic expression for the size of one axis, suitable as
2065 /// an `InputDim`-style reference when composing with
2066 /// [`TracedTensor::reshape_sym`].
2067 ///
2068 /// Semantics: if this tensor's `shape_hint` has a symbolic
2069 /// (non-constant) entry for `axis`, that entry is returned
2070 /// verbatim. Otherwise — including when `shape_hint[axis]` is a
2071 /// concrete `SymDim::Concrete(n)` — a
2072 /// `SymDim::tensor_axis(self.id, axis)` reference is returned so the
2073 /// resulting graph remains shape-polymorphic if the same graph is
2074 /// later evaluated against a differently-shaped binding.
2075 ///
2076 /// For a canonical "what is the size of this axis?" query that
2077 /// reports the concrete size when it is known, prefer
2078 /// [`Self::axis_sym_dim`].
2079 ///
2080 /// # Examples
2081 ///
2082 /// ```rust
2083 /// # use tenferro_runtime::TracedTensor;
2084 /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2085 /// let rows = x.sym_size(0)?;
2086 /// let cols = x.sym_size(1)?;
2087 /// let y = x.reshape_sym(&[rows * cols]).unwrap();
2088 /// # Ok::<(), tenferro_runtime::Error>(())
2089 /// ```
2090 ///
2091 /// # Errors
2092 ///
2093 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2094 /// outside this tensor's rank.
2095 pub fn sym_size(&self, axis: usize) -> Result<SymDim> {
2096 validate_traced_axis(self, axis, "TracedTensor::sym_size")?;
2097 Ok(self
2098 .shape_hint
2099 .as_ref()
2100 .and_then(|shape| shape.get(axis))
2101 .filter(|dim| dim.constant_value().is_none())
2102 .cloned()
2103 .unwrap_or_else(|| SymDim::tensor_axis(self.id, axis)))
2104 }
2105
2106 /// Return the canonical `SymDim` for `axis` — the concrete
2107 /// `SymDim::Concrete(n)` when the size is known, otherwise a symbolic
2108 /// expression identifying this tensor's axis.
2109 ///
2110 /// Unlike [`Self::sym_size`], this method does **not** rewrite
2111 /// concrete axes into `TensorAxis` references. It is the accessor
2112 /// external composition wrappers should use when building mixed
2113 /// concrete/symbolic target shapes for operations like
2114 /// [`Self::broadcast_in_dim_sym`].
2115 ///
2116 /// # Examples
2117 ///
2118 /// ```
2119 /// use tenferro_tensor::DType;
2120 /// use tenferro_runtime::TracedTensor;
2121 ///
2122 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2123 /// // Concrete axis: reports the constant size.
2124 /// assert_eq!(a.axis_sym_dim(0).unwrap().constant_value(), Some(2));
2125 ///
2126 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
2127 /// // Fully symbolic leaf: reports a TensorAxis reference.
2128 /// assert!(b.axis_sym_dim(0).unwrap().constant_value().is_none());
2129 /// ```
2130 ///
2131 /// # Errors
2132 ///
2133 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2134 /// outside this tensor's rank.
2135 pub fn axis_sym_dim(&self, axis: usize) -> Result<SymDim> {
2136 validate_traced_axis(self, axis, "TracedTensor::axis_sym_dim")?;
2137 match self.shape_hint.as_ref().and_then(|shape| shape.get(axis)) {
2138 Some(dim) => Ok(dim.clone()),
2139 None => Ok(SymDim::tensor_axis(self.id, axis)),
2140 }
2141 }
2142
2143 /// Return the full symbolic shape of this tensor when a `shape_hint`
2144 /// is present.
2145 ///
2146 /// Returns `None` for fully-symbolic placeholders produced via
2147 /// [`Self::input_symbolic_shape`] (where `shape_hint` is intentionally
2148 /// absent). For those, build the shape axis-by-axis via
2149 /// [`Self::axis_sym_dim`].
2150 ///
2151 /// # Examples
2152 ///
2153 /// ```
2154 /// use tenferro_tensor::DType;
2155 /// use tenferro_runtime::TracedTensor;
2156 ///
2157 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2158 /// assert!(a.sym_shape().is_some());
2159 /// assert_eq!(a.sym_shape().unwrap().len(), 2);
2160 ///
2161 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
2162 /// assert!(b.sym_shape().is_none());
2163 /// ```
2164 pub fn sym_shape(&self) -> Option<&[SymDim]> {
2165 self.shape_hint.as_deref()
2166 }
2167
2168 /// Reshape using symbolic dimensions derived from traced tensor axes.
2169 ///
2170 /// # Examples
2171 ///
2172 /// ```rust
2173 /// # use tenferro_runtime::TracedTensor;
2174 /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2175 /// let rows = x.sym_size(0)?;
2176 /// let cols = x.sym_size(1)?;
2177 /// let y = x.reshape_sym(&[rows * cols]).unwrap();
2178 /// # Ok::<(), tenferro_runtime::Error>(())
2179 /// ```
2180 ///
2181 /// # Errors
2182 ///
2183 /// Returns [`Error::SymbolicShapeConversion`] when a supplied symbolic
2184 /// dimension cannot be mapped to this graph, or [`Error::RuntimeStateSource`]
2185 /// when result metadata cannot be registered.
2186 ///
2187 /// # Deferred errors
2188 ///
2189 /// Element-count compatibility for symbolic dimensions is checked when
2190 /// concrete inputs reach compilation or execution. A mismatch is returned
2191 /// as [`Error::TensorRuntime`] with a typed `ShapeMismatch` source.
2192 pub fn reshape_sym(&self, shape: &[SymDim]) -> Result<TracedTensor> {
2193 let tensor_map = [(self.id, 0usize)];
2194 let to_shape = shape
2195 .iter()
2196 .map(|dim| {
2197 dim.to_dim_expr(&tensor_map)
2198 .map_err(|source| Error::SymbolicShapeConversion {
2199 op: "TracedTensor::reshape_sym",
2200 phase: ErrorPhase::GraphBuild,
2201 source,
2202 })
2203 })
2204 .collect::<Result<Vec<_>>>()?;
2205 let out_shape_hint = Some(shape.to_vec());
2206 apply_unary(
2207 StdTensorOp::Reshape { to_shape },
2208 self,
2209 shape.len(),
2210 out_shape_hint,
2211 )
2212 }
2213
2214 /// Broadcast into a larger shape with explicit dimension placement.
2215 ///
2216 /// # Examples
2217 ///
2218 /// ```rust
2219 /// # use tenferro_runtime::TracedTensor;
2220 /// # let x = TracedTensor::from_vec_col_major(vec![3], vec![1.0_f64; 3]).unwrap();
2221 /// let y = x.broadcast_in_dim(&[2, 3], &[1])?;
2222 /// # Ok::<(), tenferro_runtime::Error>(())
2223 /// ```
2224 ///
2225 /// # Errors
2226 ///
2227 /// Returns [`Error::Validation`] with `RankMismatch` when `dims` does not
2228 /// have one entry per input axis, `AxisOutOfBounds` or `DuplicateAxis` for
2229 /// an invalid output mapping, or `InvalidArgument` when known dimensions
2230 /// cannot broadcast. [`Error::RuntimeStateSource`] reports failure to
2231 /// register the result metadata.
2232 pub fn broadcast_in_dim(&self, shape: &[usize], dims: &[usize]) -> Result<TracedTensor> {
2233 let out_shape_hint: Vec<SymDim> = shape.iter().copied().map(SymDim::from).collect();
2234 validate_broadcast_in_dim_args(
2235 self,
2236 &out_shape_hint,
2237 dims,
2238 "TracedTensor::broadcast_in_dim",
2239 )?;
2240 apply_unary(
2241 StdTensorOp::BroadcastInDim {
2242 shape: DimExpr::from_concrete(shape),
2243 dims: dims.to_vec(),
2244 },
2245 self,
2246 shape.len(),
2247 Some(out_shape_hint),
2248 )
2249 }
2250
2251 /// Broadcast into a symbolic target shape with explicit dimension
2252 /// placement.
2253 ///
2254 /// Unlike [`Self::broadcast_in_dim`], each axis of `shape` is a
2255 /// [`SymDim`], so the target shape can mix concrete sizes (via
2256 /// `SymDim::from(n)`) with symbolic references to this tensor's axes
2257 /// (via [`Self::axis_sym_dim`]) or to axes of other traced tensors.
2258 ///
2259 /// When `shape` contains a `SymDim` that references a traced tensor
2260 /// other than `self`, the referenced tensor(s) must be supplied in
2261 /// `shape_refs`. They are wired into the built op as auxiliary
2262 /// shape-reference inputs — the op does not read their data, only
2263 /// their runtime shape. `shape_refs` must be listed in the same order
2264 /// in which their tensor IDs first appear when walking `shape` after
2265 /// any references to `self`. Usually the simplest correct thing is to
2266 /// pass each unique non-self reference tensor once.
2267 ///
2268 /// # Examples
2269 ///
2270 /// ```
2271 /// use tenferro_runtime::TracedTensor;
2272 ///
2273 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2274 /// let b = TracedTensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
2275 /// let m = a.axis_sym_dim(0)?;
2276 /// let k = a.axis_sym_dim(1)?;
2277 /// let n = b.axis_sym_dim(1)?;
2278 /// // Broadcast `a[m, k]` to `[m, k, n]`, placing `a`'s axes at 0, 1
2279 /// // and taking `n` from `b` as an auxiliary shape reference.
2280 /// let a_b = a.broadcast_in_dim_sym(&[m, k, n], &[0, 1], &[&b])?;
2281 /// assert_eq!(a_b.rank, 3);
2282 /// # Ok::<(), tenferro_runtime::Error>(())
2283 /// ```
2284 ///
2285 /// # Errors
2286 ///
2287 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2288 /// `DuplicateAxis`, or `InvalidArgument` when the output mapping or shape
2289 /// references are invalid, [`Error::SymbolicShapeConversion`] for an
2290 /// unmappable symbolic dimension, or [`Error::RuntimeStateSource`] when
2291 /// metadata cannot be registered.
2292 ///
2293 /// # Deferred errors
2294 ///
2295 /// If a symbolic output dimension is smaller than a non-unit input axis,
2296 /// the concrete broadcast check is deferred to compilation or execution
2297 /// and is returned as [`Error::TensorRuntime`] with a typed validation
2298 /// source.
2299 pub fn broadcast_in_dim_sym(
2300 &self,
2301 shape: &[SymDim],
2302 dims: &[usize],
2303 shape_refs: &[&TracedTensor],
2304 ) -> Result<TracedTensor> {
2305 validate_broadcast_in_dim_args(self, shape, dims, "TracedTensor::broadcast_in_dim_sym")?;
2306
2307 // Build a dedup'd list of shape-reference tensors (first occurrence
2308 // wins) and index them starting at 1 — the primary input `self`
2309 // is at 0.
2310 let mut dedup_refs: Vec<&TracedTensor> = Vec::with_capacity(shape_refs.len());
2311 let mut tensor_map: Vec<(u64, usize)> = vec![(self.id, 0)];
2312 for &t in shape_refs {
2313 if !tensor_map.iter().any(|(id, _)| *id == t.id) {
2314 let idx = tensor_map.len();
2315 tensor_map.push((t.id, idx));
2316 dedup_refs.push(t);
2317 }
2318 }
2319
2320 let to_shape: Vec<DimExpr> = shape
2321 .iter()
2322 .map(|dim| {
2323 dim.to_dim_expr(&tensor_map)
2324 .map_err(|source| Error::SymbolicShapeConversion {
2325 op: "broadcast_in_dim_sym",
2326 phase: ErrorPhase::GraphBuild,
2327 source,
2328 })
2329 })
2330 .collect::<Result<Vec<_>>>()?;
2331
2332 // Trim auxiliary shape-reference inputs down to those actually
2333 // used by the generated `DimExpr`s. If the target shape resolved
2334 // to all constants (the concrete-shape case) the op is a plain
2335 // unary broadcast with no extra parents. Otherwise the op needs
2336 // a contiguous prefix of shape-ref inputs covering every
2337 // referenced `input_idx`.
2338 let max_used_idx = DimExpr::max_input_idx_all(&to_shape).unwrap_or(0);
2339 let used_refs: Vec<&TracedTensor> = dedup_refs.into_iter().take(max_used_idx).collect();
2340
2341 let out_shape_hint = Some(shape.to_vec());
2342 apply_unary_with_shape_refs(
2343 StdTensorOp::BroadcastInDim {
2344 shape: to_shape,
2345 dims: dims.to_vec(),
2346 },
2347 self,
2348 &used_refs,
2349 shape.len(),
2350 out_shape_hint,
2351 )
2352 }
2353
2354 /// Slice with explicit start, limit, and stride per axis.
2355 ///
2356 /// # Errors
2357 ///
2358 /// Returns [`Error::Validation`] with `RankMismatch` when the start/limit/
2359 /// stride vectors do not match the input rank, `InvalidSliceStep` when a
2360 /// stride is zero, `InvalidSliceBounds` when a limit precedes its start,
2361 /// or [`Error::RuntimeStateSource`] when output metadata cannot be
2362 /// registered.
2363 pub fn slice(&self, config: SliceConfig) -> Result<TracedTensor> {
2364 let op = StdTensorOp::Slice(config);
2365 let (out_rank, out_shape_hint) =
2366 infer_traced_single_output_shape("TracedTensor::slice", &op, &[self])?;
2367 apply_unary(op, self, out_rank, out_shape_hint)
2368 }
2369
2370 /// Pad with zeros using StableHLO-style edge and interior padding.
2371 ///
2372 /// # Errors
2373 ///
2374 /// Returns [`Error::Validation`] with `RankMismatch` when padding vectors
2375 /// do not match the input rank, `InvalidArgument` for negative interior
2376 /// padding, or `IntegerOverflow` when the padded extent exceeds `usize`.
2377 /// [`Error::RuntimeStateSource`] is returned when output metadata cannot be
2378 /// registered.
2379 pub fn pad(&self, config: PadConfig) -> Result<TracedTensor> {
2380 let op = StdTensorOp::Pad(config);
2381 let (out_rank, out_shape_hint) =
2382 infer_traced_single_output_shape("TracedTensor::pad", &op, &[self])?;
2383 apply_unary(op, self, out_rank, out_shape_hint)
2384 }
2385
2386 /// Reverse the order of elements along the requested axes.
2387 ///
2388 /// # Errors
2389 ///
2390 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2391 /// outside the input rank or `DuplicateAxis` when `axes` repeats one, or
2392 /// [`Error::RuntimeStateSource`] when result metadata cannot be
2393 /// registered.
2394 pub fn reverse(&self, axes: &[usize]) -> Result<TracedTensor> {
2395 validate_traced_axes(self.rank, axes, "TracedTensor::reverse")?;
2396 apply_unary(
2397 StdTensorOp::Reverse {
2398 axes: axes.to_vec(),
2399 },
2400 self,
2401 self.rank,
2402 self.shape_hint.clone(),
2403 )
2404 }
2405
2406 /// Gather slices from `self` using integer start indices.
2407 ///
2408 /// # Errors
2409 ///
2410 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2411 /// `DuplicateAxis`, or `ShapeMismatch` when indices or the gather
2412 /// configuration is incompatible with the input, and
2413 /// [`Error::RuntimeStateSource`] when output metadata cannot be
2414 /// registered.
2415 ///
2416 /// # Deferred errors
2417 ///
2418 /// Runtime index values are checked after binding. An out-of-range index
2419 /// is returned as [`Error::TensorRuntime`] with the backend's typed
2420 /// validation source and [`ErrorPhase::Execution`].
2421 pub fn gather(&self, indices: &TracedTensor, config: GatherConfig) -> Result<TracedTensor> {
2422 let op = StdTensorOp::Gather(config);
2423 let (out_rank, out_shape_hint) =
2424 infer_traced_single_output_shape("TracedTensor::gather", &op, &[self, indices])?;
2425 apply_binary_preserve_input_dtypes(op, self, indices, out_rank, out_shape_hint, self.dtype)
2426 }
2427
2428 /// Scatter updates into `self` using StableHLO scatter semantics.
2429 ///
2430 /// # Errors
2431 ///
2432 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2433 /// `DuplicateAxis`, or `ShapeMismatch` when indices, updates, or the
2434 /// scatter configuration is incompatible, [`Error::TensorRuntime`] with
2435 /// `UnsupportedDTypeConversion` when dtype promotion cannot be
2436 /// represented, or [`Error::RuntimeStateSource`] when output metadata
2437 /// cannot be registered.
2438 ///
2439 /// # Deferred errors
2440 ///
2441 /// Runtime index/update values are checked after binding. An invalid
2442 /// index or update shape is returned as [`Error::TensorRuntime`] with its
2443 /// typed validation source and [`ErrorPhase::Execution`].
2444 pub fn scatter(
2445 &self,
2446 indices: &TracedTensor,
2447 updates: &TracedTensor,
2448 config: ScatterConfig,
2449 ) -> Result<TracedTensor> {
2450 let op = StdTensorOp::Scatter(config);
2451 let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
2452 "TracedTensor::scatter",
2453 &op,
2454 &[self, indices, updates],
2455 )?;
2456 let out_dtype = crate::shape_infer::promote_dtype(self.dtype, updates.dtype);
2457 let operand = if self.dtype != out_dtype {
2458 self.cast(out_dtype)?
2459 } else {
2460 self.clone()
2461 };
2462 let updates = if updates.dtype != out_dtype {
2463 updates.cast(out_dtype)?
2464 } else {
2465 updates.clone()
2466 };
2467 apply_ternary_with_output_dtype(
2468 op,
2469 &operand,
2470 indices,
2471 &updates,
2472 out_rank,
2473 out_shape_hint,
2474 out_dtype,
2475 )
2476 }
2477
2478 /// Slice using runtime start indices.
2479 ///
2480 /// # Errors
2481 ///
2482 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2483 /// or `InvalidArgument` when `starts` or `sizes` has an incompatible rank
2484 /// or extent, and [`Error::RuntimeStateSource`] when output metadata cannot
2485 /// be registered.
2486 ///
2487 /// # Deferred errors
2488 ///
2489 /// Runtime start values are checked after binding. An out-of-range start
2490 /// is returned as [`Error::TensorRuntime`] with the backend's typed
2491 /// validation source and [`ErrorPhase::Execution`].
2492 pub fn dynamic_slice(&self, starts: &TracedTensor, sizes: &[usize]) -> Result<TracedTensor> {
2493 let op = StdTensorOp::DynamicSlice {
2494 slice_sizes: sizes.to_vec(),
2495 };
2496 let (out_rank, out_shape_hint) =
2497 infer_traced_single_output_shape("TracedTensor::dynamic_slice", &op, &[self, starts])?;
2498 apply_binary_preserve_input_dtypes(op, self, starts, out_rank, out_shape_hint, self.dtype)
2499 }
2500
2501 /// Keep the lower triangle and zero the rest.
2502 ///
2503 /// # Examples
2504 ///
2505 /// ```rust
2506 /// # use tenferro_runtime::TracedTensor;
2507 /// let matrix = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2508 /// let lower = matrix.tril(0)?;
2509 /// assert_eq!(lower.rank, 2);
2510 /// # Ok::<(), tenferro_runtime::Error>(())
2511 /// ```
2512 ///
2513 /// # Errors
2514 ///
2515 /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2516 /// registration is unavailable or inconsistent with the graph.
2517 pub fn tril(&self, k: i64) -> Result<TracedTensor> {
2518 apply_unary(
2519 StdTensorOp::Tril { k },
2520 self,
2521 self.rank,
2522 self.shape_hint.clone(),
2523 )
2524 }
2525
2526 /// Keep the upper triangle and zero the rest.
2527 ///
2528 /// # Examples
2529 ///
2530 /// ```rust
2531 /// # use tenferro_runtime::TracedTensor;
2532 /// let matrix = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2533 /// let upper = matrix.triu(0)?;
2534 /// assert_eq!(upper.rank, 2);
2535 /// # Ok::<(), tenferro_runtime::Error>(())
2536 /// ```
2537 ///
2538 /// # Errors
2539 ///
2540 /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2541 /// registration is unavailable or inconsistent with the graph.
2542 pub fn triu(&self, k: i64) -> Result<TracedTensor> {
2543 apply_unary(
2544 StdTensorOp::Triu { k },
2545 self,
2546 self.rank,
2547 self.shape_hint.clone(),
2548 )
2549 }
2550
2551 /// Permute tensor axes.
2552 ///
2553 /// # Examples
2554 ///
2555 /// ```rust
2556 /// # use tenferro_runtime::TracedTensor;
2557 /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2558 /// let y = x.transpose(&[1, 0])?;
2559 /// # Ok::<(), tenferro_runtime::Error>(())
2560 /// ```
2561 ///
2562 /// # Errors
2563 ///
2564 /// Returns [`Error::Validation`] with `InvalidPermutationLength`,
2565 /// `AxisOutOfBounds`, or `DuplicateAxis` when `perm` is not a valid
2566 /// permutation of the tensor axes, or [`Error::RuntimeStateSource`] when
2567 /// output metadata registration fails.
2568 pub fn transpose(&self, perm: &[usize]) -> Result<TracedTensor> {
2569 validate_traced_perm(self.rank, perm, "TracedTensor::transpose")?;
2570 let out_shape_hint = self
2571 .shape_hint
2572 .as_ref()
2573 .map(|shape| perm.iter().map(|&p| shape[p].clone()).collect());
2574 apply_unary(
2575 StdTensorOp::Transpose {
2576 perm: perm.to_vec(),
2577 },
2578 self,
2579 self.rank,
2580 out_shape_hint,
2581 )
2582 }
2583
2584 /// Extract the diagonal along two axes.
2585 ///
2586 /// # Examples
2587 ///
2588 /// ```rust
2589 /// # use tenferro_runtime::TracedTensor;
2590 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2591 /// let y = x.extract_diag(0, 1)?;
2592 /// # Ok::<(), tenferro_runtime::Error>(())
2593 /// ```
2594 ///
2595 /// # Errors
2596 ///
2597 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when either axis
2598 /// is outside the input rank or `InvalidArgument` when `axis_a == axis_b`.
2599 pub fn extract_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2600 validate_traced_axis(self, axis_a, "TracedTensor::extract_diag")?;
2601 validate_traced_axis(self, axis_b, "TracedTensor::extract_diag")?;
2602 if axis_a == axis_b {
2603 return Err(graph_invalid_argument(
2604 "TracedTensor::extract_diag",
2605 "axes",
2606 "diagonal axes must be distinct",
2607 ));
2608 }
2609 let op = StdTensorOp::ExtractDiag { axis_a, axis_b };
2610 let (out_rank, out_shape_hint) =
2611 infer_traced_single_output_shape("TracedTensor::extract_diag", &op, &[self])?;
2612 apply_unary(op, self, out_rank, out_shape_hint)
2613 }
2614
2615 /// Embed a vector or lower-rank tensor along a diagonal.
2616 ///
2617 /// # Examples
2618 ///
2619 /// ```rust
2620 /// # use tenferro_runtime::TracedTensor;
2621 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64; 2]).unwrap();
2622 /// let y = x.embed_diag(0, 1)?;
2623 /// # Ok::<(), tenferro_runtime::Error>(())
2624 /// ```
2625 ///
2626 /// # Errors
2627 ///
2628 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis_a` is
2629 /// outside the input rank or `InvalidArgument` when `axis_b` is not a
2630 /// valid insertion axis.
2631 pub fn embed_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2632 validate_traced_axis(self, axis_a, "TracedTensor::embed_diag")?;
2633 validate_traced_insert_axis(self.rank, axis_b, "TracedTensor::embed_diag")?;
2634 let out_shape_hint = self.shape_hint.as_ref().map(|shape| {
2635 let mut out_shape = shape.clone();
2636 out_shape.insert(axis_b, shape[axis_a].clone());
2637 out_shape
2638 });
2639 apply_unary(
2640 StdTensorOp::EmbedDiag { axis_a, axis_b },
2641 self,
2642 self.rank + 1,
2643 out_shape_hint,
2644 )
2645 }
2646
2647 /// Return the runtime size of one axis as a scalar `f64` tensor.
2648 ///
2649 /// The result is metadata-derived and therefore has no gradient.
2650 ///
2651 /// # Examples
2652 ///
2653 /// ```
2654 /// use tenferro_cpu::CpuBackend;
2655 /// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
2656 ///
2657 /// let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
2658 /// let cols = x.shape_of(1)?;
2659 /// let mut compiler = GraphCompiler::new();
2660 /// let program = compiler.compile(&cols).unwrap();
2661 /// let backend = CpuBackend::new();
2662 /// let mut builder = Runtime::builder();
2663 /// builder
2664 /// .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
2665 /// .unwrap();
2666 /// let runtime = builder.build().unwrap();
2667 /// let outputs = runtime.run_compiled(&program, &[]).unwrap();
2668 /// let out = &outputs[0];
2669 /// assert_eq!(out.shape(), &[] as &[usize]);
2670 /// # Ok::<(), tenferro_runtime::Error>(())
2671 /// ```
2672 ///
2673 /// # Errors
2674 ///
2675 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2676 /// outside the input rank, or [`Error::RuntimeStateSource`] when scalar
2677 /// output metadata cannot be registered.
2678 pub fn shape_of(&self, axis: usize) -> Result<TracedTensor> {
2679 validate_traced_axis(self, axis, "TracedTensor::shape_of")?;
2680 apply_unary_with_dtype(
2681 StdTensorOp::ShapeOf { axis },
2682 self,
2683 0,
2684 Some(vec![]),
2685 DType::F64,
2686 )
2687 }
2688
2689 /// Truncate this tensor along `axis` to the first `size` elements.
2690 ///
2691 /// `size` is read at runtime from a scalar traced tensor. Values are
2692 /// rounded to the nearest integer, clamped to `[0, self.shape[axis]]`,
2693 /// and the output keeps the same element dtype as the input.
2694 ///
2695 /// # Examples
2696 ///
2697 /// ```
2698 /// use tenferro_cpu::CpuBackend;
2699 /// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
2700 ///
2701 /// let x = TracedTensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
2702 /// let size = TracedTensor::from_vec_col_major(vec![], vec![2.0_f64]).unwrap();
2703 /// let y = x.dynamic_truncate(&size, 0)?;
2704 /// let mut compiler = GraphCompiler::new();
2705 /// let program = compiler.compile(&y).unwrap();
2706 /// let backend = CpuBackend::new();
2707 /// let mut builder = Runtime::builder();
2708 /// builder
2709 /// .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
2710 /// .unwrap();
2711 /// let runtime = builder.build().unwrap();
2712 /// let outputs = runtime.run_compiled(&program, &[]).unwrap();
2713 /// let out = &outputs[0];
2714 /// assert_eq!(out.shape(), &[2]);
2715 /// # Ok::<(), tenferro_runtime::Error>(())
2716 /// ```
2717 ///
2718 /// # Errors
2719 ///
2720 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2721 /// outside the input rank or `RankMismatch` when `size` is not scalar.
2722 ///
2723 /// # Deferred errors
2724 ///
2725 /// At execution, non-`f32`/`f64`/`i64` size dtypes return
2726 /// [`Error::TensorRuntime`] with `Unsupported`, non-finite size values
2727 /// return a typed `InvalidArgument`, and an empty scalar buffer returns a
2728 /// typed runtime-state source.
2729 pub fn dynamic_truncate(&self, size: &TracedTensor, axis: usize) -> Result<TracedTensor> {
2730 validate_traced_axis(self, axis, "TracedTensor::dynamic_truncate")?;
2731 if size.rank != 0 {
2732 return Err(graph_validation(
2733 "TracedTensor::dynamic_truncate",
2734 ValidationError::RankMismatch {
2735 expected: 0,
2736 actual: size.rank,
2737 },
2738 ));
2739 }
2740 apply_binary_preserve_input_dtypes(
2741 StdTensorOp::DynamicTruncate { axis },
2742 self,
2743 size,
2744 self.rank,
2745 None,
2746 self.dtype,
2747 )
2748 }
2749
2750 /// Pad this tensor with zeros along `axis` to match `reference.shape[axis]`.
2751 ///
2752 /// If `reference` is smaller along that axis, this is a no-op.
2753 ///
2754 /// # Examples
2755 ///
2756 /// ```
2757 /// use tenferro_cpu::CpuBackend;
2758 /// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
2759 ///
2760 /// let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
2761 /// let reference = TracedTensor::from_vec_col_major(vec![4], vec![0.0_f64, 0.0, 0.0, 0.0]).unwrap();
2762 /// let y = x.pad_to_match(&reference, 0)?;
2763 /// let mut compiler = GraphCompiler::new();
2764 /// let program = compiler.compile(&y).unwrap();
2765 /// let backend = CpuBackend::new();
2766 /// let mut builder = Runtime::builder();
2767 /// builder
2768 /// .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
2769 /// .unwrap();
2770 /// let runtime = builder.build().unwrap();
2771 /// let outputs = runtime.run_compiled(&program, &[]).unwrap();
2772 /// let out = &outputs[0];
2773 /// assert_eq!(out.shape(), &[4]);
2774 /// # Ok::<(), tenferro_runtime::Error>(())
2775 /// ```
2776 ///
2777 /// # Errors
2778 ///
2779 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2780 /// outside either tensor's rank, or [`Error::RuntimeStateSource`] when
2781 /// output metadata cannot be registered.
2782 pub fn pad_to_match(&self, reference: &TracedTensor, axis: usize) -> Result<TracedTensor> {
2783 validate_traced_axis(self, axis, "TracedTensor::pad_to_match")?;
2784 validate_traced_axis(reference, axis, "TracedTensor::pad_to_match")?;
2785 let op = StdTensorOp::PadToMatch { axis };
2786 let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
2787 "TracedTensor::pad_to_match",
2788 &op,
2789 &[self, reference],
2790 )?;
2791 apply_binary_preserve_input_dtypes(
2792 op,
2793 self,
2794 reference,
2795 out_rank,
2796 out_shape_hint,
2797 self.dtype,
2798 )
2799 }
2800}
2801
2802pub(crate) fn apply_unary(
2803 op: StdTensorOp,
2804 input: &TracedTensor,
2805 out_rank: usize,
2806 out_shape_hint: Option<Vec<SymDim>>,
2807) -> Result<TracedTensor> {
2808 let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], "apply_unary")?;
2809 apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
2810}
2811
2812fn try_apply_unary(
2813 op: StdTensorOp,
2814 input: &TracedTensor,
2815 out_rank: usize,
2816 out_shape_hint: Option<Vec<SymDim>>,
2817 context: &'static str,
2818) -> Result<TracedTensor> {
2819 let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], context)?;
2820 apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
2821}
2822
2823pub(crate) fn apply_unary_with_dtype(
2824 op: StdTensorOp,
2825 input: &TracedTensor,
2826 out_rank: usize,
2827 out_shape_hint: Option<Vec<SymDim>>,
2828 out_dtype: DType,
2829) -> Result<TracedTensor> {
2830 let mut builder = GraphBuilder::new();
2831 builder.add_parent(input.graph.clone());
2832 let input_ref = ValueRef::External(input.graph.values()[input.val].key.clone());
2833 let outputs = builder.add_operation(op, vec![input_ref], OperationRole::Primary);
2834 builder.set_outputs(outputs.clone());
2835 let graph = Arc::new(builder.build());
2836 let metadata_scope =
2837 register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
2838
2839 Ok(TracedTensor {
2840 id: next_traced_id(),
2841 rank: out_rank,
2842 dtype: out_dtype,
2843 graph,
2844 val: outputs[0],
2845 data: None,
2846 shape_hint: out_shape_hint,
2847 inputs_map: input.inputs_map.clone(),
2848 extra_roots: input.extra_roots.clone(),
2849 checkpoint_chain: input.checkpoint_chain.clone(),
2850 metadata_scopes: MetadataScopeChain::with_new(metadata_scope, [&input.metadata_scopes]),
2851 constraint_scopes: input.constraint_scopes.clone(),
2852 })
2853}
2854
2855/// Apply a unary-primary op that additionally references one or more
2856/// tensors for shape resolution only.
2857///
2858/// The primary `input` becomes op input 0; each tensor in `shape_refs`
2859/// becomes op input 1, 2, … in order. Used by
2860/// [`TracedTensor::broadcast_in_dim_sym`] when the target shape
2861/// references axes of tensors other than the primary input; the op
2862/// reads only their runtime shape, not their data.
2863pub(crate) fn apply_unary_with_shape_refs(
2864 op: StdTensorOp,
2865 input: &TracedTensor,
2866 shape_refs: &[&TracedTensor],
2867 out_rank: usize,
2868 out_shape_hint: Option<Vec<SymDim>>,
2869) -> Result<TracedTensor> {
2870 let mut builder = GraphBuilder::new();
2871 builder.add_parent(input.graph.clone());
2872 for t in shape_refs {
2873 builder.add_parent(t.graph.clone());
2874 }
2875 let mut op_inputs: Vec<ValueRef<StdTensorOp>> = Vec::with_capacity(1 + shape_refs.len());
2876 op_inputs.push(ValueRef::External(
2877 input.graph.values()[input.val].key.clone(),
2878 ));
2879 for t in shape_refs {
2880 op_inputs.push(ValueRef::External(t.graph.values()[t.val].key.clone()));
2881 }
2882 let outputs = builder.add_operation(op, op_inputs, OperationRole::Primary);
2883 builder.set_outputs(outputs.clone());
2884 let graph = Arc::new(builder.build());
2885 let metadata_scope =
2886 register_single_output_metadata(graph.as_ref(), outputs[0], input.dtype, &out_shape_hint)?;
2887
2888 let inputs_map =
2889 merge_traced_inputs_map(std::iter::once(input).chain(shape_refs.iter().copied()));
2890
2891 let mut extra_roots = input.extra_roots.clone();
2892 for t in shape_refs {
2893 extra_roots.extend(t.extra_roots.iter().cloned());
2894 }
2895
2896 let mut checkpoint_chain = input.checkpoint_chain.clone();
2897 for t in shape_refs {
2898 checkpoint_chain =
2899 CheckpointNode::merge_chains(checkpoint_chain, t.checkpoint_chain.clone());
2900 }
2901
2902 Ok(TracedTensor {
2903 id: next_traced_id(),
2904 rank: out_rank,
2905 dtype: input.dtype,
2906 graph,
2907 val: outputs[0],
2908 data: None,
2909 shape_hint: out_shape_hint,
2910 inputs_map,
2911 extra_roots,
2912 checkpoint_chain,
2913 metadata_scopes: MetadataScopeChain::with_new(
2914 metadata_scope,
2915 std::iter::once(&input.metadata_scopes)
2916 .chain(shape_refs.iter().map(|tensor| &tensor.metadata_scopes)),
2917 ),
2918 constraint_scopes: ConstraintScopeChain::merge(
2919 std::iter::once(&input.constraint_scopes)
2920 .chain(shape_refs.iter().map(|tensor| &tensor.constraint_scopes)),
2921 ),
2922 })
2923}
2924
2925pub(crate) fn apply_nullary(
2926 op: StdTensorOp,
2927 rank: usize,
2928 dtype: DType,
2929 shape_hint: Option<Vec<SymDim>>,
2930) -> Result<TracedTensor> {
2931 let mut builder = GraphBuilder::new();
2932 let outputs = builder.add_operation(op, vec![], OperationRole::Primary);
2933 builder.set_outputs(outputs.clone());
2934 let graph = Arc::new(builder.build());
2935 let metadata_scope =
2936 register_single_output_metadata(graph.as_ref(), outputs[0], dtype, &shape_hint)?;
2937
2938 Ok(TracedTensor {
2939 id: next_traced_id(),
2940 rank,
2941 dtype,
2942 graph,
2943 val: outputs[0],
2944 data: None,
2945 shape_hint,
2946 inputs_map: Arc::new(HashMap::new()),
2947 extra_roots: Vec::new(),
2948 checkpoint_chain: None,
2949 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
2950 constraint_scopes: ConstraintScopeChain::empty(),
2951 })
2952}
2953
2954pub(crate) fn apply_binary(
2955 op: StdTensorOp,
2956 lhs: &TracedTensor,
2957 rhs: &TracedTensor,
2958 out_rank: usize,
2959 out_shape_hint: Option<Vec<SymDim>>,
2960) -> Result<TracedTensor> {
2961 let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
2962 let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], "apply_binary")?;
2963
2964 // Insert Convert ops when an input dtype differs from the primitive input dtype.
2965 let lhs = if lhs.dtype != input_dtype {
2966 lhs.cast(input_dtype)?
2967 } else {
2968 lhs.clone()
2969 };
2970 let rhs = if rhs.dtype != input_dtype {
2971 rhs.cast(input_dtype)?
2972 } else {
2973 rhs.clone()
2974 };
2975
2976 apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
2977}
2978
2979fn try_apply_binary(
2980 op: StdTensorOp,
2981 lhs: &TracedTensor,
2982 rhs: &TracedTensor,
2983 out_rank: usize,
2984 out_shape_hint: Option<Vec<SymDim>>,
2985 context: &'static str,
2986) -> Result<TracedTensor> {
2987 let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
2988 let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], context)?;
2989
2990 let lhs = if lhs.dtype != input_dtype {
2991 lhs.cast(input_dtype)?
2992 } else {
2993 lhs.clone()
2994 };
2995 let rhs = if rhs.dtype != input_dtype {
2996 rhs.cast(input_dtype)?
2997 } else {
2998 rhs.clone()
2999 };
3000
3001 apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
3002}
3003
3004pub(crate) fn apply_binary_preserve_input_dtypes(
3005 op: StdTensorOp,
3006 lhs: &TracedTensor,
3007 rhs: &TracedTensor,
3008 out_rank: usize,
3009 out_shape_hint: Option<Vec<SymDim>>,
3010 out_dtype: DType,
3011) -> Result<TracedTensor> {
3012 apply_binary_with_output_dtype(op, lhs, rhs, out_rank, out_shape_hint, out_dtype)
3013}
3014
3015pub(crate) fn apply_broadcast_binary_op(
3016 op: StdTensorOp,
3017 lhs: &TracedTensor,
3018 rhs: &TracedTensor,
3019) -> Result<TracedTensor> {
3020 let (lhs, rhs) = broadcast_binary(lhs, rhs)?;
3021 try_apply_binary(
3022 op,
3023 &lhs,
3024 &rhs,
3025 lhs.rank,
3026 lhs.shape_hint.clone(),
3027 "broadcast_binary",
3028 )
3029}
3030
3031pub(crate) fn apply_broadcast_ternary_op(
3032 op: StdTensorOp,
3033 first: &TracedTensor,
3034 second: &TracedTensor,
3035 third: &TracedTensor,
3036) -> Result<TracedTensor> {
3037 let (first, second, third) = broadcast_ternary(first, second, third)?;
3038 try_apply_ternary(
3039 op,
3040 &first,
3041 &second,
3042 &third,
3043 first.rank,
3044 first.shape_hint.clone(),
3045 "broadcast_ternary",
3046 )
3047}
3048
3049fn try_apply_ternary(
3050 op: StdTensorOp,
3051 first: &TracedTensor,
3052 second: &TracedTensor,
3053 third: &TracedTensor,
3054 out_rank: usize,
3055 out_shape_hint: Option<Vec<SymDim>>,
3056 context: &'static str,
3057) -> Result<TracedTensor> {
3058 let out_dtype =
3059 try_inferred_output_dtype(&op, &[first.dtype, second.dtype, third.dtype], context)?;
3060 let (first, second, third) = match op {
3061 StdTensorOp::Select => {
3062 let value_dtype = crate::shape_infer::promote_dtype(second.dtype, third.dtype);
3063 let second = if second.dtype != value_dtype {
3064 second.cast(value_dtype)?
3065 } else {
3066 second.clone()
3067 };
3068 let third = if third.dtype != value_dtype {
3069 third.cast(value_dtype)?
3070 } else {
3071 third.clone()
3072 };
3073 (first.clone(), second, third)
3074 }
3075 _ => {
3076 let input_dtype =
3077 crate::shape_infer::promote_dtypes([first.dtype, second.dtype, third.dtype]);
3078 let first = if first.dtype != input_dtype {
3079 first.cast(input_dtype)?
3080 } else {
3081 first.clone()
3082 };
3083 let second = if second.dtype != input_dtype {
3084 second.cast(input_dtype)?
3085 } else {
3086 second.clone()
3087 };
3088 let third = if third.dtype != input_dtype {
3089 third.cast(input_dtype)?
3090 } else {
3091 third.clone()
3092 };
3093 (first, second, third)
3094 }
3095 };
3096 apply_ternary_with_output_dtype(
3097 op,
3098 &first,
3099 &second,
3100 &third,
3101 out_rank,
3102 out_shape_hint,
3103 out_dtype,
3104 )
3105}
3106
3107fn apply_binary_with_output_dtype(
3108 op: StdTensorOp,
3109 lhs: &TracedTensor,
3110 rhs: &TracedTensor,
3111 out_rank: usize,
3112 out_shape_hint: Option<Vec<SymDim>>,
3113 out_dtype: DType,
3114) -> Result<TracedTensor> {
3115 let lhs_ref = ValueRef::External(lhs.graph.values()[lhs.val].key.clone());
3116 let rhs_ref = ValueRef::External(rhs.graph.values()[rhs.val].key.clone());
3117
3118 let mut builder = GraphBuilder::new();
3119 builder.add_parent(lhs.graph.clone());
3120 builder.add_parent(rhs.graph.clone());
3121 let outputs = builder.add_operation(op, vec![lhs_ref, rhs_ref], OperationRole::Primary);
3122 builder.set_outputs(outputs.clone());
3123 let graph = Arc::new(builder.build());
3124 let metadata_scope =
3125 register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
3126
3127 let mut extra_roots = lhs.extra_roots.clone();
3128 extra_roots.extend(rhs.extra_roots.iter().cloned());
3129
3130 Ok(TracedTensor {
3131 id: next_traced_id(),
3132 rank: out_rank,
3133 dtype: out_dtype,
3134 graph,
3135 val: outputs[0],
3136 data: None,
3137 shape_hint: out_shape_hint,
3138 inputs_map: merge_traced_inputs_map([lhs, rhs]),
3139 extra_roots,
3140 checkpoint_chain: CheckpointNode::merge_chains(
3141 lhs.checkpoint_chain.clone(),
3142 rhs.checkpoint_chain.clone(),
3143 ),
3144 metadata_scopes: MetadataScopeChain::with_new(
3145 metadata_scope,
3146 [&lhs.metadata_scopes, &rhs.metadata_scopes],
3147 ),
3148 constraint_scopes: ConstraintScopeChain::merge([
3149 &lhs.constraint_scopes,
3150 &rhs.constraint_scopes,
3151 ]),
3152 })
3153}
3154
3155fn apply_ternary_with_output_dtype(
3156 op: StdTensorOp,
3157 first: &TracedTensor,
3158 second: &TracedTensor,
3159 third: &TracedTensor,
3160 out_rank: usize,
3161 out_shape_hint: Option<Vec<SymDim>>,
3162 out_dtype: DType,
3163) -> Result<TracedTensor> {
3164 let first_ref = ValueRef::External(first.graph.values()[first.val].key.clone());
3165 let second_ref = ValueRef::External(second.graph.values()[second.val].key.clone());
3166 let third_ref = ValueRef::External(third.graph.values()[third.val].key.clone());
3167
3168 let mut builder = GraphBuilder::new();
3169 builder.add_parent(first.graph.clone());
3170 builder.add_parent(second.graph.clone());
3171 builder.add_parent(third.graph.clone());
3172 let outputs = builder.add_operation(
3173 op,
3174 vec![first_ref, second_ref, third_ref],
3175 OperationRole::Primary,
3176 );
3177 builder.set_outputs(outputs.clone());
3178 let graph = Arc::new(builder.build());
3179 let metadata_scope =
3180 register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
3181
3182 let mut extra_roots = first.extra_roots.clone();
3183 extra_roots.extend(second.extra_roots.iter().cloned());
3184 extra_roots.extend(third.extra_roots.iter().cloned());
3185
3186 let checkpoint_chain = CheckpointNode::merge_chains(
3187 CheckpointNode::merge_chains(
3188 first.checkpoint_chain.clone(),
3189 second.checkpoint_chain.clone(),
3190 ),
3191 third.checkpoint_chain.clone(),
3192 );
3193
3194 Ok(TracedTensor {
3195 id: next_traced_id(),
3196 rank: out_rank,
3197 dtype: out_dtype,
3198 graph,
3199 val: outputs[0],
3200 data: None,
3201 shape_hint: out_shape_hint,
3202 inputs_map: merge_traced_inputs_map([first, second, third]),
3203 extra_roots,
3204 checkpoint_chain,
3205 metadata_scopes: MetadataScopeChain::with_new(
3206 metadata_scope,
3207 [
3208 &first.metadata_scopes,
3209 &second.metadata_scopes,
3210 &third.metadata_scopes,
3211 ],
3212 ),
3213 constraint_scopes: ConstraintScopeChain::merge([
3214 &first.constraint_scopes,
3215 &second.constraint_scopes,
3216 &third.constraint_scopes,
3217 ]),
3218 })
3219}
3220
3221fn register_single_output_metadata(
3222 graph: &Graph<StdTensorOp>,
3223 output: LocalValueId,
3224 dtype: DType,
3225 shape_hint: &Option<Vec<SymDim>>,
3226) -> Result<GlobalMetadataScope> {
3227 if let Some(shape) = shape_hint {
3228 // Fresh graph output keys are generated in this builder, so metadata
3229 // registration failure would indicate a global metadata invariant bug.
3230 register_metadata_or_runtime_state(register_scoped_value_metadata(
3231 graph.values()[output].key.clone(),
3232 tensor_meta(dtype, shape.clone()),
3233 ))
3234 } else {
3235 // Fresh graph output keys are generated in this builder, so metadata
3236 // registration failure would indicate a global metadata invariant bug.
3237 register_metadata_or_runtime_state(register_scoped_graph_metadata(
3238 graph,
3239 std::iter::empty(),
3240 ))
3241 }
3242}
3243
3244impl TracedTensor {
3245 pub(crate) fn resolve_roots(&self) -> Vec<Arc<Graph<StdTensorOp>>> {
3246 let mut roots = Vec::with_capacity(1 + self.extra_roots.len());
3247 roots.push(self.graph.clone());
3248 roots.extend(self.extra_roots.iter().cloned());
3249 roots
3250 }
3251}
3252
3253#[cfg(test)]
3254mod tests;