ZibinDong/ActionCodec-bridge
010
1import math2from copy import deepcopy3from typing import List, Literal, Optional, Tuple, Union4 5import einops6import numpy as np7import torch8import torch.nn as nn9import torch.nn.functional as F10 11from .configuration_actioncodec import ActionCodecConfig12 13 14def apply_rotary_pos_emb(x: torch.Tensor, sin: torch.Tensor, cos: torch.Tensor) -> torch.Tensor:15 original_dtype = x.dtype16 17 x = x.to(torch.float32)18 sin = sin.to(torch.float32)19 cos = cos.to(torch.float32)20 21 x1 = x[..., 0::2]22 x2 = x[..., 1::2]23 24 rotated_x1 = x1 * cos - x2 * sin25 rotated_x2 = x1 * sin + x2 * cos26 27 x_out = torch.empty_like(x)28 x_out[..., 0::2] = rotated_x129 x_out[..., 1::2] = rotated_x230 31 return x_out.to(original_dtype)32 33 34def attention_op(35 q: torch.Tensor,36 k: torch.Tensor,37 v: torch.Tensor,38 mask: torch.Tensor | None = None,39 is_causal: bool = False,40) -> torch.Tensor:41 """42 43 Args:44 q (torch.Tensor): (*b, h, l, d)45 k (torch.Tensor): (*b, k, s, d)46 v (torch.Tensor): (*b, k, s, d)47 mask (torch.Tensor | None, optional): (*b, l, s), where `True` indicates the element should take part in attention. Defaults to None.48 is_causal (bool, optional): Whether to apply causal mask. Defaults to False.49 50 Returns:51 torch.Tensor: (*b, h, l, d)52 """53 heads, kv_heads = q.shape[-3], k.shape[-3]54 if heads != kv_heads:55 assert heads % kv_heads == 0, f"q_heads must be divisible by kv_heads, but got {heads} and {kv_heads}"56 heads_per_kv_head = heads // kv_heads57 k, v = map(lambda t: t.repeat_interleave(heads_per_kv_head, dim=1), (k, v))58 59 if mask is not None:60 if mask.dim() == 3:61 mask = mask.unsqueeze(1)62 mask = mask.expand(mask.shape[0], heads, -1, -1)63 64 out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=0.0, is_causal=is_causal)65 return out66 67 68class L2Norm(nn.Module):69 def forward(self, x: torch.Tensor):70 return F.normalize(x, p=2, dim=-1)71 72 73class Attention(nn.Module):74 """75 Args:76 hidden_size (int): Hidden size of the input tensor.77 num_heads (int): Number of attention heads.78 num_kv_heads (int, optional): Number of key/value heads. Defaults to None.79 qk_norm (Literal["l2", "ln", "none"], optional): Type of normalization to apply to query/key. Defaults to "none".80 bias (bool, optional): Whether to use bias in linear layers. Defaults to False.81 82 """83 84 def __init__(85 self,86 hidden_size: int,87 num_heads: int,88 num_kv_heads: int | None = None,89 qk_norm: Literal["l2", "ln", "none"] = "none",90 bias: bool = False,91 zero_init_output: bool = False,92 ):93 super().__init__()94 num_kv_heads = num_heads if num_kv_heads is None else num_kv_heads95 self.dim = hidden_size // num_heads96 self.num_heads, self.num_kv_heads = num_heads, num_kv_heads97 98 self.q_proj = nn.Linear(hidden_size, hidden_size, bias=bias)99 self.k_proj = nn.Linear(hidden_size, self.dim * num_kv_heads, bias=bias)100 self.v_proj = nn.Linear(hidden_size, self.dim * num_kv_heads, bias=bias)101 self.out_proj = nn.Linear(hidden_size, hidden_size, bias=bias)102 103 if qk_norm == "l2":104 self.q_norm = L2Norm()105 self.k_norm = L2Norm()106 elif qk_norm == "ln":107 self.q_norm = nn.LayerNorm(self.dim, elementwise_affine=False)108 self.k_norm = nn.LayerNorm(self.dim, elementwise_affine=False)109 else:110 self.q_norm = nn.Identity()111 self.k_norm = nn.Identity()112 113 if zero_init_output:114 nn.init.zeros_(self.out_proj.weight)115 if self.out_proj.bias is not None:116 nn.init.zeros_(self.out_proj.bias)117 118 def forward(119 self,120 x: torch.Tensor,121 context: torch.Tensor | None = None,122 mask: torch.Tensor | None = None,123 rotary_pos_emb: Tuple[torch.Tensor, torch.Tensor] | None = None,124 is_causal: bool = False,125 ) -> torch.Tensor:126 context = x if context is None else context127 128 q = self.q_proj(x)129 k, v = self.k_proj(context), self.v_proj(context)130 131 q = einops.rearrange(q, "b l (h d) -> b h l d", h=self.num_heads)132 k = einops.rearrange(k, "b s (h d) -> b h s d", h=self.num_kv_heads)133 v = einops.rearrange(v, "b s (h d) -> b h s d", h=self.num_kv_heads)134 135 q, k = self.q_norm(q), self.k_norm(k)136 137 if rotary_pos_emb is not None:138 q, k = map(lambda t: apply_rotary_pos_emb(t, *rotary_pos_emb), (q, k))139 140 out = attention_op(q, k, v, mask=mask, is_causal=is_causal)141 out = einops.rearrange(out, "b h l d -> b l (h d)")142 out = self.out_proj(out)143 144 return out145 146 147class PositionalEmbedding(nn.Module):148 def __init__(149 self,150 dim: int,151 encoding_type: Literal["sincos", "fourier"] = "sincos",152 scale: float = 2.0,153 ):154 super().__init__()155 self.dim = dim156 self.encoding_type = encoding_type157 158 if encoding_type == "fourier":159 self.register_buffer("freqs", torch.randn(dim // 2) * scale, persistent=True)160 elif encoding_type == "sincos":161 pass162 else:163 raise ValueError(f"encoding_type must be 'sincos' or 'fourier', but got {encoding_type}")164 165 def _create_sincos_emb(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:166 position = torch.arange(seq_len, device=device, dtype=torch.float32).unsqueeze(1)167 div_term = torch.exp(168 torch.arange(0, self.dim, 2, device=device, dtype=torch.float32) * -(math.log(10000.0) / self.dim)169 )170 171 pos_emb = torch.zeros(seq_len, self.dim, device=device, dtype=dtype)172 pos_emb[:, 0::2] = torch.sin(position * div_term).to(dtype)173 pos_emb[:, 1::2] = torch.cos(position * div_term).to(dtype)174 175 return pos_emb176 177 def _create_fourier_emb(self, timestamps: torch.Tensor, device: torch.device, dtype: torch.dtype) -> torch.Tensor:178 # Ensure freqs is on the correct device179 freqs = self.freqs.to(device)180 pos_emb = torch.einsum("b t, d -> b t d", timestamps, 2 * np.pi * freqs).to(device, torch.float32)181 pos_emb = torch.cat([pos_emb.cos(), pos_emb.sin()], dim=-1).to(dtype)182 return pos_emb183 184 def forward(185 self, x: torch.Tensor, freq: Optional[Union[float, torch.Tensor]] = None, dtype: torch.dtype = torch.float32186 ) -> torch.Tensor:187 b, t = x.shape[0], x.shape[1]188 device = x.device189 190 if self.encoding_type == "sincos":191 pos_emb = self._create_sincos_emb(t, device, dtype)192 pos_emb = pos_emb.unsqueeze(0).expand(b, -1, -1)193 return pos_emb * 0.1194 195 elif self.encoding_type == "fourier":196 if freq is None:197 raise ValueError(198 "freq must be provided when encoding_type is 'fourier'. Please provide the sequence frequency."199 )200 if isinstance(freq, float):201 freq = torch.tensor(freq, dtype=dtype, device=device)[None].expand(b)202 timestamps = torch.einsum("t, b -> b t", torch.arange(t, dtype=dtype, device=device), 1 / freq)203 pos_emb = self._create_fourier_emb(timestamps, device, dtype)204 return pos_emb * 0.1205 else:206 raise ValueError(f"Unknown encoding_type: {self.encoding_type}")207 208 209class SinusoidalPositionalEmbedding(PositionalEmbedding):210 def __init__(self, dim: int):211 super().__init__(dim=dim, encoding_type="sincos")212 213 def forward(self, x: torch.Tensor, pos: Optional[torch.Tensor] = None) -> torch.Tensor:214 return super().forward(x, freq=None)215 216 217class FeedForward(nn.Module):218 def __init__(self, hidden_size: int, intermediate_size: int, bias: bool = False):219 super().__init__()220 self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=bias)221 self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=bias)222 self.act_fn = nn.GELU()223 224 def forward(self, x: torch.Tensor) -> torch.Tensor:225 down_proj = self.down_proj(self.act_fn(self.up_proj(x)))226 return down_proj227 228 229class LayerScale(nn.Module):230 def __init__(self, dim, init_val=1e-2):231 super().__init__()232 self.scale = nn.Parameter(torch.full([dim], init_val))233 234 def forward(self, x):235 return x * self.scale236 237 238class PerceiverTransformerBlock(nn.Module):239 def __init__(240 self,241 dim: int,242 num_heads: int,243 mlp_ratio: int = 4,244 dropout: float = 0.0,245 qk_norm: str = "ln",246 layer_scale: bool = True,247 zero_init_output: bool = False,248 add_self_attn: bool = False,249 add_causal_mask: bool = False,250 ):251 super().__init__()252 self.add_self_attn = add_self_attn253 self.add_causal_mask = add_causal_mask254 255 self.norm1 = nn.LayerNorm(dim, eps=1e-2)256 self.cross_attn = Attention(257 hidden_size=dim, num_heads=num_heads, qk_norm=qk_norm, bias=False, zero_init_output=zero_init_output258 )259 260 if add_self_attn:261 self.norm_self_attn = nn.LayerNorm(dim, eps=1e-2)262 self.self_attn = Attention(263 hidden_size=dim, num_heads=num_heads, qk_norm=qk_norm, bias=False, zero_init_output=zero_init_output264 )265 else:266 self.self_attn = None267 268 self.norm2 = nn.LayerNorm(dim, eps=1e-2)269 self.mlp = FeedForward(hidden_size=dim, intermediate_size=int(mlp_ratio * dim), bias=True)270 self.dropout = nn.Dropout(dropout)271 272 self.attn_scale = LayerScale(dim) if layer_scale else nn.Identity()273 self.mlp_scale = LayerScale(dim) if layer_scale else nn.Identity()274 275 if zero_init_output:276 nn.init.zeros_(self.mlp.down_proj.weight)277 if self.mlp.down_proj.bias is not None:278 nn.init.zeros_(self.mlp.down_proj.bias)279 280 def forward(281 self,282 x: torch.Tensor,283 context: torch.Tensor,284 context_mask: Optional[torch.Tensor] = None,285 rotary_pos_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,286 ) -> torch.Tensor:287 residual = x288 x = self.norm1(x)289 x = self.cross_attn(x=x, context=context, mask=context_mask, rotary_pos_emb=rotary_pos_emb, is_causal=False)290 x = self.dropout(x)291 x = self.attn_scale(x)292 x = x + residual293 294 if self.add_self_attn:295 residual = x296 x = self.norm_self_attn(x)297 x = self.self_attn(298 x=x,299 context=None,300 mask=None,301 rotary_pos_emb=rotary_pos_emb,302 is_causal=self.add_causal_mask,303 )304 x = self.dropout(x)305 x = self.attn_scale(x)306 x = x + residual307 308 residual = x309 x = self.norm2(x)310 x = self.mlp(x)311 x = self.dropout(x)312 x = self.mlp_scale(x)313 x = x + residual314 315 return x316 317 318class EmbodimentEmbedding(nn.Module):319 def __init__(self, embodiment_config: dict, out_len: int, out_dim: int) -> None:320 super().__init__()321 self.out_len, self.out_dim = out_len, out_dim322 323 self.embodiment_config = embodiment_config324 self.num_embodiments = len(self.embodiment_config)325 326 self.embedding = nn.Embedding(self.num_embodiments, out_dim * out_len)327 328 @torch.no_grad()329 def expand_embodiment(self, embodiment_config: dict):330 for k in embodiment_config.keys():331 assert k not in self.embodiment_config.keys()332 self.embodiment_config.update(embodiment_config)333 self.num_embodiments = len(self.embodiment_config)334 335 extra_embodiments = len(embodiment_config)336 337 old_weights = torch.clone(self.embedding.weight)338 self.embedding = nn.Embedding(self.num_embodiments, self.out_dim * self.out_len)339 self.embedding.weight.data[:-extra_embodiments] = old_weights340 return self341 342 def keys(self) -> list[str]:343 return list(self.embodiment_config.keys())344 345 def ids_to_keys(self, ids: torch.Tensor) -> List[str]:346 return [self.keys()[i] for i in ids]347 348 def keys_to_ids(self, keys: List[str]) -> torch.Tensor:349 return torch.tensor([self.keys().index(k) for k in keys])350 351 def forward(self, x: torch.Tensor) -> torch.Tensor:352 return einops.rearrange(self.embedding(x), "b (l d) -> b l d", d=self.out_dim)353 354 355class PerceiverEncoder(nn.Module):356 def __init__(self, config: ActionCodecConfig):357 super().__init__()358 self.config = config359 self.embodiment_config = deepcopy(config.embodiment_config)360 361 out_len = int(config.n_tokens // config.n_quantizers)362 dim = config.encoder_dim363 364 _action_dim, _freq, _duration = list(), list(), list()365 for k, v in self.embodiment_config.items():366 _action_dim.append(v["action_dim"])367 _freq.append(v["freq"])368 _duration.append(v["duration"])369 self.register_buffer("_action_dim", torch.tensor(_action_dim), persistent=False)370 self.register_buffer("_freq", torch.tensor(_freq), persistent=False)371 self.register_buffer("_duration", torch.tensor(_duration), persistent=False)372 373 self.max_action_dim = max(v["action_dim"] for v in self.embodiment_config.values())374 self.input_proj = nn.Linear(self.max_action_dim, dim)375 376 self.cls_tokens = EmbodimentEmbedding(self.embodiment_config, out_len, dim)377 378 self.pos_emb_q = PositionalEmbedding(dim, encoding_type="sincos")379 self.pos_emb_kv = PositionalEmbedding(dim, encoding_type=config.encoder_pos_encoding_type)380 381 self.layers = nn.ModuleList(382 [383 PerceiverTransformerBlock(384 dim=dim,385 num_heads=config.encoder_n_heads,386 add_self_attn=config.encoder_add_self_attn,387 add_causal_mask=config.encoder_add_causal_mask,388 )389 for _ in range(config.encoder_n_layers)390 ]391 )392 393 self.output_proj = nn.Linear(dim, config.z_dim)394 self._init_weights()395 396 def _init_weights(self):397 nn.init.trunc_normal_(self.input_proj.weight, std=0.02)398 if self.input_proj.bias is not None:399 nn.init.zeros_(self.input_proj.bias)400 nn.init.trunc_normal_(self.output_proj.weight, std=0.02)401 if self.output_proj.bias is not None:402 nn.init.zeros_(self.output_proj.bias)403 404 nn.init.trunc_normal_(self.cls_tokens.embedding.weight, std=0.02)405 406 @torch.no_grad()407 def expand_embodiment(self, embodiment_config: dict):408 self.cls_tokens.expand_embodiment(embodiment_config)409 self.embodiment_config = self.cls_tokens.embodiment_config410 _action_dim, _freq, _duration = list(), list(), list()411 for k, v in self.embodiment_config.items():412 _action_dim.append(v["action_dim"])413 _freq.append(v["freq"])414 _duration.append(v["duration"])415 self._action_dim = torch.tensor(_action_dim)416 self._freq = torch.tensor(_freq)417 self._duration = torch.tensor(_duration)418 419 max_action_dim = max(v["action_dim"] for v in self.embodiment_config.values())420 if max_action_dim > self.max_action_dim:421 old_weights = torch.clone(self.input_proj.weight)422 old_bias = torch.clone(self.input_proj.bias)423 self.input_proj = nn.Linear(max_action_dim, self.config.encoder_dim)424 self.input_proj.weight.data[:, : self.max_action_dim] = old_weights425 self.input_proj.bias.data = old_bias426 self.max_action_dim = max_action_dim427 428 return self429 430 def forward(431 self,432 x: torch.Tensor,433 embodiment_ids: torch.Tensor | int,434 padding_mask: Optional[torch.Tensor] = None,435 ) -> torch.Tensor:436 """Encode action sequences into latent representations.437 438 Args:439 x (torch.Tensor): Action sequences to encode. Shape: (b, seq_len, max_action_dim).440 Assumes that the action dimension is zero-padded to the max action dimension.441 `seq_len` is supposed to be `int(duration * freq)` for each embodiment and padded to the max sequence length.442 embodiment_ids (torch.Tensor | int): Embodiment IDs. Shape: (b,).443 If int, the same embodiment ID is repeated for all sequences in the batch.444 It specifies the embodiment to encode.445 padding_mask (Optional[torch.Tensor], optional): Padding mask, where `False` values indicate padding. Shape: (b, seq_len). Defaults to None.446 It is used to mask the padding tokens on `seq_len` dimension.447 448 Returns:449 torch.Tensor: Encoded latent representations. Shape: (b, n_tokens_per_quantizer, z_dim).450 """451 b, seq_len, _ = x.shape452 453 x = self.input_proj(x)454 455 if isinstance(embodiment_ids, int):456 embodiment_ids = torch.tensor([embodiment_ids], dtype=torch.long, device=x.device).repeat(b)457 458 cls_tokens = self.cls_tokens(embodiment_ids)459 460 freqs = self._freq[embodiment_ids].to(x.device, x.dtype)461 462 pos_emb_q = self.pos_emb_q(cls_tokens)463 pos_emb_kv = self.pos_emb_kv(x, freqs)464 465 cls_tokens = cls_tokens + pos_emb_q466 x = x + pos_emb_kv467 468 if padding_mask is not None:469 padding_mask = padding_mask.unsqueeze(1).expand(-1, cls_tokens.shape[1], -1)470 471 for layer in self.layers:472 cls_tokens = layer(x=cls_tokens, context=x, context_mask=padding_mask)473 474 return self.output_proj(cls_tokens)475 476 477class PerceiverDecoder(nn.Module):478 def __init__(self, config: ActionCodecConfig):479 super().__init__()480 self.config = config481 self.embodiment_config = deepcopy(config.embodiment_config)482 483 dim = config.decoder_dim484 485 _action_dim, _freq, _duration = list(), list(), list()486 for k, v in self.embodiment_config.items():487 _action_dim.append(v["action_dim"])488 _freq.append(v["freq"])489 _duration.append(v["duration"])490 self.register_buffer("_action_dim", torch.tensor(_action_dim), persistent=False)491 self.register_buffer("_freq", torch.tensor(_freq), persistent=False)492 self.register_buffer("_duration", torch.tensor(_duration), persistent=False)493 494 self.max_action_dim = max(v["action_dim"] for v in self.embodiment_config.values())495 self.input_proj = nn.Linear(config.z_dim, dim)496 497 self.cls_tokens = EmbodimentEmbedding(self.embodiment_config, config.decoder_cls_size, dim)498 499 self.pos_emb_q = PositionalEmbedding(dim, encoding_type=config.decoder_pos_encoding_type)500 self.pos_emb_kv = PositionalEmbedding(dim, encoding_type="sincos")501 502 self.layers = nn.ModuleList(503 [504 PerceiverTransformerBlock(505 dim=dim,506 num_heads=config.decoder_n_heads,507 add_self_attn=config.decoder_add_self_attn,508 add_causal_mask=config.decoder_add_causal_mask,509 )510 for _ in range(config.decoder_n_layers)511 ]512 )513 514 self.output_proj = nn.Linear(dim, self.max_action_dim)515 self._init_weights()516 517 def _init_weights(self):518 nn.init.trunc_normal_(self.input_proj.weight, std=0.02)519 if self.input_proj.bias is not None:520 nn.init.zeros_(self.input_proj.bias)521 nn.init.trunc_normal_(self.output_proj.weight, std=0.02)522 if self.output_proj.bias is not None:523 nn.init.zeros_(self.output_proj.bias)524 nn.init.trunc_normal_(self.cls_tokens.embedding.weight, std=0.02)525 526 @torch.no_grad()527 def expand_embodiment(self, embodiment_config: dict):528 self.cls_tokens.expand_embodiment(embodiment_config)529 self.embodiment_config = self.cls_tokens.embodiment_config530 531 _action_dim, _freq, _duration = list(), list(), list()532 for k, v in self.embodiment_config.items():533 _action_dim.append(v["action_dim"])534 _freq.append(v["freq"])535 _duration.append(v["duration"])536 self._action_dim = torch.tensor(_action_dim)537 self._freq = torch.tensor(_freq)538 self._duration = torch.tensor(_duration)539 540 max_action_dim = max(v["action_dim"] for v in self.embodiment_config.values())541 542 if max_action_dim > self.max_action_dim:543 old_weights = torch.clone(self.output_proj.weight)544 old_bias = torch.clone(self.output_proj.bias)545 546 self.output_proj = nn.Linear(self.config.decoder_dim, max_action_dim)547 548 self.output_proj.weight.data[: self.max_action_dim, :] = old_weights549 self.output_proj.bias.data[: self.max_action_dim] = old_bias550 551 self.max_action_dim = max_action_dim552 553 return self554 555 def forward(556 self, x: torch.Tensor, embodiment_ids: torch.Tensor | int, durations: torch.Tensor | None = None557 ) -> torch.Tensor:558 """Decode latent representations into action sequences.559 560 Args:561 x (torch.Tensor): Latent representations to decode. Shape: (b, n_tokens_per_quantizer, z_dim).562 embodiment_ids (torch.Tensor | int): Embodiment IDs. Shape: (b,).563 If int, the same embodiment ID is repeated for all sequences in the batch.564 It specifies the embodiment to decode.565 durations (torch.Tensor | None, optional): Duration of each action sequence. Shape: (b,).566 If `None`, the duration is inferred from the default values in `embodiment_config`.567 568 Returns:569 torch.Tensor: Decoded action sequences. Shape: (b, seq_len, max_action_dim).570 Assumes that the action dimension is zero-padded to the max action dimension.571 `seq_len` is supposed to be `int(duration * freq)` for each embodiment and padded to the max sequence length.572 """573 b, seq_len, _ = x.shape574 x = self.input_proj(x)575 576 if isinstance(embodiment_ids, int):577 embodiment_ids = torch.tensor([embodiment_ids], dtype=torch.long, device=x.device).repeat(b)578 579 cls_tokens = self.cls_tokens(embodiment_ids)580 581 freqs = self._freq[embodiment_ids]582 if freqs.device != x.device:583 freqs = freqs.to(x.device)584 585 durations = self._duration[embodiment_ids] if durations is None else durations586 if isinstance(durations, torch.Tensor) and durations.device != x.device:587 durations = durations.to(x.device)588 589 action_horizons = (durations * freqs).long()590 max_horizon = action_horizons.max().item()591 padding_mask = torch.arange(max_horizon, device=x.device).expand(b, -1) < action_horizons.unsqueeze(1)592 593 if self.config.decoder_cls_size == 1:594 cls_tokens = cls_tokens.repeat(1, max_horizon, 1)595 596 pos_emb_q = self.pos_emb_q(cls_tokens, freqs)597 pos_emb_kv = self.pos_emb_kv(x)598 599 cls_tokens = cls_tokens + pos_emb_q600 x = x + pos_emb_kv601 602 for layer in self.layers:603 cls_tokens = layer(x=cls_tokens, context=x)604 605 output = self.output_proj(cls_tokens)606 607 return output, padding_mask608 609 610if __name__ == "__main__":611 # ------------------------------------------612 # 1. Initialization613 # ------------------------------------------614 print("=== Test 1: Initialization ===")615 616 # Define initial config with two smaller robots617 initial_embodiment_config = {618 "robot_small_7d": {"action_dim": 7, "freq": 20, "duration": 1, "description": "Original Robot"},619 "robot_tiny_3d": {"action_dim": 3, "freq": 10, "duration": 2, "description": "Tiny Robot"},620 }621 622 config = ActionCodecConfig(embodiment_config=initial_embodiment_config)623 624 # Set seed for reproducibility625 torch.manual_seed(42)626 627 encoder = PerceiverEncoder(config)628 decoder = PerceiverDecoder(config)629 630 encoder.eval()631 decoder.eval()632 print("✅ Models initialized successfully.")633 634 # ------------------------------------------635 # 2. Baseline Inference (Before Expansion)636 # ------------------------------------------637 print("\n=== Test 2: Baseline Inference (Before Expansion) ===")638 639 # Simulate Robot 1 (7-dim) data640 # Max action dim currently is 7.641 batch_size = 1642 seq_len = 20 # 20Hz * 1s643 644 # Input: (1, 20, 7)645 input_action_v0 = torch.randn(batch_size, seq_len, 7)646 emb_id_v0 = torch.tensor([0], dtype=torch.long) # ID 0 -> robot_small_7d647 648 with torch.no_grad():649 z_ref = encoder(input_action_v0, emb_id_v0)650 rec_action_ref, _ = decoder(z_ref, emb_id_v0)651 652 print(f"Reference Latent Shape: {z_ref.shape}")653 print(f"Reference Recon Shape: {rec_action_ref.shape}")654 655 # ------------------------------------------656 # 3. Model Expansion (Add New Embodiment)657 # ------------------------------------------658 print("\n=== Test 3: Model Expansion ===")659 660 # Add a larger robot: 10-dim, high frequency661 new_embodiment_config = {662 "robot_large_10d": {"action_dim": 10, "freq": 30, "duration": 1, "description": "New Large Robot"}663 }664 665 print(f"Expanding from Max Dim {encoder.max_action_dim} to 10...")666 encoder.expand_embodiment(new_embodiment_config)667 decoder.expand_embodiment(new_embodiment_config)668 669 # Verify buffer updates670 assert encoder._action_dim[-1] == 10671 assert encoder.max_action_dim == 10672 assert decoder.max_action_dim == 10673 print(f"✅ Expansion successful. New Encoder Input Dim: {encoder.input_proj.weight.shape[1]}")674 print(f"✅ New Decoder Output Dim: {decoder.output_proj.weight.shape[0]}")675 676 # ------------------------------------------677 # 4. Encoder Invariance Check678 # ------------------------------------------679 print("\n=== Test 4: Encoder Invariance Check ===")680 681 # Pad old data (7 dims) to new max dim (10 dims) with ZEROS.682 input_action_padded = torch.zeros(batch_size, seq_len, 10)683 input_action_padded[:, :, :7] = input_action_v0684 685 with torch.no_grad():686 z_new = encoder(input_action_padded, emb_id_v0)687 688 # Compare latents689 diff_z = (z_ref - z_new).abs().max().item()690 print(f"Latent Difference (Max Abs): {diff_z:.8f}")691 692 if diff_z < 1e-6:693 print("✅ PASS: Encoder produces identical latents for old data.")694 else:695 print("❌ FAIL: Encoder outputs changed after expansion!")696 697 # ------------------------------------------698 # 5. Decoder Invariance Check699 # ------------------------------------------700 print("\n=== Test 5: Decoder Invariance Check ===")701 702 with torch.no_grad():703 # Feed old latent to expanded decoder704 rec_action_new_full, _ = decoder(z_ref, emb_id_v0)705 706 # Output shape should be (1, 20, 10)707 print(f"Expanded Decoder Output Shape: {rec_action_new_full.shape}")708 709 # Slice first 7 dims, should match reference710 rec_action_new_sliced = rec_action_new_full[:, :, :7]711 712 diff_rec = (rec_action_ref - rec_action_new_sliced).abs().max().item()713 print(f"Reconstruction Difference (Max Abs on valid dims): {diff_rec:.8f}")714 715 if diff_rec < 1e-6:716 print("✅ PASS: Decoder produces identical action values for valid dimensions.")717 else:718 print("❌ FAIL: Decoder outputs changed!")719 720 # Check phantom dimensions (7-9)721 # For old embodiment, these are driven by random weights and should be random722 new_dims_mean = rec_action_new_full[:, :, 7:].abs().mean().item()723 print(f"Values in new phantom dimensions (should be random garbage): {new_dims_mean:.4f}")724 725 # ------------------------------------------726 # 6. New Embodiment Inference727 # ------------------------------------------728 print("\n=== Test 6: New Embodiment Inference ===")729 730 # ID 2 -> robot_large_10d731 emb_id_new = torch.tensor([2], dtype=torch.long)732 seq_len_new = 30 # 30Hz * 1s733 734 input_action_new = torch.randn(1, seq_len_new, 10)735 736 with torch.no_grad():737 z_large = encoder(input_action_new, emb_id_new)738 rec_large, mask_large = decoder(z_large, emb_id_new)739 740 print(f"New Embodiment Output Shape: {rec_large.shape}")741 742 if rec_large.shape == (1, 30, 10):743 print("✅ PASS: New embodiment handled correctly with full dimensions.")744 else:745 print(f"❌ FAIL: Expected (1, 30, 10), got {rec_large.shape}")746 747 # ------------------------------------------748 # 7. Mixed Batch Processing (Masking)749 # ------------------------------------------750 print("\n=== Test 7: Mixed Batch Processing ===")751 752 # Batch size 2: [Robot 0 (20Hz, 7dim), Robot 2 (30Hz, 10dim)]753 mixed_emb_ids = torch.tensor([0, 2], dtype=torch.long)754 755 # Max seq len is 30. Max action dim is 10.756 batch_input = torch.zeros(2, 30, 10)757 758 # Fill data759 # Batch 0: Length 20, Dim 7 valid760 batch_input[0, :20, :7] = torch.randn(20, 7)761 # Batch 1: Length 30, Dim 10 valid762 batch_input[1, :30, :10] = torch.randn(30, 10)763 764 # Encoder Mask: True = Valid765 enc_padding_mask = torch.zeros(2, 30, dtype=torch.bool)766 enc_padding_mask[0, :20] = True767 enc_padding_mask[1, :30] = True768 769 print("Running mixed batch...")770 with torch.no_grad():771 z_mixed = encoder(batch_input, mixed_emb_ids, padding_mask=enc_padding_mask)772 rec_mixed, dec_padding_mask = decoder(z_mixed, mixed_emb_ids)773 774 print(f"Mixed Reconstruction Shape: {rec_mixed.shape}") # Should be (2, 30, 10)775 776 # Verify Decoder Generated Mask777 valid_len_0 = dec_padding_mask[0].sum().item()778 valid_len_1 = dec_padding_mask[1].sum().item()779 780 print(f"Decoder Mask Valid Lengths: Batch 0={valid_len_0}, Batch 1={valid_len_1}")781 782 if valid_len_0 == 20 and valid_len_1 == 30:783 print("✅ PASS: Decoder correctly generated masks based on frequency and duration.")784 else:785 print("❌ FAIL: Decoder masks are incorrect.")786 787 print("\n✨ All Tests Completed ✨")788 