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