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
45impl Drop for AutogradMeta {
46 fn drop(&mut self) {
56 let mut stack: Vec<Tensor> = Vec::new();
57 let shed = |meta: &mut AutogradMeta, stack: &mut Vec<Tensor>| {
58 if let Some(gf) = meta.grad_fn.take() {
59 stack.extend(gf.inputs);
60 }
61 if let Ok(slot) = meta.grad.get_mut()
62 && let Some(g) = slot.take()
63 {
64 stack.push(g);
65 }
66 };
67 shed(self, &mut stack);
68 while let Some(mut tensor) = stack.pop() {
69 if let Some(node) = tensor.take_autograd()
72 && let Some(mut owned) = Arc::into_inner(node)
73 {
74 shed(&mut owned, &mut stack);
75 }
76 }
77 }
78}
79
80thread_local! {
81 static RECORDING: Cell<bool> = const { Cell::new(true) };
82}
83
84pub fn is_recording() -> bool {
86 RECORDING.with(Cell::get)
87}
88
89pub fn no_grad<R>(f: impl FnOnce() -> R) -> R {
93 let _guard = NoGradGuard::new();
94 f()
95}
96
97pub struct NoGradGuard {
99 previous: bool,
100}
101
102impl NoGradGuard {
103 pub fn new() -> Self {
105 let previous = RECORDING.with(|r| r.replace(false));
106 Self { previous }
107 }
108}
109
110impl Default for NoGradGuard {
111 fn default() -> Self {
112 Self::new()
113 }
114}
115
116impl Drop for NoGradGuard {
117 fn drop(&mut self) {
118 let previous = self.previous;
119 RECORDING.with(|r| r.set(previous));
120 }
121}
122
123fn meta_id(meta: &Arc<AutogradMeta>) -> usize {
124 Arc::as_ptr(meta) as usize
125}
126
127pub(crate) fn run_backward(root: &Tensor, seed: Tensor) -> Result<()> {
130 no_grad(|| run_backward_inner(root, seed))
132}
133
134fn run_backward_inner(root: &Tensor, seed: Tensor) -> Result<()> {
135 let Some(root_meta) = root.autograd_meta() else {
136 return Ok(());
137 };
138
139 let mut order: Vec<Tensor> = Vec::new();
141 let mut visited: HashMap<usize, ()> = HashMap::new();
142 let mut stack: Vec<(Tensor, bool)> = vec![(root.clone(), false)];
143 while let Some((t, expanded)) = stack.pop() {
144 let Some(meta) = t.autograd_meta() else {
145 continue;
146 };
147 let id = meta_id(&meta);
148 if expanded {
149 order.push(t);
150 continue;
151 }
152 if visited.contains_key(&id) {
153 continue;
154 }
155 visited.insert(id, ());
156 stack.push((t.clone(), true));
157 if let Some(gf) = &meta.grad_fn {
158 for input in &gf.inputs {
159 stack.push((input.clone(), false));
160 }
161 }
162 }
163
164 let mut pending: HashMap<usize, Tensor> = HashMap::new();
165 pending.insert(meta_id(&root_meta), seed);
166
167 for t in order.into_iter().rev() {
168 let meta = t.autograd_meta().expect("ordered tensors carry meta");
169 let id = meta_id(&meta);
170 let Some(grad) = pending.remove(&id) else {
171 continue;
172 };
173
174 if meta.requires_grad {
175 let mut slot = meta.grad.lock().expect("grad mutex poisoned");
176 *slot = Some(match slot.take() {
177 Some(existing) => existing.add(&grad)?,
178 None => grad.clone(),
179 });
180 }
181
182 if let Some(gf) = &meta.grad_fn {
183 let input_grads = (gf.vjp)(&grad)?;
184 debug_assert_eq!(input_grads.len(), gf.inputs.len());
185 for (input, ig) in gf.inputs.iter().zip(input_grads) {
186 let (Some(im), Some(ig)) = (input.autograd_meta(), ig) else {
187 continue;
188 };
189 let iid = meta_id(&im);
190 let accumulated = match pending.remove(&iid) {
191 Some(existing) => existing.add(&ig)?,
192 None => ig,
193 };
194 pending.insert(iid, accumulated);
195 }
196 }
197 }
198 Ok(())
199}