Add brains/sovereign-v3/model.py
Browse files- brains/sovereign-v3/model.py +108 -4
brains/sovereign-v3/model.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
SOVEREIGN v3 — ~
|
| 3 |
-
territorial.io (last-survivor elimination).
|
| 4 |
|
| 5 |
= DESIGN SUMMARY (full reasoning: ARCHITECTURE.md, training: train_notes.md)
|
| 6 |
Size mandate: >=200M, <400M (user-specified). Four modules:
|
|
@@ -115,11 +115,21 @@ class SovereignConfig:
|
|
| 115 |
aux_seg_classes: int = 4 # water / neutral / me / enemy
|
| 116 |
dropout_mlp: float = 0.1
|
| 117 |
grad_ckpt: bool = False # checkpoint the 4 biggest stages
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
@property
|
| 120 |
def ctx_full_dim(self) -> int:
|
| 121 |
return self.ctx_dim + self.rtg_dim
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
# --------------------------------------------------------------------------
|
| 125 |
# Blocks
|
|
@@ -431,6 +441,32 @@ class Sovereign(nn.Module):
|
|
| 431 |
self.mem = nn.GRU(pool_dim, c.mem_hidden, 1, batch_first=True)
|
| 432 |
self.mem_proj = nn.Linear(c.mem_hidden, c.cortex_d)
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
self.apply(_init_weights)
|
| 435 |
|
| 436 |
# -- helpers ------------------------------------------------------------
|
|
@@ -468,6 +504,64 @@ class Sovereign(nn.Module):
|
|
| 468 |
align_corners=False)
|
| 469 |
return logg
|
| 470 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
# -- forward ------------------------------------------------------------
|
| 472 |
|
| 473 |
def forward(self, rgb: torch.Tensor, nums: Optional[torch.Tensor] = None,
|
|
@@ -783,8 +877,9 @@ def param_breakdown(net: Sovereign) -> dict:
|
|
| 783 |
if net.cfg.use_cell_value:
|
| 784 |
heads += n(net.cell_value)
|
| 785 |
mem = (n(net.mem) + n(net.mem_proj)) if net.mem is not None else 0
|
|
|
|
| 786 |
return dict(spine=spine, cortex=cortex, decoder=decoder, heads=heads,
|
| 787 |
-
mem=mem)
|
| 788 |
|
| 789 |
|
| 790 |
def make_model(cfg: Optional[SovereignConfig] = None,
|
|
@@ -803,7 +898,8 @@ if __name__ == "__main__":
|
|
| 803 |
bd = param_breakdown(m)
|
| 804 |
print(f"params: {total/1e6:.2f}M (spine {bd['spine']/1e6:.1f}M | "
|
| 805 |
f"cortex {bd['cortex']/1e6:.1f}M | decoder {bd['decoder']/1e6:.1f}M "
|
| 806 |
-
f"| heads {bd['heads']/1e6:.1f}M | mem {bd['mem']/1e6:.1f}M
|
|
|
|
| 807 |
H = cfg.map_size
|
| 808 |
rgb = torch.rand(2, 3, H, H)
|
| 809 |
nums = torch.rand(2, 8)
|
|
@@ -821,6 +917,14 @@ if __name__ == "__main__":
|
|
| 821 |
for k, v in a.items() if k not in ("kind_probs",)})
|
| 822 |
a2 = m.act(rgb, nums, rtg=rtg, state=a["state"])
|
| 823 |
print("mem carry ok:", a2["state"].shape)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
# no-rtg path (contract default)
|
| 825 |
_, _, _, _ = m(rgb, nums)
|
| 826 |
print("no-rtg contract path OK")
|
|
|
|
| 1 |
"""
|
| 2 |
+
SOVEREIGN v3 — ~290M-param return-conditioned spatial policy with MCTS
|
| 3 |
+
planning for territorial.io (last-survivor elimination).
|
| 4 |
|
| 5 |
= DESIGN SUMMARY (full reasoning: ARCHITECTURE.md, training: train_notes.md)
|
| 6 |
Size mandate: >=200M, <400M (user-specified). Four modules:
|
|
|
|
| 115 |
aux_seg_classes: int = 4 # water / neutral / me / enemy
|
| 116 |
dropout_mlp: float = 0.1
|
| 117 |
grad_ckpt: bool = False # checkpoint the 4 biggest stages
|
| 118 |
+
# -- planning / search block (MuZero-lite) ------------------------------
|
| 119 |
+
use_search: bool = True # latent encode/predict/dynamics for MCTS
|
| 120 |
+
lat_c: int = 32 # compressed spatial channels in the latent
|
| 121 |
+
lat_spatial: int = 8 # latent map size (8x8)
|
| 122 |
+
dyn_hidden: int = 1024 # dynamics MLP hidden width
|
| 123 |
+
act_embed: int = 512 # action embedding width
|
| 124 |
|
| 125 |
@property
|
| 126 |
def ctx_full_dim(self) -> int:
|
| 127 |
return self.ctx_dim + self.rtg_dim
|
| 128 |
|
| 129 |
+
@property
|
| 130 |
+
def latent_dim(self) -> int:
|
| 131 |
+
return self.cortex_d + self.lat_c * self.lat_spatial * self.lat_spatial
|
| 132 |
+
|
| 133 |
|
| 134 |
# --------------------------------------------------------------------------
|
| 135 |
# Blocks
|
|
|
|
| 441 |
self.mem = nn.GRU(pool_dim, c.mem_hidden, 1, batch_first=True)
|
| 442 |
self.mem_proj = nn.Linear(c.mem_hidden, c.cortex_d)
|
| 443 |
|
| 444 |
+
# -- planning block (MuZero-lite): latent encode/predict/dynamics ---
|
| 445 |
+
# Trained on CONSECUTIVE shard frames (rgb_t, a_t -> rgb_{t+1}):
|
| 446 |
+
# encode -> z (B, latent_dim)
|
| 447 |
+
# prediction(z) -> prior logits over (kind,cell), value, win
|
| 448 |
+
# dynamics(z, a) -> z_next, reward_hat
|
| 449 |
+
# Drives MCTS at decision time (see search.py). ~17M params.
|
| 450 |
+
self.planning = None
|
| 451 |
+
if c.use_search:
|
| 452 |
+
m = nn.Module()
|
| 453 |
+
m.lat_proj = nn.Sequential(
|
| 454 |
+
nn.Conv2d(c.enc_ch[-1], c.lat_c, 1, bias=False),
|
| 455 |
+
nn.BatchNorm2d(c.lat_c), nn.SiLU())
|
| 456 |
+
m.a_emb_kind = nn.Embedding(c.num_kinds, 64)
|
| 457 |
+
m.a_emb_cell = nn.Embedding(c.grid_default * c.grid_default, 256)
|
| 458 |
+
m.a_emb_pct = nn.Linear(1, 128)
|
| 459 |
+
m.a_emb_out = nn.Linear(64 + 256 + 128, c.act_embed)
|
| 460 |
+
m.dyn_mlp = nn.Sequential(
|
| 461 |
+
nn.Linear(c.latent_dim + c.act_embed, c.dyn_hidden),
|
| 462 |
+
nn.SiLU(), nn.Linear(c.dyn_hidden, c.latent_dim))
|
| 463 |
+
m.reward_head = nn.Linear(c.latent_dim, 1)
|
| 464 |
+
m.pred_prior = nn.Linear(
|
| 465 |
+
c.latent_dim, c.num_kinds * c.grid_default * c.grid_default)
|
| 466 |
+
m.pred_value = nn.Linear(c.latent_dim, 1)
|
| 467 |
+
m.pred_win = nn.Linear(c.latent_dim, 2)
|
| 468 |
+
self.planning = m
|
| 469 |
+
|
| 470 |
self.apply(_init_weights)
|
| 471 |
|
| 472 |
# -- helpers ------------------------------------------------------------
|
|
|
|
| 504 |
align_corners=False)
|
| 505 |
return logg
|
| 506 |
|
| 507 |
+
# -- planning interface (used by search.py) -----------------------------
|
| 508 |
+
|
| 509 |
+
def encode(self, rgb: torch.Tensor, nums: Optional[torch.Tensor] = None,
|
| 510 |
+
rtg: Optional[torch.Tensor] = None) -> torch.Tensor:
|
| 511 |
+
"""Compressed latent state z (B, latent_dim) for MCTS + dynamics
|
| 512 |
+
training. Runs the vision spine + cortex (no decoder)."""
|
| 513 |
+
assert self.planning is not None, "planning block disabled"
|
| 514 |
+
cfg = self.cfg
|
| 515 |
+
B, C, H, W = rgb.shape
|
| 516 |
+
assert H == W == cfg.map_size, \
|
| 517 |
+
f"expected {cfg.map_size}x{cfg.map_size}, got {H}x{W}"
|
| 518 |
+
if nums is None:
|
| 519 |
+
nums = torch.zeros(B, cfg.ctx_dim, device=rgb.device)
|
| 520 |
+
ctx_full = self._ctx_full(nums, rtg)
|
| 521 |
+
x = rgb
|
| 522 |
+
if cfg.use_coord:
|
| 523 |
+
x = torch.cat([x, self.coords.expand(B, -1, -1, -1)], dim=1)
|
| 524 |
+
x = self.stem(x)
|
| 525 |
+
prev = None
|
| 526 |
+
for i, stage in enumerate(self.enc):
|
| 527 |
+
prev = x
|
| 528 |
+
x = self._staged(stage, x) if i >= len(self.enc) - 2 else stage(x)
|
| 529 |
+
# x = e5 output, prev = e4 output (cortex fine tokens)
|
| 530 |
+
bot = self.film(x, ctx_full)
|
| 531 |
+
_, g = self.cortex(bot, prev)
|
| 532 |
+
lat_map = F.adaptive_avg_pool2d(
|
| 533 |
+
self.planning.lat_proj(bot),
|
| 534 |
+
(cfg.lat_spatial, cfg.lat_spatial)).flatten(1)
|
| 535 |
+
return torch.cat([g, lat_map], dim=1) # (B, latent_dim)
|
| 536 |
+
|
| 537 |
+
def prediction(self, z: torch.Tensor
|
| 538 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 539 |
+
"""MuZero-lite prediction net: prior logits over (kind,cell) joint
|
| 540 |
+
actions (B, 3*g*g), value (B,), win logits (B,2)."""
|
| 541 |
+
assert self.planning is not None, "planning block disabled"
|
| 542 |
+
prior = self.planning.pred_prior(z)
|
| 543 |
+
value = self.planning.pred_value(z).squeeze(-1)
|
| 544 |
+
win = self.planning.pred_win(z)
|
| 545 |
+
return prior, value, win
|
| 546 |
+
|
| 547 |
+
def dynamics(self, z: torch.Tensor, kind: torch.Tensor,
|
| 548 |
+
cell: torch.Tensor,
|
| 549 |
+
pct: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 550 |
+
"""One-step latent transition: (z, a) -> (z_next, reward_hat)."""
|
| 551 |
+
assert self.planning is not None, "planning block disabled"
|
| 552 |
+
cfg = self.cfg
|
| 553 |
+
B = z.shape[0]
|
| 554 |
+
ka = self.planning.a_emb_kind(kind) # (B,64)
|
| 555 |
+
ca = self.planning.a_emb_cell(
|
| 556 |
+
cell.clamp(0, cfg.grid_default * cfg.grid_default - 1)) # (B,256)
|
| 557 |
+
pa = self.planning.a_emb_pct(
|
| 558 |
+
pct.clamp(0.0, 1.0).to(z.dtype).view(B, 1)) # (B,128)
|
| 559 |
+
a = self.planning.a_emb_out(torch.cat([ka, ca, pa], dim=1)) # (B,512)
|
| 560 |
+
dz = self.planning.dyn_mlp(torch.cat([z, a], dim=1))
|
| 561 |
+
z_next = z + dz # residual
|
| 562 |
+
r_hat = self.planning.reward_head(z_next).squeeze(-1)
|
| 563 |
+
return z_next, r_hat
|
| 564 |
+
|
| 565 |
# -- forward ------------------------------------------------------------
|
| 566 |
|
| 567 |
def forward(self, rgb: torch.Tensor, nums: Optional[torch.Tensor] = None,
|
|
|
|
| 877 |
if net.cfg.use_cell_value:
|
| 878 |
heads += n(net.cell_value)
|
| 879 |
mem = (n(net.mem) + n(net.mem_proj)) if net.mem is not None else 0
|
| 880 |
+
planning = n(net.planning) if net.planning is not None else 0
|
| 881 |
return dict(spine=spine, cortex=cortex, decoder=decoder, heads=heads,
|
| 882 |
+
mem=mem, planning=planning)
|
| 883 |
|
| 884 |
|
| 885 |
def make_model(cfg: Optional[SovereignConfig] = None,
|
|
|
|
| 898 |
bd = param_breakdown(m)
|
| 899 |
print(f"params: {total/1e6:.2f}M (spine {bd['spine']/1e6:.1f}M | "
|
| 900 |
f"cortex {bd['cortex']/1e6:.1f}M | decoder {bd['decoder']/1e6:.1f}M "
|
| 901 |
+
f"| heads {bd['heads']/1e6:.1f}M | mem {bd['mem']/1e6:.1f}M | "
|
| 902 |
+
f"planning {bd['planning']/1e6:.1f}M)")
|
| 903 |
H = cfg.map_size
|
| 904 |
rgb = torch.rand(2, 3, H, H)
|
| 905 |
nums = torch.rand(2, 8)
|
|
|
|
| 917 |
for k, v in a.items() if k not in ("kind_probs",)})
|
| 918 |
a2 = m.act(rgb, nums, rtg=rtg, state=a["state"])
|
| 919 |
print("mem carry ok:", a2["state"].shape)
|
| 920 |
+
# planning block smoke (encode -> predict -> dynamics round trip)
|
| 921 |
+
z = m.encode(rgb, nums, rtg=rtg)
|
| 922 |
+
prior_z, v_z, win_z = m.prediction(z)
|
| 923 |
+
z2, r_hat = m.dynamics(z, torch.zeros(2, dtype=torch.long),
|
| 924 |
+
torch.zeros(2, dtype=torch.long),
|
| 925 |
+
torch.full((2,), 0.25))
|
| 926 |
+
print("planning:", prior_z.shape, v_z.shape, win_z.shape, z2.shape,
|
| 927 |
+
r_hat.shape)
|
| 928 |
# no-rtg path (contract default)
|
| 929 |
_, _, _, _ = m(rgb, nums)
|
| 930 |
print("no-rtg contract path OK")
|