oxmera_tensor/
autograd.rs1use std::cell::Cell;
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10
11use oxmera_core::Result;
12
13use crate::tensor::Tensor;
14
15pub struct GradFn {
19 pub inputs: Vec<Tensor>,
21 #[allow(clippy::type_complexity)]
23 pub vjp: Box<dyn Fn(&Tensor) -> Result<Vec<Option<Tensor>>> + Send + Sync>,
24}
25
26impl std::fmt::Debug for GradFn {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("GradFn")
29 .field("inputs", &self.inputs.len())
30 .finish()
31 }
32}
33
34#[derive(Debug)]
36pub struct AutogradMeta {
37 pub(crate) requires_grad: bool,
39 pub(crate) grad: Mutex<Option<Tensor>>,
41 pub(crate) grad_fn: Option<GradFn>,
43}
44
45thread_local! {
46 static RECORDING: Cell<bool> = const { Cell::new(true) };
47}
48
49pub fn is_recording() -> bool {
51 RECORDING.with(Cell::get)
52}
53
54pub fn no_grad<R>(f: impl FnOnce() -> R) -> R {
58 let _guard = NoGradGuard::new();
59 f()
60}
61
62pub struct NoGradGuard {
64 previous: bool,
65}
66
67impl NoGradGuard {
68 pub fn new() -> Self {
70 let previous = RECORDING.with(|r| r.replace(false));
71 Self { previous }
72 }
73}
74
75impl Default for NoGradGuard {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl Drop for NoGradGuard {
82 fn drop(&mut self) {
83 let previous = self.previous;
84 RECORDING.with(|r| r.set(previous));
85 }
86}
87
88fn meta_id(meta: &Arc<AutogradMeta>) -> usize {
89 Arc::as_ptr(meta) as usize
90}
91
92pub(crate) fn run_backward(root: &Tensor, seed: Tensor) -> Result<()> {
95 no_grad(|| run_backward_inner(root, seed))
97}
98
99fn run_backward_inner(root: &Tensor, seed: Tensor) -> Result<()> {
100 let Some(root_meta) = root.autograd_meta() else {
101 return Ok(());
102 };
103
104 let mut order: Vec<Tensor> = Vec::new();
106 let mut visited: HashMap<usize, ()> = HashMap::new();
107 let mut stack: Vec<(Tensor, bool)> = vec![(root.clone(), false)];
108 while let Some((t, expanded)) = stack.pop() {
109 let Some(meta) = t.autograd_meta() else {
110 continue;
111 };
112 let id = meta_id(&meta);
113 if expanded {
114 order.push(t);
115 continue;
116 }
117 if visited.contains_key(&id) {
118 continue;
119 }
120 visited.insert(id, ());
121 stack.push((t.clone(), true));
122 if let Some(gf) = &meta.grad_fn {
123 for input in &gf.inputs {
124 stack.push((input.clone(), false));
125 }
126 }
127 }
128
129 let mut pending: HashMap<usize, Tensor> = HashMap::new();
130 pending.insert(meta_id(&root_meta), seed);
131
132 for t in order.into_iter().rev() {
133 let meta = t.autograd_meta().expect("ordered tensors carry meta");
134 let id = meta_id(&meta);
135 let Some(grad) = pending.remove(&id) else {
136 continue;
137 };
138
139 if meta.requires_grad {
140 let mut slot = meta.grad.lock().expect("grad mutex poisoned");
141 *slot = Some(match slot.take() {
142 Some(existing) => existing.add(&grad)?,
143 None => grad.clone(),
144 });
145 }
146
147 if let Some(gf) = &meta.grad_fn {
148 let input_grads = (gf.vjp)(&grad)?;
149 debug_assert_eq!(input_grads.len(), gf.inputs.len());
150 for (input, ig) in gf.inputs.iter().zip(input_grads) {
151 let (Some(im), Some(ig)) = (input.autograd_meta(), ig) else {
152 continue;
153 };
154 let iid = meta_id(&im);
155 let accumulated = match pending.remove(&iid) {
156 Some(existing) => existing.add(&ig)?,
157 None => ig,
158 };
159 pending.insert(iid, accumulated);
160 }
161 }
162 }
163 Ok(())
164}