Merge pull request 'SMT D314: gradcheck tanh/sigmoid + GatedMemoryUpdater (GRU cell)' (#8) from d314-gated-updater into main
CI / Build CPU-Only (Explicit) (push) Has been cancelled
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
Documentation / Build API Documentation (push) Has been cancelled
Documentation / Build User Guide (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / CI Success (push) Has been cancelled
CI / Build CPU-Only (Explicit) (push) Has been cancelled
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
Documentation / Build API Documentation (push) Has been cancelled
Documentation / Build User Guide (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / CI Success (push) Has been cancelled
This commit was merged in pull request #8.
This commit is contained in:
@@ -172,3 +172,96 @@ fn gradcheck_softmax_sum() {
|
|||||||
let numerical = fd_grad(&x, loss_of_x);
|
let numerical = fd_grad(&x, loss_of_x);
|
||||||
assert_close(&analytical, &numerical, "softmax_sum d/dX");
|
assert_close(&analytical, &numerical, "softmax_sum d/dX");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `loss = sum(tanh(X))` — tanh VJP (`grad_out * (1 - tanh(x)^2)`). Needed for
|
||||||
|
/// the gated memory cell's candidate state.
|
||||||
|
#[test]
|
||||||
|
fn gradcheck_tanh_sum() {
|
||||||
|
let x = [0.5f32, -1.0, 2.0, 0.25, -0.5, 1.5]; // [2,3]
|
||||||
|
|
||||||
|
let loss_of_x = |xv: &[f32]| -> f32 {
|
||||||
|
let xt = CpuBackend::from_data(xv, [2, 3], &cpu_dev());
|
||||||
|
CpuBackend::to_data(&CpuBackend::sum(CpuBackend::tanh(xt)))[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
let xt = leaf2(&x, [2, 3]);
|
||||||
|
let x_id = xt.id().0;
|
||||||
|
let loss = Ad::sum(Ad::tanh(xt));
|
||||||
|
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, "tanh_sum d/dX");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `loss = sum(sigmoid(X))` — sigmoid VJP (`grad_out * σ(x) * (1 - σ(x))`).
|
||||||
|
/// Needed for the gated memory cell's update gate.
|
||||||
|
#[test]
|
||||||
|
fn gradcheck_sigmoid_sum() {
|
||||||
|
let x = [0.5f32, -1.0, 2.0, 0.25, -0.5, 1.5]; // [2,3]
|
||||||
|
|
||||||
|
let loss_of_x = |xv: &[f32]| -> f32 {
|
||||||
|
let xt = CpuBackend::from_data(xv, [2, 3], &cpu_dev());
|
||||||
|
CpuBackend::to_data(&CpuBackend::sum(CpuBackend::sigmoid(xt)))[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
let xt = leaf2(&x, [2, 3]);
|
||||||
|
let x_id = xt.id().0;
|
||||||
|
let loss = Ad::sum(Ad::sigmoid(xt));
|
||||||
|
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, "sigmoid_sum d/dX");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exact composite the gated memory cell uses:
|
||||||
|
/// `loss = sum((1 - z) ⊙ m + z ⊙ c)` with `z = sigmoid(A)`, `c = tanh(B)`, `m`
|
||||||
|
/// constant — checks sigmoid·tanh·sub·mul·add compose with correct gradients
|
||||||
|
/// (vary the gate logits `A`).
|
||||||
|
#[test]
|
||||||
|
fn gradcheck_gated_update() {
|
||||||
|
let a = [0.3f32, -0.7, 1.2, 0.1]; // gate logits [2,2]
|
||||||
|
let bb = [0.5f32, -1.0, 0.25, 2.0]; // candidate logits [2,2]
|
||||||
|
let m = [0.4f32, -0.2, 0.9, -0.6]; // previous memory (constant) [2,2]
|
||||||
|
|
||||||
|
let loss_of_a = |av: &[f32]| -> f32 {
|
||||||
|
let at = CpuBackend::from_data(av, [2, 2], &cpu_dev());
|
||||||
|
let bt = CpuBackend::from_data(&bb, [2, 2], &cpu_dev());
|
||||||
|
let mt = CpuBackend::from_data(&m, [2, 2], &cpu_dev());
|
||||||
|
let z = CpuBackend::sigmoid(at);
|
||||||
|
let c = CpuBackend::tanh(bt);
|
||||||
|
let one = CpuBackend::ones([2, 2], &cpu_dev());
|
||||||
|
let keep = CpuBackend::sub(one, z.clone());
|
||||||
|
let new = CpuBackend::add(CpuBackend::mul(keep, mt), CpuBackend::mul(z, c));
|
||||||
|
CpuBackend::to_data(&CpuBackend::sum(new))[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
let at = leaf2(&a, [2, 2]);
|
||||||
|
let bt = leaf2(&bb, [2, 2]);
|
||||||
|
let mt = Ad::from_data(&m, [2, 2], &ad_dev());
|
||||||
|
let a_id = at.id().0;
|
||||||
|
let z = Ad::sigmoid(at);
|
||||||
|
let c = Ad::tanh(bt);
|
||||||
|
let one = Ad::from_data(&[1.0f32; 4], [2, 2], &ad_dev());
|
||||||
|
let keep = Ad::sub(one, z.clone());
|
||||||
|
let new = Ad::add(Ad::mul(keep, mt), Ad::mul(z, c));
|
||||||
|
let loss = Ad::sum(new);
|
||||||
|
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, "gated_update d/d(gate logits)");
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
//! `GatedMemoryUpdater` — a GRU-style **gated** recurrent memory cell for
|
||||||
|
//! Supervised Memory Training (Phase F / D314).
|
||||||
|
//!
|
||||||
|
//! The residual-MLP [`ClonedMemoryUpdater`](super::cloned_memory_updater) cell
|
||||||
|
//! (`M_t = ρ·M_{t-1} + Δ` + clamp) cannot both *jump* at a regime switch and
|
||||||
|
//! *average* in steady state, so when streamed it lags through non-stationary
|
||||||
|
//! switches. This cell replaces the fixed leaky decay with a learned per-dim
|
||||||
|
//! **update gate**, so the recurrence adapts step by step:
|
||||||
|
//! ```text
|
||||||
|
//! z = sigmoid([M_{t-1} ‖ x] · W_z) # update gate ∈ (0,1), per memory dim
|
||||||
|
//! c = tanh([M_{t-1} ‖ x] · W_c) # candidate state ∈ (-1,1)
|
||||||
|
//! M_t = (1 - z) ⊙ M_{t-1} + z ⊙ c # z≈1 → jump; z≈0 → hold + denoise
|
||||||
|
//! x̂ = M_t · W_read # predict-the-future readout
|
||||||
|
//! ```
|
||||||
|
//! Because `M_t` is a **convex combination** of `M_{t-1}` and the bounded
|
||||||
|
//! candidate `c ∈ (-1,1)`, `|M_t| ≤ max(|M_0|, 1)` for all `t` — the recurrence
|
||||||
|
//! is a non-expansion, so a free autoregressive rollout stays bounded **without**
|
||||||
|
//! a clamp.
|
||||||
|
//!
|
||||||
|
//! Trains on `Autodiff<CpuBackend>` with a self-contained deterministic Adam,
|
||||||
|
//! exposing the same method surface as `ClonedMemoryUpdater` (so the omni-think
|
||||||
|
//! facade is cell-generic). sigmoid/tanh/mul/add/sub VJPs are finite-difference
|
||||||
|
//! gated in `rtx-autograd/tests/tape_cpu_gradcheck.rs`.
|
||||||
|
|
||||||
|
use rtx_autograd::autodiff::{Autodiff, AutodiffDevice, GradTensor, TensorId, backward_impl};
|
||||||
|
use rtx_backend::{AutodiffBackend, Backend};
|
||||||
|
use rtx_backend_cpu::CpuBackend;
|
||||||
|
|
||||||
|
use super::cloned_memory_updater::ClonedUpdaterConfig;
|
||||||
|
|
||||||
|
type Ad = Autodiff<CpuBackend>;
|
||||||
|
|
||||||
|
/// Deterministic init in `[-scale, scale)` via a SplitMix64-style LCG.
|
||||||
|
fn lcg_init(n: usize, scale: f32, seed: u64) -> Vec<f32> {
|
||||||
|
let mut s = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||||
|
(0..n)
|
||||||
|
.map(|_| {
|
||||||
|
s = s
|
||||||
|
.wrapping_mul(6_364_136_223_846_793_005)
|
||||||
|
.wrapping_add(1_442_695_040_888_963_407);
|
||||||
|
let u = ((s >> 33) as f32) / ((1u64 << 31) as f32);
|
||||||
|
(u * 2.0 - 1.0) * scale
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Numerically-naive sigmoid matching `rtx-backend-cpu`'s, so off-tape `step`
|
||||||
|
/// agrees with the trained graph.
|
||||||
|
#[inline]
|
||||||
|
fn sigmoid(x: f32) -> f32 {
|
||||||
|
1.0 / (1.0 + (-x).exp())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trainable parameter matrix with its own Adam moments (host-side).
|
||||||
|
struct Param {
|
||||||
|
data: Vec<f32>,
|
||||||
|
rows: usize,
|
||||||
|
cols: usize,
|
||||||
|
m: Vec<f32>,
|
||||||
|
v: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Param {
|
||||||
|
fn new(rows: usize, cols: usize, scale: f32, seed: u64) -> Self {
|
||||||
|
let n = rows * cols;
|
||||||
|
Self {
|
||||||
|
data: lcg_init(n, scale, seed),
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
m: vec![0.0; n],
|
||||||
|
v: vec![0.0; n],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape(&self) -> [usize; 2] {
|
||||||
|
[self.rows, self.cols]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaf(
|
||||||
|
&self,
|
||||||
|
dev: &AutodiffDevice<CpuBackend>,
|
||||||
|
) -> (<Ad as Backend>::TensorPrimitive<2>, usize) {
|
||||||
|
let t = Ad::require_grad(Ad::from_data(&self.data, self.shape(), dev));
|
||||||
|
let id = t.id().0;
|
||||||
|
(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adam(&mut self, grad: &[f32], lr: f32, t: u32) {
|
||||||
|
const B1: f32 = 0.9;
|
||||||
|
const B2: f32 = 0.999;
|
||||||
|
const EPS: f32 = 1e-8;
|
||||||
|
let bc1 = 1.0 - B1.powi(t as i32);
|
||||||
|
let bc2 = 1.0 - B2.powi(t as i32);
|
||||||
|
for ((w, g), (m, v)) in self
|
||||||
|
.data
|
||||||
|
.iter_mut()
|
||||||
|
.zip(grad.iter())
|
||||||
|
.zip(self.m.iter_mut().zip(self.v.iter_mut()))
|
||||||
|
{
|
||||||
|
*m = B1 * *m + (1.0 - B1) * g;
|
||||||
|
*v = B2 * *v + (1.0 - B2) * g * g;
|
||||||
|
*w -= lr * (*m / bc1) / ((*v / bc2).sqrt() + EPS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grad_of(storage: &rtx_autograd::autodiff::GradientStorage<CpuBackend>, id: usize) -> Vec<f32> {
|
||||||
|
match storage
|
||||||
|
.get(TensorId(id))
|
||||||
|
.unwrap_or_else(|| panic!("no gradient for leaf {id}"))
|
||||||
|
{
|
||||||
|
GradTensor::D2(t) => t.to_vec(),
|
||||||
|
other => panic!("expected D2 gradient, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A GRU-style gated recurrent memory updater + predict-the-future readout.
|
||||||
|
pub struct GatedMemoryUpdater {
|
||||||
|
cfg: ClonedUpdaterConfig,
|
||||||
|
w_z: Param, // [d_mem + d_in, d_mem] update gate
|
||||||
|
w_c: Param, // [d_mem + d_in, d_mem] candidate
|
||||||
|
w_read: Param, // [d_mem, d_in]
|
||||||
|
step: u32,
|
||||||
|
dev: AutodiffDevice<CpuBackend>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GatedMemoryUpdater {
|
||||||
|
/// Build with deterministic, seeded weights. `cfg.d_hidden` is unused by the
|
||||||
|
/// gated cell (gates project `[M ‖ x]` straight to `d_mem`); it is kept in
|
||||||
|
/// the shared config so this is a drop-in for `ClonedMemoryUpdater`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(cfg: ClonedUpdaterConfig) -> Self {
|
||||||
|
let s = |i: u64| cfg.seed.wrapping_mul(0x100_0000_01b3).wrapping_add(i);
|
||||||
|
let fan = cfg.d_mem + cfg.d_in;
|
||||||
|
let sc = 1.0 / (fan as f32).sqrt();
|
||||||
|
Self {
|
||||||
|
w_z: Param::new(fan, cfg.d_mem, sc, s(1)),
|
||||||
|
w_c: Param::new(fan, cfg.d_mem, sc, s(2)),
|
||||||
|
w_read: Param::new(cfg.d_mem, cfg.d_in, 1.0 / (cfg.d_mem as f32).sqrt(), s(3)),
|
||||||
|
step: 0,
|
||||||
|
dev: AutodiffDevice::<CpuBackend>::default(),
|
||||||
|
cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Memory size `|M|`.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn d_mem(&self) -> usize {
|
||||||
|
self.cfg.d_mem
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Off-tape gated step `g(prev_mem, x) → new_mem` (`[d_mem]`).
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics on shape mismatch.
|
||||||
|
#[must_use]
|
||||||
|
pub fn step(&self, prev_mem: &[f32], x: &[f32]) -> Vec<f32> {
|
||||||
|
let dm = self.cfg.d_mem;
|
||||||
|
let din = self.cfg.d_in;
|
||||||
|
assert_eq!(prev_mem.len(), dm);
|
||||||
|
assert_eq!(x.len(), din);
|
||||||
|
let cat_at = |i: usize| if i < dm { prev_mem[i] } else { x[i - dm] };
|
||||||
|
let fan = dm + din;
|
||||||
|
let mut out = vec![0.0f32; dm];
|
||||||
|
for j in 0..dm {
|
||||||
|
let mut zlin = 0.0f32;
|
||||||
|
let mut clin = 0.0f32;
|
||||||
|
for i in 0..fan {
|
||||||
|
let ci = cat_at(i);
|
||||||
|
zlin += ci * self.w_z.data[i * dm + j];
|
||||||
|
clin += ci * self.w_c.data[i * dm + j];
|
||||||
|
}
|
||||||
|
let z = sigmoid(zlin);
|
||||||
|
let c = clin.tanh();
|
||||||
|
out[j] = (1.0 - z) * prev_mem[j] + z * c;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Off-tape readout: predict the next latent from a memory (`[d_in]`).
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if `mem.len() != d_mem`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn predict(&self, mem: &[f32]) -> Vec<f32> {
|
||||||
|
let dm = self.cfg.d_mem;
|
||||||
|
let din = self.cfg.d_in;
|
||||||
|
assert_eq!(mem.len(), dm);
|
||||||
|
let mut out = vec![0.0f32; din];
|
||||||
|
for (j, oj) in out.iter_mut().enumerate() {
|
||||||
|
let mut acc = 0.0f32;
|
||||||
|
for (i, &mi) in mem.iter().enumerate() {
|
||||||
|
acc += mi * self.w_read.data[i * din + j];
|
||||||
|
}
|
||||||
|
*oj = acc;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the gated update graph on the tape, returning the `new_mem`
|
||||||
|
/// ([1, d_mem]) node. Shared by both training entry points.
|
||||||
|
fn graph(
|
||||||
|
&self,
|
||||||
|
prev_mem: &[f32],
|
||||||
|
x: &[f32],
|
||||||
|
w_z: &<Ad as Backend>::TensorPrimitive<2>,
|
||||||
|
w_c: &<Ad as Backend>::TensorPrimitive<2>,
|
||||||
|
) -> <Ad as Backend>::TensorPrimitive<2> {
|
||||||
|
let dm = self.cfg.d_mem;
|
||||||
|
let din = self.cfg.d_in;
|
||||||
|
let mut cat = Vec::with_capacity(dm + din);
|
||||||
|
cat.extend_from_slice(prev_mem);
|
||||||
|
cat.extend_from_slice(x);
|
||||||
|
let cat_t = Ad::from_data(&cat, [1, dm + din], &self.dev);
|
||||||
|
let prev_t = Ad::from_data(prev_mem, [1, dm], &self.dev);
|
||||||
|
let z = Ad::sigmoid(Ad::matmul(cat_t.clone(), w_z.clone())); // [1, d_mem]
|
||||||
|
let c = Ad::tanh(Ad::matmul(cat_t, w_c.clone()));
|
||||||
|
let one = Ad::from_data(&vec![1.0f32; dm], [1, dm], &self.dev);
|
||||||
|
let keep = Ad::sub(one, z.clone());
|
||||||
|
// M_t = (1 - z) ⊙ prev + z ⊙ c
|
||||||
|
Ad::add(Ad::mul(keep, prev_t), Ad::mul(z, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One behavioral-cloning step: train the gate + candidate to map
|
||||||
|
/// `(prev_mem, x) → target_mem` and the readout to predict `target_next`.
|
||||||
|
/// Returns the total loss before the update.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics on shape mismatch.
|
||||||
|
pub fn train_step(
|
||||||
|
&mut self,
|
||||||
|
prev_mem: &[f32],
|
||||||
|
x: &[f32],
|
||||||
|
target_mem: &[f32],
|
||||||
|
target_next: &[f32],
|
||||||
|
) -> f32 {
|
||||||
|
let dm = self.cfg.d_mem;
|
||||||
|
let din = self.cfg.d_in;
|
||||||
|
assert_eq!(prev_mem.len(), dm);
|
||||||
|
assert_eq!(x.len(), din);
|
||||||
|
assert_eq!(target_mem.len(), dm);
|
||||||
|
assert_eq!(target_next.len(), din);
|
||||||
|
|
||||||
|
let (w_z, z_id) = self.w_z.leaf(&self.dev);
|
||||||
|
let (w_c, c_id) = self.w_c.leaf(&self.dev);
|
||||||
|
let (w_read, read_id) = self.w_read.leaf(&self.dev);
|
||||||
|
|
||||||
|
let new_mem = self.graph(prev_mem, x, &w_z, &w_c);
|
||||||
|
let pred = Ad::matmul(new_mem.clone(), w_read);
|
||||||
|
let tgt_mem_t = Ad::from_data(target_mem, [1, dm], &self.dev);
|
||||||
|
let tgt_next_t = Ad::from_data(target_next, [1, din], &self.dev);
|
||||||
|
let mem_err = Ad::sub(new_mem, tgt_mem_t);
|
||||||
|
let read_err = Ad::sub(pred, tgt_next_t);
|
||||||
|
let loss = Ad::add(
|
||||||
|
Ad::sum(Ad::mul(mem_err.clone(), mem_err)),
|
||||||
|
Ad::sum(Ad::mul(read_err.clone(), read_err)),
|
||||||
|
);
|
||||||
|
let loss_val = Ad::to_data(&loss)[0];
|
||||||
|
|
||||||
|
let storage = backward(&loss);
|
||||||
|
if !loss_val.is_finite() {
|
||||||
|
return loss_val;
|
||||||
|
}
|
||||||
|
self.step += 1;
|
||||||
|
let t = self.step;
|
||||||
|
const LR: f32 = 0.01;
|
||||||
|
self.w_z.adam(&clip(grad_of(&storage, z_id)), LR, t);
|
||||||
|
self.w_c.adam(&clip(grad_of(&storage, c_id)), LR, t);
|
||||||
|
self.w_read.adam(&clip(grad_of(&storage, read_id)), LR, t);
|
||||||
|
loss_val
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DAgger memory-only correction at learning rate `lr`: train just the gate +
|
||||||
|
/// candidate to map `(prev_mem, x) → target_mem`, leaving the readout intact.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics on shape mismatch.
|
||||||
|
pub fn train_step_memory(
|
||||||
|
&mut self,
|
||||||
|
prev_mem: &[f32],
|
||||||
|
x: &[f32],
|
||||||
|
target_mem: &[f32],
|
||||||
|
lr: f32,
|
||||||
|
) -> f32 {
|
||||||
|
let dm = self.cfg.d_mem;
|
||||||
|
assert_eq!(prev_mem.len(), dm);
|
||||||
|
assert_eq!(x.len(), self.cfg.d_in);
|
||||||
|
assert_eq!(target_mem.len(), dm);
|
||||||
|
|
||||||
|
let (w_z, z_id) = self.w_z.leaf(&self.dev);
|
||||||
|
let (w_c, c_id) = self.w_c.leaf(&self.dev);
|
||||||
|
let new_mem = self.graph(prev_mem, x, &w_z, &w_c);
|
||||||
|
let tgt_mem_t = Ad::from_data(target_mem, [1, dm], &self.dev);
|
||||||
|
let mem_err = Ad::sub(new_mem, tgt_mem_t);
|
||||||
|
let loss = Ad::sum(Ad::mul(mem_err.clone(), mem_err));
|
||||||
|
let loss_val = Ad::to_data(&loss)[0];
|
||||||
|
|
||||||
|
let storage = backward(&loss);
|
||||||
|
if !loss_val.is_finite() {
|
||||||
|
return loss_val;
|
||||||
|
}
|
||||||
|
self.step += 1;
|
||||||
|
let t = self.step;
|
||||||
|
self.w_z.adam(&clip(grad_of(&storage, z_id)), lr, t);
|
||||||
|
self.w_c.adam(&clip(grad_of(&storage, c_id)), lr, t);
|
||||||
|
loss_val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the tape backward from a scalar loss.
|
||||||
|
fn backward(
|
||||||
|
loss: &<Ad as Backend>::TensorPrimitive<1>,
|
||||||
|
) -> rtx_autograd::autodiff::GradientStorage<CpuBackend> {
|
||||||
|
backward_impl(
|
||||||
|
loss,
|
||||||
|
Some(GradTensor::from_d1(CpuBackend::ones(
|
||||||
|
[1],
|
||||||
|
&<CpuBackend as Backend>::Device::default(),
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.expect("backward")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-element gradient clip for stability on extreme rollout states.
|
||||||
|
fn clip(mut g: Vec<f32>) -> Vec<f32> {
|
||||||
|
const CLIP: f32 = 1.0;
|
||||||
|
for v in &mut g {
|
||||||
|
*v = v.clamp(-CLIP, CLIP);
|
||||||
|
}
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gated_updater_learns_a_transition() {
|
||||||
|
let cfg = ClonedUpdaterConfig::new(4, 6, 16, 1);
|
||||||
|
let mut g = GatedMemoryUpdater::new(cfg);
|
||||||
|
let prev = lcg_init(6, 0.5, 10);
|
||||||
|
let x = lcg_init(4, 1.0, 11);
|
||||||
|
let target_mem = lcg_init(6, 0.4, 12);
|
||||||
|
let target_next = lcg_init(4, 0.4, 13);
|
||||||
|
let first = g.train_step(&prev, &x, &target_mem, &target_next);
|
||||||
|
let mut last = first;
|
||||||
|
for _ in 0..400 {
|
||||||
|
last = g.train_step(&prev, &x, &target_mem, &target_next);
|
||||||
|
assert!(last.is_finite());
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
last < first * 0.5,
|
||||||
|
"gated updater did not learn: {first:.4} -> {last:.4}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn free_rollout_is_bounded_without_clamp() {
|
||||||
|
// The convex update is a non-expansion: |M_t| ≤ max(|M_0|, 1) forever.
|
||||||
|
let cfg = ClonedUpdaterConfig::new(4, 6, 8, 2);
|
||||||
|
let g = GatedMemoryUpdater::new(cfg);
|
||||||
|
let mut mem = vec![0.3f32; 6];
|
||||||
|
for t in 0..500 {
|
||||||
|
let x = lcg_init(4, 1.0, t as u64);
|
||||||
|
mem = g.step(&mem, &x);
|
||||||
|
assert!(
|
||||||
|
mem.iter().all(|v| v.abs() <= 1.0 + 1e-4),
|
||||||
|
"memory left [-1,1] at step {t}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,10 @@ pub mod set_encoder_teacher;
|
|||||||
// trajectory, no BPTT — see `cloned_memory_updater`).
|
// trajectory, no BPTT — see `cloned_memory_updater`).
|
||||||
pub mod cloned_memory_updater;
|
pub mod cloned_memory_updater;
|
||||||
|
|
||||||
|
// SMT gated (GRU-style) memory updater — switch-tracking, bounded recurrence
|
||||||
|
// for non-stationary streams (see `gated_memory_updater`).
|
||||||
|
pub mod gated_memory_updater;
|
||||||
|
|
||||||
// Ring attention for long context
|
// Ring attention for long context
|
||||||
pub mod ring_attention;
|
pub mod ring_attention;
|
||||||
// pub mod mamba_integration;
|
// pub mod mamba_integration;
|
||||||
|
|||||||
Reference in New Issue
Block a user