Skip to main content

tenferro_ad/
eager.rs

1use std::cell::{Cell, RefCell};
2use std::cmp::Reverse;
3use std::collections::HashMap;
4use std::env;
5use std::fmt;
6use std::sync::{Arc, Mutex, MutexGuard, OnceLock, Weak};
7use std::time::{Duration, Instant};
8
9use crate::extension_cache::{ExtensionCacheLimits, ExtensionCacheStore};
10use crate::extension_runtime::{ExtensionExecutor, ExtensionRuntimeRegistryError};
11#[cfg(test)]
12use computegraph::graph::Graph;
13use computegraph::ValueKey;
14#[cfg(test)]
15use computegraph::ValueRef;
16use tenferro_cpu::CpuBackend;
17#[cfg(feature = "cuda")]
18use tenferro_gpu::CudaBackend;
19#[cfg(feature = "webgpu")]
20use tenferro_gpu::WebGpuBackend;
21use tenferro_ops::input_key::TensorInputKey;
22use tenferro_ops::std_tensor_op::StdTensorOp;
23use tenferro_ops::ExtensionRuleSet;
24use tenferro_ops::ShapeGuardContext;
25use tenferro_runtime::ad_support::ones_tensor;
26#[cfg(test)]
27use tenferro_tensor::BackendSessionHost;
28use tenferro_tensor::{
29    CacheStats, DType, Tensor, TensorBackend, TensorElementwise, TensorRead, TensorValue,
30    TypedTensor,
31};
32use tidu::eager::{self, EagerInput, EagerOutput, KeySource, RecordedGraph, Recorder, Trace};
33use tidu::{ADRuleError, ADRuleKind, LinearizedGraph};
34
35use self::backward::TenferroBackwardCallbacks;
36use self::functional::{functional_jvp, functional_vjp_optional};
37use crate::eager_backend::EagerBackend;
38#[cfg(test)]
39use crate::eager_exec::exec_standard_op_on_tensor_reads_in_session;
40use crate::eager_exec::{
41    exec_op_on_tensor_reads_with_extension_executor, exec_op_on_tensors_with_extension_executor,
42};
43use crate::error::{ContextId, Error, Result};
44#[cfg(test)]
45use crate::metadata::push_metadata_scope;
46use crate::metadata::{
47    metadata_scopes_for_scope, register_scoped_metadata_batch, register_scoped_value_metadata,
48    tensor_meta_from_tensor, GlobalMetadataScope,
49};
50use crate::traced::next_input_key;
51use crate::transform_cache::{AdTransformCache, AdTransformCacheLimits, EagerAdTransformCacheKey};
52
53use crate::AdContext;
54
55mod backward;
56mod functional;
57
58pub(crate) type GradSlot = Arc<Mutex<Option<Arc<Tensor>>>>;
59pub(crate) type WeakGradSlot = Weak<Mutex<Option<Arc<Tensor>>>>;
60
61#[derive(Debug, Default, Clone)]
62struct EagerOpProfileEntry {
63    calls: usize,
64    total_time: Duration,
65}
66
67thread_local! {
68    static EAGER_OP_PROFILE_STATE: RefCell<HashMap<&'static str, EagerOpProfileEntry>> =
69        RefCell::new(HashMap::new());
70    static EAGER_NO_GRAD_DEPTH: Cell<usize> = const { Cell::new(0) };
71    #[cfg(test)]
72    static EAGER_OP_PROFILE_ENABLED_OVERRIDE: RefCell<Option<bool>> = const { RefCell::new(None) };
73    #[cfg(test)]
74    static EAGER_OP_PROFILE_PRINT_EVERY_OVERRIDE: RefCell<Option<Option<usize>>> = const { RefCell::new(None) };
75}
76
77pub(crate) fn eager_grad_recording_enabled() -> bool {
78    EAGER_NO_GRAD_DEPTH.with(|depth| depth.get() == 0)
79}
80
81/// Scope guard that temporarily disables eager operation recording.
82///
83/// Values computed while this guard is alive are concrete eager tensors, but
84/// they do not participate in reverse-mode gradient tracking.
85///
86/// # Examples
87///
88/// ```
89/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
90/// use tenferro_cpu::CpuBackend;
91///
92/// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
93/// let x = EagerTensor::requires_grad_in(
94///     Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap(),
95///     ctx.clone(),
96/// )?;
97/// let y = {
98///     let _guard = ctx.no_grad();
99///     x.mul(&x)?
100/// };
101/// assert!(!y.tracks_grad());
102/// # Ok::<(), tenferro_ad::Error>(())
103/// ```
104#[derive(Debug)]
105pub struct EagerNoGradGuard {
106    active: bool,
107}
108
109impl Drop for EagerNoGradGuard {
110    fn drop(&mut self) {
111        if !self.active {
112            return;
113        }
114        EAGER_NO_GRAD_DEPTH.with(|depth| {
115            depth.set(depth.get().saturating_sub(1));
116        });
117        self.active = false;
118    }
119}
120
121pub(crate) fn eager_op_profile_enabled() -> bool {
122    #[cfg(test)]
123    if let Some(value) = EAGER_OP_PROFILE_ENABLED_OVERRIDE.with(|state| *state.borrow()) {
124        return value;
125    }
126
127    static ENABLED: OnceLock<bool> = OnceLock::new();
128    *ENABLED.get_or_init(|| env::var("TENFERRO_PROFILE_EAGER_OP_AGG").is_ok())
129}
130
131pub(crate) fn record_eager_op_profile(section: &'static str, elapsed: Duration) {
132    if !eager_op_profile_enabled() {
133        return;
134    }
135    EAGER_OP_PROFILE_STATE.with(|state| {
136        let mut state = state.borrow_mut();
137        let entry = state.entry(section).or_default();
138        entry.calls += 1;
139        entry.total_time += elapsed;
140    });
141}
142
143pub(crate) fn profile_eager_op_section<T>(section: &'static str, f: impl FnOnce() -> T) -> T {
144    if !eager_op_profile_enabled() {
145        return f();
146    }
147    let started = Instant::now();
148    let result = f();
149    record_eager_op_profile(section, started.elapsed());
150    result
151}
152
153pub(crate) fn maybe_print_eager_op_profile() {
154    if !eager_op_profile_enabled() {
155        return;
156    }
157    let Some(print_every) = eager_op_profile_print_every() else {
158        return;
159    };
160    if print_every == 0 {
161        return;
162    }
163
164    let should_print = EAGER_OP_PROFILE_STATE.with(|state| {
165        state
166            .borrow()
167            .get("nary_op.total")
168            .is_some_and(|entry| entry.calls % print_every == 0)
169    });
170    if should_print {
171        print_and_reset_eager_op_profile();
172    }
173}
174
175fn eager_op_profile_print_every() -> Option<usize> {
176    #[cfg(test)]
177    if let Some(value) = EAGER_OP_PROFILE_PRINT_EVERY_OVERRIDE.with(|state| *state.borrow()) {
178        return value;
179    }
180
181    env::var("TENFERRO_PROFILE_EAGER_OP_PRINT_EVERY")
182        .ok()?
183        .parse()
184        .ok()
185}
186
187pub(crate) fn print_and_reset_eager_op_profile() {
188    EAGER_OP_PROFILE_STATE.with(|state| {
189        let mut entries: Vec<_> = state
190            .borrow()
191            .iter()
192            .map(|(section, entry)| (*section, entry.clone()))
193            .collect();
194        state.borrow_mut().clear();
195        entries.sort_by_key(|(_, entry)| Reverse(entry.total_time));
196
197        eprintln!("=== tenferro eager op profile ===");
198        for (section, entry) in entries {
199            let Some(per_call_us) = eager_op_profile_per_call_us(&entry) else {
200                continue;
201            };
202            eprintln!(
203                "{section}: calls={} total={:.6}ms per_call={:.3}us",
204                entry.calls,
205                entry.total_time.as_secs_f64() * 1.0e3,
206                per_call_us,
207            );
208        }
209    });
210}
211
212fn eager_op_profile_per_call_us(entry: &EagerOpProfileEntry) -> Option<f64> {
213    (entry.calls != 0).then(|| entry.total_time.as_secs_f64() * 1.0e6 / entry.calls as f64)
214}
215
216/// Stats for caches owned by an [`EagerRuntime`].
217///
218/// `retained_bytes` fields are logical payload estimates, not process RSS.
219#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220pub struct EagerRuntimeCacheStats {
221    /// Generic extension runtime caches.
222    pub extensions: CacheStats,
223    /// Eager AD transform memoization cache.
224    pub ad_transforms: CacheStats,
225}
226
227#[cfg(test)]
228pub(crate) struct EagerGraphExecution {
229    pub(crate) outputs: Vec<Arc<Tensor>>,
230    pub(crate) retained_values: HashMap<ValueKey<StdTensorOp>, Arc<Tensor>>,
231}
232
233/// Shared eager execution context for tensors on a backend.
234///
235/// Reusing one context lets eager tensors share backend state, extension
236/// runtime caches, and gradient storage across a computation.
237///
238/// # Examples
239///
240/// ```
241/// use tenferro_cpu::CpuBackend;
242/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
243///
244/// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
245/// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap(), ctx.clone()).unwrap();
246/// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap(), ctx).unwrap();
247/// let z = x.add(&y).unwrap();
248///
249/// assert_eq!(z.materialized().unwrap().as_slice::<f64>().unwrap(), &[3.0]);
250/// ```
251pub struct EagerRuntime {
252    id: ContextId,
253    pub(crate) backend: Mutex<EagerBackend>,
254    pub(crate) extension_executor: Mutex<ExtensionExecutor<EagerBackend>>,
255    extension_rules: Option<ExtensionRuleSet>,
256    grad_slots: Mutex<HashMap<ValueKey<StdTensorOp>, WeakGradSlot>>,
257    value_records: Mutex<HashMap<ValueKey<StdTensorOp>, Weak<EagerTensorRecord>>>,
258    value_ptr_records: Mutex<HashMap<usize, Weak<EagerTensorRecord>>>,
259    ad_transform_cache: Arc<AdTransformCache>,
260}
261
262impl fmt::Debug for EagerRuntime {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        let mut debug = f.debug_struct("EagerRuntime");
265        debug.field("id", &self.id);
266        match self.backend.try_lock() {
267            Ok(backend) => {
268                debug.field("backend", &*backend);
269            }
270            Err(_) => {
271                debug.field("backend", &"<locked>");
272            }
273        }
274        match self.extension_executor.try_lock() {
275            Ok(executor) => {
276                debug.field("extension_executor", &*executor);
277            }
278            Err(_) => {
279                debug.field("extension_executor", &"<locked>");
280            }
281        }
282        debug.field("has_extension_rules", &self.extension_rules.is_some());
283        match self.grad_slots.try_lock() {
284            Ok(slots) => {
285                debug.field("grad_slots_len", &slots.len());
286            }
287            Err(_) => {
288                debug.field("grad_slots_len", &"<locked>");
289            }
290        }
291        match self.value_records.try_lock() {
292            Ok(records) => {
293                debug.field("value_records_len", &records.len());
294            }
295            Err(_) => {
296                debug.field("value_records_len", &"<locked>");
297            }
298        }
299        match self.value_ptr_records.try_lock() {
300            Ok(records) => {
301                debug.field("value_ptr_records_len", &records.len());
302            }
303            Err(_) => {
304                debug.field("value_ptr_records_len", &"<locked>");
305            }
306        }
307        match self.ad_transform_cache.stats() {
308            Ok(stats) => {
309                debug.field("ad_transform_cache_stats", &stats);
310            }
311            Err(err) => {
312                debug.field("ad_transform_cache_stats", &format_args!("{err}"));
313            }
314        }
315        debug.finish_non_exhaustive()
316    }
317}
318
319impl EagerRuntime {
320    fn lock_backend(&self) -> Result<MutexGuard<'_, EagerBackend>> {
321        self.backend
322            .lock()
323            .map_err(|_| Error::Internal("backend lock poisoned".to_string()))
324    }
325
326    fn lock_extension_executor(&self) -> Result<MutexGuard<'_, ExtensionExecutor<EagerBackend>>> {
327        self.extension_executor
328            .lock()
329            .map_err(|_| Error::Internal("extension executor lock poisoned".to_string()))
330    }
331
332    fn lock_grad_slots(
333        &self,
334    ) -> Result<MutexGuard<'_, HashMap<ValueKey<StdTensorOp>, WeakGradSlot>>> {
335        self.grad_slots
336            .lock()
337            .map_err(|_| Error::Internal("gradient slot registry lock poisoned".to_string()))
338    }
339
340    fn lock_value_records(
341        &self,
342    ) -> Result<MutexGuard<'_, HashMap<ValueKey<StdTensorOp>, Weak<EagerTensorRecord>>>> {
343        self.value_records
344            .lock()
345            .map_err(|_| Error::Internal("eager value registry lock poisoned".to_string()))
346    }
347
348    fn lock_value_ptr_records(
349        &self,
350    ) -> Result<MutexGuard<'_, HashMap<usize, Weak<EagerTensorRecord>>>> {
351        self.value_ptr_records
352            .lock()
353            .map_err(|_| Error::Internal("eager value pointer registry lock poisoned".to_string()))
354    }
355
356    fn from_backend(backend: EagerBackend) -> Self {
357        Self::from_backend_with_extension_rules(backend, None)
358    }
359
360    fn from_backend_with_extension_rules(
361        backend: EagerBackend,
362        extension_rules: Option<ExtensionRuleSet>,
363    ) -> Self {
364        Self::from_backend_with_extension_rules_and_cache(
365            backend,
366            extension_rules,
367            Arc::new(AdTransformCache::new()),
368        )
369    }
370
371    fn from_backend_with_extension_rules_and_cache(
372        backend: EagerBackend,
373        extension_rules: Option<ExtensionRuleSet>,
374        ad_transform_cache: Arc<AdTransformCache>,
375    ) -> Self {
376        Self {
377            id: ContextId::fresh(),
378            backend: Mutex::new(backend),
379            extension_executor: Mutex::new(ExtensionExecutor::new()),
380            extension_rules,
381            grad_slots: Mutex::new(HashMap::new()),
382            value_records: Mutex::new(HashMap::new()),
383            value_ptr_records: Mutex::new(HashMap::new()),
384            ad_transform_cache,
385        }
386    }
387
388    /// Create a shared CPU eager execution context.
389    ///
390    /// # Examples
391    ///
392    /// ```
393    /// use tenferro_ad::EagerRuntime;
394    ///
395    /// let ctx = EagerRuntime::new();
396    /// assert_eq!(std::sync::Arc::strong_count(&ctx), 1);
397    /// ```
398    pub fn new() -> Arc<Self> {
399        Self::with_cpu_backend(CpuBackend::new())
400    }
401
402    /// Create a shared eager execution context from a configured CPU backend.
403    ///
404    /// # Examples
405    ///
406    /// ```
407    /// use tenferro_cpu::CpuBackend;
408    /// use tenferro_ad::{EagerRuntime};
409    ///
410    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::with_threads(1).unwrap());
411    /// assert_eq!(std::sync::Arc::strong_count(&ctx), 1);
412    /// ```
413    pub fn with_cpu_backend(backend: CpuBackend) -> Arc<Self> {
414        Arc::new(Self::from_backend(EagerBackend::cpu(backend)))
415    }
416
417    /// Create a shared CPU eager context with explicit AD extension rules.
418    ///
419    /// # Examples
420    ///
421    /// ```rust
422    /// use tenferro_cpu::CpuBackend;
423    /// use tenferro_ad::{AdContext, EagerRuntime};
424    ///
425    /// let ad = AdContext::builder().build().unwrap();
426    /// let ctx = EagerRuntime::with_cpu_backend_and_ad_context(CpuBackend::new(), &ad);
427    /// assert_eq!(std::sync::Arc::strong_count(&ctx), 1);
428    /// ```
429    pub fn with_cpu_backend_and_ad_context(backend: CpuBackend, ad: &AdContext) -> Arc<Self> {
430        Arc::new(Self::from_backend_with_extension_rules_and_cache(
431            EagerBackend::cpu(backend),
432            Some(ad.extension_rule_set()),
433            ad.ad_transform_cache(),
434        ))
435    }
436
437    /// Create a shared eager execution context from a configured CUDA backend.
438    ///
439    /// # Examples
440    ///
441    /// ```
442    /// use tenferro_gpu::CudaBackend;
443    /// use tenferro_ad::EagerRuntime;
444    ///
445    /// let _ctor: fn(CudaBackend) -> std::sync::Arc<EagerRuntime> =
446    ///     EagerRuntime::with_cuda_backend;
447    /// ```
448    #[cfg(feature = "cuda")]
449    pub fn with_cuda_backend(backend: CudaBackend) -> Arc<Self> {
450        Arc::new(Self::from_backend(EagerBackend::cuda(backend)))
451    }
452
453    /// Create a shared CUDA eager context with explicit AD extension rules.
454    ///
455    /// # Examples
456    ///
457    /// ```rust
458    /// use tenferro_ad::{AdContext, EagerRuntime};
459    /// use tenferro_gpu::CudaBackend;
460    ///
461    /// let _ctor: fn(CudaBackend, &AdContext) -> std::sync::Arc<EagerRuntime> =
462    ///     EagerRuntime::with_cuda_backend_and_ad_context;
463    /// ```
464    #[cfg(feature = "cuda")]
465    pub fn with_cuda_backend_and_ad_context(backend: CudaBackend, ad: &AdContext) -> Arc<Self> {
466        Arc::new(Self::from_backend_with_extension_rules_and_cache(
467            EagerBackend::cuda(backend),
468            Some(ad.extension_rule_set()),
469            ad.ad_transform_cache(),
470        ))
471    }
472
473    /// Create a shared eager execution context from a configured WebGPU backend.
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use tenferro_ad::EagerRuntime;
479    /// use tenferro_gpu::WebGpuBackend;
480    ///
481    /// let _ctor: fn(WebGpuBackend) -> std::sync::Arc<EagerRuntime> =
482    ///     EagerRuntime::with_webgpu_backend;
483    /// ```
484    #[cfg(feature = "webgpu")]
485    pub fn with_webgpu_backend(backend: WebGpuBackend) -> Arc<Self> {
486        Arc::new(Self::from_backend(EagerBackend::webgpu(backend)))
487    }
488
489    /// Create a shared WebGPU eager context with explicit AD extension rules.
490    ///
491    /// # Examples
492    ///
493    /// ```rust
494    /// use tenferro_ad::{AdContext, EagerRuntime};
495    /// use tenferro_gpu::WebGpuBackend;
496    ///
497    /// let _ctor: fn(WebGpuBackend, &AdContext) -> std::sync::Arc<EagerRuntime> =
498    ///     EagerRuntime::with_webgpu_backend_and_ad_context;
499    /// ```
500    #[cfg(feature = "webgpu")]
501    pub fn with_webgpu_backend_and_ad_context(backend: WebGpuBackend, ad: &AdContext) -> Arc<Self> {
502        Arc::new(Self::from_backend_with_extension_rules_and_cache(
503            EagerBackend::webgpu(backend),
504            Some(ad.extension_rule_set()),
505            ad.ad_transform_cache(),
506        ))
507    }
508
509    /// Return an opaque identifier for this context.
510    ///
511    /// # Examples
512    ///
513    /// ```
514    /// use tenferro_cpu::CpuBackend;
515    /// use tenferro_ad::{EagerRuntime};
516    ///
517    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
518    /// assert_ne!(ctx.id(), EagerRuntime::with_cpu_backend(CpuBackend::new()).id());
519    /// ```
520    pub fn id(&self) -> ContextId {
521        self.id
522    }
523
524    /// Disable eager operation recording on the current thread until the guard is dropped.
525    ///
526    /// This is useful for optimizer updates, metric calculations, and other
527    /// eager computations that should not become part of the AD tape.
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
533    /// use tenferro_cpu::CpuBackend;
534    ///
535    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
536    /// let x = EagerTensor::requires_grad_in(
537    ///     Tensor::from_vec_col_major(vec![1], vec![3.0_f64]).unwrap(),
538    ///     ctx.clone(),
539    /// )?;
540    /// let y = {
541    ///     let _guard = ctx.no_grad();
542    ///     x.mul(&x)?
543    /// };
544    /// assert!(!y.tracks_grad());
545    /// # Ok::<(), tenferro_ad::Error>(())
546    /// ```
547    pub fn no_grad(&self) -> EagerNoGradGuard {
548        EAGER_NO_GRAD_DEPTH.with(|depth| {
549            depth.set(depth.get().saturating_add(1));
550        });
551        EagerNoGradGuard { active: true }
552    }
553
554    /// Register one extension runtime on this eager context.
555    pub fn register_extension(
556        &self,
557        register: impl FnOnce(
558            &mut ExtensionExecutor<EagerBackend>,
559        ) -> std::result::Result<(), ExtensionRuntimeRegistryError>,
560    ) -> std::result::Result<(), ExtensionRuntimeRegistryError> {
561        let mut executor = self.extension_executor.lock().map_err(|_| {
562            ExtensionRuntimeRegistryError::PoisonedLock {
563                name: "extension executor lock",
564            }
565        })?;
566        register(&mut executor)
567    }
568
569    /// Clear generic extension runtime cache entries.
570    ///
571    /// # Examples
572    ///
573    /// ```
574    /// use tenferro_cpu::CpuBackend;
575    /// use tenferro_ad::{EagerRuntime};
576    ///
577    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
578    /// ctx.clear_extension_caches()?;
579    /// assert_eq!(ctx.cache_stats()?.extensions.entries, 0);
580    /// # Ok::<(), tenferro_ad::Error>(())
581    /// ```
582    pub fn clear_extension_caches(&self) -> Result<()> {
583        self.lock_extension_executor()?.clear_caches();
584        Ok(())
585    }
586
587    /// Clear every cache owned by this eager context.
588    ///
589    /// # Examples
590    ///
591    /// ```
592    /// use tenferro_cpu::CpuBackend;
593    /// use tenferro_ad::{EagerRuntime};
594    ///
595    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
596    /// ctx.clear_caches()?;
597    /// assert_eq!(ctx.cache_stats()?.extensions.entries, 0);
598    /// assert_eq!(ctx.cache_stats()?.ad_transforms.entries, 0);
599    /// # Ok::<(), tenferro_ad::Error>(())
600    /// ```
601    pub fn clear_caches(&self) -> Result<()> {
602        self.clear_extension_caches()?;
603        self.clear_ad_transform_caches()?;
604        Ok(())
605    }
606
607    /// Return eager runtime cache-entry and retained-byte stats.
608    ///
609    /// # Examples
610    ///
611    /// ```
612    /// use tenferro_cpu::CpuBackend;
613    /// use tenferro_ad::{EagerRuntime};
614    ///
615    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
616    /// let stats = ctx.cache_stats()?;
617    /// assert_eq!(stats.extensions.entries, 0);
618    /// assert_eq!(stats.ad_transforms.entries, 0);
619    /// # Ok::<(), tenferro_ad::Error>(())
620    /// ```
621    pub fn cache_stats(&self) -> Result<EagerRuntimeCacheStats> {
622        Ok(EagerRuntimeCacheStats {
623            extensions: self.lock_extension_executor()?.cache_stats(),
624            ad_transforms: self.ad_transform_cache.stats()?,
625        })
626    }
627
628    /// Return the AD transform cache retention limits.
629    ///
630    /// # Examples
631    ///
632    /// ```
633    /// use tenferro_ad::EagerRuntime;
634    /// use tenferro_cpu::CpuBackend;
635    ///
636    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
637    /// assert!(ctx.ad_transform_cache_limits()?.max_entries().get() > 0);
638    /// # Ok::<(), tenferro_ad::Error>(())
639    /// ```
640    pub fn ad_transform_cache_limits(&self) -> Result<AdTransformCacheLimits> {
641        self.ad_transform_cache.limits()
642    }
643
644    /// Replace AD transform cache retention limits.
645    ///
646    /// # Examples
647    ///
648    /// ```
649    /// use std::num::NonZeroUsize;
650    /// use tenferro_ad::{AdTransformCacheLimits, EagerRuntime};
651    /// use tenferro_cpu::CpuBackend;
652    ///
653    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
654    /// let limits = AdTransformCacheLimits::new(NonZeroUsize::new(1).unwrap());
655    /// ctx.set_ad_transform_cache_limits(limits)?;
656    /// assert_eq!(ctx.ad_transform_cache_limits()?, limits);
657    /// # Ok::<(), tenferro_ad::Error>(())
658    /// ```
659    pub fn set_ad_transform_cache_limits(&self, limits: AdTransformCacheLimits) -> Result<()> {
660        self.ad_transform_cache.set_limits(limits)
661    }
662
663    /// Clear AD transform cache entries visible through this eager runtime.
664    ///
665    /// # Examples
666    ///
667    /// ```
668    /// use tenferro_ad::EagerRuntime;
669    /// use tenferro_cpu::CpuBackend;
670    ///
671    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
672    /// ctx.clear_ad_transform_caches()?;
673    /// assert_eq!(ctx.cache_stats()?.ad_transforms.entries, 0);
674    /// # Ok::<(), tenferro_ad::Error>(())
675    /// ```
676    pub fn clear_ad_transform_caches(&self) -> Result<()> {
677        self.ad_transform_cache.clear()
678    }
679
680    /// Return the extension cache retention limits.
681    pub fn extension_cache_limits(&self) -> Result<ExtensionCacheLimits> {
682        Ok(self.lock_extension_executor()?.cache_limits())
683    }
684
685    /// Replace extension cache retention limits.
686    pub fn set_extension_cache_limits(&self, limits: ExtensionCacheLimits) -> Result<()> {
687        self.lock_extension_executor()?.set_cache_limits(limits);
688        Ok(())
689    }
690
691    /// Mutably borrow generic extension runtime cache storage.
692    ///
693    /// This hook is for standard extension crates that need cache entries
694    /// owned by an eager runtime while preserving eager value semantics outside
695    /// a registered extension execution boundary.
696    ///
697    /// # Examples
698    ///
699    /// ```
700    /// use tenferro_ad::EagerRuntime;
701    /// use tenferro_cpu::CpuBackend;
702    /// use tenferro_runtime::ExtensionCacheKey;
703    ///
704    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
705    /// let key = ExtensionCacheKey::new("example.cache.v1", "plans", 1);
706    ///
707    /// ctx.with_extension_caches_mut(|caches| {
708    ///     caches.put(key, 7_usize, std::mem::size_of::<usize>());
709    /// });
710    ///
711    /// assert_eq!(ctx.cache_stats().unwrap().extensions.entries, 1);
712    /// ```
713    pub fn with_extension_caches_mut<R>(
714        &self,
715        f: impl FnOnce(&mut ExtensionCacheStore) -> R,
716    ) -> Result<R> {
717        let mut executor = self.lock_extension_executor()?;
718        Ok(f(executor.caches_mut()))
719    }
720
721    /// Mutably borrow this runtime's backend.
722    ///
723    /// This hook lets standard extension crates run a whole contraction program
724    /// in a single backend session (instead of one eager op per step) while
725    /// preserving eager value semantics for untracked tensors.
726    ///
727    /// # Examples
728    ///
729    /// ```
730    /// use tenferro_ad::EagerRuntime;
731    /// use tenferro_cpu::CpuBackend;
732    ///
733    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
734    /// // The closure receives `&mut EagerBackend`; standard extension crates
735    /// // use it to open one backend session for a whole contraction program.
736    /// let answer = ctx.with_backend_mut(|_backend| 42).unwrap();
737    /// assert_eq!(answer, 42);
738    /// ```
739    pub fn with_backend_mut<R>(&self, f: impl FnOnce(&mut EagerBackend) -> R) -> Result<R> {
740        let mut backend = self.lock_backend()?;
741        Ok(f(&mut backend))
742    }
743
744    /// Block the current thread until backend work submitted by this eager runtime completes.
745    ///
746    /// CPU runtimes return immediately. CUDA and WebGPU runtimes synchronize
747    /// their current backend work queue.
748    ///
749    /// # Examples
750    ///
751    /// ```
752    /// use tenferro_cpu::CpuBackend;
753    /// use tenferro_ad::EagerRuntime;
754    ///
755    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
756    /// ctx.synchronize().unwrap();
757    /// ```
758    pub fn synchronize(&self) -> Result<()> {
759        self.lock_backend()?.synchronize().map_err(Error::from)
760    }
761
762    fn exec_outputs_with_optional_extension_lock<R>(
763        &self,
764        lock_backend_section: &'static str,
765        lock_extensions_section: &'static str,
766        exec_section: &'static str,
767        op: &StdTensorOp,
768        execute: impl FnOnce(
769            &mut EagerBackend,
770            Option<&mut ExtensionExecutor<EagerBackend>>,
771        ) -> Result<R>,
772    ) -> Result<R> {
773        let mut backend = profile_eager_op_section(lock_backend_section, || self.lock_backend())?;
774        if matches!(op, StdTensorOp::Extension(_)) {
775            // Lock ordering: eager execution always acquires backend before
776            // extension_executor. Extension runtimes must not re-enter this
777            // same EagerRuntime while their callback is running.
778            let mut extension_executor = profile_eager_op_section(lock_extensions_section, || {
779                self.lock_extension_executor()
780            })?;
781            return profile_eager_op_section(exec_section, || {
782                execute(&mut backend, Some(&mut *extension_executor))
783            });
784        }
785
786        profile_eager_op_section(exec_section, || execute(&mut backend, None))
787    }
788
789    pub(crate) fn exec_outputs(&self, op: &StdTensorOp, inputs: &[&Tensor]) -> Result<Vec<Tensor>> {
790        self.exec_outputs_with_optional_extension_lock(
791            "exec_outputs.lock_backend",
792            "exec_outputs.lock_extensions",
793            "exec_outputs.exec_op",
794            op,
795            |backend, extension_executor| {
796                exec_op_on_tensors_with_extension_executor(op, inputs, backend, extension_executor)
797            },
798        )
799    }
800
801    pub(crate) fn exec_outputs_read(
802        &self,
803        op: &StdTensorOp,
804        inputs: &[TensorRead<'_>],
805    ) -> Result<Vec<Tensor>> {
806        self.exec_outputs_with_optional_extension_lock(
807            "exec_outputs_read.lock_backend",
808            "exec_outputs_read.lock_extensions",
809            "exec_outputs_read.exec_op",
810            op,
811            |backend, extension_executor| {
812                exec_op_on_tensor_reads_with_extension_executor(
813                    op,
814                    inputs,
815                    backend,
816                    extension_executor,
817                )
818            },
819        )
820    }
821
822    #[cfg(test)]
823    pub(crate) fn exec_standard_graph_outputs(
824        &self,
825        graph: &Graph<StdTensorOp>,
826        initial_data: &HashMap<ValueKey<StdTensorOp>, Arc<Tensor>>,
827    ) -> Result<EagerGraphExecution> {
828        let mut backend =
829            profile_eager_op_section("exec_graph.lock_backend", || self.lock_backend())?;
830        let mut all_values = initial_data.clone();
831
832        profile_eager_op_section("exec_graph.with_backend_session", || {
833            backend.with_backend_session(|exec| -> Result<()> {
834                for op_node in graph.operations() {
835                    let outputs = {
836                        let input_values = op_node
837                            .inputs
838                            .iter()
839                            .map(|input| {
840                                let key = match input {
841                                    ValueRef::Local(local_id) => &graph.values()[*local_id].key,
842                                    ValueRef::External(key) => key,
843                                };
844                                all_values.get(key).cloned().ok_or_else(|| {
845                                    Error::Internal(format!(
846                                        "standard graph eager execution missing value for {key:?}"
847                                    ))
848                                })
849                            })
850                            .collect::<Result<Vec<_>>>()?;
851                        let input_reads = input_values
852                            .iter()
853                            .map(|value| TensorRead::from_tensor(value.as_ref()))
854                            .collect::<Vec<_>>();
855                        exec_standard_op_on_tensor_reads_in_session(
856                            &op_node.operation,
857                            &input_reads,
858                            exec,
859                        )?
860                    };
861
862                    if outputs.len() != op_node.outputs.len() {
863                        return Err(Error::Internal(format!(
864                            "standard graph eager execution expected {} outputs for {:?}, got {}",
865                            op_node.outputs.len(),
866                            op_node.operation,
867                            outputs.len()
868                        )));
869                    }
870
871                    for (output_id, output) in op_node.outputs.iter().zip(outputs) {
872                        let key = graph.values()[*output_id].key.clone();
873                        all_values.insert(key, Arc::new(output));
874                    }
875                }
876                Ok(())
877            })
878        })?;
879
880        let outputs = graph
881            .outputs()
882            .iter()
883            .map(|&output_id| {
884                let key = &graph.values()[output_id].key;
885                all_values.get(key).cloned().ok_or_else(|| {
886                    Error::Internal(format!(
887                        "standard graph eager execution missing graph output {key:?}"
888                    ))
889                })
890            })
891            .collect::<Result<Vec<_>>>()?;
892
893        Ok(EagerGraphExecution {
894            outputs,
895            retained_values: all_values,
896        })
897    }
898
899    pub(crate) fn try_register_grad_slot(
900        &self,
901        key: &ValueKey<StdTensorOp>,
902        slot: &GradSlot,
903    ) -> Result<()> {
904        self.lock_grad_slots()?
905            .insert(key.clone(), Arc::downgrade(slot));
906        Ok(())
907    }
908
909    pub(crate) fn try_register_value_record(
910        &self,
911        key: &ValueKey<StdTensorOp>,
912        record: &Arc<EagerTensorRecord>,
913    ) -> Result<()> {
914        self.lock_value_records()?
915            .insert(key.clone(), Arc::downgrade(record));
916        self.try_register_value_record_ptr(record)?;
917        Ok(())
918    }
919
920    pub(crate) fn try_register_value_record_ptr(
921        &self,
922        record: &Arc<EagerTensorRecord>,
923    ) -> Result<()> {
924        let tensor = match record.value.as_tensor_arc() {
925            Some(tensor) => Some(Arc::clone(tensor)),
926            None => record.materialized_cache.get().cloned(),
927        };
928        let Some(tensor) = tensor else {
929            return Ok(());
930        };
931        self.lock_value_ptr_records()?
932            .insert(tensor_ptr(&tensor), Arc::downgrade(record));
933        Ok(())
934    }
935
936    pub(crate) fn value_record(
937        &self,
938        key: &ValueKey<StdTensorOp>,
939    ) -> Result<Option<Arc<EagerTensorRecord>>> {
940        let mut records = self.lock_value_records()?;
941        let Some(record) = records.get(key).cloned() else {
942            return Ok(None);
943        };
944        match record.upgrade() {
945            Some(record) => Ok(Some(record)),
946            None => {
947                records.remove(key);
948                Ok(None)
949            }
950        }
951    }
952
953    pub(crate) fn value_record_by_tensor(
954        &self,
955        tensor: &Arc<Tensor>,
956    ) -> Result<Option<Arc<EagerTensorRecord>>> {
957        let ptr = tensor_ptr(tensor);
958        let mut records = self.lock_value_ptr_records()?;
959        let Some(record) = records.get(&ptr).cloned() else {
960            return Ok(None);
961        };
962        match record.upgrade() {
963            Some(record) => Ok(Some(record)),
964            None => {
965                records.remove(&ptr);
966                Ok(None)
967            }
968        }
969    }
970
971    pub(crate) fn cached_linearize_recorded_graph(
972        &self,
973        graph: &RecordedGraph<StdTensorOp>,
974        output_slots: &[usize],
975        ctx: &mut ShapeGuardContext,
976    ) -> tidu::ADRuleResult<Arc<LinearizedGraph<StdTensorOp>>> {
977        let key = EagerAdTransformCacheKey::new(graph, output_slots);
978        if let Some(linear) = self
979            .ad_transform_cache
980            .get_eager_linearized(&key)
981            .map_err(eager_ad_transform_cache_error)?
982        {
983            return Ok(linear);
984        }
985
986        let linear = Arc::new(graph.linearize(output_slots, ctx)?);
987        self.ad_transform_cache
988            .put_eager_linearized(key, Arc::clone(&linear))
989            .map_err(eager_ad_transform_cache_error)?;
990        Ok(linear)
991    }
992
993    /// Clear all live gradient slots tracked by this context.
994    ///
995    /// This resets the stored gradients to `None` without unregistering the
996    /// tensors, so future `backward()` calls can accumulate again.
997    ///
998    /// # Examples
999    ///
1000    /// ```
1001    /// use tenferro_cpu::CpuBackend;
1002    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1003    ///
1004    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1005    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx.clone()).unwrap();
1006    /// let y = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![3], vec![4.0_f64, 5.0, 6.0]).unwrap(), ctx.clone()).unwrap();
1007    /// let loss = x.mul(&y).unwrap().reduce_sum(&[0]).unwrap();
1008    /// let _ = loss.backward().unwrap();
1009    ///
1010    /// ctx.clear_grads()?;
1011    ///
1012    /// assert!(x.grad()?.is_none());
1013    /// assert!(y.grad()?.is_none());
1014    /// # Ok::<(), tenferro_ad::Error>(())
1015    /// ```
1016    pub fn clear_grads(&self) -> Result<()> {
1017        let live_slots = {
1018            let mut live_slots = Vec::new();
1019            self.lock_grad_slots()?.retain(|_, slot| {
1020                if let Some(slot) = slot.upgrade() {
1021                    live_slots.push(slot);
1022                    true
1023                } else {
1024                    false
1025                }
1026            });
1027            live_slots
1028        };
1029
1030        let mut poisoned_slot = false;
1031        for slot in live_slots {
1032            match slot.lock() {
1033                Ok(mut current) => {
1034                    *current = None;
1035                }
1036                Err(_) => {
1037                    poisoned_slot = true;
1038                }
1039            }
1040        }
1041        if poisoned_slot {
1042            return Err(Error::Internal("gradient slot lock poisoned".to_string()));
1043        }
1044        Ok(())
1045    }
1046
1047    /// Import a concrete tensor into this context as an untracked constant.
1048    ///
1049    /// The returned tensor does not participate in gradient tracking.
1050    /// Use this for fixed masks, quadrature weights, physical constants,
1051    /// and other data that should not receive gradients.
1052    ///
1053    /// # Examples
1054    ///
1055    /// ```
1056    /// use tenferro_cpu::CpuBackend;
1057    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1058    ///
1059    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1060    /// let c = ctx.constant_from(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap())?;
1061    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx)?;
1062    /// let z = x.add(&c).unwrap();
1063    ///
1064    /// assert_eq!(z.materialized()?.as_slice::<f64>().unwrap(), &[4.0, 6.0]);
1065    /// # Ok::<(), tenferro_ad::Error>(())
1066    /// ```
1067    pub fn constant_from(self: &Arc<Self>, tensor: Tensor) -> Result<EagerTensor> {
1068        EagerTensor::new_leaf(Arc::clone(self), tensor, false)
1069    }
1070
1071    /// Import a concrete tensor into this context as a trainable variable.
1072    ///
1073    /// The returned tensor participates in gradient tracking; its gradient
1074    /// slot is registered in this context.
1075    ///
1076    /// # Examples
1077    ///
1078    /// ```
1079    /// use tenferro_cpu::CpuBackend;
1080    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1081    ///
1082    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1083    /// let p = ctx.variable_from(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap())?;
1084    /// let loss = p.exp().unwrap().reduce_sum(&[0]).unwrap();
1085    /// let _ = loss.backward().unwrap();
1086    ///
1087    /// let grad = p.grad().unwrap().unwrap();
1088    /// assert_eq!(grad.shape(), &[2]);
1089    /// # Ok::<(), tenferro_ad::Error>(())
1090    /// ```
1091    pub fn variable_from(self: &Arc<Self>, tensor: Tensor) -> Result<EagerTensor> {
1092        EagerTensor::new_leaf(Arc::clone(self), tensor, true)
1093    }
1094
1095    /// Gradient of a scalar eager output with respect to an eager tensor.
1096    ///
1097    /// Functional eager gradients return ordinary eager tensors and do not
1098    /// write into `grad()` slots. The returned tensor keeps a trace when the
1099    /// derivative computation depends on tracked eager values.
1100    ///
1101    /// # Examples
1102    ///
1103    /// ```
1104    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1105    /// use tenferro_cpu::CpuBackend;
1106    ///
1107    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1108    /// let x = EagerTensor::requires_grad_in(
1109    ///     Tensor::from_vec_col_major(vec![], vec![3.0_f64]).unwrap(),
1110    ///     ctx.clone(),
1111    /// )?;
1112    /// let loss = x.mul(&x)?;
1113    /// let dx = ctx.grad(&loss, &x)?;
1114    /// assert_eq!(dx.materialized()?.as_slice::<f64>().unwrap(), &[6.0]);
1115    /// # Ok::<(), tenferro_ad::Error>(())
1116    /// ```
1117    pub fn grad(self: &Arc<Self>, output: &EagerTensor, wrt: &EagerTensor) -> Result<EagerTensor> {
1118        self.grad_optional(output, wrt)?
1119            .ok_or_else(|| Error::Internal(format!("grad output is inactive for {:?}", wrt.key)))
1120    }
1121
1122    /// Gradient that returns `None` when `wrt` is inactive.
1123    ///
1124    /// # Examples
1125    ///
1126    /// ```
1127    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1128    /// use tenferro_cpu::CpuBackend;
1129    ///
1130    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1131    /// let x = EagerTensor::requires_grad_in(
1132    ///     Tensor::from_vec_col_major(vec![], vec![3.0_f64]).unwrap(),
1133    ///     ctx.clone(),
1134    /// )?;
1135    /// let y = EagerTensor::requires_grad_in(
1136    ///     Tensor::from_vec_col_major(vec![], vec![4.0_f64]).unwrap(),
1137    ///     ctx.clone(),
1138    /// )?;
1139    /// let loss = y.mul(&y)?;
1140    /// assert!(ctx.grad_optional(&loss, &x)?.is_none());
1141    /// # Ok::<(), tenferro_ad::Error>(())
1142    /// ```
1143    pub fn grad_optional(
1144        self: &Arc<Self>,
1145        output: &EagerTensor,
1146        wrt: &EagerTensor,
1147    ) -> Result<Option<EagerTensor>> {
1148        if !output.shape().is_empty() {
1149            return Err(Error::NonScalarGrad {
1150                shape: output.shape().to_vec(),
1151            });
1152        }
1153
1154        let value = output.materialized_arc()?;
1155        let seed = {
1156            let mut backend = self.lock_backend()?;
1157            one_like_tensor(value.as_ref(), &mut *backend)?
1158        };
1159        let seed = EagerTensor::new_result_arc(
1160            Arc::clone(self),
1161            eager_val_key(),
1162            Arc::new(seed),
1163            false,
1164            None,
1165            Vec::new(),
1166        )?;
1167        self.vjp_optional(output, wrt, &seed)
1168    }
1169
1170    /// Reverse-mode vector-Jacobian product for eager tensors.
1171    ///
1172    /// # Examples
1173    ///
1174    /// ```
1175    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1176    /// use tenferro_cpu::CpuBackend;
1177    ///
1178    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1179    /// let x = EagerTensor::requires_grad_in(
1180    ///     Tensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0]).unwrap(),
1181    ///     ctx.clone(),
1182    /// )?;
1183    /// let y = x.mul(&x)?;
1184    /// let seed = EagerTensor::from_tensor_in(
1185    ///     Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 1.0]).unwrap(),
1186    ///     ctx.clone(),
1187    /// )?;
1188    /// let dx = ctx.vjp(&y, &x, &seed)?;
1189    /// assert_eq!(dx.materialized()?.as_slice::<f64>().unwrap(), &[4.0, 6.0]);
1190    /// # Ok::<(), tenferro_ad::Error>(())
1191    /// ```
1192    pub fn vjp(
1193        self: &Arc<Self>,
1194        output: &EagerTensor,
1195        wrt: &EagerTensor,
1196        cotangent: &EagerTensor,
1197    ) -> Result<EagerTensor> {
1198        self.vjp_optional(output, wrt, cotangent)?
1199            .ok_or_else(|| Error::Internal(format!("vjp output is inactive for {:?}", wrt.key)))
1200    }
1201
1202    /// Reverse-mode vector-Jacobian product that returns `None` for inactive inputs.
1203    ///
1204    /// # Examples
1205    ///
1206    /// ```
1207    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1208    /// use tenferro_cpu::CpuBackend;
1209    ///
1210    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1211    /// let x = EagerTensor::requires_grad_in(
1212    ///     Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap(),
1213    ///     ctx.clone(),
1214    /// )?;
1215    /// let y = EagerTensor::requires_grad_in(
1216    ///     Tensor::from_vec_col_major(vec![1], vec![4.0_f64]).unwrap(),
1217    ///     ctx.clone(),
1218    /// )?;
1219    /// let seed = EagerTensor::from_tensor_in(
1220    ///     Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap(),
1221    ///     ctx.clone(),
1222    /// )?;
1223    /// let loss = y.mul(&y)?;
1224    /// assert!(ctx.vjp_optional(&loss, &x, &seed)?.is_none());
1225    /// # Ok::<(), tenferro_ad::Error>(())
1226    /// ```
1227    pub fn vjp_optional(
1228        self: &Arc<Self>,
1229        output: &EagerTensor,
1230        wrt: &EagerTensor,
1231        cotangent: &EagerTensor,
1232    ) -> Result<Option<EagerTensor>> {
1233        validate_same_runtime(self, output, "vjp output")?;
1234        validate_same_runtime(self, wrt, "vjp wrt")?;
1235        validate_same_runtime(self, cotangent, "vjp cotangent")?;
1236        validate_seed_tensor("vjp", output, cotangent)?;
1237        functional_vjp_optional(self, output, wrt, cotangent)
1238    }
1239
1240    /// Forward-mode Jacobian-vector product for eager tensors.
1241    ///
1242    /// # Examples
1243    ///
1244    /// ```
1245    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1246    /// use tenferro_cpu::CpuBackend;
1247    ///
1248    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1249    /// let x = EagerTensor::requires_grad_in(
1250    ///     Tensor::from_vec_col_major(vec![1], vec![3.0_f64]).unwrap(),
1251    ///     ctx.clone(),
1252    /// )?;
1253    /// let tangent = EagerTensor::from_tensor_in(
1254    ///     Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap(),
1255    ///     ctx.clone(),
1256    /// )?;
1257    /// let y = x.mul(&x)?;
1258    /// let dy = ctx.jvp(&y, &x, &tangent)?;
1259    /// assert_eq!(dy.materialized()?.as_slice::<f64>().unwrap(), &[6.0]);
1260    /// # Ok::<(), tenferro_ad::Error>(())
1261    /// ```
1262    pub fn jvp(
1263        self: &Arc<Self>,
1264        output: &EagerTensor,
1265        wrt: &EagerTensor,
1266        tangent: &EagerTensor,
1267    ) -> Result<EagerTensor> {
1268        self.jvp_optional(output, wrt, tangent)?
1269            .ok_or_else(|| Error::Internal(format!("jvp output is inactive for {:?}", wrt.key)))
1270    }
1271
1272    /// Forward-mode Jacobian-vector product that returns `None` for inactive outputs.
1273    ///
1274    /// # Examples
1275    ///
1276    /// ```
1277    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1278    /// use tenferro_cpu::CpuBackend;
1279    ///
1280    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1281    /// let x = EagerTensor::requires_grad_in(
1282    ///     Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap(),
1283    ///     ctx.clone(),
1284    /// )?;
1285    /// let y = EagerTensor::requires_grad_in(
1286    ///     Tensor::from_vec_col_major(vec![1], vec![4.0_f64]).unwrap(),
1287    ///     ctx.clone(),
1288    /// )?;
1289    /// let tangent = EagerTensor::from_tensor_in(
1290    ///     Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap(),
1291    ///     ctx.clone(),
1292    /// )?;
1293    /// let loss = y.mul(&y)?;
1294    /// assert!(ctx.jvp_optional(&loss, &x, &tangent)?.is_none());
1295    /// # Ok::<(), tenferro_ad::Error>(())
1296    /// ```
1297    pub fn jvp_optional(
1298        self: &Arc<Self>,
1299        output: &EagerTensor,
1300        wrt: &EagerTensor,
1301        tangent: &EagerTensor,
1302    ) -> Result<Option<EagerTensor>> {
1303        validate_same_runtime(self, output, "jvp output")?;
1304        validate_same_runtime(self, wrt, "jvp wrt")?;
1305        validate_same_runtime(self, tangent, "jvp tangent")?;
1306        validate_seed_tensor("jvp", wrt, tangent)?;
1307        functional_jvp(self, output, wrt, tangent)
1308    }
1309
1310    fn store_grads(
1311        &self,
1312        cotangents: &HashMap<ValueKey<StdTensorOp>, Arc<Tensor>>,
1313        backend: &mut EagerBackend,
1314    ) -> Result<()> {
1315        let mut updates = Vec::new();
1316
1317        {
1318            let mut slots = self.lock_grad_slots()?;
1319            slots.retain(|key, slot| {
1320                let Some(slot) = slot.upgrade() else {
1321                    return false;
1322                };
1323
1324                if let Some(incoming) = cotangents.get(key) {
1325                    updates.push((slot, Arc::clone(incoming)));
1326                }
1327
1328                true
1329            });
1330        }
1331
1332        for (slot, incoming) in updates {
1333            let mut current = slot
1334                .lock()
1335                .map_err(|_| Error::Internal("gradient slot lock poisoned".to_string()))?;
1336            let next = match current.as_ref() {
1337                Some(existing) => Arc::new(backend.add(existing.as_ref(), incoming.as_ref())?),
1338                None => incoming,
1339            };
1340            *current = Some(next);
1341        }
1342
1343        Ok(())
1344    }
1345}
1346
1347fn validate_same_runtime(
1348    runtime: &Arc<EagerRuntime>,
1349    tensor: &EagerTensor,
1350    role: &'static str,
1351) -> Result<()> {
1352    if tensor.ctx_id() != runtime.id() {
1353        return Err(Error::ContextMismatch {
1354            lhs: runtime.id(),
1355            rhs: tensor.ctx_id(),
1356        });
1357    }
1358    let _ = role;
1359    Ok(())
1360}
1361
1362pub(crate) fn tensor_ptr(tensor: &Arc<Tensor>) -> usize {
1363    Arc::as_ptr(tensor) as usize
1364}
1365
1366fn validate_seed_tensor(op: &'static str, primal: &EagerTensor, seed: &EagerTensor) -> Result<()> {
1367    if primal.dtype() != seed.dtype() {
1368        return Err(tenferro_tensor::Error::InvalidConfig {
1369            op,
1370            message: format!(
1371                "{op} cotangent dtype must match primal dtype: {:?} vs {:?}",
1372                seed.dtype(),
1373                primal.dtype()
1374            ),
1375        }
1376        .into());
1377    }
1378    if primal.shape() != seed.shape() {
1379        return Err(tenferro_tensor::Error::ShapeMismatch {
1380            op,
1381            lhs: primal.shape().to_vec(),
1382            rhs: seed.shape().to_vec(),
1383        }
1384        .into());
1385    }
1386    Ok(())
1387}
1388
1389/// Eager tensor with reverse-mode autodiff over concrete tensor values.
1390///
1391/// This executes each primitive immediately and records a lightweight reverse
1392/// DAG for `backward()`. Gradients accumulate across repeated `backward()`
1393/// calls until they are cleared explicitly.
1394///
1395/// # Examples
1396///
1397/// ```
1398/// use tenferro_cpu::CpuBackend;
1399/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1400///
1401/// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1402/// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx)?;
1403/// let loss = x.mul(&x).unwrap().reduce_sum(&[0]).unwrap();
1404/// let _cotangents = loss.backward().unwrap();
1405/// let loss = x.mul(&x).unwrap().reduce_sum(&[0]).unwrap();
1406/// let _cotangents = loss.backward().unwrap();
1407///
1408/// assert_eq!(x.grad().unwrap().unwrap().as_slice::<f64>().unwrap(), &[4.0, 8.0, 12.0]);
1409/// x.clear_grad();
1410///
1411/// assert!(x.grad().unwrap().is_none());
1412/// # Ok::<(), tenferro_ad::Error>(())
1413/// ```
1414#[derive(Clone)]
1415pub struct EagerTensor {
1416    pub(crate) value: Arc<TensorValue>,
1417    materialized_cache: Arc<OnceLock<Arc<Tensor>>>,
1418    pub(crate) key: ValueKey<StdTensorOp>,
1419    pub(crate) trace: Option<Trace<StdTensorOp>>,
1420    pub(crate) requires_grad: bool,
1421    grad_slot: GradSlot,
1422    pub(crate) metadata_scopes: Vec<Arc<GlobalMetadataScope>>,
1423    pub(crate) ctx: Arc<EagerRuntime>,
1424    _record: Arc<EagerTensorRecord>,
1425}
1426
1427pub(crate) struct EagerTensorRecord {
1428    value: Arc<TensorValue>,
1429    materialized_cache: Arc<OnceLock<Arc<Tensor>>>,
1430    key: ValueKey<StdTensorOp>,
1431    trace: Option<Trace<StdTensorOp>>,
1432    requires_grad: bool,
1433    grad_slot: GradSlot,
1434    metadata_scopes: Vec<Arc<GlobalMetadataScope>>,
1435    ctx: Arc<EagerRuntime>,
1436}
1437
1438impl fmt::Debug for EagerTensor {
1439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1440        f.debug_struct("EagerTensor")
1441            .field("dtype", &self.dtype())
1442            .field("shape", &self.shape())
1443            .field("key", &self.key)
1444            .field("requires_grad", &self.requires_grad)
1445            .field("has_trace", &self.trace.is_some())
1446            .field("ctx_id", &self.ctx_id())
1447            .finish_non_exhaustive()
1448    }
1449}
1450
1451impl EagerTensor {
1452    /// Create an untracked eager tensor inside an existing eager context.
1453    ///
1454    /// # Examples
1455    ///
1456    /// ```
1457    /// use tenferro_cpu::CpuBackend;
1458    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1459    ///
1460    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1461    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx)?;
1462    ///
1463    /// assert_eq!(x.materialized()?.as_slice::<f64>().unwrap(), &[1.0, 2.0]);
1464    /// # Ok::<(), tenferro_ad::Error>(())
1465    /// ```
1466    pub fn from_tensor_in(tensor: Tensor, ctx: Arc<EagerRuntime>) -> Result<Self> {
1467        Self::new_leaf(ctx, tensor, false)
1468    }
1469
1470    /// Create a tracked eager leaf inside an existing eager context.
1471    ///
1472    /// # Examples
1473    ///
1474    /// ```
1475    /// use tenferro_cpu::CpuBackend;
1476    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1477    ///
1478    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1479    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx)?;
1480    ///
1481    /// assert!(x.grad().unwrap().is_none());
1482    /// # Ok::<(), tenferro_ad::Error>(())
1483    /// ```
1484    pub fn requires_grad_in(tensor: Tensor, ctx: Arc<EagerRuntime>) -> Result<Self> {
1485        Self::new_leaf(ctx, tensor, true)
1486    }
1487
1488    pub(crate) fn new_leaf(
1489        ctx: Arc<EagerRuntime>,
1490        tensor: Tensor,
1491        requires_grad: bool,
1492    ) -> Result<Self> {
1493        let key = eager_val_key();
1494        let metadata_scope =
1495            register_scoped_value_metadata(key.clone(), tensor_meta_from_tensor(&tensor)).map_err(
1496                |err| Error::Internal(format!("eager leaf metadata registration failed: {err}")),
1497            )?;
1498        Self::from_parts(
1499            ctx,
1500            key,
1501            requires_grad,
1502            None,
1503            Arc::new(TensorValue::from_tensor_arc(Arc::new(tensor))),
1504            metadata_scopes_for_scope(metadata_scope),
1505            true,
1506        )
1507    }
1508
1509    pub(crate) fn new_result(
1510        ctx: Arc<EagerRuntime>,
1511        key: ValueKey<StdTensorOp>,
1512        tensor: Tensor,
1513        requires_grad: bool,
1514        trace: Option<Trace<StdTensorOp>>,
1515        metadata_scopes: Vec<Arc<GlobalMetadataScope>>,
1516    ) -> Result<Self> {
1517        Self::new_result_arc(
1518            ctx,
1519            key,
1520            Arc::new(tensor),
1521            requires_grad,
1522            trace,
1523            metadata_scopes,
1524        )
1525    }
1526
1527    pub(crate) fn new_result_arc(
1528        ctx: Arc<EagerRuntime>,
1529        key: ValueKey<StdTensorOp>,
1530        tensor: Arc<Tensor>,
1531        requires_grad: bool,
1532        trace: Option<Trace<StdTensorOp>>,
1533        metadata_scopes: Vec<Arc<GlobalMetadataScope>>,
1534    ) -> Result<Self> {
1535        Self::from_parts(
1536            ctx,
1537            key,
1538            requires_grad,
1539            trace,
1540            Arc::new(TensorValue::from_tensor_arc(tensor)),
1541            metadata_scopes,
1542            true,
1543        )
1544    }
1545
1546    pub(crate) fn new_result_value(
1547        ctx: Arc<EagerRuntime>,
1548        key: ValueKey<StdTensorOp>,
1549        value: TensorValue,
1550        requires_grad: bool,
1551        trace: Option<Trace<StdTensorOp>>,
1552        metadata_scopes: Vec<Arc<GlobalMetadataScope>>,
1553    ) -> Result<Self> {
1554        Self::from_parts(
1555            ctx,
1556            key,
1557            requires_grad,
1558            trace,
1559            Arc::new(value),
1560            metadata_scopes,
1561            true,
1562        )
1563    }
1564
1565    fn from_parts(
1566        ctx: Arc<EagerRuntime>,
1567        key: ValueKey<StdTensorOp>,
1568        requires_grad: bool,
1569        trace: Option<Trace<StdTensorOp>>,
1570        value: Arc<TensorValue>,
1571        metadata_scopes: Vec<Arc<GlobalMetadataScope>>,
1572        register_value: bool,
1573    ) -> Result<Self> {
1574        let grad_slot = Arc::new(Mutex::new(None));
1575        if requires_grad {
1576            ctx.try_register_grad_slot(&key, &grad_slot)?;
1577        }
1578        let materialized_cache = Arc::new(OnceLock::new());
1579        let record = Arc::new(EagerTensorRecord {
1580            value: Arc::clone(&value),
1581            materialized_cache: Arc::clone(&materialized_cache),
1582            key: key.clone(),
1583            trace: trace.clone(),
1584            requires_grad,
1585            grad_slot: Arc::clone(&grad_slot),
1586            metadata_scopes: metadata_scopes.clone(),
1587            ctx: Arc::clone(&ctx),
1588        });
1589        if register_value {
1590            ctx.try_register_value_record(&key, &record)?;
1591        }
1592
1593        Ok(Self {
1594            value,
1595            materialized_cache,
1596            key,
1597            trace,
1598            requires_grad,
1599            grad_slot,
1600            metadata_scopes,
1601            ctx,
1602            _record: record,
1603        })
1604    }
1605
1606    pub(crate) fn new_untracked_result(ctx: Arc<EagerRuntime>, tensor: Tensor) -> Result<Self> {
1607        Self::new_result(ctx, eager_val_key(), tensor, false, None, Vec::new())
1608    }
1609
1610    pub(crate) fn new_untracked_value_result(ctx: Arc<EagerRuntime>, value: TensorValue) -> Self {
1611        let value = Arc::new(value);
1612        let materialized_cache = Arc::new(OnceLock::new());
1613        let key = eager_val_key();
1614        let grad_slot = Arc::new(Mutex::new(None));
1615        let record = Arc::new(EagerTensorRecord {
1616            value: Arc::clone(&value),
1617            materialized_cache: Arc::clone(&materialized_cache),
1618            key: key.clone(),
1619            trace: None,
1620            requires_grad: false,
1621            grad_slot: Arc::clone(&grad_slot),
1622            metadata_scopes: Vec::new(),
1623            ctx: Arc::clone(&ctx),
1624        });
1625        Self {
1626            value,
1627            materialized_cache,
1628            key,
1629            trace: None,
1630            requires_grad: false,
1631            grad_slot,
1632            metadata_scopes: Vec::new(),
1633            ctx,
1634            _record: record,
1635        }
1636    }
1637
1638    pub(crate) fn from_record(record: Arc<EagerTensorRecord>) -> Self {
1639        Self {
1640            value: Arc::clone(&record.value),
1641            materialized_cache: Arc::clone(&record.materialized_cache),
1642            key: record.key.clone(),
1643            trace: record.trace.clone(),
1644            requires_grad: record.requires_grad,
1645            grad_slot: Arc::clone(&record.grad_slot),
1646            metadata_scopes: record.metadata_scopes.clone(),
1647            ctx: Arc::clone(&record.ctx),
1648            _record: record,
1649        }
1650    }
1651
1652    /// Detach this tensor from the reverse graph.
1653    ///
1654    /// The returned tensor keeps the concrete value but no longer contributes
1655    /// gradients to the original graph.
1656    ///
1657    /// # Examples
1658    ///
1659    /// ```
1660    /// use tenferro_cpu::CpuBackend;
1661    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1662    ///
1663    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1664    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx)?;
1665    /// let y = x.detach();
1666    ///
1667    /// assert_eq!(y.materialized()?.as_slice::<f64>().unwrap(), &[1.0, 2.0]);
1668    /// assert!(y.grad().unwrap().is_none());
1669    /// # Ok::<(), tenferro_ad::Error>(())
1670    /// ```
1671    pub fn detach(&self) -> Self {
1672        Self::new_untracked_value_result(self.ctx.clone(), self.value.as_ref().clone())
1673    }
1674
1675    /// Detach this tensor from its graph and re-register it in a different
1676    /// context as an untracked leaf.
1677    ///
1678    /// # Examples
1679    ///
1680    /// ```
1681    /// use tenferro_cpu::CpuBackend;
1682    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1683    ///
1684    /// let ctx_a = EagerRuntime::with_cpu_backend(CpuBackend::new());
1685    /// let ctx_b = EagerRuntime::with_cpu_backend(CpuBackend::new());
1686    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx_a)?;
1687    /// let d = x.detach_into(&ctx_b)?;
1688    ///
1689    /// assert!(!d.tracks_grad());
1690    /// assert_eq!(d.ctx_id(), ctx_b.id());
1691    /// # Ok::<(), tenferro_ad::Error>(())
1692    /// ```
1693    pub fn detach_into(&self, ctx: &Arc<EagerRuntime>) -> Result<Self> {
1694        Self::from_tensor_in(self.to_tensor()?, Arc::clone(ctx))
1695    }
1696
1697    /// Materialize and share the concrete tensor value.
1698    ///
1699    /// # Examples
1700    ///
1701    /// ```
1702    /// use tenferro_cpu::CpuBackend;
1703    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1704    ///
1705    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1706    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![3.0_f64]).unwrap(), ctx)?;
1707    /// assert_eq!(x.materialized()?.as_slice::<f64>().unwrap(), &[3.0]);
1708    /// # Ok::<(), tenferro_ad::Error>(())
1709    /// ```
1710    pub fn materialized(&self) -> Result<Arc<Tensor>> {
1711        self.materialized_arc()
1712    }
1713
1714    /// Return this tensor's scalar dtype without materializing through
1715    /// [`materialized`](Self::materialized).
1716    pub fn dtype(&self) -> DType {
1717        self.value.dtype()
1718    }
1719
1720    /// Return this tensor's logical shape without materializing through
1721    /// [`materialized`](Self::materialized).
1722    pub fn shape(&self) -> &[usize] {
1723        self.value.shape()
1724    }
1725
1726    /// Borrow this tensor value as a [`TensorRead`].
1727    ///
1728    /// This is the preferred borrowed input boundary for executor calls. It
1729    /// preserves the option to replace eager storage with non-contiguous views
1730    /// without forcing callers through [`materialized`](Self::materialized).
1731    pub fn tensor_read(&self) -> TensorRead<'_> {
1732        self.value.tensor_read()
1733    }
1734
1735    /// Materialize this eager tensor as an owned [`Tensor`].
1736    ///
1737    /// This is the owned materialization boundary for callers that need a
1738    /// standalone compact tensor. The operation is fallible because eager
1739    /// values may be backed by lazy or backend-resident storage.
1740    pub fn to_tensor(&self) -> Result<Tensor> {
1741        self.value.to_tensor().map_err(Error::from)
1742    }
1743
1744    pub(crate) fn materialized_arc(&self) -> Result<Arc<Tensor>> {
1745        if let Some(tensor) = self.value.as_tensor_arc() {
1746            self.ctx.try_register_value_record_ptr(&self._record)?;
1747            return Ok(Arc::clone(tensor));
1748        }
1749        if let Some(tensor) = self.materialized_cache.get() {
1750            self.ctx.try_register_value_record_ptr(&self._record)?;
1751            return Ok(Arc::clone(tensor));
1752        }
1753
1754        let materialized = Arc::new(self.value.to_tensor().map_err(Error::from)?);
1755        let _ = self.materialized_cache.set(Arc::clone(&materialized));
1756        self.ctx.try_register_value_record_ptr(&self._record)?;
1757        Ok(self
1758            .materialized_cache
1759            .get()
1760            .map(Arc::clone)
1761            .unwrap_or(materialized))
1762    }
1763
1764    #[cfg(test)]
1765    pub(crate) fn materialized_cache_is_initialized(&self) -> bool {
1766        self.materialized_cache.get().is_some()
1767    }
1768
1769    /// Return the accumulated gradient currently stored for this tensor.
1770    ///
1771    /// The stored gradient accumulates across repeated `backward()` calls
1772    /// until it is cleared explicitly.
1773    ///
1774    /// For complex scalar losses, stored gradients use tenferro's
1775    /// Hermitian-adjoint cotangent convention. See
1776    /// <https://tensor4all.org/tenferro-rs/guides/complex-ad.html>.
1777    ///
1778    /// # Examples
1779    ///
1780    /// ```
1781    /// use tenferro_cpu::CpuBackend;
1782    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1783    ///
1784    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1785    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx).unwrap();
1786    /// let loss = x.exp().unwrap().reduce_sum(&[0]).unwrap();
1787    /// let _cotangents = loss.backward().unwrap();
1788    ///
1789    /// let grad = x.grad()?.unwrap();
1790    /// assert_eq!(grad.shape(), &[2]);
1791    /// # Ok::<(), tenferro_ad::Error>(())
1792    /// ```
1793    pub fn grad(&self) -> Result<Option<Arc<Tensor>>> {
1794        self.grad_slot
1795            .lock()
1796            .map_err(|_| Error::Internal("gradient slot lock poisoned".to_string()))
1797            .map(|slot| slot.clone())
1798    }
1799
1800    /// Clear the accumulated gradient stored for this tensor.
1801    ///
1802    /// This only affects this tensor's gradient slot. Other tensors in the
1803    /// same context retain their gradients until they are cleared explicitly or
1804    /// overwritten by later accumulation.
1805    ///
1806    /// # Examples
1807    ///
1808    /// ```
1809    /// use tenferro_cpu::CpuBackend;
1810    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1811    ///
1812    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1813    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx.clone()).unwrap();
1814    /// let y = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![3], vec![4.0_f64, 5.0, 6.0]).unwrap(), ctx).unwrap();
1815    /// let loss = x.mul(&y).unwrap().reduce_sum(&[0]).unwrap();
1816    /// let _ = loss.backward().unwrap();
1817    ///
1818    /// x.clear_grad()?;
1819    ///
1820    /// assert!(x.grad()?.is_none());
1821    /// assert!(y.grad()?.is_some());
1822    /// # Ok::<(), tenferro_ad::Error>(())
1823    /// ```
1824    pub fn clear_grad(&self) -> Result<()> {
1825        *self
1826            .grad_slot
1827            .lock()
1828            .map_err(|_| Error::Internal("gradient slot lock poisoned".to_string()))? = None;
1829        Ok(())
1830    }
1831
1832    /// Report whether this tensor participates in gradient tracking.
1833    ///
1834    /// Tracked tensors keep a gradient slot in their eager context; untracked
1835    /// tensors and detached tensors do not.
1836    ///
1837    /// # Examples
1838    ///
1839    /// ```
1840    /// use tenferro_cpu::CpuBackend;
1841    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1842    ///
1843    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1844    /// let plain = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
1845    /// let tracked = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
1846    /// let detached = tracked.detach();
1847    ///
1848    /// assert!(!plain.tracks_grad());
1849    /// assert!(tracked.tracks_grad());
1850    /// assert!(!detached.tracks_grad());
1851    /// ```
1852    pub fn tracks_grad(&self) -> bool {
1853        self.requires_grad
1854    }
1855
1856    #[cfg(test)]
1857    fn debug_trace_saved_value_count(&self) -> Option<usize> {
1858        self.trace.as_ref().map(|trace| trace.saved_values().len())
1859    }
1860
1861    /// Return the opaque identifier of the context this tensor belongs to.
1862    ///
1863    /// # Examples
1864    ///
1865    /// ```
1866    /// use tenferro_cpu::CpuBackend;
1867    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1868    ///
1869    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1870    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap(), ctx.clone()).unwrap();
1871    ///
1872    /// assert_eq!(x.ctx_id(), ctx.id());
1873    /// ```
1874    pub fn ctx_id(&self) -> ContextId {
1875        self.ctx.id()
1876    }
1877
1878    /// Borrow the eager runtime context that owns this tensor.
1879    pub fn runtime(&self) -> &Arc<EagerRuntime> {
1880        &self.ctx
1881    }
1882
1883    /// Check whether two tensors belong to the same eager context.
1884    ///
1885    /// # Examples
1886    ///
1887    /// ```
1888    /// use tenferro_cpu::CpuBackend;
1889    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1890    ///
1891    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
1892    /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap(), ctx.clone()).unwrap();
1893    /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap(), ctx).unwrap();
1894    ///
1895    /// assert!(x.same_context(&y));
1896    /// ```
1897    pub fn same_context(&self, other: &Self) -> bool {
1898        self.ctx_id() == other.ctx_id()
1899    }
1900
1901    #[cfg(test)]
1902    pub(crate) fn standard_graph_op(
1903        inputs: &[&Self],
1904        build_graph: impl FnOnce(&[TensorInputKey]) -> Result<Arc<Graph<StdTensorOp>>>,
1905    ) -> Result<Vec<Self>> {
1906        let Some(first) = inputs.first() else {
1907            return Err(Error::Internal(
1908                "standard eager graph op requires at least one input tensor".to_string(),
1909            ));
1910        };
1911        let ctx = Arc::clone(&first.ctx);
1912        for tensor in inputs.iter().skip(1) {
1913            if !first.same_context(tensor) {
1914                return Err(Error::ContextMismatch {
1915                    lhs: first.ctx_id(),
1916                    rhs: tensor.ctx_id(),
1917                });
1918            }
1919        }
1920
1921        let mut recorder = Recorder::new(EagerTensorKeySource);
1922        let graph_input_keys = recorder.fresh_input_keys::<StdTensorOp>(inputs.len());
1923        let graph = build_graph(&graph_input_keys)?;
1924        let initial_data = graph_input_keys
1925            .iter()
1926            .zip(inputs.iter())
1927            .map(|(key, tensor)| Ok((ValueKey::Input(key.clone()), tensor.materialized_arc()?)))
1928            .collect::<Result<HashMap<_, _>>>()?;
1929        let execution = ctx.exec_standard_graph_outputs(graph.as_ref(), &initial_data)?;
1930        if execution.outputs.len() != graph.outputs().len() {
1931            return Err(Error::Internal(format!(
1932                "standard eager graph op expected {} graph outputs, got {}",
1933                graph.outputs().len(),
1934                execution.outputs.len()
1935            )));
1936        }
1937
1938        if !eager_grad_recording_enabled() || !inputs.iter().any(|input| input.requires_grad) {
1939            return execution
1940                .outputs
1941                .into_iter()
1942                .map(|output| {
1943                    Self::new_result_arc(
1944                        Arc::clone(&ctx),
1945                        eager_val_key(),
1946                        output,
1947                        false,
1948                        None,
1949                        Vec::new(),
1950                    )
1951                })
1952                .collect();
1953        }
1954
1955        let output_keys = graph
1956            .outputs()
1957            .iter()
1958            .map(|&output_id| graph.values()[output_id].key.clone())
1959            .collect();
1960        let recorded_graph = RecordedGraph::new(Arc::clone(&graph), graph_input_keys, output_keys)
1961            .map_err(eager_record_error)?;
1962        let recorded = record_eager_recorded_graph_outputs(
1963            &mut recorder,
1964            recorded_graph,
1965            &execution.outputs,
1966            execution.retained_values,
1967            inputs,
1968        )?;
1969        if recorded.traces.len() != execution.outputs.len() {
1970            return Err(Error::Internal(format!(
1971                "standard eager graph op expected {} eager traces, got {}",
1972                execution.outputs.len(),
1973                recorded.traces.len()
1974            )));
1975        }
1976
1977        let mut metadata_scopes = vec![Arc::clone(&recorded.metadata_scope)];
1978        for input in inputs {
1979            for scope in &input.metadata_scopes {
1980                push_metadata_scope(&mut metadata_scopes, Arc::clone(scope));
1981            }
1982        }
1983
1984        recorded
1985            .traces
1986            .into_iter()
1987            .zip(execution.outputs)
1988            .map(|(trace, output)| {
1989                Self::new_result_arc(
1990                    Arc::clone(&ctx),
1991                    trace.key,
1992                    output,
1993                    trace.requires_grad,
1994                    trace.trace,
1995                    metadata_scopes.clone(),
1996                )
1997            })
1998            .collect()
1999    }
2000
2001    /// Run reverse-mode AD from this scalar output.
2002    ///
2003    /// Returns the full cotangent map produced by the reverse pass and also
2004    /// accumulates into `grad()` for tracked eager tensors reachable from this
2005    /// output.
2006    ///
2007    /// For complex scalar outputs, cotangents use tenferro's Hermitian
2008    /// real-inner-product convention. See
2009    /// <https://tensor4all.org/tenferro-rs/guides/complex-ad.html>.
2010    ///
2011    /// # Examples
2012    ///
2013    /// ```
2014    /// use tenferro_cpu::CpuBackend;
2015    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
2016    ///
2017    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
2018    /// let x = EagerTensor::requires_grad_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx).unwrap();
2019    /// let loss = x.add(&x).unwrap().reduce_sum(&[0]).unwrap();
2020    /// let _cotangents = loss.backward().unwrap();
2021    /// let loss = x.add(&x).unwrap().reduce_sum(&[0]).unwrap();
2022    /// let _cotangents = loss.backward().unwrap();
2023    ///
2024    /// assert_eq!(x.grad().unwrap().unwrap().as_slice::<f64>().unwrap(), &[4.0, 4.0, 4.0]);
2025    /// ```
2026    pub fn backward(&self) -> Result<HashMap<ValueKey<StdTensorOp>, Arc<Tensor>>> {
2027        if !self.shape().is_empty() {
2028            return Err(Error::NonScalarGrad {
2029                shape: self.shape().to_vec(),
2030            });
2031        }
2032
2033        let value = self.materialized_arc()?;
2034        let seed = {
2035            let mut backend = self.ctx.lock_backend()?;
2036            Arc::new(one_like_tensor(value.as_ref(), &mut *backend)?)
2037        };
2038        self.backward_from_seed(seed)
2039    }
2040
2041    /// Run reverse-mode AD from this output with an explicit cotangent seed.
2042    ///
2043    /// This is the stateful eager VJP sugar: it returns the cotangent map and
2044    /// accumulates reachable tracked leaves into their `grad()` slots. Use
2045    /// [`EagerRuntime::vjp`] when the VJP result should be returned as a
2046    /// composable eager tensor without touching grad slots.
2047    ///
2048    /// # Examples
2049    ///
2050    /// ```
2051    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
2052    /// use tenferro_cpu::CpuBackend;
2053    ///
2054    /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new());
2055    /// let x = EagerTensor::requires_grad_in(
2056    ///     Tensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0]).unwrap(),
2057    ///     ctx.clone(),
2058    /// )?;
2059    /// let seed = EagerTensor::from_tensor_in(
2060    ///     Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(),
2061    ///     ctx,
2062    /// )?;
2063    /// let y = x.mul(&x)?;
2064    /// y.backward_with(&seed)?;
2065    /// assert_eq!(x.grad()?.unwrap().as_slice::<f64>().unwrap(), &[4.0, 12.0]);
2066    /// # Ok::<(), tenferro_ad::Error>(())
2067    /// ```
2068    pub fn backward_with(
2069        &self,
2070        cotangent: &EagerTensor,
2071    ) -> Result<HashMap<ValueKey<StdTensorOp>, Arc<Tensor>>> {
2072        if !self.same_context(cotangent) {
2073            return Err(Error::ContextMismatch {
2074                lhs: self.ctx_id(),
2075                rhs: cotangent.ctx_id(),
2076            });
2077        }
2078        validate_seed_tensor("backward", self, cotangent)?;
2079        self.backward_from_seed(cotangent.materialized_arc()?)
2080    }
2081
2082    fn backward_from_seed(
2083        &self,
2084        seed: Arc<Tensor>,
2085    ) -> Result<HashMap<ValueKey<StdTensorOp>, Arc<Tensor>>> {
2086        let mut backend = self.ctx.lock_backend()?;
2087        let mut extension_executor = self.ctx.lock_extension_executor()?;
2088        let mut callbacks = TenferroBackwardCallbacks::with_runtime(
2089            &mut *backend,
2090            Some(&mut *extension_executor),
2091            Some(self.ctx.as_ref()),
2092            self.metadata_scopes.clone(),
2093        );
2094        let mut ad_ctx = ShapeGuardContext::with_global_metadata();
2095        if let Some(extension_rules) = &self.ctx.extension_rules {
2096            ad_ctx = ad_ctx.with_extension_rules(extension_rules.clone());
2097        }
2098        let cotangents_result = eager::backward(
2099            &self.key,
2100            self.trace.as_ref(),
2101            seed,
2102            &mut callbacks,
2103            &mut ad_ctx,
2104        );
2105        let callback_error = callbacks.take_error();
2106        drop(callbacks);
2107        let cotangents = match (cotangents_result, callback_error) {
2108            (_, Some(err)) => return Err(crate::ad_rule_error::ad_rule_error("backward", err)),
2109            (Err(err), None) => return Err(crate::ad_rule_error::ad_rule_error("backward", err)),
2110            (Ok(cotangents), None) => cotangents,
2111        };
2112        self.ctx.store_grads(&cotangents, &mut backend)?;
2113        Ok(cotangents)
2114    }
2115}
2116
2117pub(crate) fn eager_val_key() -> ValueKey<StdTensorOp> {
2118    ValueKey::Input(next_input_key())
2119}
2120
2121pub(crate) struct EagerTensorKeySource;
2122
2123impl KeySource<StdTensorOp> for EagerTensorKeySource {
2124    fn fresh_input_key(&mut self) -> TensorInputKey {
2125        next_input_key()
2126    }
2127}
2128
2129pub(crate) fn eager_value(tensor: &EagerTensor) -> Result<EagerInput<StdTensorOp>> {
2130    Ok(EagerInput {
2131        key: tensor.key.clone(),
2132        trace: tensor.trace.clone(),
2133        requires_grad: tensor.requires_grad,
2134        data: tensor.materialized_arc()?,
2135    })
2136}
2137
2138pub(crate) struct RecordedEagerOutputs {
2139    pub(crate) traces: Vec<EagerOutput<StdTensorOp>>,
2140    pub(crate) metadata_scope: Arc<GlobalMetadataScope>,
2141}
2142
2143pub(crate) fn record_eager_outputs(
2144    op: &StdTensorOp,
2145    outputs: &[Arc<Tensor>],
2146    inputs: &[&EagerTensor],
2147) -> Result<RecordedEagerOutputs> {
2148    let mut recorder = Recorder::new(EagerTensorKeySource);
2149    let graph_input_keys = recorder.fresh_input_keys::<StdTensorOp>(inputs.len());
2150    let graph =
2151        RecordedGraph::from_primitive(op.clone(), graph_input_keys).map_err(eager_record_error)?;
2152    let retained_values = graph
2153        .output_keys()
2154        .iter()
2155        .cloned()
2156        .zip(outputs.iter().cloned())
2157        .collect();
2158    record_eager_recorded_graph_outputs(&mut recorder, graph, outputs, retained_values, inputs)
2159}
2160
2161pub(crate) fn record_eager_recorded_graph_outputs(
2162    recorder: &mut Recorder<EagerTensorKeySource>,
2163    graph: RecordedGraph<StdTensorOp>,
2164    outputs: &[Arc<Tensor>],
2165    retained_values: HashMap<ValueKey<StdTensorOp>, Arc<Tensor>>,
2166    inputs: &[&EagerTensor],
2167) -> Result<RecordedEagerOutputs> {
2168    let input_values: Vec<_> = inputs
2169        .iter()
2170        .map(|tensor| eager_value(tensor))
2171        .collect::<Result<_>>()?;
2172    let traces = recorder
2173        .record_graph(graph, &input_values, outputs, retained_values)
2174        .map_err(eager_record_error)?;
2175
2176    let mut registrations = Vec::new();
2177    for trace in &traces {
2178        if let Some(output) = outputs.get(trace.output_slot) {
2179            registrations.push((trace.key.clone(), tensor_meta_from_tensor(output.as_ref())));
2180        }
2181    }
2182
2183    if let Some(trace) = traces.iter().find_map(|output| output.trace.as_ref()) {
2184        for (key, value) in trace.saved_values() {
2185            registrations.push((key.clone(), tensor_meta_from_tensor(value.as_ref())));
2186        }
2187    }
2188
2189    Ok(RecordedEagerOutputs {
2190        traces,
2191        metadata_scope: Arc::new(register_scoped_metadata_batch(registrations)?),
2192    })
2193}
2194
2195fn eager_record_error(err: tidu::eager::EagerRecordError) -> Error {
2196    Error::Internal(format!("invalid eager recording metadata: {err}"))
2197}
2198
2199fn eager_ad_transform_cache_error(message: impl ToString) -> ADRuleError {
2200    ADRuleError::invalid_input(
2201        "tenferro-ad.eager.transform-cache",
2202        ADRuleKind::Jvp,
2203        message.to_string(),
2204    )
2205}
2206
2207pub(crate) fn exec_single_output(
2208    op: &StdTensorOp,
2209    inputs: &[&Tensor],
2210    ctx: &EagerRuntime,
2211) -> Result<Tensor> {
2212    let mut outputs = ctx.exec_outputs(op, inputs)?;
2213    if outputs.len() != 1 {
2214        return Err(Error::Internal(format!(
2215            "expected one eager output for {:?}, got {}",
2216            op,
2217            outputs.len()
2218        )));
2219    }
2220    Ok(profile_eager_op_section(
2221        "exec_single_output.remove_output",
2222        || outputs.remove(0),
2223    ))
2224}
2225
2226pub(crate) fn exec_single_output_read(
2227    op: &StdTensorOp,
2228    inputs: &[TensorRead<'_>],
2229    ctx: &EagerRuntime,
2230) -> Result<Tensor> {
2231    let mut outputs = ctx.exec_outputs_read(op, inputs)?;
2232    if outputs.len() != 1 {
2233        return Err(Error::Internal(format!(
2234            "expected one eager output for {:?}, got {}",
2235            op,
2236            outputs.len()
2237        )));
2238    }
2239    Ok(profile_eager_op_section(
2240        "exec_single_output_read.remove_output",
2241        || outputs.remove(0),
2242    ))
2243}
2244
2245pub(crate) fn zero_like_tensor<B: TensorBackend>(
2246    input: &Tensor,
2247    backend: &mut B,
2248) -> Result<Tensor> {
2249    let host = match input {
2250        Tensor::F32(tensor) => Tensor::F32(TypedTensor::zeros(tensor.shape().to_vec())?),
2251        Tensor::F64(tensor) => Tensor::F64(TypedTensor::zeros(tensor.shape().to_vec())?),
2252        Tensor::I32(tensor) => Tensor::I32(TypedTensor::zeros(tensor.shape().to_vec())?),
2253        Tensor::I64(tensor) => Tensor::I64(TypedTensor::zeros(tensor.shape().to_vec())?),
2254        Tensor::Bool(tensor) => Tensor::Bool(TypedTensor::from_vec_col_major(
2255            tensor.shape().to_vec(),
2256            vec![false; tensor.n_elements()],
2257        )?),
2258        Tensor::C32(tensor) => Tensor::C32(TypedTensor::zeros(tensor.shape().to_vec())?),
2259        Tensor::C64(tensor) => Tensor::C64(TypedTensor::zeros(tensor.shape().to_vec())?),
2260    };
2261    backend.upload_host_tensor(&host).map_err(Error::from)
2262}
2263
2264pub(crate) fn one_like_tensor<B: TensorBackend>(input: &Tensor, backend: &mut B) -> Result<Tensor> {
2265    let host = ones_tensor(input.dtype(), input.shape().to_vec())?;
2266    backend.upload_host_tensor(&host).map_err(Error::from)
2267}
2268
2269#[cfg(test)]
2270mod tests;