djmango commited on
Commit
0586ff6
·
verified ·
1 Parent(s): 991f0af

Upload model_v3.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. model_v3.py +111 -18
model_v3.py CHANGED
@@ -17,10 +17,20 @@ policy could just read the bit. The bypass split:
17
 
18
  Latent: z_grid (B, latent_c, H/16, W/16). No global vector: the policy can
19
  pool spatially itself, and per-player/global state arrives via bypass.
 
 
 
 
 
 
 
 
 
20
  """
21
 
22
  import torch
23
  import torch.nn as nn
 
24
 
25
  from ae.units import STATIC_CLASSES
26
 
@@ -49,50 +59,133 @@ def deconv_block(c_in: int, c_out: int) -> nn.Sequential:
49
  )
50
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  class SpatialAE(nn.Module):
53
- def __init__(self, latent_c: int = 64):
 
 
 
 
 
 
 
 
 
 
54
  super().__init__()
 
 
 
 
 
 
55
  self.latent_c = latent_c
 
 
 
56
  self.owner_emb = nn.Embedding(MAX_SLOTS, OWNER_EMB_DIM)
57
 
58
- self.enc_stem = nn.Sequential(
59
  conv_block(OWNER_EMB_DIM + TERRAIN_CHANNELS, 32, stride=1),
60
  conv_block(32, 64, stride=2),
61
  conv_block(64, 96, stride=2),
62
  conv_block(96, 128, stride=2),
63
- conv_block(128, 128, stride=2), # -> 1/16
64
- )
 
 
65
  self.enc_fuse = nn.Sequential(
66
  conv_block(128 + NUM_STATIC, 128, stride=1),
67
  nn.Conv2d(128, latent_c, kernel_size=1),
68
  )
69
 
70
- self.dec_in = conv_block(latent_c, 128, stride=1)
71
- self.dec_tiles = nn.Sequential(
72
- deconv_block(128, 128),
73
- deconv_block(128, 96),
74
- deconv_block(96, 64),
75
- deconv_block(64, 32),
76
- nn.Conv2d(32, MAX_SLOTS, kernel_size=1),
77
- )
78
- # Static structure occupancy logits at 1/16 resolution.
 
 
 
 
 
 
 
 
 
 
79
  self.dec_units = nn.Conv2d(128, NUM_STATIC, kernel_size=1)
80
 
81
  def encode(
82
  self,
83
  owners: torch.Tensor, # (B, H, W) int64
84
  terrain: torch.Tensor, # (B, 3, H, W)
85
- static_planes: torch.Tensor, # (B, NUM_STATIC, H/16, W/16)
86
  ) -> torch.Tensor:
87
  emb = self.owner_emb(owners).permute(0, 3, 1, 2)
88
  g = self.enc_stem(torch.cat([emb, terrain], dim=1))
89
  return self.enc_fuse(torch.cat([g, static_planes], dim=1))
90
 
91
- def decode(self, z_grid: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
92
- h = self.dec_in(z_grid)
93
- return self.dec_tiles(h), self.dec_units(h)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  def forward(self, owners, terrain, static_planes):
96
  z_grid = self.encode(owners, terrain, static_planes)
97
- tile_logits, unit_logits = self.decode(z_grid)
 
 
98
  return tile_logits, unit_logits, z_grid
 
17
 
18
  Latent: z_grid (B, latent_c, H/16, W/16). No global vector: the policy can
19
  pool spatially itself, and per-player/global state arrives via bypass.
20
+
21
+ v3.1 additions (all off by default so old v3 checkpoints load unchanged):
22
+ - terrain_cond: the decoder consumes the 2 STATIC terrain planes (land,
23
+ magnitude) as free side-information at every scale, so the latent only
24
+ has to encode ownership relative to terrain. Fallout is dynamic state
25
+ and is never fed to the decoder.
26
+ - upsample_decoder: nearest-upsample + 3x3 conv stages (no checkerboard)
27
+ plus a full-resolution 3x3 refinement block before the classifier.
28
+ - latent_down: 8 or 16; latent grid at 1/8 or 1/16 resolution.
29
  """
30
 
31
  import torch
32
  import torch.nn as nn
33
+ import torch.nn.functional as F
34
 
35
  from ae.units import STATIC_CLASSES
36
 
 
59
  )
60
 
61
 
62
+ class UpsampleBlock(nn.Module):
63
+ """Nearest 2x upsample + 3x3 conv (no ConvTranspose checkerboard).
64
+
65
+ Optionally concatenates static terrain planes (at the post-upsample
66
+ resolution) before the conv.
67
+ """
68
+
69
+ def __init__(self, c_in: int, c_out: int, extra_c: int = 0):
70
+ super().__init__()
71
+ self.conv = conv_block(c_in + extra_c, c_out, stride=1)
72
+
73
+ def forward(self, x: torch.Tensor, extra: torch.Tensor | None = None):
74
+ x = F.interpolate(x, scale_factor=2, mode="nearest")
75
+ if extra is not None:
76
+ x = torch.cat([x, extra], dim=1)
77
+ return self.conv(x)
78
+
79
+
80
+ STATIC_TERRAIN_C = 2 # land, magnitude (fallout is dynamic: never decoded from)
81
+
82
+
83
  class SpatialAE(nn.Module):
84
+ """Defaults preserve the original v3 architecture (old checkpoints load
85
+ with strict state_dict matching). v3.1 runs set terrain_cond=True and
86
+ upsample_decoder=True (and optionally latent_down=8)."""
87
+
88
+ def __init__(
89
+ self,
90
+ latent_c: int = 64,
91
+ terrain_cond: bool = False,
92
+ upsample_decoder: bool = False,
93
+ latent_down: int = 16,
94
+ ):
95
  super().__init__()
96
+ if latent_down not in (8, 16):
97
+ raise ValueError(f"latent_down must be 8 or 16, got {latent_down}")
98
+ if latent_down == 8 and not upsample_decoder:
99
+ raise ValueError("latent_down=8 requires the v3.1 upsample decoder")
100
+ if terrain_cond and not upsample_decoder:
101
+ raise ValueError("terrain_cond requires the v3.1 upsample decoder")
102
  self.latent_c = latent_c
103
+ self.terrain_cond = terrain_cond
104
+ self.upsample_decoder = upsample_decoder
105
+ self.latent_down = latent_down
106
  self.owner_emb = nn.Embedding(MAX_SLOTS, OWNER_EMB_DIM)
107
 
108
+ stem = [
109
  conv_block(OWNER_EMB_DIM + TERRAIN_CHANNELS, 32, stride=1),
110
  conv_block(32, 64, stride=2),
111
  conv_block(64, 96, stride=2),
112
  conv_block(96, 128, stride=2),
113
+ ]
114
+ if latent_down == 16:
115
+ stem.append(conv_block(128, 128, stride=2)) # -> 1/16
116
+ self.enc_stem = nn.Sequential(*stem)
117
  self.enc_fuse = nn.Sequential(
118
  conv_block(128 + NUM_STATIC, 128, stride=1),
119
  nn.Conv2d(128, latent_c, kernel_size=1),
120
  )
121
 
122
+ cond_c = STATIC_TERRAIN_C if terrain_cond else 0
123
+ self.dec_in = conv_block(latent_c + cond_c, 128, stride=1)
124
+ if upsample_decoder:
125
+ chans = [128, 128, 96, 64, 32] if latent_down == 16 else [128, 96, 64, 32]
126
+ self.dec_up = nn.ModuleList(
127
+ UpsampleBlock(chans[i], chans[i + 1], extra_c=cond_c)
128
+ for i in range(len(chans) - 1)
129
+ )
130
+ self.dec_refine = conv_block(32 + cond_c, 32, stride=1)
131
+ self.dec_out = nn.Conv2d(32, MAX_SLOTS, kernel_size=1)
132
+ else:
133
+ self.dec_tiles = nn.Sequential(
134
+ deconv_block(128, 128),
135
+ deconv_block(128, 96),
136
+ deconv_block(96, 64),
137
+ deconv_block(64, 32),
138
+ nn.Conv2d(32, MAX_SLOTS, kernel_size=1),
139
+ )
140
+ # Static structure occupancy logits at latent resolution.
141
  self.dec_units = nn.Conv2d(128, NUM_STATIC, kernel_size=1)
142
 
143
  def encode(
144
  self,
145
  owners: torch.Tensor, # (B, H, W) int64
146
  terrain: torch.Tensor, # (B, 3, H, W)
147
+ static_planes: torch.Tensor, # (B, NUM_STATIC, H/down, W/down)
148
  ) -> torch.Tensor:
149
  emb = self.owner_emb(owners).permute(0, 3, 1, 2)
150
  g = self.enc_stem(torch.cat([emb, terrain], dim=1))
151
  return self.enc_fuse(torch.cat([g, static_planes], dim=1))
152
 
153
+ def decode(
154
+ self,
155
+ z_grid: torch.Tensor,
156
+ terrain: torch.Tensor | None = None, # (B, >=2, H, W); only [:, :2] used
157
+ ) -> tuple[torch.Tensor, torch.Tensor]:
158
+ if not self.terrain_cond:
159
+ h = self.dec_in(z_grid)
160
+ if self.upsample_decoder:
161
+ x = h
162
+ for up in self.dec_up:
163
+ x = up(x)
164
+ return self.dec_out(self.dec_refine(x)), self.dec_units(h)
165
+ return self.dec_tiles(h), self.dec_units(h)
166
+
167
+ if terrain is None:
168
+ raise ValueError("terrain_cond model needs terrain in decode()")
169
+ # Static side-information pyramid: full res, 1/2, 1/4, ... latent res.
170
+ static_t = terrain[:, :STATIC_TERRAIN_C]
171
+ pyramid = {1: static_t}
172
+ down = 2
173
+ while down <= self.latent_down:
174
+ pyramid[down] = F.avg_pool2d(static_t, kernel_size=down)
175
+ down *= 2
176
+
177
+ h = self.dec_in(torch.cat([z_grid, pyramid[self.latent_down]], dim=1))
178
+ x = h
179
+ scale = self.latent_down
180
+ for up in self.dec_up:
181
+ scale //= 2
182
+ x = up(x, pyramid[scale])
183
+ x = self.dec_refine(torch.cat([x, pyramid[1]], dim=1))
184
+ return self.dec_out(x), self.dec_units(h)
185
 
186
  def forward(self, owners, terrain, static_planes):
187
  z_grid = self.encode(owners, terrain, static_planes)
188
+ tile_logits, unit_logits = self.decode(
189
+ z_grid, terrain if self.terrain_cond else None
190
+ )
191
  return tile_logits, unit_logits, z_grid