diff --git a/.jules/bolt.md b/.jules/bolt.md index 2ac6922..efb4d5f 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,7 @@ ## 2026-05-01 - Avoid High-Level Tensor Ops in Scalar Reductions **Learning:** High-level `Tensor` operations like `sub()` and `mul()` trigger intermediate heap allocations for shape and stride metadata. When computing scalar reductions (like MSE, distances, or loss functions), using these operations introduces severe memory overhead inside hot loops. Attempting to use `.min()` length truncation as a safeguard is an anti-pattern as it masks shape mismatch errors. **Action:** For scalar reductions, assert shape equality (`assert_eq!(a.shape, b.shape)`) and perform a single-pass iteration directly over the underlying borrowed data arrays (`a.data.borrow()`) to eliminate intermediate allocations and safely compute the result. + +## 2026-06-05 - Avoid Redundant Tensor Clones in Autograd Backprop +**Learning:** In the `backward` pass of the autograd engine, retrieving gradients from the `grads` vector using `.clone()` causes unnecessary heap allocations for `Tensor` metadata (shape and strides). Because the `grads` vector is a `Vec>`, we can use `.take()` to take ownership of the gradient, perform our calculations, and then place it back in the vector. Furthermore, `accumulate_grad` was passing `Tensor` references and then cloning inside when empty. Taking `Tensor` by value in `accumulate_grad` avoids cloning when placing the new tensor into a `None` slot. +**Action:** When working with vectors of `Option` where `T` is heap-allocated or expensive to clone, use `.take()` to temporarily borrow ownership if the vector does not need to be accessed concurrently. Pass such types by value into accumulator functions to avoid redundant allocations when storing them. diff --git a/crates/aether-core/src/ml/autograd.rs b/crates/aether-core/src/ml/autograd.rs index 3856cc5..01332b8 100644 --- a/crates/aether-core/src/ml/autograd.rs +++ b/crates/aether-core/src/ml/autograd.rs @@ -17,7 +17,6 @@ // ═══════════════════════════════════════════════════════════════════════════════ // - #[cfg(not(feature = "std"))] extern crate alloc; @@ -201,30 +200,32 @@ impl<'a> Context<'a> { match op { Op::Add { out, lhs, rhs } => { // Solves borrow checker by cloning Option first - let grad_out = grads[out.index].clone(); + let grad_out = grads[out.index].take(); if let Some(grad) = grad_out { // dL/d(lhs) += dL/dout * 1 - Self::accumulate_grad(&mut grads, lhs.index, &grad); - Self::accumulate_grad(&mut grads, rhs.index, &grad); + Self::accumulate_grad(&mut grads, lhs.index, grad.clone()); + Self::accumulate_grad(&mut grads, rhs.index, grad.clone()); + grads[out.index] = Some(grad); } } Op::Mul { out, lhs, rhs } => { - let grad_out = grads[out.index].clone(); + let grad_out = grads[out.index].take(); if let Some(grad) = grad_out { let lhs_val = self.heap.get(*lhs).unwrap(); let rhs_val = self.heap.get(*rhs).unwrap(); // dL/dLhs = grad_out * rhs let d_lhs: Tensor = grad.mul(rhs_val); - Self::accumulate_grad(&mut grads, lhs.index, &d_lhs); + Self::accumulate_grad(&mut grads, lhs.index, d_lhs); // dL/dRhs = grad_out * lhs let d_rhs: Tensor = grad.mul(lhs_val); - Self::accumulate_grad(&mut grads, rhs.index, &d_rhs); + Self::accumulate_grad(&mut grads, rhs.index, d_rhs); + grads[out.index] = Some(grad); } } Op::MatMul { out, lhs, rhs } => { - let grad_out = grads[out.index].clone(); + let grad_out = grads[out.index].take(); if let Some(grad) = grad_out { let lhs_val = self.heap.get(*lhs).unwrap(); let rhs_val = self.heap.get(*rhs).unwrap(); @@ -232,21 +233,23 @@ impl<'a> Context<'a> { // C = A @ B // dA = dC @ B^T let d_lhs: Tensor = grad.matmul(&rhs_val.transpose()); - Self::accumulate_grad(&mut grads, lhs.index, &d_lhs); + Self::accumulate_grad(&mut grads, lhs.index, d_lhs); // dB = A^T @ dC let d_rhs: Tensor = lhs_val.transpose().matmul(&grad); - Self::accumulate_grad(&mut grads, rhs.index, &d_rhs); + Self::accumulate_grad(&mut grads, rhs.index, d_rhs); + grads[out.index] = Some(grad); } } Op::ReLU { out, input } => { - let grad_out = grads[out.index].clone(); + let grad_out = grads[out.index].take(); if let Some(grad) = grad_out { let input_val = self.heap.get(*input).unwrap(); // dL/dx = grad_out * (1 if x > 0 else 0) let mask = input_val.map(|x| if x > 0.0 { 1.0 } else { 0.0 }); let d_input: Tensor = grad.mul(&mask); - Self::accumulate_grad(&mut grads, input.index, &d_input); + Self::accumulate_grad(&mut grads, input.index, d_input); + grads[out.index] = Some(grad); } } } @@ -255,18 +258,18 @@ impl<'a> Context<'a> { grads } - fn accumulate_grad(grads: &mut Vec>, idx: usize, grad: &Tensor) { + fn accumulate_grad(grads: &mut Vec>, idx: usize, grad: Tensor) { if idx >= grads.len() { grads.resize(idx + 1 + 256, None); } match &mut grads[idx] { Some(existing) => { - let new = existing.add(grad); + let new = existing.add(&grad); grads[idx] = Some(new); } None => { - grads[idx] = Some(grad.clone()); + grads[idx] = Some(grad); } } }