Skip to content

Commit e093e6a

Browse files
committed
v40: bounded retention portfolio, historical Fisher and experimental capacity growth
1 parent 8f28296 commit e093e6a

14 files changed

Lines changed: 962 additions & 6 deletions

‎RETENTION_RESEARCH.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Continual-learning retention portfolio (v100.40)
2+
3+
The recommended starting point for this hardware is a frozen pretrained base,
4+
verified historical replay, predecessor KL, immutable accepted versions and
5+
independent regression gates. Additional constraints should be compared as
6+
separate candidates. Stacking every method is not evidence of improvement:
7+
stability constraints can prevent useful new learning and add expensive forwards.
8+
No finite test establishes universal zero forgetting.
9+
10+
## Primary research and implementation decisions
11+
12+
| Approach | Evidence / limitation | Implementation |
13+
| --- | --- | --- |
14+
| Historical replay | LLM CL surveys identify rehearsal as a major family; sampling coverage and distribution matter. | Existing mandatory verified prior-training records remain in A/B datasets. Existing split ledger excludes validation/audit sources. New reference sampler covers historical domains round-robin, at most eight examples. |
15+
| Learning without Forgetting / KL | Li & Hoiem, ECCV 2016, https://arxiv.org/abs/1606.09282. Original work is vision, not a Gemma guarantee. | Existing shared frozen-base teacher adapter and supervised-token KL remain active; model chooses strength. No second 12B base allocation. |
16+
| EWC | Kirkpatrick et al., PNAS 2017, https://doi.org/10.1073/pnas.1611835114. Empirical diagonal Fisher is an approximation and misses cross-parameter interactions. | New per-example squared-gradient Fisher on at most eight verified historical training records, quadratic anchor penalty, bounded LoRA parameters only. Reference weights/Fisher pinned and restored across checkpoint resume. Not original full-model EWC and not online-EWC. |
17+
| Parameter anchoring | L2-SP, Li et al., ICML 2018, https://proceedings.mlr.press/v80/li18a.html. Parameter closeness does not imply identical outputs. | New L2 anchor to the initial predecessor adapter; frozen base unchanged. |
18+
| Orthogonality | O-LoRA, EMNLP 2023, https://aclanthology.org/2023.findings-emnlp.715/; OGD, AISTATS 2020, https://proceedings.mlr.press/v108/farajtabar20a.html. | Experimental delta-A overlap penalty with predecessor row directions. Initialization penalty is zero. This is inspired by orthogonal methods, **not an O-LoRA/OGD reproduction**. |
19+
| A-GEM | Chaudhry et al., ICLR 2019, https://arxiv.org/abs/1812.00420. Half-space protection is first-order on sampled replay; AdamW momentum/finite steps can still cause forgetting. | New conflicting-gradient projection after AMP unscale/clip and before optimizer step. Historical replay references rotate; adds one reference forward/backward per optimizer attempt. Nonfinite AMP attempts are left to the existing health gate. |
20+
| Capacity growth | Progressive Neural Networks, Rusu et al., https://arxiv.org/abs/1606.04671, protect old columns when old task routes stay fixed. Route errors remain a problem. | New doubling of standard LoRA rank (maximum 64 and measured budget), zero new B columns, unchanged alpha/r and old tensors. It preserves the **initial** delta, not future behavior. Normal PEFT/GGUF candidate export and all quality gates remain required. |
21+
| New layers / embeddings | Architectural growth can isolate interference, but changes export/tokenization and resource use. | `capacity_growth.py` supplies isolated tiny-model residual columns with zero output initialization, frozen predecessor route, and appended embedding rows with unchanged old IDs. Maximum two million old/new parameters for these prototypes. **Not live Gemma layer surgery, tokenizer expansion or a native GGUF serving feature.** |
22+
| External memory / RAG | Retrieval avoids some weight updates but has stale/missing retrieval and routing failure modes. | Existing CPU hybrid persistent memory remains separate from weights. It is not proof that weights retain knowledge. |
23+
| Evaluation / rollback | GEM describes backward transfer and continual-learning evaluation. Repeated public/development tests can overfit. | Existing case-by-case ancestor gates, official grading, one-use fresh audits and preserved accepted versions remain mandatory. Fresh audit answers are not training/replay/Fisher inputs. Old-version routing remains an available fallback, not proof a new version retained skills. |
24+
25+
Survey: Shi et al., *Continual Learning of Large Language Models: A Comprehensive
26+
Survey*, https://arxiv.org/abs/2404.16789. Methods target different kinds of forgetting
27+
and stages of training; results on small classification networks do not establish
28+
performance on sequential Gemma 12B fine-tuning or financial outcomes.
29+
30+
## What the controller can select
31+
32+
With `resources.retention_experiments=true`, bounded A/B planning adds:
33+
34+
- `retention_mode`: 0 replay/KL; 1 L2; 2 EWC; 3 L2 + delta-A orthogonality;
35+
4 EWC + delta-A orthogonality; 5 A-GEM projection.
36+
- `retention_strength`: 0.001, 0.01, 0.1, 1.0 (unused for projection).
37+
- `retention_rank_growth`: 1 or 2, only when an existing rank can double within
38+
the measured maximum rank and the absolute limit of 64.
39+
40+
Delta-A orthogonality needs a predecessor adapter. EWC/projection are unavailable until a pinned predecessor and historical verified
41+
training examples exist. Anchors/Fisher are capped at 160 million adapter
42+
parameters. References are selected from the historical training portion of the
43+
immutable combined pool, never the withheld validation set. These bounded
44+
references are not comprehensive coverage of all pretrained capabilities.
45+
46+
Artifacts in each trial's training directory:
47+
48+
- `retention.json`, `retention-anchor.safetensors`: measured/reference provenance.
49+
- `capacity-growth.json`, `expanded-initial/`: rank expansion evidence.
50+
- Existing `training_health.json`, metrics, checkpoint manifests and quality
51+
judgments: actual successful optimizer counters and acceptance evidence.
52+
- Activity events `retention-reference` and `replay-gradient-projection`.
53+
54+
The mission evidence collector exposes current-run reference/growth reports.
55+
A configured method or a saved reference is **not** proof of a successful update,
56+
accepted weights, improved retention, new task accuracy or profit.
57+
58+
## Experimental boundaries and omissions
59+
60+
Full GEM quadratic programming, full 12B Fisher/Jacobian matrices, unconstrained
61+
layer replication, live vocabulary changes, and running many 12B model copies
62+
are not enabled. They have unresolved compute/export/validation costs on a 32 GiB
63+
V100. The implementation provides a testable portfolio, not every CL algorithm
64+
or all published implementations. Newer variants require separate reproduction
65+
and hardware measurements before being added to this pipeline.
66+
67+
Frozen-column and embedding prototypes have CPU tests of initial equality and
68+
unchanged old-route/old-ID outputs after new-parameter training. LoRA growth has
69+
structural factor/scaling tests. Fisher, penalties, projection and tamper-sensitive
70+
resume have deterministic tensor tests. **Full NF4/FP16 Gemma training, retention
71+
improvement and native export of an expanded adapter still need on-device tests.**

‎V100_AUDIT.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,18 @@ argument schema. It can also decline tools with a real concise answer. Task scop
276276
argument validation, token limits and the original input remain enforced.
277277
Oversized tool results are retained in SHA-256-addressed files and activity traces;
278278
model-visible previews explicitly mark truncation and do not certify missing facts.
279+
# Retention experiments v100.40
280+
281+
`RETENTION_RESEARCH.md` distinguishes existing replay/KL/fresh regression gates
282+
from new bounded LoRA L2, empirical diagonal Fisher EWC, experimental delta-A
283+
orthogonality, A-GEM projection and exact-initial-delta rank expansion. Model
284+
planning can compare them; historical methods require previous verified training
285+
references, never audit labels. Anchors/Fisher are pinned across resume, and
286+
retention/capacity artifacts appear in current-run training evidence. Existing
287+
ancestor/public/one-use audit gates and original accepted checkpoints remain.
288+
289+
`capacity_growth.py` implements small isolated frozen residual-column and appended
290+
embedding prototypes. Their old routes/IDs stay unchanged in tensor tests; this
291+
does not implement live Gemma layer/vocabulary surgery or prove retention on
292+
unseen tasks. Full NF4/FP16 training and grown-adapter native export require V100
293+
acceptance. The dedicated upgrade preserves setup and skips calibration sweeps.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "rlms"
33
authors = [
44
{name = "Alex Zhang", email = "altzhang@mit.edu"},
55
]
6-
version = "0.1.3+v100.39"
6+
version = "0.1.3+v100.40"
77
description = "Recursive Language Models."
88
readme = "README.md"
99
license = "MIT"

‎rlm/v100/campaign.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def stage(name, function):
145145

146146
stage("private-desktop", lambda: prepare_desktop(root))
147147
resources = profile.setdefault("resources", {})
148+
resources["retention_experiments"] = True
148149
if benchmarks and current(root) and resources.get("public_baseline"):
149150
from rlm.v100.protection import file_hash
150151

‎rlm/v100/capacity_growth.py‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Experimental frozen-column growth primitives for isolated PyTorch pilots.
2+
3+
These change architecture and cannot be exported as an unchanged native Gemma.
4+
Old routes remain exact only when callers keep the old route and frozen state.
5+
"""
6+
7+
8+
def residual_column(predecessor, width: int, layers: int = 1, bottleneck: int = 16):
9+
import copy
10+
11+
import torch
12+
from torch import nn
13+
14+
if any(type(n) is not int for n in (width, layers, bottleneck)) or not (
15+
1 <= layers <= 4 and 1 <= bottleneck <= width <= 4096
16+
):
17+
raise ValueError("Growth column dimensions exceed the pilot budget")
18+
if layers * width * bottleneck * 2 > 2_000_000:
19+
raise ValueError("Growth column exceeds two million new parameters")
20+
if sum(p.numel() for p in predecessor.parameters()) > 2_000_000:
21+
raise ValueError("Frozen-column primitives are bounded tiny-model pilots")
22+
predecessor = copy.deepcopy(predecessor)
23+
24+
class FrozenColumn(nn.Module):
25+
def __init__(self):
26+
super().__init__()
27+
self.predecessor = predecessor
28+
self.predecessor.requires_grad_(False)
29+
self.predecessor.eval()
30+
self.blocks = nn.ModuleList()
31+
parameter = next(predecessor.parameters())
32+
for _ in range(layers):
33+
block = nn.Sequential(
34+
nn.Linear(width, bottleneck), nn.GELU(), nn.Linear(bottleneck, width)
35+
)
36+
block.to(device=parameter.device, dtype=parameter.dtype)
37+
nn.init.zeros_(block[-1].weight)
38+
nn.init.zeros_(block[-1].bias)
39+
self.blocks.append(block)
40+
41+
def train(self, mode=True):
42+
super().train(mode)
43+
self.predecessor.eval()
44+
return self
45+
46+
def forward(self, value, route="previous"):
47+
if route not in ("previous", "expanded"):
48+
raise ValueError("Unknown growth route")
49+
with torch.no_grad():
50+
value = self.predecessor(value)
51+
if route == "expanded":
52+
for block in self.blocks:
53+
value = value + block(value)
54+
return value
55+
56+
return FrozenColumn()
57+
58+
59+
def embedding_extension(predecessor, new_rows: int):
60+
"""Freeze original IDs; append trainable rows without renumbering any token.
61+
62+
Requires a matching tokenizer AND output head in an architecture pilot.
63+
This primitive alone does not add vocabulary to the serving language model.
64+
"""
65+
import torch
66+
from torch import nn
67+
68+
if type(new_rows) is not int or not 1 <= new_rows <= 4096:
69+
raise ValueError("Embedding extension needs 1..4096 rows")
70+
if (
71+
not isinstance(predecessor, nn.Embedding)
72+
or new_rows * predecessor.embedding_dim > 2_000_000
73+
or predecessor.weight.numel() > 2_000_000
74+
):
75+
raise ValueError("Embedding extension exceeds its parameter budget")
76+
77+
class ExpandedEmbedding(nn.Module):
78+
def __init__(self):
79+
super().__init__()
80+
self.register_buffer("original", predecessor.weight.detach().clone())
81+
self.extra = nn.Embedding(new_rows, predecessor.embedding_dim).to(
82+
device=predecessor.weight.device, dtype=predecessor.weight.dtype
83+
)
84+
self.extra.weight.data.copy_(self.original.mean(dim=0).expand_as(self.extra.weight))
85+
86+
def forward(self, ids):
87+
if (
88+
ids.dtype not in (torch.int32, torch.int64)
89+
or (ids < 0).any()
90+
or (ids >= len(self.original) + new_rows).any()
91+
):
92+
raise ValueError("Embedding token ID outside expanded vocabulary")
93+
table = torch.cat((self.original, self.extra.weight), dim=0)
94+
return torch.nn.functional.embedding(ids, table)
95+
96+
return ExpandedEmbedding()

‎rlm/v100/experiments.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def parameter_schema(profile: dict) -> dict:
9595
lengths = [n for n in lengths if n <= budget["max_length"]]
9696
if not rank or not lengths:
9797
raise ValueError("Parent adapter exceeds measured training budget; recalibrate")
98-
return {
98+
schema = {
9999
"learning_rate": {
100100
"type": "number",
101101
"minimum": 0.000001,
@@ -125,6 +125,25 @@ def parameter_schema(profile: dict) -> dict:
125125
"enum": [0.01, 0.05, 0.1, 0.2, 0.5, 1.0],
126126
},
127127
}
128+
if profile.get("resources", {}).get("retention_experiments"):
129+
old_rank = rank[0] if profile["training"]["init_adapter"] else None
130+
max_rank = budget["max_rank"] if budget else 64
131+
schema.update(
132+
retention_mode={
133+
"type": "integer",
134+
"description": "0=replay+KL only; 1=L2 anchor; 2=empirical diagonal EWC; 3=L2+experimental delta-A orthogonality; 4=EWC+delta-A orthogonality; 5=A-GEM replay gradient projection (first-order, AdamW is not a no-forgetting guarantee). Historical modes require prior verified TRAINING data. Compare time, new learning and retention, not training loss alone.",
135+
"enum": [0, 1]
136+
+ ([3] if old_rank else [])
137+
+ ([2, 4, 5] if profile["training"].get("retention_has_history") else []),
138+
},
139+
retention_strength={"type": "number", "enum": [0.001, 0.01, 0.1, 1.0]},
140+
retention_rank_growth={
141+
"type": "integer",
142+
"description": "1=keep rank; 2=double accepted LoRA rank with zero new B columns and preserved alpha/r. More memory/time; candidate only and all original quality gates remain mandatory.",
143+
"enum": [1] + ([2] if old_rank and old_rank * 2 <= max_rank else []),
144+
},
145+
)
146+
return schema
128147

129148

130149
def validate_parameters(values: dict, schema: dict) -> None:
@@ -383,6 +402,10 @@ def plan_duel(
383402
branch_profile["training"].update(
384403
init_adapter=parent["init_adapter"], teacher_adapter=parent["teacher_adapter"]
385404
)
405+
historical = [row for row in train if record_id(row) in mandatory]
406+
branch_profile["training"]["retention_has_history"] = bool(
407+
historical and branch_profile["training"]["init_adapter"]
408+
)
386409
decision = choose_experiment(client, branch, catalog, branch_profile, shared.recent())
387410
selected = set(decision["selected_ids"]) | mandatory
388411
branch_path = output / branch
@@ -397,6 +420,9 @@ def plan_duel(
397420
chosen = copy.deepcopy(branch_profile)
398421
chosen["runtime"]["activity_branch"] = branch
399422
chosen["training"].update(decision["parameters"])
423+
chosen["training"]["retention_reference_groups"] = sorted(
424+
{row["group"] for row in historical}
425+
)
400426
chosen["training"].update(
401427
output=str(branch_path / "training"),
402428
microbatch=1,

‎rlm/v100/foundation.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ def trial(
143143
base_model=str(base),
144144
init_adapter="",
145145
teacher_adapter="",
146+
retention_mode=0,
147+
retention_rank_growth=1,
148+
retention_reference_groups=[],
146149
output=str(folder / "training"),
147150
max_steps=25,
148151
eval_steps=5,

‎rlm/v100/mission_evidence.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,20 @@ def collect(root: Path) -> dict:
8181
!= counters["optimizer_updates"] + counters["amp_skipped_steps"]
8282
):
8383
raise ValueError("Inconsistent optimizer counters")
84-
result["training_runs"].append({"path": str(path), **counters})
84+
evidence = {"path": str(path), **counters}
85+
retention = path.parent / "retention.json"
86+
if retention.exists():
87+
record = read_record(retention)
88+
evidence["retention"] = {
89+
"path": str(retention),
90+
"configuration": record.get("configuration"),
91+
"fisher_samples": record.get("fisher_samples"),
92+
"scope": "Configured reference; not proof of retention or promotion",
93+
}
94+
growth = path.parent / "capacity-growth.json"
95+
if growth.exists():
96+
evidence["capacity_growth"] = {"path": str(growth), **read_record(growth)}
97+
result["training_runs"].append(evidence)
8598
except (ValueError, OSError) as error:
8699
result["errors"].append({"path": str(path), "detail": str(error)[:300]})
87100
if result["training_runs"]:

0 commit comments

Comments
 (0)