berna-r8 / code /forgetting /bounded.py
muhamedkamil's picture
Upload code/forgetting/bounded.py with huggingface_hub
f223eaa verified
Raw History Blame Contribute Delete
4.27 kB
"""
Bounded Forgetting (R8) — full implementation.
Definitions:
P_j(t) = performance on task j at time t (we use 1/loss).
F_j = max(0, L_after - L_before)
Goal: F_j <= EPS_NEG = 1e-6
Mechanism:
1. Freeze saturated cells (S >= S* for T consecutive steps).
2. Frozen cells: gradient hooks zero out their parameter ranges.
3. New domain → new cells born (disabled in stub), old cells untouched.
"""
import json
from pathlib import Path
from typing import Callable, Dict, List, Optional
import numpy as np
import torch
EPS_NEG = 1e-6
S_STAR = 0.85
T_FREEZE = 3
class CellFreezer:
def __init__(self, S_star: float = S_STAR, T: int = T_FREEZE):
self.S_star = S_star
self.T = T
self.frozen_count = 0
def maybe_freeze(self, cell, current_step: int) -> bool:
if getattr(cell, "frozen", False):
return False
recent = cell.S_history[-self.T:] if hasattr(cell, "S_history") else []
if len(recent) >= self.T and all(s >= self.S_star for s in recent):
cell.frozen = True
cell.freeze_step = current_step
self.frozen_count += 1
return True
return False
def freeze_all_saturated(self, cells, current_step: int) -> int:
n = 0
for c in cells:
if c.state == "active" and self.maybe_freeze(c, current_step):
n += 1
return n
def install_freeze_hooks(model, cells_by_layer):
"""
Register gradient hooks on CellAwareMLP gate/up projections
that zero gradients for frozen cells' neuron ranges.
"""
handles = []
for layer_idx, layer in enumerate(model.model.layers):
mlp = layer.mlp
cells = cells_by_layer.get(layer_idx, [])
if not cells:
continue
def make_hook(cells_ref):
def hook(grad):
g = grad.clone()
for c in cells_ref:
if getattr(c, "frozen", False):
g[..., c.neuron_start:c.neuron_end] = 0.0
return g
return hook
try:
h1 = mlp.gate_proj.weight.register_hook(make_hook(cells))
handles.append(h1)
except AttributeError:
pass
try:
h2 = mlp.up_proj.weight.register_hook(make_hook(cells))
handles.append(h2)
except AttributeError:
pass
return handles
@torch.no_grad()
def evaluate_loss(model, tokens, seq_len=2048, max_batches=10, device="cpu"):
"""Compute average loss on a token stream."""
was_training = model.training
model.eval()
tokens = np.asarray(tokens, dtype=np.int64)
if len(tokens) < seq_len + 1:
if was_training:
model.train()
return float("nan")
n = min(len(tokens) // (seq_len + 1), max_batches)
if n == 0:
if was_training:
model.train()
return float("nan")
total = 0.0
for i in range(n):
s = i * seq_len
x = torch.from_numpy(tokens[s:s + seq_len]).unsqueeze(0).to(device)
y = torch.from_numpy(tokens[s + 1:s + seq_len + 1]).unsqueeze(0).to(device)
out = model(input_ids=x, labels=y)
total += float(out.loss.item())
if was_training:
model.train()
return total / n
def measure_forgetting(model, task_tokens, baseline_losses, device="cpu"):
"""
F_j = max(0, L_after - L_before).
Bounded if all F_j <= EPS_NEG.
"""
results = {}
for task, tokens in task_tokens.items():
l_before = baseline_losses[task]
l_after = evaluate_loss(model, tokens, device=device)
F_j = max(0.0, l_after - l_before)
results[task] = {
"L_before": l_before,
"L_after": l_after,
"F_j": F_j,
"bounded": F_j <= EPS_NEG,
}
return results
def save_forgetting_report(results, path):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
summary = {
"max_F": max((r["F_j"] for r in results.values()), default=0.0),
"all_bounded": all(r["bounded"] for r in results.values()),
"per_task": results,
}
path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
return path