tenferro_ops/ad/context.rs
1//! AD context for guard-based shape resolution and value metadata queries.
2//!
3//! During AD graph construction, linalg rules such as SVD, QR, and LU need
4//! concrete matrix dimensions to choose between structurally different
5//! subgraphs. `ShapeGuardContext` records those dimension comparisons as guards
6//! so cached AD graphs can later be invalidated when the observed shape
7//! relationship changes.
8
9use std::cmp::Ordering;
10use std::collections::HashMap;
11use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
12#[cfg(feature = "autodiff")]
13use std::sync::Arc;
14use std::sync::{Mutex, OnceLock};
15
16use computegraph::graph::Graph;
17use computegraph::types::{ValueKey, ValueRef};
18use tenferro_tensor::DType;
19
20#[cfg(feature = "autodiff")]
21use crate::ad::{ADRuleError, ADRuleKind, ExtensionAdDispatcher};
22use crate::dim_expr::{DimExpr, DimExprEvalError};
23use crate::shape_extent::ShapeExtent;
24use crate::std_tensor_op::StdTensorOp;
25use crate::sym_dim::SymDim;
26
27type MetadataMap = HashMap<ValueKey<StdTensorOp>, TensorMeta>;
28
29type GlobalMetadataMap = HashMap<ValueKey<StdTensorOp>, GlobalMetadataEntry>;
30
31#[derive(Clone, Debug)]
32struct GlobalMetadataEntry {
33 stack: Vec<GlobalMetadataRegistration>,
34}
35
36#[derive(Clone, Debug)]
37struct GlobalMetadataRegistration {
38 token: u64,
39 meta: TensorMeta,
40}
41
42#[derive(Clone, Debug)]
43struct ScopedGlobalMetadataRegistration {
44 key: ValueKey<StdTensorOp>,
45 token: u64,
46}
47
48/// Error returned when the process-global AD metadata registry is unavailable.
49#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
50pub enum MetadataRegistryError {
51 /// A previous panic poisoned the global metadata mutex.
52 #[error("AD global metadata registry lock poisoned")]
53 LockPoisoned,
54}
55
56/// Error returned when shape-guard metadata cannot be resolved.
57///
58/// # Examples
59///
60/// ```
61/// use tenferro_ops::ShapeGuardError;
62///
63/// let error = ShapeGuardError::LocalWithoutAttachedGraph { local_id: 0 };
64/// assert!(error.to_string().contains("attached graph"));
65/// ```
66#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
67pub enum ShapeGuardError {
68 /// A local graph value was queried before a graph was attached.
69 #[error("cannot resolve local value {local_id} without an attached graph")]
70 LocalWithoutAttachedGraph {
71 /// Graph-local value id.
72 local_id: usize,
73 },
74 /// A local graph value id is outside the attached graph's value table.
75 #[error("local value {local_id} is out of bounds for the attached graph")]
76 LocalOutOfBounds {
77 /// Graph-local value id.
78 local_id: usize,
79 },
80 /// No metadata was registered for the resolved value key.
81 #[error("missing TensorMeta for {key:?}")]
82 MissingMetadata {
83 /// Resolved value key.
84 key: ValueKey<StdTensorOp>,
85 },
86 /// Metadata exists, but at least one axis is only bounded or unknown.
87 #[error("TensorMeta for {key:?} does not have an exact shape; query extents instead")]
88 NonExactShape {
89 /// Resolved value key.
90 key: ValueKey<StdTensorOp>,
91 },
92}
93
94/// Result type used by shape-guard metadata queries.
95///
96/// The error side preserves a [`ShapeGuardFailure`] wrapper so an AD callback
97/// can retain the original [`ShapeGuardError`] even when a foreign callback
98/// protocol accepts only a rendered message.
99///
100/// # Examples
101///
102/// ```
103/// use tenferro_ops::ShapeGuardResult;
104///
105/// let result: ShapeGuardResult<()> = Ok(());
106/// assert!(result.is_ok());
107/// ```
108pub type ShapeGuardResult<T> = Result<T, ShapeGuardFailure>;
109
110#[cfg(feature = "autodiff")]
111impl From<ShapeGuardFailure> for ADRuleError {
112 fn from(err: ShapeGuardFailure) -> Self {
113 err.record_for_ad_boundary();
114 ADRuleError::invalid_input(
115 "tenferro.shape_guard",
116 ADRuleKind::Jvp,
117 err.typed_source().to_string(),
118 )
119 }
120}
121
122/// Error returned by a shape-guard metadata query.
123///
124/// The public [`ShapeGuardError`] remains the typed source. The private side
125/// channel is shared with the owning [`ShapeGuardContext`] so an external
126/// message-only AD callback can report the same typed source at the runtime
127/// boundary without changing the callback protocol.
128///
129/// # Examples
130///
131/// ```
132/// use computegraph::types::{ValueKey, ValueRef};
133/// use tenferro_ops::input_key::TensorInputKey;
134/// use tenferro_ops::std_tensor_op::StdTensorOp;
135/// use tenferro_ops::{ShapeGuardContext, ShapeGuardError};
136///
137/// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 8 });
138/// let value = ValueRef::External(key);
139/// let mut ctx = ShapeGuardContext::default();
140/// let failure = ctx.shape_of(&value).unwrap_err();
141/// assert!(matches!(
142/// failure.typed_source(),
143/// ShapeGuardError::MissingMetadata { .. }
144/// ));
145/// ```
146#[derive(Clone, Debug)]
147pub struct ShapeGuardFailure {
148 source: ShapeGuardError,
149 #[cfg(feature = "autodiff")]
150 deferred: Arc<Mutex<Option<ShapeGuardError>>>,
151}
152
153impl ShapeGuardFailure {
154 #[cfg(feature = "autodiff")]
155 fn new(source: ShapeGuardError, deferred: Arc<Mutex<Option<ShapeGuardError>>>) -> Self {
156 Self { source, deferred }
157 }
158
159 #[cfg(not(feature = "autodiff"))]
160 fn new(source: ShapeGuardError) -> Self {
161 Self { source }
162 }
163
164 /// Return the original typed shape-guard failure.
165 pub fn typed_source(&self) -> &ShapeGuardError {
166 &self.source
167 }
168
169 /// Consume this boundary error and return its original typed failure.
170 pub fn into_typed_source(self) -> ShapeGuardError {
171 self.source
172 }
173
174 #[cfg(feature = "autodiff")]
175 fn record_for_ad_boundary(&self) {
176 if let Ok(mut deferred) = self.deferred.lock() {
177 if deferred.is_none() {
178 *deferred = Some(self.source.clone());
179 }
180 }
181 }
182}
183
184impl std::fmt::Display for ShapeGuardFailure {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 self.source.fmt(f)
187 }
188}
189
190impl std::error::Error for ShapeGuardFailure {
191 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
192 Some(&self.source)
193 }
194}
195
196impl PartialEq for ShapeGuardFailure {
197 fn eq(&self, other: &Self) -> bool {
198 self.source == other.source
199 }
200}
201
202impl Eq for ShapeGuardFailure {}
203
204/// Global metadata registry.
205///
206/// Stored as a tokenized stack per value key: duplicate scoped registrations
207/// shadow older metadata while they are live, and dropping scopes in any order
208/// removes only the matching token. `ShapeGuardContext::metadata_of` reaches into
209/// the registry lazily via [`lookup_global_metadata`] and caches the result into
210/// the context's local map.
211///
212/// Earlier designs either cloned the whole map up-front into each AD
213/// `ShapeGuardContext` or kept the map in an `Arc` and cloned on every write.
214/// Both variants were quadratic across the monotonically growing registry and
215/// dominated oracle_replay runtime.
216static GLOBAL_METADATA: OnceLock<Mutex<GlobalMetadataMap>> = OnceLock::new();
217static NEXT_GLOBAL_METADATA_TOKEN: AtomicU64 = AtomicU64::new(0);
218
219fn global_metadata_registry() -> &'static Mutex<GlobalMetadataMap> {
220 GLOBAL_METADATA.get_or_init(|| Mutex::new(HashMap::new()))
221}
222
223/// Lifetime token for graph-scoped global metadata.
224///
225/// Dropping the last frontend owner of a traced graph drops this scope and
226/// releases the metadata keys that were registered for that graph graph.
227#[doc(hidden)]
228#[derive(Debug)]
229pub struct GlobalMetadataScope {
230 registrations: Vec<ScopedGlobalMetadataRegistration>,
231}
232
233impl Drop for GlobalMetadataScope {
234 fn drop(&mut self) {
235 release_scoped_global_metadata(&self.registrations);
236 }
237}
238
239/// Per-value tensor metadata used by AD rules.
240///
241/// Shape information is stored as per-axis [`ShapeExtent`] values. Callers must
242/// explicitly choose whether they need an exact shape or only a known bound.
243///
244/// # Examples
245///
246/// ```
247/// use tenferro_ops::{SymDim, TensorMeta};
248/// use tenferro_tensor::DType;
249///
250/// let meta = TensorMeta::exact(DType::F64, vec![SymDim::from(2usize), SymDim::from(3usize)]);
251/// assert_eq!(meta.rank(), 2);
252/// ```
253#[derive(Clone, Debug, PartialEq, Eq)]
254pub struct TensorMeta {
255 /// Element dtype of the tensor value.
256 pub dtype: DType,
257 /// Per-axis shape guarantees.
258 pub extents: Vec<ShapeExtent<SymDim>>,
259}
260
261impl TensorMeta {
262 /// Construct metadata whose every axis is exact.
263 ///
264 /// # Examples
265 ///
266 /// ```
267 /// use tenferro_ops::{SymDim, TensorMeta};
268 /// use tenferro_tensor::DType;
269 ///
270 /// let meta = TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]);
271 /// assert_eq!(meta.exact_shape(), Some(vec![SymDim::from(4usize)]));
272 /// ```
273 pub fn exact(dtype: DType, shape: Vec<SymDim>) -> Self {
274 let extents = shape.iter().cloned().map(ShapeExtent::exact).collect();
275 Self { dtype, extents }
276 }
277
278 /// Construct metadata from per-axis extents.
279 ///
280 /// # Examples
281 ///
282 /// ```
283 /// use tenferro_ops::{ShapeExtent, SymDim, TensorMeta};
284 /// use tenferro_tensor::DType;
285 ///
286 /// let meta = TensorMeta::with_extents(
287 /// DType::F64,
288 /// vec![ShapeExtent::upper_bound(SymDim::from(8usize))],
289 /// );
290 /// assert_eq!(meta.exact_shape(), None);
291 /// ```
292 pub fn with_extents(dtype: DType, extents: Vec<ShapeExtent<SymDim>>) -> Self {
293 Self { dtype, extents }
294 }
295
296 /// Return the tensor rank known by this metadata record.
297 pub fn rank(&self) -> usize {
298 self.extents.len()
299 }
300
301 /// Return the per-axis shape guarantees.
302 ///
303 /// # Examples
304 ///
305 /// ```
306 /// use tenferro_ops::{SymDim, TensorMeta};
307 /// use tenferro_tensor::DType;
308 ///
309 /// let meta = TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]);
310 /// assert_eq!(meta.extents().len(), 1);
311 /// ```
312 pub fn extents(&self) -> &[ShapeExtent<SymDim>] {
313 &self.extents
314 }
315
316 /// Return the shape only when every axis is exact.
317 ///
318 /// # Examples
319 ///
320 /// ```
321 /// use tenferro_ops::{ShapeExtent, SymDim, TensorMeta};
322 /// use tenferro_tensor::DType;
323 ///
324 /// let meta = TensorMeta::with_extents(
325 /// DType::F64,
326 /// vec![ShapeExtent::upper_bound(SymDim::from(8usize))],
327 /// );
328 /// assert_eq!(meta.exact_shape(), None);
329 /// ```
330 pub fn exact_shape(&self) -> Option<Vec<SymDim>> {
331 self.extents
332 .iter()
333 .map(|extent| extent.as_exact().cloned())
334 .collect()
335 }
336
337 /// Return one known bound per axis when every axis has a bound.
338 ///
339 /// This is intentionally separate from [`TensorMeta::exact_shape`]: a bound
340 /// is not proof of the runtime size.
341 pub fn bound_shape(&self) -> Option<Vec<SymDim>> {
342 self.extents
343 .iter()
344 .map(|extent| extent.bound_expr().cloned())
345 .collect()
346 }
347}
348
349/// A recorded dimension comparison made during AD graph construction.
350///
351/// # Examples
352///
353/// ```
354/// use std::cmp::Ordering;
355/// use tenferro_ops::ShapeGuard;
356///
357/// let guard = ShapeGuard {
358/// dim_a: 5,
359/// dim_b: 3,
360/// ordering: Ordering::Greater,
361/// };
362///
363/// assert_eq!(guard.ordering, Ordering::Greater);
364/// ```
365#[derive(Clone, Debug, PartialEq, Eq)]
366pub struct ShapeGuard {
367 /// First dimension value, such as `m`.
368 pub dim_a: usize,
369 /// Second dimension value, such as `n`.
370 pub dim_b: usize,
371 /// The observed ordering `dim_a.cmp(&dim_b)`.
372 pub ordering: Ordering,
373}
374
375/// AD context providing dimension resolution, guard recording, and value metadata.
376///
377/// # Examples
378///
379/// ```
380/// use tenferro_ops::ShapeGuardContext;
381///
382/// let ctx = ShapeGuardContext::default();
383/// assert!(ctx.guards().is_empty());
384/// ```
385#[derive(Clone, Debug, Default)]
386pub struct ShapeGuardContext {
387 guards: Vec<ShapeGuard>,
388 metadata: MetadataMap,
389 shape_sources: HashMap<u64, ValueKey<StdTensorOp>>,
390 use_global_registry: bool,
391 local_keys: Option<Vec<ValueKey<StdTensorOp>>>,
392 #[cfg(feature = "autodiff")]
393 deferred_shape_error: Arc<Mutex<Option<ShapeGuardError>>>,
394 #[cfg(feature = "autodiff")]
395 extension_ad_dispatcher: Option<Arc<dyn ExtensionAdDispatcher>>,
396 #[cfg(feature = "autodiff")]
397 active_value_keys: Option<std::sync::Arc<std::collections::HashSet<ValueKey<StdTensorOp>>>>,
398 #[cfg(feature = "autodiff")]
399 transpose_primal_outputs: Option<Vec<ValueKey<StdTensorOp>>>,
400 #[cfg(feature = "autodiff")]
401 transpose_primal_outputs_used: bool,
402}
403
404impl ShapeGuardContext {
405 /// Create a context backed by the global metadata registry.
406 ///
407 /// Instead of cloning the entire global registry up-front (which used
408 /// to be O(N) per AD pass and quadratic across oracle_replay), the
409 /// context keeps a flag and lazily fetches entries from the shared
410 /// [`lookup_global_metadata`] on first miss, caching into its local
411 /// `metadata` map for subsequent reads within the same pass.
412 ///
413 /// # Examples
414 ///
415 /// ```
416 /// let ctx = tenferro_ops::ShapeGuardContext::with_global_metadata();
417 /// assert!(ctx.guards().is_empty());
418 /// ```
419 pub fn with_global_metadata() -> Self {
420 Self {
421 use_global_registry: true,
422 ..Self::default()
423 }
424 }
425
426 #[doc(hidden)]
427 /// Keep global-registry lookup enabled after a pass boundary.
428 ///
429 /// This is intentionally a no-op for cached entries: global metadata is
430 /// already read lazily on cache misses, and clearing the local cache would
431 /// also discard metadata inserted directly into this context.
432 pub fn refresh_global_metadata(&mut self) {
433 self.use_global_registry = true;
434 }
435
436 #[doc(hidden)]
437 pub fn insert_shape_source(&mut self, tensor_id: u64, key: ValueKey<StdTensorOp>) {
438 self.shape_sources.entry(tensor_id).or_insert(key);
439 }
440
441 #[doc(hidden)]
442 pub fn shape_source(&self, tensor_id: u64) -> Option<&ValueKey<StdTensorOp>> {
443 self.shape_sources.get(&tensor_id)
444 }
445
446 #[doc(hidden)]
447 #[cfg(feature = "autodiff")]
448 pub fn with_extension_ad_dispatcher(
449 mut self,
450 dispatcher: Arc<dyn ExtensionAdDispatcher>,
451 ) -> Self {
452 self.extension_ad_dispatcher = Some(dispatcher);
453 self
454 }
455
456 #[doc(hidden)]
457 #[cfg(feature = "autodiff")]
458 pub(crate) fn extension_ad_dispatcher(&self) -> Option<Arc<dyn ExtensionAdDispatcher>> {
459 self.extension_ad_dispatcher.as_ref().map(Arc::clone)
460 }
461
462 #[cfg(feature = "autodiff")]
463 pub fn with_linearize_active_values(
464 mut self,
465 keys: std::sync::Arc<std::collections::HashSet<ValueKey<StdTensorOp>>>,
466 ) -> Self {
467 self.active_value_keys = Some(keys);
468 self
469 }
470
471 /// Whether a primal value lies on a path from the current linearize targets.
472 ///
473 /// When no active set was attached, every value is treated as active so
474 /// existing callers keep the conservative full JVP graphs.
475 #[cfg(feature = "autodiff")]
476 pub fn is_value_active_in_linearize(&self, key: &ValueKey<StdTensorOp>) -> bool {
477 self.active_value_keys
478 .as_ref()
479 .is_none_or(|set| set.contains(key))
480 }
481
482 /// Primal output keys for the operation currently being transposed.
483 ///
484 /// Primary-mode extension transpose rules such as `Eigh` use these to reuse
485 /// forward eigenvectors instead of recomputing a decomposition.
486 #[cfg(feature = "autodiff")]
487 pub fn set_transpose_primal_outputs(&mut self, keys: Option<Vec<ValueKey<StdTensorOp>>>) {
488 self.transpose_primal_outputs = keys;
489 self.transpose_primal_outputs_used = false;
490 }
491
492 /// Return the current primal outputs and mark them as consumed by this rule.
493 #[cfg(feature = "autodiff")]
494 pub fn transpose_primal_outputs(&mut self) -> Option<&[ValueKey<StdTensorOp>]> {
495 if self.transpose_primal_outputs.is_some() {
496 self.transpose_primal_outputs_used = true;
497 }
498 self.transpose_primal_outputs.as_deref()
499 }
500
501 #[cfg(feature = "autodiff")]
502 pub fn transpose_primal_outputs_were_used(&self) -> bool {
503 self.transpose_primal_outputs_used
504 }
505
506 /// Returns the guards recorded so far.
507 ///
508 /// # Examples
509 ///
510 /// ```
511 /// use tenferro_ops::ShapeGuardContext;
512 ///
513 /// let ctx = ShapeGuardContext::default();
514 /// assert_eq!(ctx.guards(), &[]);
515 /// ```
516 pub fn guards(&self) -> &[ShapeGuard] {
517 &self.guards
518 }
519
520 /// Clears all recorded guards.
521 ///
522 /// # Examples
523 ///
524 /// ```
525 /// use tenferro_ops::ShapeGuardContext;
526 ///
527 /// let mut ctx = ShapeGuardContext::default();
528 /// ctx.clear_guards();
529 /// assert!(ctx.guards().is_empty());
530 /// ```
531 pub fn clear_guards(&mut self) {
532 self.guards.clear();
533 }
534
535 /// Take the first typed shape-guard failure recorded while crossing an AD
536 /// callback boundary.
537 ///
538 /// AD callbacks expose only a message-bearing error. AD
539 /// frontends call this after the callback returns and attach the typed
540 /// value to their public runtime error.
541 #[doc(hidden)]
542 #[cfg(feature = "autodiff")]
543 pub fn take_deferred_shape_error(&mut self) -> Option<ShapeGuardError> {
544 self.deferred_shape_error
545 .lock()
546 .ok()
547 .and_then(|mut error| error.take())
548 }
549
550 /// Return the shape metadata for a value reference.
551 ///
552 /// # Examples
553 ///
554 /// ```
555 /// use computegraph::types::{ValueKey, ValueRef};
556 /// use tenferro_ops::input_key::TensorInputKey;
557 /// use tenferro_ops::std_tensor_op::StdTensorOp;
558 /// use tenferro_ops::{ShapeGuardContext, SymDim, TensorMeta};
559 /// use tenferro_tensor::DType;
560 ///
561 /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
562 /// let value = ValueRef::External(key.clone());
563 /// let mut ctx = ShapeGuardContext::default();
564 /// ctx.insert_metadata(key, TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]));
565 ///
566 /// let shape = ctx.shape_of(&value).unwrap();
567 /// assert_eq!(shape, &[SymDim::from(4usize)]);
568 /// ```
569 ///
570 /// # Errors
571 ///
572 /// Returns [`ShapeGuardError`] when the value cannot be resolved, metadata
573 /// is missing, or the metadata does not describe an exact shape.
574 pub fn shape_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<Vec<SymDim>> {
575 let key = self.resolve_key(val)?.clone();
576 self.ensure_metadata_loaded(&key);
577 let meta = self.metadata.get(&key).ok_or_else(|| {
578 self.shape_guard_failure(ShapeGuardError::MissingMetadata { key: key.clone() })
579 })?;
580 meta.exact_shape()
581 .ok_or_else(|| self.shape_guard_failure(ShapeGuardError::NonExactShape { key }))
582 }
583
584 /// Return the rank for a value reference without requiring exact extents.
585 ///
586 /// Use this when an AD rule only needs axis count or needs to build
587 /// runtime-shape references. Calling [`ShapeGuardContext::shape_of`] in those
588 /// cases would reject valid values such as `DynamicTruncate` outputs whose
589 /// runtime extent is known only as an upper bound.
590 ///
591 /// # Examples
592 ///
593 /// ```
594 /// use computegraph::types::{ValueKey, ValueRef};
595 /// use tenferro_ops::input_key::TensorInputKey;
596 /// use tenferro_ops::std_tensor_op::StdTensorOp;
597 /// use tenferro_ops::{ShapeExtent, ShapeGuardContext, SymDim, TensorMeta};
598 /// use tenferro_tensor::DType;
599 ///
600 /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
601 /// let value = ValueRef::External(key.clone());
602 /// let mut ctx = ShapeGuardContext::default();
603 /// ctx.insert_metadata(
604 /// key,
605 /// TensorMeta::with_extents(DType::F64, vec![ShapeExtent::upper_bound(SymDim::from(8usize))]),
606 /// );
607 ///
608 /// assert_eq!(ctx.rank_of(&value).unwrap(), 1);
609 /// ```
610 ///
611 /// # Errors
612 ///
613 /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
614 /// metadata is unavailable.
615 pub fn rank_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<usize> {
616 self.metadata_of(val).map(TensorMeta::rank)
617 }
618
619 /// Return per-axis shape guarantees for a value reference.
620 ///
621 /// # Examples
622 ///
623 /// ```
624 /// use computegraph::types::{ValueKey, ValueRef};
625 /// use tenferro_ops::input_key::TensorInputKey;
626 /// use tenferro_ops::std_tensor_op::StdTensorOp;
627 /// use tenferro_ops::{ShapeExtent, ShapeGuardContext, SymDim, TensorMeta};
628 /// use tenferro_tensor::DType;
629 ///
630 /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
631 /// let value = ValueRef::External(key.clone());
632 /// let mut ctx = ShapeGuardContext::default();
633 /// ctx.insert_metadata(
634 /// key,
635 /// TensorMeta::with_extents(DType::F64, vec![ShapeExtent::upper_bound(SymDim::from(8usize))]),
636 /// );
637 ///
638 /// let extents = ctx.extents_of(&value).unwrap();
639 /// assert_eq!(extents[0], ShapeExtent::upper_bound(SymDim::from(8usize)));
640 /// ```
641 ///
642 /// # Errors
643 ///
644 /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
645 /// metadata is unavailable.
646 pub fn extents_of(
647 &mut self,
648 val: &ValueRef<StdTensorOp>,
649 ) -> ShapeGuardResult<&[ShapeExtent<SymDim>]> {
650 self.metadata_of(val).map(TensorMeta::extents)
651 }
652
653 /// Return the exact shape for a value reference, if all axes are exact.
654 ///
655 /// # Examples
656 ///
657 /// ```
658 /// use computegraph::types::{ValueKey, ValueRef};
659 /// use tenferro_ops::input_key::TensorInputKey;
660 /// use tenferro_ops::std_tensor_op::StdTensorOp;
661 /// use tenferro_ops::{ShapeExtent, ShapeGuardContext, SymDim, TensorMeta};
662 /// use tenferro_tensor::DType;
663 ///
664 /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
665 /// let value = ValueRef::External(key.clone());
666 /// let mut ctx = ShapeGuardContext::default();
667 /// ctx.insert_metadata(
668 /// key,
669 /// TensorMeta::with_extents(DType::F64, vec![ShapeExtent::upper_bound(SymDim::from(8usize))]),
670 /// );
671 ///
672 /// let maybe_shape = ctx.exact_shape_of(&value).unwrap();
673 /// assert_eq!(maybe_shape, None);
674 /// ```
675 ///
676 /// # Errors
677 ///
678 /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
679 /// metadata is unavailable.
680 pub fn exact_shape_of(
681 &mut self,
682 val: &ValueRef<StdTensorOp>,
683 ) -> ShapeGuardResult<Option<Vec<SymDim>>> {
684 self.metadata_of(val).map(TensorMeta::exact_shape)
685 }
686
687 #[doc(hidden)]
688 pub fn shape_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<Vec<SymDim>> {
689 self.metadata_if_available(val)
690 .and_then(TensorMeta::exact_shape)
691 }
692
693 /// Return the dtype metadata for a value reference.
694 ///
695 /// # Examples
696 ///
697 /// ```
698 /// use computegraph::types::{ValueKey, ValueRef};
699 /// use tenferro_ops::input_key::TensorInputKey;
700 /// use tenferro_ops::std_tensor_op::StdTensorOp;
701 /// use tenferro_ops::{ShapeGuardContext, SymDim, TensorMeta};
702 /// use tenferro_tensor::DType;
703 ///
704 /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
705 /// let value = ValueRef::External(key.clone());
706 /// let mut ctx = ShapeGuardContext::default();
707 /// ctx.insert_metadata(key, TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]));
708 ///
709 /// let dtype = ctx.dtype_of(&value).unwrap();
710 /// assert_eq!(dtype, DType::F64);
711 /// ```
712 ///
713 /// # Errors
714 ///
715 /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
716 /// metadata is unavailable.
717 pub fn dtype_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<DType> {
718 self.metadata_of(val).map(|meta| meta.dtype)
719 }
720
721 /// Return the complete metadata record for a value reference.
722 ///
723 /// # Examples
724 ///
725 /// ```
726 /// use computegraph::types::{ValueKey, ValueRef};
727 /// use tenferro_ops::input_key::TensorInputKey;
728 /// use tenferro_ops::std_tensor_op::StdTensorOp;
729 /// use tenferro_ops::{ShapeGuardContext, SymDim, TensorMeta};
730 /// use tenferro_tensor::DType;
731 ///
732 /// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 1 });
733 /// let value = ValueRef::External(key.clone());
734 /// let mut ctx = ShapeGuardContext::default();
735 /// ctx.insert_metadata(key, TensorMeta::exact(DType::F64, vec![SymDim::from(4usize)]));
736 ///
737 /// let meta = ctx.metadata_of(&value).unwrap();
738 /// assert_eq!(meta.dtype, DType::F64);
739 /// ```
740 ///
741 /// # Errors
742 ///
743 /// Returns [`ShapeGuardError`] when the value cannot be resolved or its
744 /// metadata is unavailable.
745 pub fn metadata_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<&TensorMeta> {
746 let key = self.resolve_key(val)?.clone();
747 self.ensure_metadata_loaded(&key);
748 self.metadata
749 .get(&key)
750 .ok_or_else(|| self.shape_guard_failure(ShapeGuardError::MissingMetadata { key }))
751 }
752
753 #[doc(hidden)]
754 pub fn metadata_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<&TensorMeta> {
755 let key = self.resolve_key_if_available(val)?.clone();
756 self.ensure_metadata_loaded(&key);
757 self.metadata.get(&key)
758 }
759
760 #[doc(hidden)]
761 pub fn attach_graph(&mut self, graph: &Graph<StdTensorOp>) {
762 self.local_keys = Some(graph.values().iter().map(|node| node.key.clone()).collect());
763 }
764
765 #[doc(hidden)]
766 pub fn insert_metadata(&mut self, key: ValueKey<StdTensorOp>, meta: TensorMeta) {
767 self.metadata.insert(key, meta);
768 }
769
770 #[doc(hidden)]
771 pub fn extend_metadata<I>(&mut self, entries: I)
772 where
773 I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
774 {
775 self.metadata.extend(entries);
776 }
777
778 fn resolve_key_if_available<'a>(
779 &'a self,
780 val: &'a ValueRef<StdTensorOp>,
781 ) -> Option<&'a ValueKey<StdTensorOp>> {
782 match val {
783 ValueRef::External(key) => Some(key),
784 ValueRef::Local(local_id) => self
785 .local_keys
786 .as_ref()
787 .and_then(|keys| keys.get(*local_id)),
788 }
789 }
790
791 fn resolve_key<'a>(
792 &'a self,
793 val: &'a ValueRef<StdTensorOp>,
794 ) -> ShapeGuardResult<&'a ValueKey<StdTensorOp>> {
795 match val {
796 ValueRef::External(key) => Ok(key),
797 ValueRef::Local(local_id) if self.local_keys.is_none() => Err(self
798 .shape_guard_failure(ShapeGuardError::LocalWithoutAttachedGraph {
799 local_id: *local_id,
800 })),
801 ValueRef::Local(local_id) => self
802 .local_keys
803 .as_ref()
804 .and_then(|keys| keys.get(*local_id))
805 .ok_or_else(|| {
806 self.shape_guard_failure(ShapeGuardError::LocalOutOfBounds {
807 local_id: *local_id,
808 })
809 }),
810 }
811 }
812
813 #[cfg(feature = "autodiff")]
814 fn shape_guard_failure(&self, source: ShapeGuardError) -> ShapeGuardFailure {
815 ShapeGuardFailure::new(source, Arc::clone(&self.deferred_shape_error))
816 }
817
818 #[cfg(not(feature = "autodiff"))]
819 fn shape_guard_failure(&self, source: ShapeGuardError) -> ShapeGuardFailure {
820 ShapeGuardFailure::new(source)
821 }
822
823 fn ensure_metadata_loaded(&mut self, key: &ValueKey<StdTensorOp>) {
824 if !self.metadata.contains_key(key) && self.use_global_registry {
825 if let Ok(Some(meta)) = lookup_global_metadata(key) {
826 self.metadata.insert(key.clone(), meta);
827 }
828 }
829 }
830}
831
832/// Look up a single metadata entry from the global registry.
833///
834/// Locks the registry briefly for a single `HashMap::get` + clone.
835///
836/// # Examples
837///
838/// ```
839/// use computegraph::types::ValueKey;
840/// use tenferro_ops::ad::context::lookup_global_metadata;
841/// use tenferro_ops::input_key::TensorInputKey;
842/// use tenferro_ops::std_tensor_op::StdTensorOp;
843///
844/// let key = ValueKey::<StdTensorOp>::Input(TensorInputKey::User { id: 99 });
845/// let meta = lookup_global_metadata(&key).unwrap();
846/// assert!(meta.is_none());
847/// ```
848///
849/// # Errors
850///
851/// Returns [`MetadataRegistryError::LockPoisoned`] when the global metadata
852/// registry lock is poisoned.
853pub fn lookup_global_metadata(
854 key: &ValueKey<StdTensorOp>,
855) -> Result<Option<TensorMeta>, MetadataRegistryError> {
856 let guard = global_metadata_registry()
857 .lock()
858 .map_err(|_| MetadataRegistryError::LockPoisoned)?;
859 Ok(guard
860 .get(key)
861 .and_then(|entry| entry.stack.last())
862 .map(|registration| registration.meta.clone()))
863}
864
865#[doc(hidden)]
866///
867/// # Errors
868///
869/// Returns [`MetadataRegistryError::LockPoisoned`] when the global metadata
870/// registry lock is poisoned.
871pub fn register_scoped_global_metadata_batch<I>(
872 entries: I,
873) -> Result<GlobalMetadataScope, MetadataRegistryError>
874where
875 I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
876{
877 let mut guard = global_metadata_registry()
878 .lock()
879 .map_err(|_| MetadataRegistryError::LockPoisoned)?;
880 let mut registrations = Vec::new();
881 for (key, meta) in entries {
882 let token = NEXT_GLOBAL_METADATA_TOKEN.fetch_add(1, AtomicOrdering::Relaxed);
883 let entry = guard
884 .entry(key.clone())
885 .or_insert_with(|| GlobalMetadataEntry { stack: Vec::new() });
886 entry.stack.push(GlobalMetadataRegistration { token, meta });
887 registrations.push(ScopedGlobalMetadataRegistration { key, token });
888 }
889 Ok(GlobalMetadataScope { registrations })
890}
891
892fn release_scoped_global_metadata(registrations: &[ScopedGlobalMetadataRegistration]) {
893 let Ok(mut guard) = global_metadata_registry().lock() else {
894 // Drop cannot return an error. Failing closed here avoids reading or
895 // mutating data from a poisoned registry at the cost of leaking entries
896 // until process exit.
897 return;
898 };
899 for registration in registrations {
900 let should_remove = if let Some(entry) = guard.get_mut(®istration.key) {
901 if let Some(position) = entry
902 .stack
903 .iter()
904 .rposition(|candidate| candidate.token == registration.token)
905 {
906 entry.stack.remove(position);
907 }
908 entry.stack.is_empty()
909 } else {
910 false
911 };
912 if should_remove {
913 guard.remove(®istration.key);
914 }
915 }
916}
917
918/// Resolve a [`DimExpr`] to a concrete `usize`.
919#[doc(hidden)]
920pub fn resolve_dim(dim: &DimExpr) -> Result<usize, DimExprEvalError> {
921 dim.eval(&[])
922}
923
924/// Resolve matrix dimensions and record their ordering as a guard.
925#[doc(hidden)]
926pub fn resolve_and_guard(
927 m: &DimExpr,
928 n: &DimExpr,
929 ctx: &mut ShapeGuardContext,
930) -> Result<(usize, usize), DimExprEvalError> {
931 let m_size = resolve_dim(m)?;
932 let n_size = resolve_dim(n)?;
933 ctx.guards.push(ShapeGuard {
934 dim_a: m_size,
935 dim_b: n_size,
936 ordering: m_size.cmp(&n_size),
937 });
938 Ok((m_size, n_size))
939}