plastic / tests /test_memory.py
dmontgomery40's picture
Launch complete Plastic dashboard and bounded shared sessions on free CPU
595cac0 verified
Raw History Blame Contribute Delete
4.95 kB
import pytest
import torch
from plastic.config import ModelConfig
from plastic.model.memory import FastWeightMemory
from plastic.model.state import SessionState
def test_chunk_and_recurrent_modes_agree(device):
cfg = ModelConfig(d_model=32, n_heads=2, chunk=16)
mem = FastWeightMemory(cfg).to(device)
u = torch.randn(2, 40, 32, device=device)
S0 = torch.randn(2, 2, 16, 16, device=device) * 0.1
m1, S1, _, _, sig1 = mem(u, S0, None, mode="chunk")
m2, S2, _, _, sig2 = mem(u, S0, None, mode="recurrent")
assert torch.allclose(m1, m2, atol=1e-4, rtol=1e-4)
assert torch.allclose(S1, S2, atol=1e-4, rtol=1e-4)
assert torch.allclose(sig1.write_norm, sig2.write_norm, atol=1e-4)
def test_beta_scale_zero_freezes_memory(device):
cfg = ModelConfig(d_model=32, n_heads=2, chunk=16)
mem = FastWeightMemory(cfg).to(device)
u = torch.randn(1, 20, 32, device=device)
S0 = torch.randn(1, 2, 16, 16, device=device) * 0.1
_, S1, _, _, sig = mem(u, S0, None, mode="chunk", beta_scale=0.0)
assert sig.write_norm.abs().max() == 0
assert (S1.abs() <= S0.abs() + 1e-6).all()
def test_signals_shapes(device):
cfg = ModelConfig(d_model=32, n_heads=2, chunk=16)
mem = FastWeightMemory(cfg).to(device)
u = torch.randn(3, 17, 32, device=device)
S0 = torch.zeros(3, 2, 16, 16, device=device)
m, S, M, _, sig = mem(u, S0, None, mode="chunk")
assert m.shape == (3, 17, 32) and S.shape == (3, 2, 16, 16) and M is None
assert sig.err.shape == sig.beta.shape == sig.alpha.shape == sig.write_norm.shape == (3, 2, 17)
assert (sig.beta > 0).all() and (sig.beta < 1).all() and (sig.alpha > 0).all() and (sig.alpha < 1).all()
def test_chunk_rule_runs_and_reads_chunk_start_weights(device):
cfg = ModelConfig(d_model=32, n_heads=2, chunk=8, rule="chunk")
mem = FastWeightMemory(cfg).to(device)
u = torch.randn(1, 20, 32, device=device)
st = SessionState.zeros(cfg, batch=1, device=device).layers[0]
m, S, M, chunk, sig = mem(u, st.S, st.M, mode="chunk", chunk0=st.chunk)
assert m.shape == (1, 20, 32) and M is not None and M.shape == S.shape
assert chunk is not None and chunk.count == 4 # 20 = 8 + 8 + 4 pending
# first chunk reads from S0 = 0 so its memory output is exactly zero
assert torch.allclose(m[:, :8], torch.zeros_like(m[:, :8]))
nz = (sig.write_norm[0, 0] != 0).nonzero().flatten().tolist()
assert nz == [7, 15] # one write per completed chunk, reported on its last token
@pytest.mark.parametrize("mode", ["chunk", "recurrent"])
def test_chunk_rule_is_stream_equivalent(device, mode):
"""Any partition of the token stream into forward calls gives identical results."""
cfg = ModelConfig(d_model=32, n_heads=2, chunk=8, rule="chunk")
mem = FastWeightMemory(cfg).to(device)
u = torch.randn(1, 37, 32, device=device)
st = SessionState.zeros(cfg, batch=1, device=device).layers[0]
st.S += torch.randn_like(st.S) * 0.1
m_full, S_full, M_full, c_full, sig_full = mem(u, st.S, st.M, mode=mode, chunk0=st.chunk)
S, M, c = st.S, st.M, st.chunk
outs, writes = [], []
cuts = [0, 3, 8, 9, 20, 21, 30, 37]
for a, b in zip(cuts[:-1], cuts[1:]):
m, S, M, c, sig = mem(u[:, a:b], S, M, mode=mode, chunk0=c)
outs.append(m)
writes.append(sig.write_norm)
assert torch.allclose(m_full, torch.cat(outs, 1), atol=1e-5, rtol=1e-5)
assert torch.allclose(S_full, S, atol=1e-5) and torch.allclose(M_full, M, atol=1e-5)
assert c.count == c_full.count == 37 % 8
assert torch.allclose(c.A, c_full.A, atol=1e-5)
assert torch.allclose(sig_full.write_norm, torch.cat(writes, 2), atol=1e-5)
def test_chunk_rule_beta_scale_caps_orthogonalized_write(device):
cfg = ModelConfig(d_model=32, n_heads=2, chunk=8, rule="chunk", chunk_orthogonalize=True)
mem = FastWeightMemory(cfg).to(device)
u = torch.randn(1, 8, 32, device=device)
st = SessionState.zeros(cfg, batch=1, device=device).layers[0]
_, S1, M1, _, _ = mem(u, st.S, st.M, mode="chunk", chunk0=st.chunk, beta_scale=1.0)
_, Sh, Mh, _, _ = mem(u, st.S, st.M, mode="chunk", chunk0=st.chunk, beta_scale=0.5)
_, S0, M0, _, sig0 = mem(u, st.S, st.M, mode="chunk", chunk0=st.chunk, beta_scale=0.0)
assert torch.allclose((Sh - st.S).norm(), 0.5 * (S1 - st.S).norm(), rtol=1e-4)
assert torch.equal(S0, st.S) and torch.equal(M0, st.M)
assert sig0.write_norm.abs().max() == 0
# warm momentum: scale 0 still writes nothing, momentum only decays, S only decays
_, S2, M2, _, sig2 = mem(u, S1, M1, mode="chunk", chunk0=st.chunk, beta_scale=0.0)
assert torch.allclose(M2, cfg.chunk_momentum * M1)
alpha_chunk = sig2.alpha.mean(dim=-1)
assert torch.allclose(S2, alpha_chunk[..., None, None] * S1, atol=1e-6)
def test_mlp_memory_not_available_yet():
with pytest.raises(NotImplementedError):
FastWeightMemory(ModelConfig(rule="chunk", memory="mlp"))