mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-23 09:46:00 +00:00
lots of changes to make existing weights work, need to massively refactor the pre and post processing
This commit is contained in:
committed by
Maximellerbach
parent
c6bf11b2d5
commit
999cc625d6
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -33,17 +34,6 @@ def swish(x: torch.Tensor) -> torch.Tensor:
|
|||||||
return x * torch.sigmoid(x)
|
return x * torch.sigmoid(x)
|
||||||
|
|
||||||
|
|
||||||
class _MLP2(nn.Module):
|
|
||||||
"""Two-layer GELU MLP with layer1/layer2 attribute names matching the original checkpoint."""
|
|
||||||
|
|
||||||
def __init__(self, in_dim: int, hidden_dim: int, out_dim: int) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.layer1 = nn.Linear(in_dim, hidden_dim)
|
|
||||||
self.layer2 = nn.Linear(hidden_dim, out_dim)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
||||||
return self.layer2(F.gelu(self.layer1(x)))
|
|
||||||
|
|
||||||
|
|
||||||
class SinusoidalPositionalEncoding(nn.Module):
|
class SinusoidalPositionalEncoding(nn.Module):
|
||||||
def __init__(self, embedding_dim: int):
|
def __init__(self, embedding_dim: int):
|
||||||
@@ -108,10 +98,11 @@ class BasicTransformerBlock(nn.Module):
|
|||||||
num_attention_heads: int,
|
num_attention_heads: int,
|
||||||
attention_head_dim: int,
|
attention_head_dim: int,
|
||||||
dropout: float,
|
dropout: float,
|
||||||
cross_attention_dim: int | None,
|
cross_attention_dim: int,
|
||||||
|
is_cross_attention: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.is_cross_attention = cross_attention_dim is not None
|
self.is_cross_attention = is_cross_attention
|
||||||
self.norm1 = AdaLayerNorm(dim)
|
self.norm1 = AdaLayerNorm(dim)
|
||||||
self.attn1 = Attention(
|
self.attn1 = Attention(
|
||||||
query_dim=dim,
|
query_dim=dim,
|
||||||
@@ -132,7 +123,8 @@ class BasicTransformerBlock(nn.Module):
|
|||||||
temb: torch.Tensor,
|
temb: torch.Tensor,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
attn_input = self.norm1(hidden_states, temb)
|
attn_input = self.norm1(hidden_states, temb)
|
||||||
hidden_states = hidden_states + self.attn1(attn_input, encoder_hidden_states=encoder_hidden_states)
|
attention_context = encoder_hidden_states if self.is_cross_attention else None
|
||||||
|
hidden_states = hidden_states + self.attn1(attn_input, encoder_hidden_states=attention_context)
|
||||||
hidden_states = hidden_states + self.ff(self.norm2(hidden_states))
|
hidden_states = hidden_states + self.ff(self.norm2(hidden_states))
|
||||||
return hidden_states
|
return hidden_states
|
||||||
|
|
||||||
@@ -160,10 +152,10 @@ class DiT(ModelMixin, ConfigMixin):
|
|||||||
num_attention_heads=num_attention_heads,
|
num_attention_heads=num_attention_heads,
|
||||||
attention_head_dim=attention_head_dim,
|
attention_head_dim=attention_head_dim,
|
||||||
dropout=dropout,
|
dropout=dropout,
|
||||||
# Even blocks attend to context (cross-attention), odd blocks are self-attention.
|
cross_attention_dim=cross_attention_dim if layer_idx % 2 == 0 else self.inner_dim,
|
||||||
cross_attention_dim=cross_attention_dim if i % 2 == 0 else None,
|
is_cross_attention=layer_idx % 2 == 0,
|
||||||
)
|
)
|
||||||
for i in range(num_layers)
|
for layer_idx in range(num_layers)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.norm_out = nn.LayerNorm(self.inner_dim, eps=1e-6, elementwise_affine=False)
|
self.norm_out = nn.LayerNorm(self.inner_dim, eps=1e-6, elementwise_affine=False)
|
||||||
@@ -179,8 +171,7 @@ class DiT(ModelMixin, ConfigMixin):
|
|||||||
temb = self.timestep_encoder(timestep)
|
temb = self.timestep_encoder(timestep)
|
||||||
x = hidden_states
|
x = hidden_states
|
||||||
for block in self.transformer_blocks:
|
for block in self.transformer_blocks:
|
||||||
es = encoder_hidden_states if block.is_cross_attention else None
|
x = block(x, encoder_hidden_states=encoder_hidden_states, temb=temb)
|
||||||
x = block(x, encoder_hidden_states=es, temb=temb)
|
|
||||||
shift, scale = self.proj_out_1(F.silu(temb)).chunk(2, dim=-1)
|
shift, scale = self.proj_out_1(F.silu(temb)).chunk(2, dim=-1)
|
||||||
x = self.norm_out(x) * (1 + scale[:, None]) + shift[:, None]
|
x = self.norm_out(x) * (1 + scale[:, None]) + shift[:, None]
|
||||||
return self.proj_out_2(x)
|
return self.proj_out_2(x)
|
||||||
@@ -205,34 +196,49 @@ class VLAJEPAActionHead(nn.Module):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
preset = DIT_PRESETS[config.action_model_type]
|
preset = DIT_PRESETS[config.action_model_type]
|
||||||
self.config = config
|
self.config = config
|
||||||
num_heads = preset.num_attention_heads
|
num_heads = config.action_num_heads or preset.num_attention_heads
|
||||||
head_dim = preset.attention_head_dim
|
head_dim = config.action_attention_head_dim or preset.attention_head_dim
|
||||||
inner_dim = num_heads * head_dim # e.g. DiT-B: 12 × 64 = 768
|
inner_dim = num_heads * head_dim # e.g. DiT-B: 12 × 64 = 768
|
||||||
|
|
||||||
self.input_embedding_dim = inner_dim
|
self.input_embedding_dim = inner_dim
|
||||||
self.action_horizon = config.chunk_size
|
self.action_horizon = config.chunk_size
|
||||||
self.num_inference_timesteps = config.num_inference_timesteps
|
self.num_inference_timesteps = config.num_inference_timesteps
|
||||||
|
|
||||||
|
hidden_size = config.action_hidden_size
|
||||||
self.model = DiT(
|
self.model = DiT(
|
||||||
num_attention_heads=num_heads,
|
num_attention_heads=num_heads,
|
||||||
attention_head_dim=head_dim,
|
attention_head_dim=head_dim,
|
||||||
output_dim=config.action_hidden_size,
|
output_dim=hidden_size,
|
||||||
num_layers=config.action_num_layers,
|
num_layers=config.action_num_layers,
|
||||||
dropout=config.action_dropout,
|
dropout=config.action_dropout,
|
||||||
cross_attention_dim=cross_attention_dim,
|
cross_attention_dim=cross_attention_dim,
|
||||||
)
|
)
|
||||||
# action_encoder/decoder and state_encoder use action_hidden_size (DiT output dim).
|
|
||||||
# action_encoder and state_encoder produce inner_dim-sized tokens (DiT input width).
|
|
||||||
# action_decoder takes DiT output (action_hidden_size) and produces action_dim predictions.
|
|
||||||
self.action_encoder = ActionEncoder(config.action_dim, inner_dim)
|
self.action_encoder = ActionEncoder(config.action_dim, inner_dim)
|
||||||
self.action_decoder = _MLP2(config.action_hidden_size, config.action_hidden_size, config.action_dim)
|
self.action_decoder = nn.Sequential(
|
||||||
self.state_encoder = (
|
OrderedDict([
|
||||||
_MLP2(config.state_dim, config.action_hidden_size, inner_dim) if config.state_dim > 0 else None
|
("layer1", nn.Linear(hidden_size, hidden_size)),
|
||||||
|
("relu", nn.ReLU()),
|
||||||
|
("layer2", nn.Linear(hidden_size, config.action_dim)),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
self.state_encoder = (
|
||||||
|
nn.Sequential(
|
||||||
|
OrderedDict([
|
||||||
|
("layer1", nn.Linear(config.state_dim, hidden_size)),
|
||||||
|
("relu", nn.ReLU()),
|
||||||
|
("layer2", nn.Linear(hidden_size, inner_dim)),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
if config.state_dim > 0
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
self.future_tokens = nn.Embedding(
|
||||||
|
config.num_embodied_action_tokens_per_instruction, inner_dim
|
||||||
|
)
|
||||||
|
self.position_embedding = nn.Embedding(
|
||||||
|
max(1024, config.chunk_size + config.num_action_tokens_per_timestep + 4),
|
||||||
|
inner_dim,
|
||||||
)
|
)
|
||||||
# future_tokens and position_embedding operate at inner_dim (DiT input width),
|
|
||||||
# not at action_hidden_size (DiT output width).
|
|
||||||
self.future_tokens = nn.Embedding(config.num_target_vision_tokens, inner_dim)
|
|
||||||
self.position_embedding = nn.Embedding(config.action_max_seq_len, inner_dim)
|
|
||||||
self.beta_dist = Beta(config.action_noise_beta_alpha, config.action_noise_beta_beta)
|
self.beta_dist = Beta(config.action_noise_beta_alpha, config.action_noise_beta_beta)
|
||||||
|
|
||||||
def sample_time(self, batch_size: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
def sample_time(self, batch_size: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from lerobot.configs.policies import PreTrainedConfig
|
from lerobot.configs.policies import PreTrainedConfig
|
||||||
from lerobot.configs.types import NormalizationMode
|
from lerobot.configs.types import NormalizationMode
|
||||||
@@ -44,6 +45,8 @@ class VLAJEPAConfig(PreTrainedConfig):
|
|||||||
action_hidden_size: int = 1024
|
action_hidden_size: int = 1024
|
||||||
action_model_type: str = "DiT-B"
|
action_model_type: str = "DiT-B"
|
||||||
action_num_layers: int = 16
|
action_num_layers: int = 16
|
||||||
|
action_num_heads: int | None = None
|
||||||
|
action_attention_head_dim: int | None = None
|
||||||
action_dropout: float = 0.2
|
action_dropout: float = 0.2
|
||||||
action_num_timestep_buckets: int = 1000
|
action_num_timestep_buckets: int = 1000
|
||||||
action_noise_beta_alpha: float = 1.5
|
action_noise_beta_alpha: float = 1.5
|
||||||
@@ -63,6 +66,9 @@ class VLAJEPAConfig(PreTrainedConfig):
|
|||||||
repeated_diffusion_steps: int = 8 # independent noise draws per batch item (CogACT-style)
|
repeated_diffusion_steps: int = 8 # independent noise draws per batch item (CogACT-style)
|
||||||
|
|
||||||
resize_images_to: tuple[int, int] | None = None
|
resize_images_to: tuple[int, int] | None = None
|
||||||
|
action_unnormalization_stats: dict[str, Any] | None = None
|
||||||
|
binarize_gripper_action: bool = True
|
||||||
|
clip_normalized_actions: bool = True
|
||||||
torch_dtype: str = "bfloat16"
|
torch_dtype: str = "bfloat16"
|
||||||
|
|
||||||
optimizer_lr: float = 1e-4
|
optimizer_lr: float = 1e-4
|
||||||
|
|||||||
@@ -25,36 +25,75 @@ Image keys SimplerEnv: OXE Bridge/RT1 are single-camera ✓ confirmed
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from huggingface_hub import HfApi
|
from huggingface_hub import HfApi
|
||||||
|
from safetensors.torch import save_file as save_safetensors
|
||||||
|
from lerobot.policies.vla_jepa.processor_vla_jepa import make_vla_jepa_pre_post_processors
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Top-level settings
|
# Top-level settings
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
SOURCE_REPO_ID = "ginwind/VLA-JEPA"
|
SOURCE_REPO_ID = "ginwind/VLA-JEPA"
|
||||||
TARGET_ORG = "lerobot"
|
TARGET_ORG = "maximellerbach"
|
||||||
COLLECTION_TITLE = "VLA-JEPA"
|
COLLECTION_TITLE = "VLA-JEPA"
|
||||||
COLLECTION_DESCRIPTION = (
|
COLLECTION_DESCRIPTION = (
|
||||||
"VLA-JEPA model checkpoints (LIBERO, Pretrain, SimplerEnv) converted from .pt to safetensors via LeRobot."
|
"VLA-JEPA model checkpoints (LIBERO, Pretrain, SimplerEnv) converted from .pt to safetensors via LeRobot."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remap state-dict key prefixes before loading into the LeRobot policy.
|
|
||||||
# E.g. {"": "model."} prepends "model." to every key.
|
|
||||||
# Leave empty if keys already match — the first run's log will tell you.
|
|
||||||
KEY_PREFIX_REMAP: dict[str, str] = {
|
|
||||||
# Specific rules must come before the "" catch-all (dict order is preserved).
|
|
||||||
"qwen_vl_interface.": "model.qwen.",
|
|
||||||
"vj_encoder.": "model.video_encoder.",
|
|
||||||
"vj_predictor.": "model.video_predictor.",
|
|
||||||
# Everything else (action_model.*) just needs the "model." wrapper.
|
|
||||||
"": "model.",
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Key mapping — mirrors todo_converter.py map_key() so both converters
|
||||||
|
# produce identical safetensors layouts that match the LeRobot action_head code.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_source_key(key: str) -> str:
|
||||||
|
return key[len("module."):] if key.startswith("module.") else key
|
||||||
|
|
||||||
|
|
||||||
|
def _map_checkpoint_key(raw_key: str) -> str | None:
|
||||||
|
"""Map original VLA-JEPA state-dict keys to LeRobot vla_jepa layout."""
|
||||||
|
key = _normalize_source_key(raw_key)
|
||||||
|
|
||||||
|
if key.startswith("qwen_vl_interface."):
|
||||||
|
return "model.qwen." + key[len("qwen_vl_interface."):]
|
||||||
|
if key.startswith("vj_encoder."):
|
||||||
|
return "model.video_encoder." + key[len("vj_encoder."):]
|
||||||
|
if key.startswith("vj_predictor."):
|
||||||
|
return "model.video_predictor." + key[len("vj_predictor."):]
|
||||||
|
if key.startswith("action_model."):
|
||||||
|
# LeRobot code uses the same sub-key names as the source checkpoint,
|
||||||
|
# so only the top-level "model." prefix needs to be added.
|
||||||
|
return "model." + key
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_action_stats(api: "HfApi", source_repo_id: str, subfolder: str) -> dict | None:
|
||||||
|
"""Try to download dataset_statistics.json and return the action stats dict."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
stats_file = f"{subfolder}/dataset_statistics.json"
|
||||||
|
try:
|
||||||
|
local = api.hf_hub_download(source_repo_id, stats_file)
|
||||||
|
data = json.loads(Path(local).read_text())
|
||||||
|
# The original repo nests stats under a robot key, e.g. {"franka": {"action": {...}}}
|
||||||
|
for robot_key in data:
|
||||||
|
if isinstance(data[robot_key], dict) and "action" in data[robot_key]:
|
||||||
|
log.info(" Loaded action stats from %s (robot key: %s)", stats_file, robot_key)
|
||||||
|
return data[robot_key]["action"]
|
||||||
|
log.warning(" %s found but no 'action' key under any robot — skipping action stats.", stats_file)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log.warning(" Could not fetch %s: %s — action_unnormalization_stats will be None.", stats_file, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Architecture — identical across all 4 variants (from config.json)
|
# Architecture — identical across all 4 variants (from config.json)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -69,6 +108,9 @@ _ARCH = {
|
|||||||
"num_inference_timesteps": 4,
|
"num_inference_timesteps": 4,
|
||||||
"action_hidden_size": 1024,
|
"action_hidden_size": 1024,
|
||||||
"action_model_type": "DiT-B",
|
"action_model_type": "DiT-B",
|
||||||
|
# Explicit dims matching DiT-B preset and ginwind checkpoint shape
|
||||||
|
"action_num_heads": 12,
|
||||||
|
"action_attention_head_dim": 64,
|
||||||
"action_num_layers": 16,
|
"action_num_layers": 16,
|
||||||
"action_dropout": 0.2,
|
"action_dropout": 0.2,
|
||||||
"repeated_diffusion_steps": 8,
|
"repeated_diffusion_steps": 8,
|
||||||
@@ -76,9 +118,6 @@ _ARCH = {
|
|||||||
"action_noise_beta_beta": 1.0,
|
"action_noise_beta_beta": 1.0,
|
||||||
"action_noise_s": 0.999,
|
"action_noise_s": 0.999,
|
||||||
"action_num_timestep_buckets": 1000,
|
"action_num_timestep_buckets": 1000,
|
||||||
# Action head embedding params (from original config.json)
|
|
||||||
"num_target_vision_tokens": 32,
|
|
||||||
"action_max_seq_len": 1024,
|
|
||||||
# World model predictor (12 blocks, confirmed from checkpoint)
|
# World model predictor (12 blocks, confirmed from checkpoint)
|
||||||
"predictor_depth": 12,
|
"predictor_depth": 12,
|
||||||
}
|
}
|
||||||
@@ -109,7 +148,12 @@ _OXE_CAMS = [
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _build_config(camera_keys: list[str], with_state: bool, enable_world_model: bool = True):
|
def _build_config(
|
||||||
|
camera_keys: list[str],
|
||||||
|
with_state: bool,
|
||||||
|
enable_world_model: bool = True,
|
||||||
|
action_stats: dict | None = None,
|
||||||
|
):
|
||||||
from lerobot.configs.types import FeatureType, PolicyFeature
|
from lerobot.configs.types import FeatureType, PolicyFeature
|
||||||
from lerobot.policies.vla_jepa.configuration_vla_jepa import VLAJEPAConfig
|
from lerobot.policies.vla_jepa.configuration_vla_jepa import VLAJEPAConfig
|
||||||
|
|
||||||
@@ -123,6 +167,9 @@ def _build_config(camera_keys: list[str], with_state: bool, enable_world_model:
|
|||||||
"action": PolicyFeature(type=FeatureType.ACTION, shape=(7,)),
|
"action": PolicyFeature(type=FeatureType.ACTION, shape=(7,)),
|
||||||
},
|
},
|
||||||
enable_world_model=enable_world_model,
|
enable_world_model=enable_world_model,
|
||||||
|
action_unnormalization_stats=action_stats,
|
||||||
|
binarize_gripper_action=True,
|
||||||
|
clip_normalized_actions=True,
|
||||||
**_ARCH,
|
**_ARCH,
|
||||||
)
|
)
|
||||||
cfg.validate_features()
|
cfg.validate_features()
|
||||||
@@ -153,20 +200,6 @@ def extract_state_dict(ckpt: object) -> dict[str, torch.Tensor]:
|
|||||||
return {k: v for k, v in sd.items() if isinstance(v, torch.Tensor)}
|
return {k: v for k, v in sd.items() if isinstance(v, torch.Tensor)}
|
||||||
|
|
||||||
|
|
||||||
def remap_keys(sd: dict[str, torch.Tensor], remap: dict[str, str]) -> dict[str, torch.Tensor]:
|
|
||||||
if not remap:
|
|
||||||
return sd
|
|
||||||
out = {}
|
|
||||||
for k, v in sd.items():
|
|
||||||
new_k = k
|
|
||||||
for old, new in remap.items():
|
|
||||||
if k.startswith(old):
|
|
||||||
new_k = new + k[len(old) :]
|
|
||||||
break
|
|
||||||
out[new_k] = v
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def subfolder_of(pt_path: str) -> str | None:
|
def subfolder_of(pt_path: str) -> str | None:
|
||||||
for part in Path(pt_path).parts:
|
for part in Path(pt_path).parts:
|
||||||
if part in VARIANTS:
|
if part in VARIANTS:
|
||||||
@@ -229,37 +262,48 @@ def main() -> None:
|
|||||||
ckpt = torch.load(local_pt, map_location="cpu") # nosec B614
|
ckpt = torch.load(local_pt, map_location="cpu") # nosec B614
|
||||||
|
|
||||||
sd = extract_state_dict(ckpt)
|
sd = extract_state_dict(ckpt)
|
||||||
sd = remap_keys(sd, KEY_PREFIX_REMAP)
|
|
||||||
log.info(" %d tensors extracted", len(sd))
|
|
||||||
log.info(" First 5 keys: %s", list(sd)[:5])
|
|
||||||
|
|
||||||
# 3. Build policy
|
# Map source key names → LeRobot layout (handles layer1→w1, transformer_blocks→blocks, etc.)
|
||||||
from lerobot.policies.vla_jepa.modeling_vla_jepa import VLAJEPAPolicy
|
mapped_sd: dict[str, torch.Tensor] = {}
|
||||||
|
skipped_keys: list[str] = []
|
||||||
|
for raw_key, value in sd.items():
|
||||||
|
target_key = _map_checkpoint_key(raw_key)
|
||||||
|
if target_key is None:
|
||||||
|
skipped_keys.append(raw_key)
|
||||||
|
else:
|
||||||
|
mapped_sd[target_key] = value
|
||||||
|
log.info(" %d tensors mapped, %d skipped", len(mapped_sd), len(skipped_keys))
|
||||||
|
if skipped_keys:
|
||||||
|
log.info(" Skipped sample: %s", skipped_keys[:5])
|
||||||
|
log.info(" First 5 mapped keys: %s", list(mapped_sd)[:5])
|
||||||
|
|
||||||
config = _build_config(camera_keys, with_state, enable_world_model)
|
# Fetch action unnormalization stats from the source repo
|
||||||
policy = VLAJEPAPolicy(config)
|
action_stats = _fetch_action_stats(api, SOURCE_REPO_ID, subfolder)
|
||||||
|
|
||||||
# 4. Load weights
|
# 3. Build config (no policy instantiation — avoids loading backbone from Hub)
|
||||||
missing, unexpected = policy.load_state_dict(sd, strict=False)
|
config = _build_config(camera_keys, with_state, enable_world_model, action_stats)
|
||||||
|
|
||||||
def _prefix_summary(keys: list[str]) -> dict[str, int]:
|
# 4. Save everything to a temp dir and upload in one shot
|
||||||
from collections import Counter
|
|
||||||
|
|
||||||
return dict(Counter(".".join(k.split(".")[:3]) for k in keys).most_common())
|
|
||||||
|
|
||||||
if missing:
|
|
||||||
log.warning(" Missing (%d) by prefix: %s", len(missing), _prefix_summary(missing))
|
|
||||||
if unexpected:
|
|
||||||
log.warning(" Unexpected (%d) by prefix: %s", len(unexpected), _prefix_summary(unexpected))
|
|
||||||
if not missing and not unexpected:
|
|
||||||
log.info(" State dict loaded cleanly.")
|
|
||||||
|
|
||||||
# 5. Push to hub (writes model.safetensors + config.json)
|
|
||||||
api.create_repo(target_repo_id, repo_type="model", exist_ok=True)
|
api.create_repo(target_repo_id, repo_type="model", exist_ok=True)
|
||||||
commit_url = policy.push_to_hub(
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
repo_id=target_repo_id,
|
save_dir = Path(tmp)
|
||||||
commit_message=f"Convert {Path(pt_filename).name} to safetensors",
|
|
||||||
)
|
log.info(" Saving model.safetensors …")
|
||||||
|
save_safetensors(mapped_sd, save_dir / "model.safetensors")
|
||||||
|
|
||||||
|
config._save_pretrained(save_dir) # writes config.json via draccus
|
||||||
|
|
||||||
|
preprocessor, postprocessor = make_vla_jepa_pre_post_processors(config)
|
||||||
|
preprocessor.save_pretrained(save_dir) # writes policy_preprocessor.json
|
||||||
|
postprocessor.save_pretrained(save_dir) # writes policy_postprocessor.json
|
||||||
|
|
||||||
|
log.info(" Uploading …")
|
||||||
|
commit_url = api.upload_folder(
|
||||||
|
folder_path=save_dir,
|
||||||
|
repo_id=target_repo_id,
|
||||||
|
repo_type="model",
|
||||||
|
commit_message=f"Convert {Path(pt_filename).name} to safetensors",
|
||||||
|
)
|
||||||
log.info(" Uploaded → %s", commit_url)
|
log.info(" Uploaded → %s", commit_url)
|
||||||
|
|
||||||
# 6. Add to collection
|
# 6. Add to collection
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import numpy as np
|
|||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F # noqa: N812
|
import torch.nn.functional as F # noqa: N812
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from safetensors.torch import load_file as load_safetensors_file
|
||||||
from torch import Tensor, nn
|
from torch import Tensor, nn
|
||||||
|
|
||||||
from lerobot.policies.pretrained import PreTrainedPolicy, T
|
from lerobot.policies.pretrained import PreTrainedPolicy, T
|
||||||
@@ -72,9 +73,18 @@ class VLAJEPAModel(nn.Module):
|
|||||||
torch_dtype=self.qwen._get_torch_dtype(config.torch_dtype),
|
torch_dtype=self.qwen._get_torch_dtype(config.torch_dtype),
|
||||||
)
|
)
|
||||||
self.video_processor = AutoVideoProcessor.from_pretrained(config.jepa_encoder_name)
|
self.video_processor = AutoVideoProcessor.from_pretrained(config.jepa_encoder_name)
|
||||||
num_views = max(len(config.image_features), 1)
|
num_views = max(1, len(config.image_features))
|
||||||
|
tubelet_size = self.video_encoder.config.tubelet_size
|
||||||
|
image_size = getattr(self.video_encoder.config, "image_size", None)
|
||||||
|
if image_size is None:
|
||||||
|
first_image_shape = next(iter(config.image_features.values())).shape
|
||||||
|
image_size = first_image_shape[-1]
|
||||||
self.video_predictor = ActionConditionedVideoPredictor(
|
self.video_predictor = ActionConditionedVideoPredictor(
|
||||||
embed_dim=num_views * self.video_encoder.config.hidden_size,
|
num_frames=config.num_video_frames // tubelet_size,
|
||||||
|
img_size=(image_size, image_size),
|
||||||
|
patch_size=16,
|
||||||
|
tubelet_size=1,
|
||||||
|
embed_dim=self.video_encoder.config.hidden_size * num_views,
|
||||||
action_embed_dim=self.qwen.model.config.hidden_size,
|
action_embed_dim=self.qwen.model.config.hidden_size,
|
||||||
predictor_embed_dim=self.video_encoder.config.hidden_size,
|
predictor_embed_dim=self.video_encoder.config.hidden_size,
|
||||||
depth=config.predictor_depth,
|
depth=config.predictor_depth,
|
||||||
@@ -91,17 +101,56 @@ class VLAJEPAModel(nn.Module):
|
|||||||
self.qwen.requires_grad_(False)
|
self.qwen.requires_grad_(False)
|
||||||
|
|
||||||
# Build prompt placeholders.
|
# Build prompt placeholders.
|
||||||
# Original uses num_frames // tubelet_size - 1 action token groups for the world model predictor.
|
# Use the encoder's actual tubelet_size when available (world model enabled),
|
||||||
# This matches the number of context temporal positions after tubelet compression.
|
# otherwise fall back to config.
|
||||||
n_wm_action_groups = max(1, self.config.num_video_frames // self.config.jepa_tubelet_size - 1)
|
_tubelet_size = (
|
||||||
|
self.video_encoder.config.tubelet_size
|
||||||
|
if config.enable_world_model
|
||||||
|
else self.config.jepa_tubelet_size
|
||||||
|
)
|
||||||
|
num_action_prompt_steps = self.config.num_video_frames // _tubelet_size - 1
|
||||||
self.replace_prompt = "".join(
|
self.replace_prompt = "".join(
|
||||||
token * self.config.num_action_tokens_per_timestep
|
token * self.config.num_action_tokens_per_timestep
|
||||||
for token in self.action_tokens[:n_wm_action_groups]
|
for token in self.action_tokens[:num_action_prompt_steps]
|
||||||
)
|
)
|
||||||
self.embodied_replace_prompt = (
|
self.embodied_replace_prompt = (
|
||||||
self.config.embodied_action_token * self.config.num_embodied_action_tokens_per_instruction
|
self.config.embodied_action_token * self.config.num_embodied_action_tokens_per_instruction
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _qwen_last_decoder_hidden(self, qwen_inputs: dict[str, torch.Tensor]) -> torch.Tensor:
|
||||||
|
"""Return Qwen's final decoder-layer output before the final RMSNorm.
|
||||||
|
|
||||||
|
starVLA trained its downstream heads on the legacy transformers-4.57
|
||||||
|
`hidden_states[-1]` value, which is the last decoder layer output before
|
||||||
|
Qwen's final RMSNorm. Newer transformers versions expose `hidden_states[-1]`
|
||||||
|
as the post-norm last hidden state, so capture the layer output directly.
|
||||||
|
"""
|
||||||
|
captured: dict[str, torch.Tensor] = {}
|
||||||
|
language_model = self.qwen.model.model.language_model
|
||||||
|
|
||||||
|
def capture_last_layer_output(
|
||||||
|
_module: nn.Module,
|
||||||
|
_inputs: tuple[torch.Tensor, ...],
|
||||||
|
output: torch.Tensor | tuple[torch.Tensor, ...],
|
||||||
|
) -> None:
|
||||||
|
captured["last_hidden"] = output[0] if isinstance(output, tuple) else output
|
||||||
|
return None
|
||||||
|
|
||||||
|
handle = language_model.layers[-1].register_forward_hook(capture_last_layer_output)
|
||||||
|
try:
|
||||||
|
self.qwen.model.model(
|
||||||
|
**qwen_inputs,
|
||||||
|
output_hidden_states=False,
|
||||||
|
output_attentions=False,
|
||||||
|
return_dict=True,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
handle.remove()
|
||||||
|
|
||||||
|
if "last_hidden" not in captured:
|
||||||
|
raise RuntimeError("Failed to capture Qwen last decoder hidden states.")
|
||||||
|
return captured["last_hidden"]
|
||||||
|
|
||||||
# ---- Native VLA-JEPA forward (follows original VLA_JEPA.py) ----
|
# ---- Native VLA-JEPA forward (follows original VLA_JEPA.py) ----
|
||||||
|
|
||||||
def forward(self, examples: list[dict]) -> dict[str, Tensor]:
|
def forward(self, examples: list[dict]) -> dict[str, Tensor]:
|
||||||
@@ -160,13 +209,7 @@ class VLAJEPAModel(nn.Module):
|
|||||||
device_type = next(self.parameters()).device.type
|
device_type = next(self.parameters()).device.type
|
||||||
|
|
||||||
with torch.autocast(device_type=device_type, dtype=torch.bfloat16):
|
with torch.autocast(device_type=device_type, dtype=torch.bfloat16):
|
||||||
qwen_outputs = self.qwen.model(
|
last_hidden = self._qwen_last_decoder_hidden(qwen_inputs) # [B, seq_len, H]
|
||||||
**qwen_inputs,
|
|
||||||
output_hidden_states=True,
|
|
||||||
output_attentions=False,
|
|
||||||
return_dict=True,
|
|
||||||
)
|
|
||||||
last_hidden = qwen_outputs.hidden_states[-1] # [B, seq_len, H]
|
|
||||||
b, _, h = last_hidden.shape
|
b, _, h = last_hidden.shape
|
||||||
|
|
||||||
if self.config.enable_world_model:
|
if self.config.enable_world_model:
|
||||||
@@ -211,20 +254,16 @@ class VLAJEPAModel(nn.Module):
|
|||||||
|
|
||||||
input_states = video_embeddings[:, : tokens_per_frame * t_enc_ctx, :]
|
input_states = video_embeddings[:, : tokens_per_frame * t_enc_ctx, :]
|
||||||
gt_states = video_embeddings[:, tokens_per_frame:, :]
|
gt_states = video_embeddings[:, tokens_per_frame:, :]
|
||||||
d_emb = input_states.shape[-1]
|
|
||||||
|
|
||||||
input_states_4d = input_states.view(b, t_enc_ctx, tokens_per_frame, d_emb)
|
|
||||||
|
|
||||||
expected_actions = t_enc_ctx * self.config.num_action_tokens_per_timestep
|
expected_actions = t_enc_ctx * self.config.num_action_tokens_per_timestep
|
||||||
if action_tokens.shape[1] < expected_actions:
|
if action_tokens.shape[1] < expected_actions:
|
||||||
pad = action_tokens[:, -1:].repeat(1, expected_actions - action_tokens.shape[1], 1)
|
pad = action_tokens[:, -1:].repeat(1, expected_actions - action_tokens.shape[1], 1)
|
||||||
action_tokens = torch.cat([action_tokens, pad], dim=1)
|
action_tokens = torch.cat([action_tokens, pad], dim=1)
|
||||||
act_4d = action_tokens[:, :expected_actions].view(
|
|
||||||
b, t_enc_ctx, self.config.num_action_tokens_per_timestep, -1
|
|
||||||
)
|
|
||||||
|
|
||||||
pred_4d = self.video_predictor(input_states_4d.float(), act_4d.float())
|
predicted_states = self.video_predictor(
|
||||||
predicted_states = pred_4d.reshape(b, -1, d_emb)
|
input_states.float(),
|
||||||
|
action_tokens[:, :expected_actions].float(),
|
||||||
|
)
|
||||||
|
|
||||||
wm_loss = F.l1_loss(predicted_states, gt_states.float(), reduction="mean")
|
wm_loss = F.l1_loss(predicted_states, gt_states.float(), reduction="mean")
|
||||||
|
|
||||||
@@ -242,15 +281,14 @@ class VLAJEPAModel(nn.Module):
|
|||||||
state_tensor = None
|
state_tensor = None
|
||||||
if state is not None:
|
if state is not None:
|
||||||
state_tensor = torch.tensor(
|
state_tensor = torch.tensor(
|
||||||
np.array(state), device=last_hidden.device, dtype=torch.float32
|
np.array(state), device=last_hidden.device, dtype=last_hidden.dtype
|
||||||
) # [B, 1, state_dim]
|
) # [B, 1, state_dim]
|
||||||
|
|
||||||
# repeated_diffusion_steps: draw R independent noise samples per batch item (CogACT-style).
|
repeated_diffusion_steps = self.config.repeated_diffusion_steps
|
||||||
# Effectively multiplies data efficiency of the action head by R with no extra Qwen/JEPA cost.
|
actions_target = actions_target.repeat(repeated_diffusion_steps, 1, 1)
|
||||||
num_repeated = self.config.repeated_diffusion_steps
|
embodied_action_tokens = embodied_action_tokens.repeat(repeated_diffusion_steps, 1, 1)
|
||||||
embodied_rep = embodied_action_tokens.float().repeat(num_repeated, 1, 1)
|
if state_tensor is not None:
|
||||||
actions_rep = actions_target.repeat(num_repeated, 1, 1)
|
state_tensor = state_tensor.repeat(repeated_diffusion_steps, 1, 1)
|
||||||
state_rep = state_tensor.repeat(num_repeated, 1, 1) if state_tensor is not None else None
|
|
||||||
|
|
||||||
action_is_pad_rep = None
|
action_is_pad_rep = None
|
||||||
if action_is_pad is not None:
|
if action_is_pad is not None:
|
||||||
@@ -263,9 +301,11 @@ class VLAJEPAModel(nn.Module):
|
|||||||
]
|
]
|
||||||
) # [B, T_full]
|
) # [B, T_full]
|
||||||
pad_tensor = pad_tensor[:, -action_horizon:] # [B, action_horizon]
|
pad_tensor = pad_tensor[:, -action_horizon:] # [B, action_horizon]
|
||||||
action_is_pad_rep = pad_tensor.repeat(num_repeated, 1) # [B*R, action_horizon]
|
action_is_pad_rep = pad_tensor.repeat(repeated_diffusion_steps, 1) # [B*R, action_horizon]
|
||||||
|
|
||||||
action_loss = self.action_model(embodied_rep, actions_rep, state_rep, action_is_pad_rep)
|
action_loss = self.action_model(
|
||||||
|
embodied_action_tokens, actions_target, state_tensor, action_is_pad_rep
|
||||||
|
)
|
||||||
|
|
||||||
return {"action_loss": action_loss, "wm_loss": wm_loss * self.config.world_model_loss_weight}
|
return {"action_loss": action_loss, "wm_loss": wm_loss * self.config.world_model_loss_weight}
|
||||||
|
|
||||||
@@ -289,6 +329,14 @@ class VLAJEPAModel(nn.Module):
|
|||||||
Returns:
|
Returns:
|
||||||
np.ndarray [B, action_horizon, action_dim] — predicted actions.
|
np.ndarray [B, action_horizon, action_dim] — predicted actions.
|
||||||
"""
|
"""
|
||||||
|
if self.config.resize_images_to is not None:
|
||||||
|
height, width = self.config.resize_images_to
|
||||||
|
resampling = getattr(Image, "Resampling", Image).BOX
|
||||||
|
batch_images = [
|
||||||
|
[image.resize((width, height), resample=resampling) for image in sample_images]
|
||||||
|
for sample_images in batch_images
|
||||||
|
]
|
||||||
|
|
||||||
qwen_inputs = self.qwen.build_inputs(
|
qwen_inputs = self.qwen.build_inputs(
|
||||||
images=batch_images,
|
images=batch_images,
|
||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
@@ -302,27 +350,19 @@ class VLAJEPAModel(nn.Module):
|
|||||||
device_type = next(self.parameters()).device.type
|
device_type = next(self.parameters()).device.type
|
||||||
|
|
||||||
with torch.autocast(device_type=device_type, dtype=torch.bfloat16):
|
with torch.autocast(device_type=device_type, dtype=torch.bfloat16):
|
||||||
qwen_outputs = self.qwen.model(
|
last_hidden = self._qwen_last_decoder_hidden(qwen_inputs) # [B, seq_len, H]
|
||||||
**qwen_inputs,
|
|
||||||
output_hidden_states=True,
|
|
||||||
output_attentions=False,
|
|
||||||
return_dict=True,
|
|
||||||
)
|
|
||||||
last_hidden = qwen_outputs.hidden_states[-1]
|
|
||||||
b, _, h = last_hidden.shape
|
b, _, h = last_hidden.shape
|
||||||
embodied_action_tokens = last_hidden[embodied_indices[0], embodied_indices[1], :].view(b, -1, h)
|
embodied_action_tokens = last_hidden[embodied_indices[0], embodied_indices[1], :].view(b, -1, h)
|
||||||
|
|
||||||
state_tensor = None
|
state_tensor = None
|
||||||
if state is not None:
|
if state is not None:
|
||||||
state_tensor = torch.from_numpy(np.array(state)).to(
|
state_tensor = torch.from_numpy(np.array(state)).to(
|
||||||
device=last_hidden.device, dtype=torch.float32
|
device=last_hidden.device, dtype=last_hidden.dtype
|
||||||
)
|
)
|
||||||
|
|
||||||
with torch.autocast(device_type=device_type, dtype=torch.float32):
|
pred_actions = self.action_model.predict_action(
|
||||||
# Cast embodied tokens to float32 for action model compatibility
|
embodied_action_tokens.float(), state_tensor.float() if state_tensor is not None else None
|
||||||
pred_actions = self.action_model.predict_action(
|
) # [B, action_horizon, action_dim]
|
||||||
embodied_action_tokens.float(), state_tensor
|
|
||||||
) # [B, action_horizon, action_dim]
|
|
||||||
|
|
||||||
return pred_actions.detach().cpu().numpy()
|
return pred_actions.detach().cpu().numpy()
|
||||||
|
|
||||||
@@ -546,8 +586,31 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
actions_np = self.model.predict_action(batch_images, instructions, state_np)
|
actions_np = self.model.predict_action(batch_images, instructions, state_np)
|
||||||
|
|
||||||
# Convert back to tensor on the right device
|
# Convert back to tensor on the right device
|
||||||
|
actions_np = self._unnormalize_actions(actions_np)
|
||||||
return torch.from_numpy(actions_np).to(device=self.config.device, dtype=torch.float32)
|
return torch.from_numpy(actions_np).to(device=self.config.device, dtype=torch.float32)
|
||||||
|
|
||||||
|
def _unnormalize_actions(self, normalized_actions: np.ndarray) -> np.ndarray:
|
||||||
|
"""Match starVLA's LIBERO action post-processing exactly."""
|
||||||
|
stats = self.config.action_unnormalization_stats
|
||||||
|
if not stats:
|
||||||
|
return normalized_actions
|
||||||
|
|
||||||
|
actions = normalized_actions.astype(np.float32, copy=True)
|
||||||
|
if self.config.clip_normalized_actions:
|
||||||
|
actions = np.clip(actions, -1.0, 1.0)
|
||||||
|
|
||||||
|
if self.config.binarize_gripper_action and actions.shape[-1] >= 7:
|
||||||
|
actions[..., 6] = np.where(actions[..., 6] < 0.5, 0.0, 1.0)
|
||||||
|
|
||||||
|
action_min = np.asarray(stats["min"], dtype=np.float32)
|
||||||
|
action_max = np.asarray(stats["max"], dtype=np.float32)
|
||||||
|
mask = np.asarray(stats.get("mask", np.ones_like(action_min, dtype=bool)), dtype=bool)
|
||||||
|
scaled = 0.5 * (actions + 1.0) * (action_max - action_min) + action_min
|
||||||
|
actions = np.where(mask, scaled, actions).astype(np.float32)
|
||||||
|
if self.config.binarize_gripper_action and actions.shape[-1] >= 7:
|
||||||
|
actions[..., 6] = 1.0 - 2.0 * (actions[..., 6] > 0.5)
|
||||||
|
return actions
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
def select_action(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
|
def select_action(self, batch: dict[str, Tensor], noise: Tensor | None = None) -> Tensor:
|
||||||
"""LeRobot select_action with action queue caching."""
|
"""LeRobot select_action with action queue caching."""
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from lerobot.processor import (
|
|||||||
PolicyProcessorPipeline,
|
PolicyProcessorPipeline,
|
||||||
ProcessorStepRegistry,
|
ProcessorStepRegistry,
|
||||||
RenameObservationsProcessorStep,
|
RenameObservationsProcessorStep,
|
||||||
UnnormalizerProcessorStep,
|
|
||||||
)
|
)
|
||||||
from lerobot.processor.converters import policy_action_to_transition, transition_to_policy_action
|
from lerobot.processor.converters import policy_action_to_transition, transition_to_policy_action
|
||||||
from lerobot.utils.constants import POLICY_POSTPROCESSOR_DEFAULT_NAME, POLICY_PREPROCESSOR_DEFAULT_NAME
|
from lerobot.utils.constants import POLICY_POSTPROCESSOR_DEFAULT_NAME, POLICY_PREPROCESSOR_DEFAULT_NAME
|
||||||
@@ -31,7 +30,6 @@ def make_vla_jepa_pre_post_processors(
|
|||||||
input_steps = [
|
input_steps = [
|
||||||
RenameObservationsProcessorStep(rename_map={}),
|
RenameObservationsProcessorStep(rename_map={}),
|
||||||
AddBatchDimensionProcessorStep(),
|
AddBatchDimensionProcessorStep(),
|
||||||
VLAJEPANewLineProcessor(),
|
|
||||||
DeviceProcessorStep(device=config.device),
|
DeviceProcessorStep(device=config.device),
|
||||||
NormalizerProcessorStep(
|
NormalizerProcessorStep(
|
||||||
features=features,
|
features=features,
|
||||||
@@ -40,11 +38,6 @@ def make_vla_jepa_pre_post_processors(
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
output_steps = [
|
output_steps = [
|
||||||
UnnormalizerProcessorStep(
|
|
||||||
features=config.output_features,
|
|
||||||
norm_map=config.normalization_mapping,
|
|
||||||
stats=dataset_stats,
|
|
||||||
),
|
|
||||||
DeviceProcessorStep(device="cpu"),
|
DeviceProcessorStep(device="cpu"),
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
@@ -64,20 +57,7 @@ def make_vla_jepa_pre_post_processors(
|
|||||||
@ProcessorStepRegistry.register(name="vla_jepa_new_line_processor")
|
@ProcessorStepRegistry.register(name="vla_jepa_new_line_processor")
|
||||||
class VLAJEPANewLineProcessor(ComplementaryDataProcessorStep):
|
class VLAJEPANewLineProcessor(ComplementaryDataProcessorStep):
|
||||||
def complementary_data(self, complementary_data):
|
def complementary_data(self, complementary_data):
|
||||||
if "task" not in complementary_data:
|
return complementary_data
|
||||||
return complementary_data
|
|
||||||
|
|
||||||
task = complementary_data["task"]
|
|
||||||
if task is None:
|
|
||||||
return complementary_data
|
|
||||||
|
|
||||||
new_complementary_data = dict(complementary_data)
|
|
||||||
if isinstance(task, str):
|
|
||||||
if not task.endswith("\n"):
|
|
||||||
new_complementary_data["task"] = f"{task}\n"
|
|
||||||
elif isinstance(task, list) and all(isinstance(t, str) for t in task):
|
|
||||||
new_complementary_data["task"] = [t if t.endswith("\n") else f"{t}\n" for t in task]
|
|
||||||
return new_complementary_data
|
|
||||||
|
|
||||||
def transform_features(self, features):
|
def transform_features(self, features):
|
||||||
return features
|
return features
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ class Qwen3VLInterface(torch.nn.Module):
|
|||||||
return torch.bfloat16
|
return torch.bfloat16
|
||||||
|
|
||||||
def expand_tokenizer(self) -> tuple[list[str], list[int], int]:
|
def expand_tokenizer(self) -> tuple[list[str], list[int], int]:
|
||||||
max_action_tokens = self.config.chunk_size * self.config.num_action_tokens_per_timestep
|
# starVLA/JEVLA checkpoints expand action tokens as action_horizon * 4,
|
||||||
|
# independent of vj2 num_action_tokens_per_timestep. Keeping this count
|
||||||
|
# is required for Qwen embedding/lm_head checkpoint shapes to match.
|
||||||
|
max_action_tokens = self.config.chunk_size * 4
|
||||||
tokenizer = self.processor.tokenizer
|
tokenizer = self.processor.tokenizer
|
||||||
action_tokens = []
|
action_tokens = []
|
||||||
action_token_ids = []
|
action_token_ids = []
|
||||||
|
|||||||
@@ -5,59 +5,298 @@ import torch.nn.functional as F # noqa: N812
|
|||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
|
|
||||||
def build_block_causal_attention_mask(num_steps: int, tokens_per_step: int, cond_tokens: int) -> torch.Tensor:
|
def build_action_block_causal_attention_mask(
|
||||||
total_tokens = num_steps * (tokens_per_step + cond_tokens)
|
num_frames: int, grid_height: int, grid_width: int, add_tokens: int = 1
|
||||||
mask = torch.full((total_tokens, total_tokens), float("-inf"))
|
) -> torch.Tensor:
|
||||||
for current_step in range(num_steps):
|
tokens_per_frame = add_tokens + grid_height * grid_width
|
||||||
row_start = current_step * (tokens_per_step + cond_tokens)
|
num_tokens = num_frames * tokens_per_frame
|
||||||
row_end = row_start + tokens_per_step + cond_tokens
|
mask = torch.zeros(num_tokens, num_tokens, dtype=torch.bool)
|
||||||
mask[row_start:row_end, :row_end] = 0
|
mask_block = torch.ones(tokens_per_frame, tokens_per_frame, dtype=torch.bool)
|
||||||
|
local_window_time = num_frames
|
||||||
|
|
||||||
|
for current_frame in range(num_frames):
|
||||||
|
first_context_frame = max(0, current_frame - local_window_time + 1)
|
||||||
|
for context_frame in range(first_context_frame, current_frame + 1):
|
||||||
|
row = slice(current_frame * tokens_per_frame, (current_frame + 1) * tokens_per_frame)
|
||||||
|
col = slice(context_frame * tokens_per_frame, (context_frame + 1) * tokens_per_frame)
|
||||||
|
mask[row, col] = mask_block
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
|
|
||||||
class _Attention(nn.Module):
|
def rotate_queries_or_keys(x: torch.Tensor, pos: torch.Tensor) -> torch.Tensor:
|
||||||
def __init__(self, embed_dim: int, num_heads: int) -> None:
|
_, _, _, dim = x.size()
|
||||||
|
if dim % 2 != 0:
|
||||||
|
raise ValueError("Embedding dimension must be even for rotary position encoding.")
|
||||||
|
|
||||||
|
omega = torch.arange(dim // 2, dtype=x.dtype, device=x.device)
|
||||||
|
omega /= dim / 2.0
|
||||||
|
omega = 1.0 / 10000**omega
|
||||||
|
freqs = torch.einsum("..., f -> ... f", pos, omega)
|
||||||
|
emb_sin = freqs.sin().squeeze(-1).repeat(1, 1, 1, 2)
|
||||||
|
emb_cos = freqs.cos().squeeze(-1).repeat(1, 1, 1, 2)
|
||||||
|
|
||||||
|
y = x.unflatten(-1, (-1, 2))
|
||||||
|
y1, y2 = y.unbind(dim=-1)
|
||||||
|
y = torch.stack((-y2, y1), dim=-1).flatten(-2)
|
||||||
|
return x * emb_cos + y * emb_sin
|
||||||
|
|
||||||
|
|
||||||
|
class DropPath(nn.Module):
|
||||||
|
def __init__(self, drop_prob: float = 0.0) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.num_heads = num_heads
|
self.drop_prob = drop_prob
|
||||||
self.head_dim = embed_dim // num_heads
|
|
||||||
self.qkv = nn.Linear(embed_dim, embed_dim * 3, bias=True)
|
|
||||||
self.proj = nn.Linear(embed_dim, embed_dim, bias=True)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor, attn_mask: torch.Tensor | None = None) -> torch.Tensor:
|
|
||||||
b, n, c = x.shape
|
|
||||||
qkv = self.qkv(x).reshape(b, n, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
|
|
||||||
q, k, v = qkv.unbind(0)
|
|
||||||
x = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
|
|
||||||
return self.proj(x.transpose(1, 2).reshape(b, n, c))
|
|
||||||
|
|
||||||
|
|
||||||
class _MLP(nn.Module):
|
|
||||||
def __init__(self, embed_dim: int, mlp_ratio: float) -> None:
|
|
||||||
super().__init__()
|
|
||||||
hidden = int(embed_dim * mlp_ratio)
|
|
||||||
self.fc1 = nn.Linear(embed_dim, hidden)
|
|
||||||
self.fc2 = nn.Linear(hidden, embed_dim)
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||||
return self.fc2(F.gelu(self.fc1(x)))
|
if self.drop_prob == 0.0 or not self.training:
|
||||||
|
return x
|
||||||
|
keep_prob = 1 - self.drop_prob
|
||||||
|
shape = (x.shape[0],) + (1,) * (x.ndim - 1)
|
||||||
|
random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
|
||||||
|
random_tensor.floor_()
|
||||||
|
return x.div(keep_prob) * random_tensor
|
||||||
|
|
||||||
|
|
||||||
class _PredictorBlock(nn.Module):
|
class MLP(nn.Module):
|
||||||
def __init__(self, embed_dim: int, num_heads: int, mlp_ratio: float) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
in_features: int,
|
||||||
|
hidden_features: int | None = None,
|
||||||
|
out_features: int | None = None,
|
||||||
|
act_layer: type[nn.Module] = nn.GELU,
|
||||||
|
drop: float = 0.0,
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.norm1 = nn.LayerNorm(embed_dim)
|
out_features = out_features or in_features
|
||||||
self.attn = _Attention(embed_dim, num_heads)
|
hidden_features = hidden_features or in_features
|
||||||
self.norm2 = nn.LayerNorm(embed_dim)
|
self.fc1 = nn.Linear(in_features, hidden_features)
|
||||||
self.mlp = _MLP(embed_dim, mlp_ratio)
|
self.act = act_layer()
|
||||||
|
self.fc2 = nn.Linear(hidden_features, out_features)
|
||||||
|
self.drop = nn.Dropout(drop)
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor, attn_mask: torch.Tensor | None = None) -> torch.Tensor:
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||||
x = x + self.attn(self.norm1(x), attn_mask=attn_mask)
|
x = self.fc1(x)
|
||||||
return x + self.mlp(self.norm2(x))
|
x = self.act(x)
|
||||||
|
x = self.drop(x)
|
||||||
|
x = self.fc2(x)
|
||||||
|
x = self.drop(x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
class ACRoPEAttention(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dim: int,
|
||||||
|
num_heads: int = 8,
|
||||||
|
qkv_bias: bool = False,
|
||||||
|
qk_scale: float | None = None,
|
||||||
|
attn_drop: float = 0.0,
|
||||||
|
proj_drop: float = 0.0,
|
||||||
|
use_sdpa: bool = True,
|
||||||
|
is_causal: bool = False,
|
||||||
|
grid_size: int = 16,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_dim = dim // num_heads
|
||||||
|
self.scale = qk_scale or self.head_dim**-0.5
|
||||||
|
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
||||||
|
self.attn_drop = nn.Dropout(attn_drop)
|
||||||
|
self.proj = nn.Linear(dim, dim)
|
||||||
|
self.proj_drop_prob = proj_drop
|
||||||
|
self.proj_drop = nn.Dropout(proj_drop)
|
||||||
|
self.use_sdpa = use_sdpa
|
||||||
|
self.d_dim = int(2 * ((self.head_dim // 3) // 2))
|
||||||
|
self.h_dim = int(2 * ((self.head_dim // 3) // 2))
|
||||||
|
self.w_dim = int(2 * ((self.head_dim // 3) // 2))
|
||||||
|
self.grid_size = grid_size
|
||||||
|
self.is_causal = is_causal
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_frame_pos(ids: torch.Tensor, height: int, width: int) -> torch.Tensor:
|
||||||
|
return ids // int(height * width)
|
||||||
|
|
||||||
|
def _get_height_pos(self, ids: torch.Tensor, height: int, width: int) -> torch.Tensor:
|
||||||
|
frame_ids = self._get_frame_pos(ids, height, width)
|
||||||
|
ids = ids - int(height * width) * frame_ids
|
||||||
|
return ids // width
|
||||||
|
|
||||||
|
def separate_positions(
|
||||||
|
self, ids: torch.Tensor, height: int, width: int
|
||||||
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
|
frame_ids = self._get_frame_pos(ids, height, width)
|
||||||
|
height_ids = self._get_height_pos(ids, height, width)
|
||||||
|
width_ids = ids - int(height * width) * frame_ids - width * height_ids
|
||||||
|
return 1.0 * frame_ids, 1.0 * height_ids, 1.0 * width_ids
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
x: torch.Tensor,
|
||||||
|
mask: torch.Tensor | None = None,
|
||||||
|
attn_mask: torch.Tensor | None = None,
|
||||||
|
num_frames: int | None = None,
|
||||||
|
grid_height: int | None = None,
|
||||||
|
grid_width: int | None = None,
|
||||||
|
action_tokens: int = 0,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
batch_size, num_tokens, channels = x.size()
|
||||||
|
if num_frames is None or grid_height is None or grid_width is None:
|
||||||
|
raise ValueError("num_frames, grid_height and grid_width are required.")
|
||||||
|
|
||||||
|
if mask is not None:
|
||||||
|
mask = mask.unsqueeze(1).repeat(1, self.num_heads, 1)
|
||||||
|
d_mask, h_mask, w_mask = self.separate_positions(mask, grid_height, grid_width)
|
||||||
|
else:
|
||||||
|
mask = torch.arange(int(num_frames * grid_height * grid_width), device=x.device)
|
||||||
|
d_mask, h_mask, w_mask = self.separate_positions(mask, grid_height, grid_width)
|
||||||
|
|
||||||
|
h_mask *= self.grid_size / grid_height
|
||||||
|
w_mask *= self.grid_size / grid_width
|
||||||
|
|
||||||
|
if action_tokens > 0:
|
||||||
|
x = x.view(batch_size, -1, action_tokens + grid_height * grid_width, channels)
|
||||||
|
action_q, action_k, action_v = [], [], []
|
||||||
|
for idx in range(action_tokens):
|
||||||
|
action_token = x[:, :, idx : idx + 1, :].flatten(1, 2)
|
||||||
|
qkv = self.qkv(action_token).unflatten(-1, (3, self.num_heads, -1)).permute(2, 0, 3, 1, 4)
|
||||||
|
q, k, v = qkv[0], qkv[1], qkv[2]
|
||||||
|
qd = rotate_queries_or_keys(q[..., : self.d_dim], pos=torch.arange(num_frames, device=x.device))
|
||||||
|
kd = rotate_queries_or_keys(k[..., : self.d_dim], pos=torch.arange(num_frames, device=x.device))
|
||||||
|
qr = q[..., self.d_dim :]
|
||||||
|
kr = k[..., self.d_dim :]
|
||||||
|
action_q.append(torch.cat([qd, qr], dim=-1).view(batch_size, self.num_heads, num_frames, 1, -1))
|
||||||
|
action_k.append(torch.cat([kd, kr], dim=-1).view(batch_size, self.num_heads, num_frames, 1, -1))
|
||||||
|
action_v.append(v.view(batch_size, self.num_heads, num_frames, 1, -1))
|
||||||
|
|
||||||
|
action_q = torch.cat(action_q, dim=3).flatten(2, 3)
|
||||||
|
action_k = torch.cat(action_k, dim=3).flatten(2, 3)
|
||||||
|
action_v = torch.cat(action_v, dim=3).flatten(2, 3)
|
||||||
|
x = x[:, :, action_tokens:, :].flatten(1, 2)
|
||||||
|
|
||||||
|
qkv = self.qkv(x).unflatten(-1, (3, self.num_heads, -1)).permute(2, 0, 3, 1, 4)
|
||||||
|
q, k, v = qkv[0], qkv[1], qkv[2]
|
||||||
|
|
||||||
|
offset = 0
|
||||||
|
qd = rotate_queries_or_keys(q[..., offset : offset + self.d_dim], pos=d_mask)
|
||||||
|
kd = rotate_queries_or_keys(k[..., offset : offset + self.d_dim], pos=d_mask)
|
||||||
|
offset += self.d_dim
|
||||||
|
qh = rotate_queries_or_keys(q[..., offset : offset + self.h_dim], pos=h_mask)
|
||||||
|
kh = rotate_queries_or_keys(k[..., offset : offset + self.h_dim], pos=h_mask)
|
||||||
|
offset += self.h_dim
|
||||||
|
qw = rotate_queries_or_keys(q[..., offset : offset + self.w_dim], pos=w_mask)
|
||||||
|
kw = rotate_queries_or_keys(k[..., offset : offset + self.w_dim], pos=w_mask)
|
||||||
|
offset += self.w_dim
|
||||||
|
|
||||||
|
if offset < self.head_dim:
|
||||||
|
q = torch.cat([qd, qh, qw, q[..., offset:]], dim=-1)
|
||||||
|
k = torch.cat([kd, kh, kw, k[..., offset:]], dim=-1)
|
||||||
|
else:
|
||||||
|
q = torch.cat([qd, qh, qw], dim=-1)
|
||||||
|
k = torch.cat([kd, kh, kw], dim=-1)
|
||||||
|
|
||||||
|
if action_tokens > 0:
|
||||||
|
|
||||||
|
def merge(frame_tokens: torch.Tensor, action_token_values: torch.Tensor) -> torch.Tensor:
|
||||||
|
frame_tokens = frame_tokens.view(
|
||||||
|
batch_size, self.num_heads, num_frames, grid_height * grid_width, -1
|
||||||
|
)
|
||||||
|
action_token_values = action_token_values.view(
|
||||||
|
batch_size, self.num_heads, num_frames, action_tokens, -1
|
||||||
|
)
|
||||||
|
return torch.cat([action_token_values, frame_tokens], dim=3).flatten(2, 3)
|
||||||
|
|
||||||
|
q = merge(q, action_q)
|
||||||
|
k = merge(k, action_k)
|
||||||
|
v = merge(v, action_v)
|
||||||
|
|
||||||
|
if attn_mask is not None or self.use_sdpa:
|
||||||
|
x = F.scaled_dot_product_attention(
|
||||||
|
q, k, v, dropout_p=self.proj_drop_prob, is_causal=self.is_causal, attn_mask=attn_mask
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
attn = (q @ k.transpose(-2, -1)) * self.scale
|
||||||
|
attn = attn.softmax(dim=-1)
|
||||||
|
attn = self.attn_drop(attn)
|
||||||
|
x = attn @ v
|
||||||
|
|
||||||
|
x = x.transpose(1, 2).reshape(batch_size, num_tokens, channels)
|
||||||
|
x = self.proj(x)
|
||||||
|
return self.proj_drop(x)
|
||||||
|
|
||||||
|
|
||||||
|
class ACBlock(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dim: int,
|
||||||
|
num_heads: int,
|
||||||
|
mlp_ratio: float = 4.0,
|
||||||
|
qkv_bias: bool = True,
|
||||||
|
qk_scale: float | None = None,
|
||||||
|
drop: float = 0.0,
|
||||||
|
attn_drop: float = 0.0,
|
||||||
|
drop_path: float = 0.0,
|
||||||
|
norm_layer: type[nn.Module] = nn.LayerNorm,
|
||||||
|
use_sdpa: bool = True,
|
||||||
|
is_causal: bool = False,
|
||||||
|
grid_size: int = 16,
|
||||||
|
use_rope: bool = True,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.norm1 = norm_layer(dim)
|
||||||
|
if not use_rope:
|
||||||
|
raise ValueError("JEVLA1 world predictor uses AC RoPE attention.")
|
||||||
|
self.attn = ACRoPEAttention(
|
||||||
|
dim,
|
||||||
|
num_heads=num_heads,
|
||||||
|
qkv_bias=qkv_bias,
|
||||||
|
qk_scale=qk_scale,
|
||||||
|
attn_drop=attn_drop,
|
||||||
|
use_sdpa=use_sdpa,
|
||||||
|
is_causal=is_causal,
|
||||||
|
grid_size=grid_size,
|
||||||
|
proj_drop=drop,
|
||||||
|
)
|
||||||
|
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
||||||
|
self.norm2 = norm_layer(dim)
|
||||||
|
self.mlp = MLP(
|
||||||
|
in_features=dim,
|
||||||
|
hidden_features=int(dim * mlp_ratio),
|
||||||
|
act_layer=nn.GELU,
|
||||||
|
drop=drop,
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
x: torch.Tensor,
|
||||||
|
attn_mask: torch.Tensor | None = None,
|
||||||
|
num_frames: int | None = None,
|
||||||
|
grid_height: int | None = None,
|
||||||
|
grid_width: int | None = None,
|
||||||
|
action_tokens: int = 0,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
y = self.norm1(x)
|
||||||
|
y = self.attn(
|
||||||
|
y,
|
||||||
|
mask=None,
|
||||||
|
attn_mask=attn_mask,
|
||||||
|
num_frames=num_frames,
|
||||||
|
grid_height=grid_height,
|
||||||
|
grid_width=grid_width,
|
||||||
|
action_tokens=action_tokens,
|
||||||
|
)
|
||||||
|
x = x + self.drop_path(y)
|
||||||
|
y = self.norm2(x)
|
||||||
|
return x + self.drop_path(self.mlp(y))
|
||||||
|
|
||||||
|
|
||||||
class ActionConditionedVideoPredictor(nn.Module):
|
class ActionConditionedVideoPredictor(nn.Module):
|
||||||
|
"""JEVLA1-compatible action-conditioned V-JEPA predictor."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
num_frames: int,
|
||||||
|
img_size: tuple[int, int],
|
||||||
|
patch_size: int,
|
||||||
|
tubelet_size: int,
|
||||||
embed_dim: int,
|
embed_dim: int,
|
||||||
action_embed_dim: int,
|
action_embed_dim: int,
|
||||||
predictor_embed_dim: int,
|
predictor_embed_dim: int,
|
||||||
@@ -65,40 +304,93 @@ class ActionConditionedVideoPredictor(nn.Module):
|
|||||||
num_heads: int,
|
num_heads: int,
|
||||||
mlp_ratio: float,
|
mlp_ratio: float,
|
||||||
num_action_tokens_per_step: int,
|
num_action_tokens_per_step: int,
|
||||||
|
use_extrinsics: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.predictor_embed = nn.Linear(embed_dim, predictor_embed_dim)
|
self.is_frame_causal = True
|
||||||
self.action_encoder = nn.Linear(action_embed_dim, predictor_embed_dim)
|
self.use_extrinsics = use_extrinsics
|
||||||
|
self.predictor_embed = nn.Linear(embed_dim, predictor_embed_dim, bias=True)
|
||||||
|
self.action_encoder = nn.Linear(action_embed_dim, predictor_embed_dim, bias=True)
|
||||||
|
self.state_encoder = nn.Linear(action_embed_dim, predictor_embed_dim, bias=True)
|
||||||
|
self.extrinsics_encoder = nn.Linear(action_embed_dim - 1, predictor_embed_dim, bias=True)
|
||||||
|
|
||||||
|
self.img_height, self.img_width = img_size
|
||||||
|
self.patch_size = patch_size
|
||||||
|
self.num_frames = num_frames
|
||||||
|
self.tubelet_size = tubelet_size
|
||||||
|
self.grid_height = self.img_height // self.patch_size
|
||||||
|
self.grid_width = self.img_width // self.patch_size
|
||||||
|
|
||||||
self.predictor_blocks = nn.ModuleList(
|
self.predictor_blocks = nn.ModuleList(
|
||||||
[_PredictorBlock(predictor_embed_dim, num_heads, mlp_ratio) for _ in range(depth)]
|
[
|
||||||
|
ACBlock(
|
||||||
|
dim=predictor_embed_dim,
|
||||||
|
num_heads=num_heads,
|
||||||
|
mlp_ratio=mlp_ratio,
|
||||||
|
qkv_bias=True,
|
||||||
|
drop=0.0,
|
||||||
|
attn_drop=0.0,
|
||||||
|
drop_path=0.0,
|
||||||
|
norm_layer=lambda dim: nn.LayerNorm(dim, eps=1e-6),
|
||||||
|
grid_size=self.grid_height,
|
||||||
|
use_rope=True,
|
||||||
|
)
|
||||||
|
for _ in range(depth)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
self.predictor_norm = nn.LayerNorm(predictor_embed_dim)
|
self.predictor_norm = nn.LayerNorm(predictor_embed_dim, eps=1e-6)
|
||||||
self.predictor_proj = nn.Linear(predictor_embed_dim, embed_dim)
|
self.predictor_proj = nn.Linear(predictor_embed_dim, embed_dim, bias=True)
|
||||||
self.num_action_tokens_per_step = num_action_tokens_per_step
|
self.num_action_tokens_per_step = num_action_tokens_per_step
|
||||||
|
|
||||||
def forward(self, frame_tokens: torch.Tensor, action_tokens: torch.Tensor) -> torch.Tensor:
|
@property
|
||||||
batch_size, num_steps, tokens_per_frame, _ = frame_tokens.shape
|
def norm(self) -> nn.LayerNorm:
|
||||||
_, action_steps, _, _ = action_tokens.shape
|
return self.predictor_norm
|
||||||
if action_steps != num_steps:
|
|
||||||
raise ValueError(f"Expected {num_steps} action steps, got {action_steps}.")
|
|
||||||
|
|
||||||
frame_tokens = self.predictor_embed(frame_tokens)
|
@property
|
||||||
action_tokens = self.action_encoder(action_tokens)
|
def proj(self) -> nn.Linear:
|
||||||
fused_steps = [
|
return self.predictor_proj
|
||||||
torch.cat([action_tokens[:, step], frame_tokens[:, step]], dim=1) for step in range(num_steps)
|
|
||||||
]
|
|
||||||
fused = torch.cat(fused_steps, dim=1)
|
|
||||||
|
|
||||||
attn_mask = build_block_causal_attention_mask(
|
def forward(
|
||||||
num_steps=num_steps,
|
self,
|
||||||
tokens_per_step=tokens_per_frame,
|
frame_tokens: torch.Tensor,
|
||||||
cond_tokens=self.num_action_tokens_per_step,
|
action_tokens: torch.Tensor,
|
||||||
).to(device=fused.device, dtype=fused.dtype)
|
extrinsics: torch.Tensor | None = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
# starVLA input convention: frame_tokens [B, T*H*W, D], actions [B, T*A, D].
|
||||||
|
x = self.predictor_embed(frame_tokens)
|
||||||
|
batch_size, num_context_tokens, hidden_dim = x.size()
|
||||||
|
num_frames = num_context_tokens // (self.grid_height * self.grid_width)
|
||||||
|
|
||||||
|
actions = self.action_encoder(action_tokens)
|
||||||
|
actions = actions.view(batch_size, num_frames, -1, hidden_dim)
|
||||||
|
cond_tokens = actions.shape[2]
|
||||||
|
|
||||||
|
x = x.view(batch_size, num_frames, self.grid_height * self.grid_width, hidden_dim)
|
||||||
|
if self.use_extrinsics:
|
||||||
|
if extrinsics is None:
|
||||||
|
raise ValueError("extrinsics are required when use_extrinsics=True.")
|
||||||
|
cond_tokens += 1
|
||||||
|
extrinsic_tokens = self.extrinsics_encoder(extrinsics).unsqueeze(2)
|
||||||
|
x = torch.cat([actions, extrinsic_tokens, x], dim=2).flatten(1, 2)
|
||||||
|
else:
|
||||||
|
x = torch.cat([actions, x], dim=2).flatten(1, 2)
|
||||||
|
|
||||||
|
attn_mask = build_action_block_causal_attention_mask(
|
||||||
|
num_frames, self.grid_height, self.grid_width, add_tokens=cond_tokens
|
||||||
|
)
|
||||||
|
attn_mask = attn_mask[: x.size(1), : x.size(1)].to(x.device, non_blocking=True)
|
||||||
|
|
||||||
for block in self.predictor_blocks:
|
for block in self.predictor_blocks:
|
||||||
fused = block(fused, attn_mask=attn_mask)
|
x = block(
|
||||||
|
x,
|
||||||
|
attn_mask=attn_mask,
|
||||||
|
num_frames=num_frames,
|
||||||
|
grid_height=self.grid_height,
|
||||||
|
grid_width=self.grid_width,
|
||||||
|
action_tokens=cond_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
fused = self.predictor_norm(fused)
|
x = x.view(batch_size, num_frames, cond_tokens + self.grid_height * self.grid_width, hidden_dim)
|
||||||
fused = fused.view(batch_size, num_steps, self.num_action_tokens_per_step + tokens_per_frame, -1)
|
x = x[:, :, cond_tokens:, :].flatten(1, 2)
|
||||||
predicted_frame_tokens = fused[:, :, self.num_action_tokens_per_step :, :]
|
x = self.predictor_norm(x)
|
||||||
return self.predictor_proj(predicted_frame_tokens)
|
return self.predictor_proj(x)
|
||||||
|
|||||||
Reference in New Issue
Block a user