Add brains/sovereign-nano/distill_nano.py
Browse files
brains/sovereign-nano/distill_nano.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
distill_nano.py — wake-sleep distillation INTO SOVEREIGN-nano.
|
| 3 |
+
|
| 4 |
+
The v3 teacher (or any contract-compatible model: forward(rgb, nums,
|
| 5 |
+
rtg=None, cell=None, return_all=True) -> dict with cell_logits/
|
| 6 |
+
kind_logits/value/win_logits/econ/pct_params) distills into nano via:
|
| 7 |
+
|
| 8 |
+
KL(cell_logits) + KL(kind) + KL(win) + MSE(value) + MSE(pct_params)
|
| 9 |
+
+ MSE(econ) + hard-cell CE + optional search-target KL
|
| 10 |
+
|
| 11 |
+
Nano's contract is identical to the teacher's, so every term is a direct
|
| 12 |
+
op — no adapters, no shape translation. search_targets (B,3,g*g) are the
|
| 13 |
+
teacher's MCTS visit distributions when available (AlphaZero-lite policy
|
| 14 |
+
improvement flowing into the cheap policy).
|
| 15 |
+
|
| 16 |
+
torch only. The smoke uses two nano-sized nets (no 290M import needed).
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
from typing import Iterator, Optional
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn.functional as F
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def distill_nano(teacher, student, batches: Iterator[dict], epochs: int = 1,
|
| 28 |
+
lr: float = 3e-4, kl_cell: float = 1.0,
|
| 29 |
+
kl_kind: float = 1.0, kl_win: float = 0.5,
|
| 30 |
+
mse_val: float = 1.0, mse_pct: float = 0.5,
|
| 31 |
+
mse_econ: float = 0.1, ce_cell: float = 0.25,
|
| 32 |
+
search_w: float = 0.5, device: str = "cuda") -> None:
|
| 33 |
+
teacher.to(device).eval()
|
| 34 |
+
student.to(device).train()
|
| 35 |
+
dev = torch.device(device)
|
| 36 |
+
opt = torch.optim.AdamW(student.parameters(), lr=lr)
|
| 37 |
+
|
| 38 |
+
for ep in range(epochs):
|
| 39 |
+
tot, nb = 0.0, 0
|
| 40 |
+
for raw in batches:
|
| 41 |
+
b = {k: v.to(dev) for k, v in raw.items()
|
| 42 |
+
if hasattr(v, "to") and k != "search_targets"}
|
| 43 |
+
rgb, nums = b["rgb"], b["nums"]
|
| 44 |
+
rtg = b.get("rtg")
|
| 45 |
+
cell = b["cell"]
|
| 46 |
+
with torch.no_grad():
|
| 47 |
+
to = teacher(rgb, nums, rtg=rtg, cell=cell,
|
| 48 |
+
return_all=True)
|
| 49 |
+
so = student(rgb, nums, rtg=rtg, cell=cell, return_all=True)
|
| 50 |
+
|
| 51 |
+
loss = kl_cell * F.kl_div(
|
| 52 |
+
F.log_softmax(so["cell_logits"], 1),
|
| 53 |
+
F.softmax(to["cell_logits"], 1), reduction="batchmean")
|
| 54 |
+
loss = loss + kl_kind * F.kl_div(
|
| 55 |
+
F.log_softmax(so["kind_logits"], 1),
|
| 56 |
+
F.softmax(to["kind_logits"], 1), reduction="batchmean")
|
| 57 |
+
loss = loss + kl_win * F.kl_div(
|
| 58 |
+
F.log_softmax(so["win_logits"], 1),
|
| 59 |
+
F.softmax(to["win_logits"], 1), reduction="batchmean")
|
| 60 |
+
loss = loss + mse_val * F.mse_loss(so["value"], to["value"])
|
| 61 |
+
loss = loss + mse_pct * (
|
| 62 |
+
F.mse_loss(so["pct_params"][0], to["pct_params"][0]) +
|
| 63 |
+
F.mse_loss(so["pct_params"][1], to["pct_params"][1]))
|
| 64 |
+
if so["econ"] is not None and to["econ"] is not None:
|
| 65 |
+
loss = loss + mse_econ * F.mse_loss(so["econ"], to["econ"])
|
| 66 |
+
loss = loss + ce_cell * F.cross_entropy(so["cell_logits"], cell)
|
| 67 |
+
if "search_targets" in raw:
|
| 68 |
+
loss = loss + search_w * F.kl_div(
|
| 69 |
+
F.log_softmax(so["cell_logits"], 1),
|
| 70 |
+
raw["search_targets"].to(dev).clamp_min(1e-6),
|
| 71 |
+
reduction="batchmean")
|
| 72 |
+
|
| 73 |
+
opt.zero_grad()
|
| 74 |
+
loss.backward()
|
| 75 |
+
opt.step()
|
| 76 |
+
tot += loss.item()
|
| 77 |
+
nb += 1
|
| 78 |
+
print(f"distill_nano ep {ep+1}: loss={tot/max(nb,1):.4f}",
|
| 79 |
+
flush=True)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def save_student(student, path: str) -> None:
|
| 83 |
+
torch.save({"config": student.cfg, "state_dict": student.state_dict()},
|
| 84 |
+
path)
|
| 85 |
+
print(f"[distill_nano] saved {path}")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
if __name__ == "__main__":
|
| 89 |
+
# smoke: teacher = nano-width net, student = thinner nano — the real
|
| 90 |
+
# teacher swaps in without code changes (same contract).
|
| 91 |
+
from model_nano import NanoConfig, make_nano
|
| 92 |
+
|
| 93 |
+
tcfg = NanoConfig(map_size=64)
|
| 94 |
+
scfg = NanoConfig(map_size=64)
|
| 95 |
+
scfg.enc_ch = (12, 24, 32, 64, 96)
|
| 96 |
+
scfg.dec_plan = ((64, 64, 1), (32, 32, 1), (24, 24, 1),
|
| 97 |
+
(20, None, 1), (20, None, 0))
|
| 98 |
+
scfg.dec_out_ch = 20
|
| 99 |
+
scfg.head_hidden = 96
|
| 100 |
+
teacher = make_nano(tcfg, seed=0)
|
| 101 |
+
student = make_nano(scfg, seed=1)
|
| 102 |
+
batch = [dict(rgb=torch.rand(2, 3, 64, 64), nums=torch.rand(2, 8),
|
| 103 |
+
rtg=torch.rand(2, 1), cell=torch.tensor([10, 40]),
|
| 104 |
+
search_targets=torch.rand(2, 3 * 16 * 16))]
|
| 105 |
+
distill_nano(teacher, student, iter(batch), epochs=1, device="cpu")
|
| 106 |
+
print("distill_nano.py smoke OK")
|