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