Skip to content

Commit 25cc091

Browse files
LayerDynamicsclaude
andcommitted
fix(ll_gen): load_checkpoint tolerates architecture drift (resilient load)
RLAlignmentTrainer.load_checkpoint loaded model state strictly, so a genuinely trained checkpoint saved under an older architecture (the M3 VAE checkpoint, predating STEPVAE's optional dim_encoder layer) raised "Missing key(s) ... dim_encoder.weight" and was unusable. Load non-strictly with a warning on missing/unexpected keys; guard optimizer restore too. The real 95%-validity VAE checkpoint (ll_gen/checkpoints/vae_rl_solid.pt) now loads via the normal API and scores 0.95 valid (verified). Regression test added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5fbf909 commit 25cc091

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

ll_gen/ll_gen/training/rl_trainer.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -598,11 +598,31 @@ def load_checkpoint(self, path: Path) -> None:
598598
_log.warning("No history file found at %s; starting fresh", history_path)
599599
self._train_history = checkpoint.get("train_history", [])
600600

601-
# Load model and optimizer state
601+
# Load model and optimizer state. Load the model resiliently: a
602+
# checkpoint saved under an older architecture (e.g. before the optional
603+
# dim_encoder dimension-conditioning layer was added to STEPVAE) must
604+
# still load, or a genuinely-trained checkpoint becomes unusable. Any
605+
# missing/unexpected keys are surfaced as a warning, not silently
606+
# dropped, so a true mismatch stays visible.
602607
if "model_state_dict" in checkpoint and self.generator._model is not None:
603-
self.generator._model.load_state_dict(checkpoint["model_state_dict"])
608+
incompatible = self.generator._model.load_state_dict(
609+
checkpoint["model_state_dict"], strict=False
610+
)
611+
if incompatible.missing_keys or incompatible.unexpected_keys:
612+
_log.warning(
613+
"Checkpoint %s loaded non-strictly: missing=%s unexpected=%s",
614+
path,
615+
list(incompatible.missing_keys),
616+
list(incompatible.unexpected_keys),
617+
)
604618
if "optimizer_state_dict" in checkpoint:
605-
self._optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
619+
# The optimizer state may not line up if the parameter set changed
620+
# (e.g. new conditioning layer); resuming the optimizer is
621+
# best-effort, never fatal.
622+
try:
623+
self._optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
624+
except (ValueError, KeyError) as exc:
625+
_log.warning("Could not restore optimizer state from %s: %s", path, exc)
606626

607627
_log.info("Checkpoint loaded from %s (step %d)", path, self._step_count)
608628

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Regression: RLAlignmentTrainer.load_checkpoint must tolerate architecture drift.
2+
3+
A genuinely-trained checkpoint saved under an older model architecture (e.g. the
4+
M3 VAE checkpoint, saved before STEPVAE gained the optional ``dim_encoder``
5+
dimension-conditioning layer) must still load. Previously ``load_checkpoint``
6+
called ``load_state_dict`` strictly and raised ``RuntimeError: Missing key(s)
7+
... dim_encoder.weight`` -- which made the real 95%-validity checkpoint
8+
unusable via the normal API. The loader now loads non-strictly and warns.
9+
"""
10+
from __future__ import annotations
11+
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
torch = pytest.importorskip("torch")
17+
18+
# repo/ll_gen/tests/<this> -> repo/ll_gen/checkpoints/vae_rl_solid.pt
19+
_CKPT = Path(__file__).resolve().parents[1] / "checkpoints" / "vae_rl_solid.pt"
20+
21+
22+
def test_load_checkpoint_tolerates_missing_keys(tmp_path):
23+
if not _CKPT.exists():
24+
pytest.skip("trained VAE checkpoint fixture not present")
25+
26+
from ll_gen.training.rl_trainer import RLAlignmentTrainer
27+
from ll_gen.training.run import build_generator
28+
29+
generator = build_generator("vae", "cpu")
30+
trainer = RLAlignmentTrainer(generator, device="cpu", output_dir=str(tmp_path))
31+
trainer._init_training()
32+
33+
# Must NOT raise even though the checkpoint predates the dim_encoder layer.
34+
trainer.load_checkpoint(_CKPT)
35+
assert generator._model is not None
36+
37+
# The current model genuinely has the key the checkpoint lacks (so the test
38+
# is exercising the resilient path, not a no-op).
39+
state_keys = set(generator._model.state_dict().keys())
40+
assert any("dim_encoder" in k for k in state_keys)

0 commit comments

Comments
 (0)