Merge pull request 'rtx-autograd: make the tape correct + sound on real backends' (#3) from autograd-trainable-real-backend into main
Performance Benchmarks / Run Benchmarks (push) Has been cancelled
CI / Format Check (push) Has been cancelled
CI / Clippy Check (push) Has been cancelled
CI / Build (macos-latest) (push) Has been cancelled
CI / Build (ubuntu-latest) (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Build CPU-Only (Explicit) (push) Has been cancelled
CI / CI Success (push) Has been cancelled
Documentation / Build API Documentation (push) Has been cancelled
Documentation / Build User Guide (push) Has been cancelled

This commit was merged in pull request #3.
This commit is contained in:
2026-06-16 04:07:05 +00:00
12 changed files with 404 additions and 123 deletions
+4
View File
@@ -36,6 +36,10 @@ criterion.workspace = true
approx = "0.5" approx = "0.5"
rand = "0.8" rand = "0.8"
# Real CPU backend for numerical gradient-checking the tape (no cycle:
# rtx-backend-cpu depends only on rtx-backend / rtx-tensor, not rtx-autograd).
rtx-backend-cpu = { path = "../rtx-backend-cpu" }
[features] [features]
default = [] default = []
disabled_tests = [] disabled_tests = []
@@ -20,6 +20,7 @@ use rtx_backend::{AutodiffBackend, Backend, DeviceId, DeviceOps, GradientMap};
use super::context::is_grad_enabled; use super::context::is_grad_enabled;
use super::graph::{GradientStorage, backward_impl}; use super::graph::{GradientStorage, backward_impl};
use super::node::{AutodiffNode, GradTensor, ParentRef}; use super::node::{AutodiffNode, GradTensor, ParentRef};
use super::ops::into_dim;
use super::tensor::AutodiffTensor; use super::tensor::AutodiffTensor;
// ============================================================================ // ============================================================================
@@ -1303,12 +1304,12 @@ where
// 2. The match arm ensures D matches the target dimension (e.g., D=1 matches D1) // 2. The match arm ensures D matches the target dimension (e.g., D=1 matches D1)
// 3. `ones` is a valid B::TensorPrimitive<D> and we copy it to the same type with explicit D // 3. `ones` is a valid B::TensorPrimitive<D> and we copy it to the same type with explicit D
let initial_grad = match D { let initial_grad = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&ones) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(ones)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&ones) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(ones)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&ones) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(ones)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&ones) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(ones)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&ones) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(ones)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&ones) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(ones)),
_ => { _ => {
return Err(AutogradError::UnsupportedDimension(D).into()); return Err(AutogradError::UnsupportedDimension(D).into());
} }
+17 -12
View File
@@ -212,19 +212,24 @@ where
b.ndim() b.ndim()
); );
// For now, just return b (proper accumulation requires Backend::add) // When a tensor fans out (is used by more than one downstream op, or
// This will be implemented properly when we have access to Backend operations // appears more than once as a parent), each path contributes a gradient
match (&a, &b) { // and the contributions must be *summed* at the shared sink. The previous
(GradTensor::D1(_), GradTensor::D1(_)) => Ok(b), // body returned `b` and discarded `a`, so any reuse (residuals, `mul(s, s)`,
(GradTensor::D2(_), GradTensor::D2(_)) => Ok(b), // shared Q/K/V inputs — universal in transformers) silently dropped a path
(GradTensor::D3(_), GradTensor::D3(_)) => Ok(b), // and produced gradients that were a fraction of the true value. Sum them
(GradTensor::D4(_), GradTensor::D4(_)) => Ok(b), // with the backend's elementwise add.
(GradTensor::D5(_), GradTensor::D5(_)) => Ok(b), let ndim_a = a.ndim();
(GradTensor::D6(_), GradTensor::D6(_)) => Ok(b), let ndim_b = b.ndim();
match (a, b) {
(GradTensor::D1(x), GradTensor::D1(y)) => Ok(GradTensor::D1(B::add(x, y))),
(GradTensor::D2(x), GradTensor::D2(y)) => Ok(GradTensor::D2(B::add(x, y))),
(GradTensor::D3(x), GradTensor::D3(y)) => Ok(GradTensor::D3(B::add(x, y))),
(GradTensor::D4(x), GradTensor::D4(y)) => Ok(GradTensor::D4(B::add(x, y))),
(GradTensor::D5(x), GradTensor::D5(y)) => Ok(GradTensor::D5(B::add(x, y))),
(GradTensor::D6(x), GradTensor::D6(y)) => Ok(GradTensor::D6(B::add(x, y))),
_ => Err(AutogradError::DimensionMismatch(format!( _ => Err(AutogradError::DimensionMismatch(format!(
"Cannot accumulate gradients of different dimensions: {} and {}", "Cannot accumulate gradients of different dimensions: {ndim_a} and {ndim_b}"
a.ndim(),
b.ndim()
))), ))),
} }
} }
@@ -9,6 +9,7 @@
//! = `grad_output * sigmoid(x) * (1 + x * (1 - sigmoid(x)))` //! = `grad_output * sigmoid(x) * (1 + x * (1 - sigmoid(x)))`
use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor}; use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor};
use crate::autodiff::ops::into_dim;
use crate::error::{AutogradError, Result}; use crate::error::{AutogradError, Result};
use rtx_backend::Backend; use rtx_backend::Backend;
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -597,32 +598,32 @@ where
match (grad, D) { match (grad, D) {
(GradTensor::D1(g), 1) => { (GradTensor::D1(g), 1) => {
// SAFETY: D=1 verified by match, layout identical across D values // SAFETY: D=1 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<1>>(derivative) }; let t = into_dim::<B, D, 1>(derivative.clone());
Ok(GradTensor::D1(B::mul(g.clone(), t))) Ok(GradTensor::D1(B::mul(g.clone(), t)))
} }
(GradTensor::D2(g), 2) => { (GradTensor::D2(g), 2) => {
// SAFETY: D=2 verified by match, layout identical across D values // SAFETY: D=2 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<2>>(derivative) }; let t = into_dim::<B, D, 2>(derivative.clone());
Ok(GradTensor::D2(B::mul(g.clone(), t))) Ok(GradTensor::D2(B::mul(g.clone(), t)))
} }
(GradTensor::D3(g), 3) => { (GradTensor::D3(g), 3) => {
// SAFETY: D=3 verified by match, layout identical across D values // SAFETY: D=3 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<3>>(derivative) }; let t = into_dim::<B, D, 3>(derivative.clone());
Ok(GradTensor::D3(B::mul(g.clone(), t))) Ok(GradTensor::D3(B::mul(g.clone(), t)))
} }
(GradTensor::D4(g), 4) => { (GradTensor::D4(g), 4) => {
// SAFETY: D=4 verified by match, layout identical across D values // SAFETY: D=4 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<4>>(derivative) }; let t = into_dim::<B, D, 4>(derivative.clone());
Ok(GradTensor::D4(B::mul(g.clone(), t))) Ok(GradTensor::D4(B::mul(g.clone(), t)))
} }
(GradTensor::D5(g), 5) => { (GradTensor::D5(g), 5) => {
// SAFETY: D=5 verified by match, layout identical across D values // SAFETY: D=5 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<5>>(derivative) }; let t = into_dim::<B, D, 5>(derivative.clone());
Ok(GradTensor::D5(B::mul(g.clone(), t))) Ok(GradTensor::D5(B::mul(g.clone(), t)))
} }
(GradTensor::D6(g), 6) => { (GradTensor::D6(g), 6) => {
// SAFETY: D=6 verified by match, layout identical across D values // SAFETY: D=6 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<6>>(derivative) }; let t = into_dim::<B, D, 6>(derivative.clone());
Ok(GradTensor::D6(B::mul(g.clone(), t))) Ok(GradTensor::D6(B::mul(g.clone(), t)))
} }
_ => Err(AutogradError::DimensionMismatch( _ => Err(AutogradError::DimensionMismatch(
@@ -9,6 +9,7 @@
//! - Neg: `grad_input = -grad_out` //! - Neg: `grad_input = -grad_out`
use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor}; use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor};
use crate::autodiff::ops::into_dim;
use crate::error::{AutogradError, Result}; use crate::error::{AutogradError, Result};
use rtx_backend::Backend; use rtx_backend::Backend;
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -294,32 +295,32 @@ where
match (grad, D) { match (grad, D) {
(GradTensor::D1(g), 1) => { (GradTensor::D1(g), 1) => {
// SAFETY: D=1 verified by match, layout identical across D values // SAFETY: D=1 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<1>>(tensor) }; let t = into_dim::<B, D, 1>(tensor.clone());
Ok(GradTensor::D1(B::mul(g, t))) Ok(GradTensor::D1(B::mul(g, t)))
} }
(GradTensor::D2(g), 2) => { (GradTensor::D2(g), 2) => {
// SAFETY: D=2 verified by match, layout identical across D values // SAFETY: D=2 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<2>>(tensor) }; let t = into_dim::<B, D, 2>(tensor.clone());
Ok(GradTensor::D2(B::mul(g, t))) Ok(GradTensor::D2(B::mul(g, t)))
} }
(GradTensor::D3(g), 3) => { (GradTensor::D3(g), 3) => {
// SAFETY: D=3 verified by match, layout identical across D values // SAFETY: D=3 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<3>>(tensor) }; let t = into_dim::<B, D, 3>(tensor.clone());
Ok(GradTensor::D3(B::mul(g, t))) Ok(GradTensor::D3(B::mul(g, t)))
} }
(GradTensor::D4(g), 4) => { (GradTensor::D4(g), 4) => {
// SAFETY: D=4 verified by match, layout identical across D values // SAFETY: D=4 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<4>>(tensor) }; let t = into_dim::<B, D, 4>(tensor.clone());
Ok(GradTensor::D4(B::mul(g, t))) Ok(GradTensor::D4(B::mul(g, t)))
} }
(GradTensor::D5(g), 5) => { (GradTensor::D5(g), 5) => {
// SAFETY: D=5 verified by match, layout identical across D values // SAFETY: D=5 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<5>>(tensor) }; let t = into_dim::<B, D, 5>(tensor.clone());
Ok(GradTensor::D5(B::mul(g, t))) Ok(GradTensor::D5(B::mul(g, t)))
} }
(GradTensor::D6(g), 6) => { (GradTensor::D6(g), 6) => {
// SAFETY: D=6 verified by match, layout identical across D values // SAFETY: D=6 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<6>>(tensor) }; let t = into_dim::<B, D, 6>(tensor.clone());
Ok(GradTensor::D6(B::mul(g, t))) Ok(GradTensor::D6(B::mul(g, t)))
} }
_ => Err(AutogradError::DimensionMismatch( _ => Err(AutogradError::DimensionMismatch(
@@ -345,32 +346,32 @@ where
match (grad, D) { match (grad, D) {
(GradTensor::D1(g), 1) => { (GradTensor::D1(g), 1) => {
// SAFETY: D=1 verified by match, layout identical across D values // SAFETY: D=1 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<1>>(tensor) }; let t = into_dim::<B, D, 1>(tensor.clone());
Ok(GradTensor::D1(B::div(g, t))) Ok(GradTensor::D1(B::div(g, t)))
} }
(GradTensor::D2(g), 2) => { (GradTensor::D2(g), 2) => {
// SAFETY: D=2 verified by match, layout identical across D values // SAFETY: D=2 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<2>>(tensor) }; let t = into_dim::<B, D, 2>(tensor.clone());
Ok(GradTensor::D2(B::div(g, t))) Ok(GradTensor::D2(B::div(g, t)))
} }
(GradTensor::D3(g), 3) => { (GradTensor::D3(g), 3) => {
// SAFETY: D=3 verified by match, layout identical across D values // SAFETY: D=3 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<3>>(tensor) }; let t = into_dim::<B, D, 3>(tensor.clone());
Ok(GradTensor::D3(B::div(g, t))) Ok(GradTensor::D3(B::div(g, t)))
} }
(GradTensor::D4(g), 4) => { (GradTensor::D4(g), 4) => {
// SAFETY: D=4 verified by match, layout identical across D values // SAFETY: D=4 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<4>>(tensor) }; let t = into_dim::<B, D, 4>(tensor.clone());
Ok(GradTensor::D4(B::div(g, t))) Ok(GradTensor::D4(B::div(g, t)))
} }
(GradTensor::D5(g), 5) => { (GradTensor::D5(g), 5) => {
// SAFETY: D=5 verified by match, layout identical across D values // SAFETY: D=5 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<5>>(tensor) }; let t = into_dim::<B, D, 5>(tensor.clone());
Ok(GradTensor::D5(B::div(g, t))) Ok(GradTensor::D5(B::div(g, t)))
} }
(GradTensor::D6(g), 6) => { (GradTensor::D6(g), 6) => {
// SAFETY: D=6 verified by match, layout identical across D values // SAFETY: D=6 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<6>>(tensor) }; let t = into_dim::<B, D, 6>(tensor.clone());
Ok(GradTensor::D6(B::div(g, t))) Ok(GradTensor::D6(B::div(g, t)))
} }
_ => Err(AutogradError::DimensionMismatch( _ => Err(AutogradError::DimensionMismatch(
@@ -8,6 +8,7 @@
//! - FlashAttention: Fused attention mechanism //! - FlashAttention: Fused attention mechanism
use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor}; use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor};
use crate::autodiff::ops::into_dim;
use crate::error::{AutogradError, Result}; use crate::error::{AutogradError, Result};
use rtx_backend::Backend; use rtx_backend::Backend;
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -66,11 +67,16 @@ where
let grad_out_tensor = extract_tensor::<B, D>(&grad_output)?; let grad_out_tensor = extract_tensor::<B, D>(&grad_output)?;
let grad_times_soft = B::mul(grad_out_tensor.clone(), softmax_out.clone()); let grad_times_soft = B::mul(grad_out_tensor.clone(), softmax_out.clone());
// Step 2: sum along dim (keeping dims for broadcast) // Step 2: sum along dim. `sum_dim` is keep-dim, so this is the full
// shape with `dim` collapsed to 1.
let sum_grad_soft = B::sum_dim(grad_times_soft, *dim); let sum_grad_soft = B::sum_dim(grad_times_soft, *dim);
// Step 3: grad_y - sum // Step 3: grad_y - sum. The elementwise backends require identical
let grad_minus_sum = B::sub(grad_out_tensor, sum_grad_soft); // shapes (no implicit broadcasting), so tile the per-row sum back to
// the full width along `dim` before subtracting.
let full = B::shape(softmax_out)[*dim];
let sum_broadcast = broadcast_along_dim::<B, D>(sum_grad_soft, *dim, full);
let grad_minus_sum = B::sub(grad_out_tensor, sum_broadcast);
// Step 4: softmax * (grad_y - sum) // Step 4: softmax * (grad_y - sum)
let grad_input = B::mul(softmax_out.clone(), grad_minus_sum); let grad_input = B::mul(softmax_out.clone(), grad_minus_sum);
@@ -595,6 +601,37 @@ where
/// ///
/// The Backend trait's softmax already implements this internally. /// The Backend trait's softmax already implements this internally.
/// Tile a keep-dimreduced tensor (size 1 along `dim`) back to `full`
/// elements along `dim`.
///
/// The elementwise backends (`add`/`sub`/`mul`) assert identical operand
/// shapes — there is no implicit broadcasting — so a per-row reduction must
/// be materialised to the full shape before it can be combined with the
/// unreduced tensor. Done generically via host data so it works for any
/// backend and dimension.
fn broadcast_along_dim<B: Backend, const D: usize>(
reduced: B::TensorPrimitive<D>,
dim: usize,
full: usize,
) -> B::TensorPrimitive<D> {
let shape = B::shape(&reduced);
let device = B::device(&reduced);
let data = B::to_data(&reduced);
let outer: usize = shape[..dim].iter().product();
let inner: usize = shape[dim + 1..].iter().product();
let mut out_shape = shape;
out_shape[dim] = full;
let mut out = Vec::with_capacity(outer * full * inner);
for o in 0..outer {
for _f in 0..full {
for i in 0..inner {
out.push(data[o * inner + i]);
}
}
}
B::from_data(&out, out_shape, &device)
}
/// Numerically stable softmax backward using log-sum-exp values. /// Numerically stable softmax backward using log-sum-exp values.
/// ///
/// Given: /// Given:
@@ -616,9 +653,13 @@ fn stable_softmax_backward<B: Backend>(
where where
B::TensorPrimitive<3>: Clone, B::TensorPrimitive<3>: Clone,
{ {
// Standard softmax backward: grad_x = y * (grad_y - sum(grad_y * y, dim)) // Standard softmax backward: grad_x = y * (grad_y - sum(grad_y * y, dim)).
// `sum_dim` is keep-dim; tile the per-row sum back to full width along
// `dim` before subtracting, since the elementwise backends don't broadcast.
let grad_times_softmax = B::mul(grad_output.clone(), softmax_output.clone()); let grad_times_softmax = B::mul(grad_output.clone(), softmax_output.clone());
let rowsum = B::sum_dim(grad_times_softmax, dim); let rowsum = B::sum_dim(grad_times_softmax, dim);
let full = B::shape(&softmax_output)[dim];
let rowsum = broadcast_along_dim::<B, 3>(rowsum, dim, full);
let grad_minus_rowsum = B::sub(grad_output, rowsum); let grad_minus_rowsum = B::sub(grad_output, rowsum);
let grad_input = B::mul(softmax_output, grad_minus_rowsum); let grad_input = B::mul(softmax_output, grad_minus_rowsum);
@@ -634,16 +675,22 @@ where
/// 3. t is a valid reference and we create a copy to the correctly-typed primitive /// 3. t is a valid reference and we create a copy to the correctly-typed primitive
fn extract_tensor<B: Backend, const D: usize>(grad: &GradTensor<B>) -> Result<B::TensorPrimitive<D>> fn extract_tensor<B: Backend, const D: usize>(grad: &GradTensor<B>) -> Result<B::TensorPrimitive<D>>
where where
B::TensorPrimitive<D>: Clone, B::TensorPrimitive<1>: Clone,
B::TensorPrimitive<2>: Clone,
B::TensorPrimitive<3>: Clone,
B::TensorPrimitive<4>: Clone,
B::TensorPrimitive<5>: Clone,
B::TensorPrimitive<6>: Clone,
{ {
// SAFETY: D is verified by each match arm; layout identical across D values // `into_dim` consumes the cloned variant tensor (transmute_copy +
// mem::forget) and reinterprets it at the runtime dimension `D`.
match (grad, D) { match (grad, D) {
(GradTensor::D1(t), 1) => Ok(unsafe { std::mem::transmute_copy(t) }), (GradTensor::D1(t), 1) => Ok(into_dim::<B, 1, D>(t.clone())),
(GradTensor::D2(t), 2) => Ok(unsafe { std::mem::transmute_copy(t) }), (GradTensor::D2(t), 2) => Ok(into_dim::<B, 2, D>(t.clone())),
(GradTensor::D3(t), 3) => Ok(unsafe { std::mem::transmute_copy(t) }), (GradTensor::D3(t), 3) => Ok(into_dim::<B, 3, D>(t.clone())),
(GradTensor::D4(t), 4) => Ok(unsafe { std::mem::transmute_copy(t) }), (GradTensor::D4(t), 4) => Ok(into_dim::<B, 4, D>(t.clone())),
(GradTensor::D5(t), 5) => Ok(unsafe { std::mem::transmute_copy(t) }), (GradTensor::D5(t), 5) => Ok(into_dim::<B, 5, D>(t.clone())),
(GradTensor::D6(t), 6) => Ok(unsafe { std::mem::transmute_copy(t) }), (GradTensor::D6(t), 6) => Ok(into_dim::<B, 6, D>(t.clone())),
_ => Err(AutogradError::DimensionMismatch( _ => Err(AutogradError::DimensionMismatch(
"llm operation".to_string(), "llm operation".to_string(),
)), )),
@@ -660,12 +707,12 @@ where
fn wrap_tensor<B: Backend, const D: usize>(tensor: B::TensorPrimitive<D>) -> Result<GradTensor<B>> { fn wrap_tensor<B: Backend, const D: usize>(tensor: B::TensorPrimitive<D>) -> Result<GradTensor<B>> {
// SAFETY: D is verified by each match arm; layout identical across D values // SAFETY: D is verified by each match arm; layout identical across D values
match D { match D {
1 => Ok(GradTensor::D1(unsafe { std::mem::transmute_copy(&tensor) })), 1 => Ok(GradTensor::D1(into_dim::<B, D, 1>(tensor))),
2 => Ok(GradTensor::D2(unsafe { std::mem::transmute_copy(&tensor) })), 2 => Ok(GradTensor::D2(into_dim::<B, D, 2>(tensor))),
3 => Ok(GradTensor::D3(unsafe { std::mem::transmute_copy(&tensor) })), 3 => Ok(GradTensor::D3(into_dim::<B, D, 3>(tensor))),
4 => Ok(GradTensor::D4(unsafe { std::mem::transmute_copy(&tensor) })), 4 => Ok(GradTensor::D4(into_dim::<B, D, 4>(tensor))),
5 => Ok(GradTensor::D5(unsafe { std::mem::transmute_copy(&tensor) })), 5 => Ok(GradTensor::D5(into_dim::<B, D, 5>(tensor))),
6 => Ok(GradTensor::D6(unsafe { std::mem::transmute_copy(&tensor) })), 6 => Ok(GradTensor::D6(into_dim::<B, D, 6>(tensor))),
_ => Err(AutogradError::UnsupportedDimension(D)), _ => Err(AutogradError::UnsupportedDimension(D)),
} }
} }
@@ -23,6 +23,38 @@ mod reductions;
mod shapes; mod shapes;
mod unary; mod unary;
use rtx_backend::Backend;
/// Reinterpret an owned `TensorPrimitive<FROM>` as a `TensorPrimitive<TO>`.
///
/// The backward functions are generic over a const dimension `D` but must
/// hand back a `GradTensor` whose variant matches `D` at runtime. Every call
/// site selects, via a `match` on the runtime dimension, the arm whose `TO`
/// equals the actual `D` — so in practice `FROM == TO` whenever this runs.
///
/// Implemented with **no `unsafe`**: it round-trips through the backend's safe
/// data API (`to_data` → `from_data`) and rebuilds the `[usize; TO]` shape
/// element-by-element. The previous implementation cast across the const
/// generic with `transmute_copy`, which (a) double-freed the data buffer on
/// any heap-backed backend — the source was left to drop alongside the
/// bit-copy — and (b) was Undefined Behaviour under Stacked Borrows (a typed
/// read punned through a reference of a different type). Both are gone here;
/// the cost is one data copy per cast, which is negligible relative to the op
/// itself and only happens on the gradient path.
///
/// For the never-executed arms where `FROM != TO`, the shape is padded with
/// zeros / truncated; the value is discarded before use, so this is harmless.
#[inline]
pub(crate) fn into_dim<B: Backend, const FROM: usize, const TO: usize>(
src: B::TensorPrimitive<FROM>,
) -> B::TensorPrimitive<TO> {
let from_shape = B::shape(&src);
let to_shape: [usize; TO] = std::array::from_fn(|i| if i < FROM { from_shape[i] } else { 0 });
let device = B::device(&src);
let data = B::to_data(&src);
B::from_data(&data, to_shape, &device)
}
// Re-export all backward functions // Re-export all backward functions
pub use activations::{ pub use activations::{
EluBackward, GeluBackward, LeakyReluBackward, ReluBackward, SigmoidBackward, SiluBackward, EluBackward, GeluBackward, LeakyReluBackward, ReluBackward, SigmoidBackward, SiluBackward,
@@ -15,6 +15,7 @@
//! 3. All transmute_copy operations create owned copies from valid references/values //! 3. All transmute_copy operations create owned copies from valid references/values
use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor}; use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor};
use crate::autodiff::ops::into_dim;
use crate::error::{AutogradError, Result}; use crate::error::{AutogradError, Result};
use rtx_backend::Backend; use rtx_backend::Backend;
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -92,16 +93,20 @@ where
_ => unreachable!(), _ => unreachable!(),
}; };
// Create full tensor with the scalar value // Create full tensor with the scalar value, then reinterpret its
// owned storage as the runtime dimension `D`. `into_dim` CONSUMES
// `grad_input` (transmute_copy + mem::forget), so the underlying
// `Vec` is freed exactly once — the previous `transmute_copy(&owned)`
// left both `grad_input` and the copy owning the same buffer, which
// double-freed under any real (heap-backed) backend.
let grad_input = B::full(*shape, grad_scalar[0], &device); let grad_input = B::full(*shape, grad_scalar[0], &device);
// SAFETY: See module-level documentation. D is verified by each match arm.
let grad_tensor = match D { let grad_tensor = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&grad_input) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(grad_input)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&grad_input) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(grad_input)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&grad_input) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(grad_input)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&grad_input) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(grad_input)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&grad_input) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(grad_input)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&grad_input) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(grad_input)),
_ => return Err(AutogradError::UnsupportedDimension(0)), _ => return Err(AutogradError::UnsupportedDimension(0)),
}; };
@@ -163,12 +168,12 @@ where
// SAFETY: See module-level documentation. D is verified by each match arm. // SAFETY: See module-level documentation. D is verified by each match arm.
let grad_input = match D { let grad_input = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&ones) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(ones)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&ones) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(ones)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&ones) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(ones)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&ones) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(ones)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&ones) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(ones)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&ones) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(ones)),
_ => return Err(AutogradError::UnsupportedDimension(0)), _ => return Err(AutogradError::UnsupportedDimension(0)),
}; };
@@ -256,12 +261,12 @@ where
// SAFETY: See module-level documentation. D is verified by each match arm. // SAFETY: See module-level documentation. D is verified by each match arm.
let grad_tensor = match D { let grad_tensor = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&grad_input) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(grad_input)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&grad_input) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(grad_input)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&grad_input) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(grad_input)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&grad_input) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(grad_input)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&grad_input) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(grad_input)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&grad_input) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(grad_input)),
_ => return Err(AutogradError::UnsupportedDimension(0)), _ => return Err(AutogradError::UnsupportedDimension(0)),
}; };
@@ -412,12 +417,12 @@ where
// SAFETY: See module-level documentation. D is verified by each match arm. // SAFETY: See module-level documentation. D is verified by each match arm.
let result = match D { let result = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&grad_input) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(grad_input)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&grad_input) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(grad_input)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&grad_input) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(grad_input)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&grad_input) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(grad_input)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&grad_input) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(grad_input)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&grad_input) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(grad_input)),
_ => return Err(AutogradError::UnsupportedDimension(0)), _ => return Err(AutogradError::UnsupportedDimension(0)),
}; };
@@ -505,12 +510,12 @@ where
// SAFETY: See module-level documentation. D is verified by each match arm. // SAFETY: See module-level documentation. D is verified by each match arm.
let result = match D { let result = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&grad_input) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(grad_input)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&grad_input) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(grad_input)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&grad_input) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(grad_input)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&grad_input) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(grad_input)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&grad_input) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(grad_input)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&grad_input) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(grad_input)),
_ => return Err(AutogradError::UnsupportedDimension(0)), _ => return Err(AutogradError::UnsupportedDimension(0)),
}; };
@@ -555,27 +560,27 @@ where
// SAFETY: See module-level documentation. D is verified by each match arm. // SAFETY: See module-level documentation. D is verified by each match arm.
match (grad, D) { match (grad, D) {
(GradTensor::D1(g), 1) => { (GradTensor::D1(g), 1) => {
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<1>>(tensor) }; let t = into_dim::<B, D, 1>(tensor.clone());
Ok(GradTensor::D1(B::mul(g.clone(), t))) Ok(GradTensor::D1(B::mul(g.clone(), t)))
} }
(GradTensor::D2(g), 2) => { (GradTensor::D2(g), 2) => {
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<2>>(tensor) }; let t = into_dim::<B, D, 2>(tensor.clone());
Ok(GradTensor::D2(B::mul(g.clone(), t))) Ok(GradTensor::D2(B::mul(g.clone(), t)))
} }
(GradTensor::D3(g), 3) => { (GradTensor::D3(g), 3) => {
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<3>>(tensor) }; let t = into_dim::<B, D, 3>(tensor.clone());
Ok(GradTensor::D3(B::mul(g.clone(), t))) Ok(GradTensor::D3(B::mul(g.clone(), t)))
} }
(GradTensor::D4(g), 4) => { (GradTensor::D4(g), 4) => {
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<4>>(tensor) }; let t = into_dim::<B, D, 4>(tensor.clone());
Ok(GradTensor::D4(B::mul(g.clone(), t))) Ok(GradTensor::D4(B::mul(g.clone(), t)))
} }
(GradTensor::D5(g), 5) => { (GradTensor::D5(g), 5) => {
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<5>>(tensor) }; let t = into_dim::<B, D, 5>(tensor.clone());
Ok(GradTensor::D5(B::mul(g.clone(), t))) Ok(GradTensor::D5(B::mul(g.clone(), t)))
} }
(GradTensor::D6(g), 6) => { (GradTensor::D6(g), 6) => {
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<6>>(tensor) }; let t = into_dim::<B, D, 6>(tensor.clone());
Ok(GradTensor::D6(B::mul(g.clone(), t))) Ok(GradTensor::D6(B::mul(g.clone(), t)))
} }
_ => Err(AutogradError::DimensionMismatch( _ => Err(AutogradError::DimensionMismatch(
@@ -723,12 +728,12 @@ where
// SAFETY: See module-level documentation. D is verified by each match arm. // SAFETY: See module-level documentation. D is verified by each match arm.
let result = match D { let result = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&grad_input) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(grad_input)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&grad_input) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(grad_input)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&grad_input) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(grad_input)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&grad_input) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(grad_input)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&grad_input) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(grad_input)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&grad_input) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(grad_input)),
_ => return Err(AutogradError::UnsupportedDimension(0)), _ => return Err(AutogradError::UnsupportedDimension(0)),
}; };
@@ -940,12 +945,12 @@ where
// SAFETY: See module-level documentation. D is verified by each match arm. // SAFETY: See module-level documentation. D is verified by each match arm.
let result = match D { let result = match D {
1 => GradTensor::D1(unsafe { std::mem::transmute_copy(&grad_input) }), 1 => GradTensor::D1(into_dim::<B, D, 1>(grad_input)),
2 => GradTensor::D2(unsafe { std::mem::transmute_copy(&grad_input) }), 2 => GradTensor::D2(into_dim::<B, D, 2>(grad_input)),
3 => GradTensor::D3(unsafe { std::mem::transmute_copy(&grad_input) }), 3 => GradTensor::D3(into_dim::<B, D, 3>(grad_input)),
4 => GradTensor::D4(unsafe { std::mem::transmute_copy(&grad_input) }), 4 => GradTensor::D4(into_dim::<B, D, 4>(grad_input)),
5 => GradTensor::D5(unsafe { std::mem::transmute_copy(&grad_input) }), 5 => GradTensor::D5(into_dim::<B, D, 5>(grad_input)),
6 => GradTensor::D6(unsafe { std::mem::transmute_copy(&grad_input) }), 6 => GradTensor::D6(into_dim::<B, D, 6>(grad_input)),
_ => return Err(AutogradError::UnsupportedDimension(0)), _ => return Err(AutogradError::UnsupportedDimension(0)),
}; };
@@ -7,6 +7,7 @@
//! - SwapDims: gradient swaps the same dimensions back //! - SwapDims: gradient swaps the same dimensions back
use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor}; use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor};
use crate::autodiff::ops::into_dim;
use crate::error::{AutogradError, Result}; use crate::error::{AutogradError, Result};
use rtx_backend::Backend; use rtx_backend::Backend;
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -214,17 +215,17 @@ fn wrap_as_grad_tensor<B: Backend, const D: usize>(
) -> Result<GradTensor<B>> { ) -> Result<GradTensor<B>> {
match D { match D {
// SAFETY: D=1 verified by match, B::TensorPrimitive layout identical across D // SAFETY: D=1 verified by match, B::TensorPrimitive layout identical across D
1 => Ok(GradTensor::D1(unsafe { std::mem::transmute_copy(&tensor) })), 1 => Ok(GradTensor::D1(into_dim::<B, D, 1>(tensor))),
// SAFETY: D=2 verified by match, B::TensorPrimitive layout identical across D // SAFETY: D=2 verified by match, B::TensorPrimitive layout identical across D
2 => Ok(GradTensor::D2(unsafe { std::mem::transmute_copy(&tensor) })), 2 => Ok(GradTensor::D2(into_dim::<B, D, 2>(tensor))),
// SAFETY: D=3 verified by match, B::TensorPrimitive layout identical across D // SAFETY: D=3 verified by match, B::TensorPrimitive layout identical across D
3 => Ok(GradTensor::D3(unsafe { std::mem::transmute_copy(&tensor) })), 3 => Ok(GradTensor::D3(into_dim::<B, D, 3>(tensor))),
// SAFETY: D=4 verified by match, B::TensorPrimitive layout identical across D // SAFETY: D=4 verified by match, B::TensorPrimitive layout identical across D
4 => Ok(GradTensor::D4(unsafe { std::mem::transmute_copy(&tensor) })), 4 => Ok(GradTensor::D4(into_dim::<B, D, 4>(tensor))),
// SAFETY: D=5 verified by match, B::TensorPrimitive layout identical across D // SAFETY: D=5 verified by match, B::TensorPrimitive layout identical across D
5 => Ok(GradTensor::D5(unsafe { std::mem::transmute_copy(&tensor) })), 5 => Ok(GradTensor::D5(into_dim::<B, D, 5>(tensor))),
// SAFETY: D=6 verified by match, B::TensorPrimitive layout identical across D // SAFETY: D=6 verified by match, B::TensorPrimitive layout identical across D
6 => Ok(GradTensor::D6(unsafe { std::mem::transmute_copy(&tensor) })), 6 => Ok(GradTensor::D6(into_dim::<B, D, 6>(tensor))),
_ => Err(AutogradError::UnsupportedDimension(D)), _ => Err(AutogradError::UnsupportedDimension(D)),
} }
} }
@@ -8,6 +8,7 @@
//! - Abs: `grad_input = grad_output * sign(input)` //! - Abs: `grad_input = grad_output * sign(input)`
use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor}; use crate::autodiff::node::{AutodiffBackwardFn, GradTensor, SavedTensor};
use crate::autodiff::ops::into_dim;
use crate::error::{AutogradError, Result}; use crate::error::{AutogradError, Result};
use rtx_backend::Backend; use rtx_backend::Backend;
use std::marker::PhantomData; use std::marker::PhantomData;
@@ -255,32 +256,32 @@ where
match (grad, D) { match (grad, D) {
(GradTensor::D1(g), 1) => { (GradTensor::D1(g), 1) => {
// SAFETY: D=1 verified by match, layout identical across D values // SAFETY: D=1 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<1>>(tensor) }; let t = into_dim::<B, D, 1>(tensor.clone());
Ok(GradTensor::D1(B::mul(g, t))) Ok(GradTensor::D1(B::mul(g, t)))
} }
(GradTensor::D2(g), 2) => { (GradTensor::D2(g), 2) => {
// SAFETY: D=2 verified by match, layout identical across D values // SAFETY: D=2 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<2>>(tensor) }; let t = into_dim::<B, D, 2>(tensor.clone());
Ok(GradTensor::D2(B::mul(g, t))) Ok(GradTensor::D2(B::mul(g, t)))
} }
(GradTensor::D3(g), 3) => { (GradTensor::D3(g), 3) => {
// SAFETY: D=3 verified by match, layout identical across D values // SAFETY: D=3 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<3>>(tensor) }; let t = into_dim::<B, D, 3>(tensor.clone());
Ok(GradTensor::D3(B::mul(g, t))) Ok(GradTensor::D3(B::mul(g, t)))
} }
(GradTensor::D4(g), 4) => { (GradTensor::D4(g), 4) => {
// SAFETY: D=4 verified by match, layout identical across D values // SAFETY: D=4 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<4>>(tensor) }; let t = into_dim::<B, D, 4>(tensor.clone());
Ok(GradTensor::D4(B::mul(g, t))) Ok(GradTensor::D4(B::mul(g, t)))
} }
(GradTensor::D5(g), 5) => { (GradTensor::D5(g), 5) => {
// SAFETY: D=5 verified by match, layout identical across D values // SAFETY: D=5 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<5>>(tensor) }; let t = into_dim::<B, D, 5>(tensor.clone());
Ok(GradTensor::D5(B::mul(g, t))) Ok(GradTensor::D5(B::mul(g, t)))
} }
(GradTensor::D6(g), 6) => { (GradTensor::D6(g), 6) => {
// SAFETY: D=6 verified by match, layout identical across D values // SAFETY: D=6 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<6>>(tensor) }; let t = into_dim::<B, D, 6>(tensor.clone());
Ok(GradTensor::D6(B::mul(g, t))) Ok(GradTensor::D6(B::mul(g, t)))
} }
_ => Err(AutogradError::DimensionMismatch("mul_grad".to_string())), _ => Err(AutogradError::DimensionMismatch("mul_grad".to_string())),
@@ -304,32 +305,32 @@ where
match (grad, D) { match (grad, D) {
(GradTensor::D1(g), 1) => { (GradTensor::D1(g), 1) => {
// SAFETY: D=1 verified by match, layout identical across D values // SAFETY: D=1 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<1>>(tensor) }; let t = into_dim::<B, D, 1>(tensor.clone());
Ok(GradTensor::D1(B::div(g, t))) Ok(GradTensor::D1(B::div(g, t)))
} }
(GradTensor::D2(g), 2) => { (GradTensor::D2(g), 2) => {
// SAFETY: D=2 verified by match, layout identical across D values // SAFETY: D=2 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<2>>(tensor) }; let t = into_dim::<B, D, 2>(tensor.clone());
Ok(GradTensor::D2(B::div(g, t))) Ok(GradTensor::D2(B::div(g, t)))
} }
(GradTensor::D3(g), 3) => { (GradTensor::D3(g), 3) => {
// SAFETY: D=3 verified by match, layout identical across D values // SAFETY: D=3 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<3>>(tensor) }; let t = into_dim::<B, D, 3>(tensor.clone());
Ok(GradTensor::D3(B::div(g, t))) Ok(GradTensor::D3(B::div(g, t)))
} }
(GradTensor::D4(g), 4) => { (GradTensor::D4(g), 4) => {
// SAFETY: D=4 verified by match, layout identical across D values // SAFETY: D=4 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<4>>(tensor) }; let t = into_dim::<B, D, 4>(tensor.clone());
Ok(GradTensor::D4(B::div(g, t))) Ok(GradTensor::D4(B::div(g, t)))
} }
(GradTensor::D5(g), 5) => { (GradTensor::D5(g), 5) => {
// SAFETY: D=5 verified by match, layout identical across D values // SAFETY: D=5 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<5>>(tensor) }; let t = into_dim::<B, D, 5>(tensor.clone());
Ok(GradTensor::D5(B::div(g, t))) Ok(GradTensor::D5(B::div(g, t)))
} }
(GradTensor::D6(g), 6) => { (GradTensor::D6(g), 6) => {
// SAFETY: D=6 verified by match, layout identical across D values // SAFETY: D=6 verified by match, layout identical across D values
let t = unsafe { std::mem::transmute_copy::<_, B::TensorPrimitive<6>>(tensor) }; let t = into_dim::<B, D, 6>(tensor.clone());
Ok(GradTensor::D6(B::div(g, t))) Ok(GradTensor::D6(B::div(g, t)))
} }
_ => Err(AutogradError::DimensionMismatch("div_grad".to_string())), _ => Err(AutogradError::DimensionMismatch("div_grad".to_string())),
@@ -169,7 +169,16 @@ where
Self { Self {
inner: self.inner.clone(), inner: self.inner.clone(),
node: self.node.clone(), // Arc clone is cheap node: self.node.clone(), // Arc clone is cheap
id: TensorId::new(), // New ID for the clone // Preserve the id: in this graph model the id identifies a node
// in the autograd graph, so a clone is the *same* logical tensor
// and must share the same gradient sink. Minting a fresh id here
// meant that using a tensor twice (which requires a clone, since
// ops take their operands by value) split its gradient across two
// ids; backward then accumulated into both separately and the
// caller, querying only the original id, saw a fraction of the
// true gradient. Sharing the id lets fan-out paths collide on one
// id and sum via `accumulate_gradients`.
id: self.id,
requires_grad: self.requires_grad, requires_grad: self.requires_grad,
} }
} }
@@ -0,0 +1,174 @@
//! Numerical finite-difference gradient checks for the `Autodiff<B>` tape,
//! run against the **real** `CpuBackend` — not the shape-only `MockBackend`
//! the rest of the suite uses (whose ops all return their input, so they
//! validate graph *structure* but never gradient *values*).
//!
//! This is the first end-to-end validation that the decorator tape produces
//! correct numerical gradients through a concrete backend, which is the
//! precondition for training a tape-native transformer (the SMT teacher).
//!
//! Method: build a scalar loss through the tape, extract the leaf gradient
//! via `backward_impl` + `GradientStorage`, and compare every element to a
//! central finite difference computed with the raw (grad-free) `CpuBackend`.
//!
//! Note on Miri: these pass under normal `cargo test`. Under `cargo miri test`
//! they abort on a Stacked-Borrows / integer-to-pointer violation that lives
//! in `rtx-backend-cpu`'s buffer internals, NOT in the autodiff tape — a
//! grad-free `from_data`+`add`+`sum` probe reproduces the identical error, so
//! the autograd path is exonerated. Fixing that backend UB is tracked
//! separately.
use rtx_autograd::autodiff::{Autodiff, AutodiffDevice, GradTensor, backward_impl};
use rtx_backend::{AutodiffBackend, Backend};
use rtx_backend_cpu::CpuBackend;
type Ad = Autodiff<CpuBackend>;
const H: f32 = 1e-3;
const TOL: f32 = 2e-2;
fn ad_dev() -> AutodiffDevice<CpuBackend> {
AutodiffDevice::<CpuBackend>::default()
}
fn cpu_dev() -> <CpuBackend as Backend>::Device {
<CpuBackend as Backend>::Device::default()
}
/// Make a grad-tracked 2-D leaf on the tape.
fn leaf2(data: &[f32], shape: [usize; 2]) -> <Ad as Backend>::TensorPrimitive<2> {
Ad::require_grad(Ad::from_data(data, shape, &ad_dev()))
}
/// Extract a leaf's 2-D gradient from the storage as a flat Vec.
fn grad2(storage: &rtx_autograd::autodiff::GradientStorage<CpuBackend>, id: usize) -> Vec<f32> {
let g = storage
.get(rtx_autograd::autodiff::TensorId(id))
.expect("gradient present for leaf");
match g {
GradTensor::D2(t) => t.to_vec(),
other => panic!("expected D2 gradient, got {other:?}"),
}
}
/// Central finite-difference gradient of a scalar loss `f(a)` wrt each
/// element of `a`, computed with the raw grad-free backend.
fn fd_grad(a: &[f32], f: impl Fn(&[f32]) -> f32) -> Vec<f32> {
let mut g = vec![0.0f32; a.len()];
let mut probe = a.to_vec();
for i in 0..a.len() {
let orig = probe[i];
probe[i] = orig + H;
let plus = f(&probe);
probe[i] = orig - H;
let minus = f(&probe);
probe[i] = orig;
g[i] = (plus - minus) / (2.0 * H);
}
g
}
fn assert_close(analytical: &[f32], numerical: &[f32], what: &str) {
assert_eq!(analytical.len(), numerical.len(), "{what}: length mismatch");
for (i, (a, n)) in analytical.iter().zip(numerical.iter()).enumerate() {
let denom = a.abs().max(n.abs()).max(1.0);
let rel = (a - n).abs() / denom;
assert!(
rel < TOL,
"{what}: grad[{i}] analytical={a} numerical={n} rel_err={rel}"
);
}
}
/// `loss = sum(A @ B)` — checks the matmul VJP for both operands plus the
/// sum VJP and the grad-extraction path.
#[test]
fn gradcheck_matmul_sum() {
let a = [0.5f32, -1.0, 2.0, 0.25, -0.5, 1.5]; // [2,3]
let b = [1.0f32, -2.0, 0.5, 3.0, -1.5, 0.75]; // [3,2]
// Raw forward used by finite differences (vary A, fix B).
let loss_of_a = |av: &[f32]| -> f32 {
let at = CpuBackend::from_data(av, [2, 3], &cpu_dev());
let bt = CpuBackend::from_data(&b, [3, 2], &cpu_dev());
CpuBackend::to_data(&CpuBackend::sum(CpuBackend::matmul(at, bt)))[0]
};
let at = leaf2(&a, [2, 3]);
let bt = leaf2(&b, [3, 2]);
let a_id = at.id().0;
let loss = Ad::sum(Ad::matmul(at, bt));
let storage = backward_impl(
&loss,
Some(GradTensor::from_d1(CpuBackend::ones([1], &cpu_dev()))),
)
.expect("backward");
let analytical = grad2(&storage, a_id);
let numerical = fd_grad(&a, loss_of_a);
assert_close(&analytical, &numerical, "matmul_sum d/dA");
}
/// `loss = sum((A + B) * (A + B))` — checks same-shape add VJP and elementwise
/// mul VJP, and specifically the **fan-out accumulation** path: `s` feeds both
/// operands of `mul(s.clone(), s)`, so its gradient arrives along two paths and
/// must be summed. This previously came out exactly halved because
/// `accumulate_gradients` discarded one path and `clone` minted a fresh id;
/// fixed by summing via `B::add` and preserving the id across `clone`.
#[test]
fn gradcheck_add_mul_sum() {
let a = [0.5f32, -1.0, 2.0, 0.25]; // [2,2]
let b = [1.0f32, -2.0, 0.5, 3.0]; // [2,2]
let loss_of_a = |av: &[f32]| -> f32 {
let at = CpuBackend::from_data(av, [2, 2], &cpu_dev());
let bt = CpuBackend::from_data(&b, [2, 2], &cpu_dev());
let s = CpuBackend::add(at, bt);
CpuBackend::to_data(&CpuBackend::sum(CpuBackend::mul(s.clone(), s)))[0]
};
let at = leaf2(&a, [2, 2]);
let bt = leaf2(&b, [2, 2]);
let a_id = at.id().0;
let s = Ad::add(at, bt);
let loss = Ad::sum(Ad::mul(s.clone(), s));
let storage = backward_impl(
&loss,
Some(GradTensor::from_d1(CpuBackend::ones([1], &cpu_dev()))),
)
.expect("backward");
let analytical = grad2(&storage, a_id);
let numerical = fd_grad(&a, loss_of_a);
assert_close(&analytical, &numerical, "add_mul_sum d/dA");
}
/// `loss = sum(softmax(X, dim=1) ^ 2)` — the riskiest transformer VJP. This
/// previously panicked in the CPU `sub` because softmax backward subtracted a
/// keep-dim `[.., 1]` row-sum from the full `[.., cols]` grad, and the
/// elementwise ops require equal shapes; fixed by tiling the row-sum to full
/// width (`broadcast_along_dim`) before the subtract.
#[test]
fn gradcheck_softmax_sum() {
let x = [0.2f32, -0.5, 1.0, 0.3, 0.8, -1.2]; // [2,3]
let loss_of_x = |xv: &[f32]| -> f32 {
let xt = CpuBackend::from_data(xv, [2, 3], &cpu_dev());
let y = CpuBackend::softmax(xt, 1);
CpuBackend::to_data(&CpuBackend::sum(CpuBackend::mul(y.clone(), y)))[0]
};
let xt = leaf2(&x, [2, 3]);
let x_id = xt.id().0;
let y = Ad::softmax(xt, 1);
let loss = Ad::sum(Ad::mul(y.clone(), y));
let storage = backward_impl(
&loss,
Some(GradTensor::from_d1(CpuBackend::ones([1], &cpu_dev()))),
)
.expect("backward");
let analytical = grad2(&storage, x_id);
let numerical = fd_grad(&x, loss_of_x);
assert_close(&analytical, &numerical, "softmax_sum d/dX");
}