Files
roboimi/roboimi/vla/models/backbones/lewm_vit_backbone.py
T

231 lines
8.8 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, Mapping, Sequence
import torch
import torch.nn as nn
import torch.nn.functional as F
from roboimi.vla.core.interfaces import VLABackbone
class _LEWMProjector(nn.Module):
"""LEWM projector MLP: 192 -> 2048 -> 192 with BatchNorm1d + GELU."""
def __init__(self, input_dim: int = 192, hidden_dim: int = 2048, output_dim: int = 192) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.GELU(),
nn.Linear(hidden_dim, output_dim),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
class LEWMViTBackbone(VLABackbone):
"""Frozen LEWM joint-multiview ViT backbone.
The backbone fuses the three camera views into a single LEWM-style image,
runs a ViT-tiny encoder plus the LEWM projector, and returns one joint
192-d embedding per timestep.
"""
def __init__(
self,
checkpoint_path: str | Path | None = None,
*,
checkpoint: Mapping[str, Any] | None = None,
camera_names: Sequence[str] = ("r_vis", "top", "front"),
fused_camera_names: Sequence[str] = ("front", "top", "r_vis"),
num_cameras: int | None = None,
dataset_image_resize_shape: Sequence[int] | None = None,
eval_image_resize_shape: Sequence[int] | None = (256, 256),
freeze_backbone: bool = True,
joint_output_dim: int = 192,
image_size: int = 224,
output_dim: int = 192,
) -> None:
super().__init__()
self.camera_names = tuple(camera_names)
self.fused_camera_names = tuple(fused_camera_names)
self.num_cameras = int(num_cameras) if num_cameras is not None else len(self.camera_names)
self.freeze_backbone = bool(freeze_backbone)
self.joint_output_dim = int(joint_output_dim)
self.image_size = int(image_size)
self._output_dim = int(output_dim)
self.dataset_image_resize_shape = (
tuple(int(v) for v in dataset_image_resize_shape)
if dataset_image_resize_shape is not None else None
)
self.eval_image_resize_shape = (
tuple(int(v) for v in eval_image_resize_shape)
if eval_image_resize_shape is not None else None
)
if self.num_cameras != len(self.camera_names):
raise ValueError(
f"num_cameras({self.num_cameras}) must match len(camera_names)({len(self.camera_names)})"
)
if set(self.fused_camera_names) != set(self.camera_names):
raise ValueError(
"fused_camera_names must contain the same cameras as camera_names. "
f"got camera_names={list(self.camera_names)}, fused_camera_names={list(self.fused_camera_names)}"
)
self.encoder = self._build_encoder(self.image_size)
self.projector = _LEWMProjector(
input_dim=self.encoder.config.hidden_size,
hidden_dim=2048,
output_dim=self.joint_output_dim,
)
self.register_buffer(
"mean",
torch.tensor([0.485, 0.456, 0.406], dtype=torch.float32).view(1, 3, 1, 1),
)
self.register_buffer(
"std",
torch.tensor([0.229, 0.224, 0.225], dtype=torch.float32).view(1, 3, 1, 1),
)
if checkpoint_path is not None and checkpoint is not None:
raise ValueError("checkpoint_path and checkpoint cannot both be provided")
if checkpoint_path is not None:
self.load_lewm_checkpoint(checkpoint_path)
elif checkpoint is not None:
self.load_lewm_checkpoint(checkpoint)
if self.freeze_backbone:
self._freeze_encoder_and_projector()
@staticmethod
def _build_encoder_config(image_size: int):
from transformers import ViTConfig
return ViTConfig(
image_size=image_size,
patch_size=14,
num_channels=3,
hidden_size=192,
intermediate_size=768,
num_hidden_layers=12,
num_attention_heads=3,
qkv_bias=True,
hidden_dropout_prob=0.0,
attention_probs_dropout_prob=0.0,
)
@classmethod
def _build_encoder(cls, image_size: int) -> nn.Module:
from transformers import ViTModel
return ViTModel(cls._build_encoder_config(image_size), add_pooling_layer=False)
@staticmethod
def _unwrap_state_dict(payload: Mapping[str, Any]) -> Mapping[str, torch.Tensor]:
state_dict = payload.get("state_dict", payload)
if not isinstance(state_dict, Mapping):
raise TypeError("checkpoint payload must contain a mapping state_dict")
return state_dict
@staticmethod
def _extract_prefixed_state_dict(
state_dict: Mapping[str, torch.Tensor],
prefix: str,
) -> Dict[str, torch.Tensor]:
extracted = {
key[len(prefix) :]: value
for key, value in state_dict.items()
if key.startswith(prefix)
}
if not extracted:
raise KeyError(f"checkpoint missing parameters with prefix {prefix!r}")
return extracted
def load_lewm_checkpoint(self, checkpoint_or_path: str | Path | Mapping[str, Any]) -> None:
if isinstance(checkpoint_or_path, (str, Path)):
payload = torch.load(Path(checkpoint_or_path), map_location="cpu", weights_only=False)
else:
payload = checkpoint_or_path
state_dict = self._unwrap_state_dict(payload)
encoder_state_dict = self._extract_prefixed_state_dict(state_dict, "model.encoder.")
projector_state_dict = self._extract_prefixed_state_dict(state_dict, "model.projector.")
self.encoder.load_state_dict(encoder_state_dict, strict=True)
self.projector.load_state_dict(projector_state_dict, strict=True)
def _freeze_encoder_and_projector(self) -> None:
for module in (self.encoder, self.projector):
module.eval()
for parameter in module.parameters():
parameter.requires_grad = False
def train(self, mode: bool = True) -> "LEWMViTBackbone":
super().train(mode)
if self.freeze_backbone:
self._freeze_encoder_and_projector()
return self
def _ordered_images(self, images: Dict[str, torch.Tensor]) -> list[torch.Tensor]:
missing = [camera_name for camera_name in self.camera_names if camera_name not in images]
if missing:
raise ValueError(
f"image input missing required cameras. missing={missing}, expected={list(self.camera_names)}"
)
ordered = [images[camera_name] for camera_name in self.camera_names]
reference_shape = ordered[0].shape
if len(reference_shape) != 5:
raise ValueError(f"expected image tensors shaped (B, T, C, H, W), got {reference_shape}")
for camera_name, image in zip(self.camera_names[1:], ordered[1:]):
if image.shape != reference_shape:
raise ValueError(
f"camera {camera_name!r} shape {tuple(image.shape)} does not match {tuple(reference_shape)}"
)
return ordered
def _prepare_pixels(self, images: Dict[str, torch.Tensor]) -> tuple[torch.Tensor, int, int]:
self._ordered_images(images)
fused = torch.cat([images[camera_name] for camera_name in self.fused_camera_names], dim=-2)
bsz, steps = fused.shape[:2]
fused = fused.reshape(bsz * steps, *fused.shape[2:]).contiguous().float()
fused = fused.clamp(0.0, 1.0)
fused = (fused - self.mean) / self.std
height, width = fused.shape[-2:]
short_side = min(height, width)
if short_side <= 0:
raise ValueError(f"invalid fused image shape: {tuple(fused.shape)}")
scale = self.image_size / float(short_side)
resized_height = int(round(height * scale))
resized_width = int(round(width * scale))
if (resized_height, resized_width) != (height, width):
fused = F.interpolate(
fused,
size=(resized_height, resized_width),
mode="bilinear",
align_corners=False,
antialias=True,
)
return fused, bsz, steps
def forward(self, images: Dict[str, torch.Tensor]) -> torch.Tensor:
pixels, bsz, steps = self._prepare_pixels(images)
with torch.set_grad_enabled(torch.is_grad_enabled() and not self.freeze_backbone):
output = self.encoder(pixel_values=pixels, interpolate_pos_encoding=True)
cls = output.last_hidden_state[:, 0]
embedding = self.projector(cls)
return embedding.view(bsz, steps, self.joint_output_dim)
@property
def output_dim(self) -> int:
return self._output_dim