use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
#[cfg(feature = "autodiff")]
use std::sync::Arc;
use std::sync::{Mutex, OnceLock};
use computegraph::graph::Graph;
use computegraph::types::{ValueKey, ValueRef};
use tenferro_tensor::DType;
#[cfg(feature = "autodiff")]
use crate::ad::{ADRuleError, ADRuleKind, ExtensionAdDispatcher};
use crate::dim_expr::{DimExpr, DimExprEvalError};
use crate::shape_extent::ShapeExtent;
use crate::std_tensor_op::StdTensorOp;
use crate::sym_dim::SymDim;
type MetadataMap = HashMap<ValueKey<StdTensorOp>, TensorMeta>;
type GlobalMetadataMap = HashMap<ValueKey<StdTensorOp>, GlobalMetadataEntry>;
#[derive(Clone, Debug)]
struct GlobalMetadataEntry {
stack: Vec<GlobalMetadataRegistration>,
}
#[derive(Clone, Debug)]
struct GlobalMetadataRegistration {
token: u64,
meta: TensorMeta,
}
#[derive(Clone, Debug)]
struct ScopedGlobalMetadataRegistration {
key: ValueKey<StdTensorOp>,
token: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum MetadataRegistryError {
#[error("AD global metadata registry lock poisoned")]
LockPoisoned,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ShapeGuardError {
#[error("cannot resolve local value {local_id} without an attached graph")]
LocalWithoutAttachedGraph {
local_id: usize,
},
#[error("local value {local_id} is out of bounds for the attached graph")]
LocalOutOfBounds {
local_id: usize,
},
#[error("missing TensorMeta for {key:?}")]
MissingMetadata {
key: ValueKey<StdTensorOp>,
},
#[error("TensorMeta for {key:?} does not have an exact shape; query extents instead")]
NonExactShape {
key: ValueKey<StdTensorOp>,
},
}
pub type ShapeGuardResult<T> = Result<T, ShapeGuardFailure>;
#[cfg(feature = "autodiff")]
impl From<ShapeGuardFailure> for ADRuleError {
fn from(err: ShapeGuardFailure) -> Self {
err.record_for_ad_boundary();
ADRuleError::invalid_input(
"tenferro.shape_guard",
ADRuleKind::Jvp,
err.typed_source().to_string(),
)
}
}
#[derive(Clone, Debug)]
pub struct ShapeGuardFailure {
source: ShapeGuardError,
#[cfg(feature = "autodiff")]
deferred: Arc<Mutex<Option<ShapeGuardError>>>,
}
impl ShapeGuardFailure {
#[cfg(feature = "autodiff")]
fn new(source: ShapeGuardError, deferred: Arc<Mutex<Option<ShapeGuardError>>>) -> Self {
Self { source, deferred }
}
#[cfg(not(feature = "autodiff"))]
fn new(source: ShapeGuardError) -> Self {
Self { source }
}
pub fn typed_source(&self) -> &ShapeGuardError {
&self.source
}
pub fn into_typed_source(self) -> ShapeGuardError {
self.source
}
#[cfg(feature = "autodiff")]
fn record_for_ad_boundary(&self) {
if let Ok(mut deferred) = self.deferred.lock() {
if deferred.is_none() {
*deferred = Some(self.source.clone());
}
}
}
}
impl std::fmt::Display for ShapeGuardFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.source.fmt(f)
}
}
impl std::error::Error for ShapeGuardFailure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
impl PartialEq for ShapeGuardFailure {
fn eq(&self, other: &Self) -> bool {
self.source == other.source
}
}
impl Eq for ShapeGuardFailure {}
static GLOBAL_METADATA: OnceLock<Mutex<GlobalMetadataMap>> = OnceLock::new();
static NEXT_GLOBAL_METADATA_TOKEN: AtomicU64 = AtomicU64::new(0);
fn global_metadata_registry() -> &'static Mutex<GlobalMetadataMap> {
GLOBAL_METADATA.get_or_init(|| Mutex::new(HashMap::new()))
}
#[doc(hidden)]
#[derive(Debug)]
pub struct GlobalMetadataScope {
registrations: Vec<ScopedGlobalMetadataRegistration>,
}
impl Drop for GlobalMetadataScope {
fn drop(&mut self) {
release_scoped_global_metadata(&self.registrations);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TensorMeta {
pub dtype: DType,
pub extents: Vec<ShapeExtent<SymDim>>,
pub scalar_identity: Option<&'static str>,
}
impl TensorMeta {
pub fn exact(dtype: DType, shape: Vec<SymDim>) -> Self {
let extents = shape.iter().cloned().map(ShapeExtent::exact).collect();
Self {
dtype,
extents,
scalar_identity: None,
}
}
pub fn with_extents(dtype: DType, extents: Vec<ShapeExtent<SymDim>>) -> Self {
Self {
dtype,
extents,
scalar_identity: None,
}
}
#[must_use]
pub fn with_scalar_identity(mut self, identity: &'static str) -> Self {
self.scalar_identity = Some(identity);
self
}
#[must_use]
pub const fn scalar_identity(&self) -> Option<&'static str> {
self.scalar_identity
}
pub fn rank(&self) -> usize {
self.extents.len()
}
pub fn extents(&self) -> &[ShapeExtent<SymDim>] {
&self.extents
}
pub fn exact_shape(&self) -> Option<Vec<SymDim>> {
self.extents
.iter()
.map(|extent| extent.as_exact().cloned())
.collect()
}
pub fn bound_shape(&self) -> Option<Vec<SymDim>> {
self.extents
.iter()
.map(|extent| extent.bound_expr().cloned())
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShapeGuard {
pub dim_a: usize,
pub dim_b: usize,
pub ordering: Ordering,
}
#[derive(Clone, Debug, Default)]
pub struct ShapeGuardContext {
guards: Vec<ShapeGuard>,
metadata: MetadataMap,
shape_sources: HashMap<u64, ValueKey<StdTensorOp>>,
use_global_registry: bool,
local_keys: Option<Vec<ValueKey<StdTensorOp>>>,
#[cfg(feature = "autodiff")]
deferred_shape_error: Arc<Mutex<Option<ShapeGuardError>>>,
#[cfg(feature = "autodiff")]
extension_ad_dispatcher: Option<Arc<dyn ExtensionAdDispatcher>>,
#[cfg(feature = "autodiff")]
active_value_keys: Option<std::sync::Arc<std::collections::HashSet<ValueKey<StdTensorOp>>>>,
#[cfg(feature = "autodiff")]
transpose_primal_outputs: Option<Vec<ValueKey<StdTensorOp>>>,
#[cfg(feature = "autodiff")]
transpose_primal_outputs_used: bool,
}
impl ShapeGuardContext {
pub fn with_global_metadata() -> Self {
Self {
use_global_registry: true,
..Self::default()
}
}
#[doc(hidden)]
pub fn refresh_global_metadata(&mut self) {
self.use_global_registry = true;
}
#[doc(hidden)]
pub fn insert_shape_source(&mut self, tensor_id: u64, key: ValueKey<StdTensorOp>) {
self.shape_sources.entry(tensor_id).or_insert(key);
}
#[doc(hidden)]
pub fn shape_source(&self, tensor_id: u64) -> Option<&ValueKey<StdTensorOp>> {
self.shape_sources.get(&tensor_id)
}
#[doc(hidden)]
#[cfg(feature = "autodiff")]
pub fn with_extension_ad_dispatcher(
mut self,
dispatcher: Arc<dyn ExtensionAdDispatcher>,
) -> Self {
self.extension_ad_dispatcher = Some(dispatcher);
self
}
#[cfg(feature = "autodiff")]
pub fn with_linearize_active_values(
mut self,
keys: std::sync::Arc<std::collections::HashSet<ValueKey<StdTensorOp>>>,
) -> Self {
self.active_value_keys = Some(keys);
self
}
#[cfg(feature = "autodiff")]
pub fn is_value_active_in_linearize(&self, key: &ValueKey<StdTensorOp>) -> bool {
self.active_value_keys
.as_ref()
.is_none_or(|set| set.contains(key))
}
#[cfg(feature = "autodiff")]
pub fn set_transpose_primal_outputs(&mut self, keys: Option<Vec<ValueKey<StdTensorOp>>>) {
self.transpose_primal_outputs = keys;
self.transpose_primal_outputs_used = false;
}
#[cfg(feature = "autodiff")]
pub fn transpose_primal_outputs(&mut self) -> Option<&[ValueKey<StdTensorOp>]> {
if self.transpose_primal_outputs.is_some() {
self.transpose_primal_outputs_used = true;
}
self.transpose_primal_outputs.as_deref()
}
#[cfg(feature = "autodiff")]
pub fn transpose_primal_outputs_were_used(&self) -> bool {
self.transpose_primal_outputs_used
}
pub fn guards(&self) -> &[ShapeGuard] {
&self.guards
}
pub fn clear_guards(&mut self) {
self.guards.clear();
}
#[doc(hidden)]
#[cfg(feature = "autodiff")]
pub fn take_deferred_shape_error(&mut self) -> Option<ShapeGuardError> {
self.deferred_shape_error
.lock()
.ok()
.and_then(|mut error| error.take())
}
pub fn shape_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<Vec<SymDim>> {
let key = self.resolve_key(val)?.clone();
self.ensure_metadata_loaded(&key);
let meta = self.metadata.get(&key).ok_or_else(|| {
self.shape_guard_failure(ShapeGuardError::MissingMetadata { key: key.clone() })
})?;
meta.exact_shape()
.ok_or_else(|| self.shape_guard_failure(ShapeGuardError::NonExactShape { key }))
}
pub fn rank_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<usize> {
self.metadata_of(val).map(TensorMeta::rank)
}
pub fn extents_of(
&mut self,
val: &ValueRef<StdTensorOp>,
) -> ShapeGuardResult<&[ShapeExtent<SymDim>]> {
self.metadata_of(val).map(TensorMeta::extents)
}
pub fn exact_shape_of(
&mut self,
val: &ValueRef<StdTensorOp>,
) -> ShapeGuardResult<Option<Vec<SymDim>>> {
self.metadata_of(val).map(TensorMeta::exact_shape)
}
#[doc(hidden)]
pub fn shape_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<Vec<SymDim>> {
self.metadata_if_available(val)
.and_then(TensorMeta::exact_shape)
}
pub fn dtype_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<DType> {
self.metadata_of(val).map(|meta| meta.dtype)
}
pub fn metadata_of(&mut self, val: &ValueRef<StdTensorOp>) -> ShapeGuardResult<&TensorMeta> {
let key = self.resolve_key(val)?.clone();
self.ensure_metadata_loaded(&key);
self.metadata
.get(&key)
.ok_or_else(|| self.shape_guard_failure(ShapeGuardError::MissingMetadata { key }))
}
#[doc(hidden)]
pub fn metadata_if_available(&mut self, val: &ValueRef<StdTensorOp>) -> Option<&TensorMeta> {
let key = self.resolve_key_if_available(val)?.clone();
self.ensure_metadata_loaded(&key);
self.metadata.get(&key)
}
#[doc(hidden)]
pub fn attach_graph(&mut self, graph: &Graph<StdTensorOp>) {
self.local_keys = Some(graph.values().iter().map(|node| node.key.clone()).collect());
}
#[doc(hidden)]
pub fn insert_metadata(&mut self, key: ValueKey<StdTensorOp>, meta: TensorMeta) {
self.metadata.insert(key, meta);
}
#[doc(hidden)]
pub fn extend_metadata<I>(&mut self, entries: I)
where
I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
{
self.metadata.extend(entries);
}
fn resolve_key_if_available<'a>(
&'a self,
val: &'a ValueRef<StdTensorOp>,
) -> Option<&'a ValueKey<StdTensorOp>> {
match val {
ValueRef::External(key) => Some(key),
ValueRef::Local(local_id) => self
.local_keys
.as_ref()
.and_then(|keys| keys.get(*local_id)),
}
}
fn resolve_key<'a>(
&'a self,
val: &'a ValueRef<StdTensorOp>,
) -> ShapeGuardResult<&'a ValueKey<StdTensorOp>> {
match val {
ValueRef::External(key) => Ok(key),
ValueRef::Local(local_id) if self.local_keys.is_none() => Err(self
.shape_guard_failure(ShapeGuardError::LocalWithoutAttachedGraph {
local_id: *local_id,
})),
ValueRef::Local(local_id) => self
.local_keys
.as_ref()
.and_then(|keys| keys.get(*local_id))
.ok_or_else(|| {
self.shape_guard_failure(ShapeGuardError::LocalOutOfBounds {
local_id: *local_id,
})
}),
}
}
#[cfg(feature = "autodiff")]
fn shape_guard_failure(&self, source: ShapeGuardError) -> ShapeGuardFailure {
ShapeGuardFailure::new(source, Arc::clone(&self.deferred_shape_error))
}
#[cfg(not(feature = "autodiff"))]
fn shape_guard_failure(&self, source: ShapeGuardError) -> ShapeGuardFailure {
ShapeGuardFailure::new(source)
}
fn ensure_metadata_loaded(&mut self, key: &ValueKey<StdTensorOp>) {
if !self.metadata.contains_key(key) && self.use_global_registry {
if let Ok(Some(meta)) = lookup_global_metadata(key) {
self.metadata.insert(key.clone(), meta);
}
}
}
}
pub fn lookup_global_metadata(
key: &ValueKey<StdTensorOp>,
) -> Result<Option<TensorMeta>, MetadataRegistryError> {
let guard = global_metadata_registry()
.lock()
.map_err(|_| MetadataRegistryError::LockPoisoned)?;
Ok(guard
.get(key)
.and_then(|entry| entry.stack.last())
.map(|registration| registration.meta.clone()))
}
#[doc(hidden)]
pub fn register_scoped_global_metadata_batch<I>(
entries: I,
) -> Result<GlobalMetadataScope, MetadataRegistryError>
where
I: IntoIterator<Item = (ValueKey<StdTensorOp>, TensorMeta)>,
{
let mut guard = global_metadata_registry()
.lock()
.map_err(|_| MetadataRegistryError::LockPoisoned)?;
let mut registrations = Vec::new();
for (key, meta) in entries {
let token = NEXT_GLOBAL_METADATA_TOKEN.fetch_add(1, AtomicOrdering::Relaxed);
let entry = guard
.entry(key.clone())
.or_insert_with(|| GlobalMetadataEntry { stack: Vec::new() });
entry.stack.push(GlobalMetadataRegistration { token, meta });
registrations.push(ScopedGlobalMetadataRegistration { key, token });
}
Ok(GlobalMetadataScope { registrations })
}
fn release_scoped_global_metadata(registrations: &[ScopedGlobalMetadataRegistration]) {
let Ok(mut guard) = global_metadata_registry().lock() else {
return;
};
for registration in registrations {
let should_remove = if let Some(entry) = guard.get_mut(®istration.key) {
if let Some(position) = entry
.stack
.iter()
.rposition(|candidate| candidate.token == registration.token)
{
entry.stack.remove(position);
}
entry.stack.is_empty()
} else {
false
};
if should_remove {
guard.remove(®istration.key);
}
}
}
#[doc(hidden)]
pub fn resolve_dim(dim: &DimExpr) -> Result<usize, DimExprEvalError> {
dim.eval(&[])
}
#[doc(hidden)]
pub fn resolve_and_guard(
m: &DimExpr,
n: &DimExpr,
ctx: &mut ShapeGuardContext,
) -> Result<(usize, usize), DimExprEvalError> {
let m_size = resolve_dim(m)?;
let n_size = resolve_dim(n)?;
ctx.guards.push(ShapeGuard {
dim_a: m_size,
dim_b: n_size,
ordering: m_size.cmp(&n_size),
});
Ok((m_size, n_size))
}