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#[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220pub struct EagerRuntimeCacheStats {
221 pub extensions: CacheStats,
223 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
233pub 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 pub fn new() -> Arc<Self> {
399 Self::with_cpu_backend(CpuBackend::new())
400 }
401
402 pub fn with_cpu_backend(backend: CpuBackend) -> Arc<Self> {
414 Arc::new(Self::from_backend(EagerBackend::cpu(backend)))
415 }
416
417 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 #[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 #[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 #[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 #[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 pub fn id(&self) -> ContextId {
521 self.id
522 }
523
524 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 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 pub fn clear_extension_caches(&self) -> Result<()> {
583 self.lock_extension_executor()?.clear_caches();
584 Ok(())
585 }
586
587 pub fn clear_caches(&self) -> Result<()> {
602 self.clear_extension_caches()?;
603 self.clear_ad_transform_caches()?;
604 Ok(())
605 }
606
607 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 pub fn ad_transform_cache_limits(&self) -> Result<AdTransformCacheLimits> {
641 self.ad_transform_cache.limits()
642 }
643
644 pub fn set_ad_transform_cache_limits(&self, limits: AdTransformCacheLimits) -> Result<()> {
660 self.ad_transform_cache.set_limits(limits)
661 }
662
663 pub fn clear_ad_transform_caches(&self) -> Result<()> {
677 self.ad_transform_cache.clear()
678 }
679
680 pub fn extension_cache_limits(&self) -> Result<ExtensionCacheLimits> {
682 Ok(self.lock_extension_executor()?.cache_limits())
683 }
684
685 pub fn set_extension_cache_limits(&self, limits: ExtensionCacheLimits) -> Result<()> {
687 self.lock_extension_executor()?.set_cache_limits(limits);
688 Ok(())
689 }
690
691 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 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 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 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 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 pub fn constant_from(self: &Arc<Self>, tensor: Tensor) -> Result<EagerTensor> {
1068 EagerTensor::new_leaf(Arc::clone(self), tensor, false)
1069 }
1070
1071 pub fn variable_from(self: &Arc<Self>, tensor: Tensor) -> Result<EagerTensor> {
1092 EagerTensor::new_leaf(Arc::clone(self), tensor, true)
1093 }
1094
1095 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 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 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 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 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 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#[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 pub fn from_tensor_in(tensor: Tensor, ctx: Arc<EagerRuntime>) -> Result<Self> {
1467 Self::new_leaf(ctx, tensor, false)
1468 }
1469
1470 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 pub fn detach(&self) -> Self {
1672 Self::new_untracked_value_result(self.ctx.clone(), self.value.as_ref().clone())
1673 }
1674
1675 pub fn detach_into(&self, ctx: &Arc<EagerRuntime>) -> Result<Self> {
1694 Self::from_tensor_in(self.to_tensor()?, Arc::clone(ctx))
1695 }
1696
1697 pub fn materialized(&self) -> Result<Arc<Tensor>> {
1711 self.materialized_arc()
1712 }
1713
1714 pub fn dtype(&self) -> DType {
1717 self.value.dtype()
1718 }
1719
1720 pub fn shape(&self) -> &[usize] {
1723 self.value.shape()
1724 }
1725
1726 pub fn tensor_read(&self) -> TensorRead<'_> {
1732 self.value.tensor_read()
1733 }
1734
1735 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 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 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 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 pub fn ctx_id(&self) -> ContextId {
1875 self.ctx.id()
1876 }
1877
1878 pub fn runtime(&self) -> &Arc<EagerRuntime> {
1880 &self.ctx
1881 }
1882
1883 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 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 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;