mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-24 02:06:15 +00:00
refactor(vla-jepa): removing gpu roundtrip for the preprocessing part
This commit is contained in:
committed by
Maximellerbach
parent
49755a3d9e
commit
877847c90e
@@ -19,10 +19,8 @@ from collections import deque
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
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 torch import Tensor, nn
|
from torch import Tensor, nn
|
||||||
|
|
||||||
from lerobot.policies.pretrained import PreTrainedPolicy, T
|
from lerobot.policies.pretrained import PreTrainedPolicy, T
|
||||||
@@ -56,11 +54,11 @@ class VLAJEPAModel(nn.Module):
|
|||||||
- V-JEPA: world model for video frame prediction
|
- V-JEPA: world model for video frame prediction
|
||||||
|
|
||||||
Input: List[dict] native format (same as original starVLA)
|
Input: List[dict] native format (same as original starVLA)
|
||||||
- "image": List[PIL.Image] (multi-view images)
|
- "image": List[Tensor [C, H, W]] (float [0,1], multi-view images)
|
||||||
- "video": np.ndarray [V, T, H, W, 3]
|
- "video": Tensor [V, T, C, H, W] (float [0,1])
|
||||||
- "lang": str (task instruction)
|
- "lang": str (task instruction)
|
||||||
- "action": np.ndarray [T, action_dim] (optional, training only)
|
- "action": Tensor [T, action_dim] (optional, training only)
|
||||||
- "state": np.ndarray [1, state_dim] (optional)
|
- "state": Tensor [1, state_dim] (optional)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: VLAJEPAConfig) -> None:
|
def __init__(self, config: VLAJEPAConfig) -> None:
|
||||||
@@ -167,18 +165,18 @@ class VLAJEPAModel(nn.Module):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
examples: List of per-sample dicts with keys:
|
examples: List of per-sample dicts with keys:
|
||||||
"image" : List[PIL.Image] — multi-view images
|
"image" : List[Tensor [C, H, W]] (float [0,1]) — multi-view images
|
||||||
"video" : np.ndarray [V, T, H, W, 3]
|
"video" : Tensor [V, T, C, H, W] (float [0,1])
|
||||||
"lang" : str — task instruction
|
"lang" : str — task instruction
|
||||||
"action" : np.ndarray [T, action_dim] (optional)
|
"action" : Tensor [T, action_dim] (optional)
|
||||||
"state" : np.ndarray [1, state_dim] (optional)
|
"state" : Tensor [1, state_dim] (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict with "action_loss" and "wm_loss" keys (scalar Tensors).
|
dict with "action_loss" and "wm_loss" keys (scalar Tensors).
|
||||||
"""
|
"""
|
||||||
# Unpack native format (same pattern as original VLA_JEPA.py)
|
# Unpack native format (same pattern as original VLA_JEPA.py)
|
||||||
batch_images = [ex["image"] for ex in examples] # List[List[PIL.Image]]
|
batch_images = [ex["image"] for ex in examples] # List[List[Tensor [C, H, W]]]
|
||||||
batch_videos = [ex["video"] for ex in examples] # List[np.ndarray]
|
batch_videos = [ex["video"] for ex in examples] # List[Tensor [V, T, C, H, W]]
|
||||||
instructions = [ex["lang"] for ex in examples] # List[str]
|
instructions = [ex["lang"] for ex in examples] # List[str]
|
||||||
has_action = "action" in examples[0] and examples[0]["action"] is not None
|
has_action = "action" in examples[0] and examples[0]["action"] is not None
|
||||||
actions = [ex["action"] for ex in examples] if has_action else None
|
actions = [ex["action"] for ex in examples] if has_action else None
|
||||||
@@ -190,9 +188,8 @@ class VLAJEPAModel(nn.Module):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stack videos: [B, V, T, H, W, 3] -> [B, V, T, 3, H, W]
|
# Stack videos: List[[V, T, 3, H, W]] -> [B, V, T, 3, H, W] (already channels-first)
|
||||||
batch_videos = np.stack(batch_videos)
|
batch_videos = torch.stack(batch_videos)
|
||||||
batch_videos = batch_videos.transpose(0, 1, 2, 5, 3, 4) # [B, V, T, 3, H, W]
|
|
||||||
|
|
||||||
# Adjust number of views for the world model:
|
# Adjust number of views for the world model:
|
||||||
# - fewer views than expected: duplicate the first view to fill up
|
# - fewer views than expected: duplicate the first view to fill up
|
||||||
@@ -200,8 +197,8 @@ class VLAJEPAModel(nn.Module):
|
|||||||
num_views_world_model = self.config.jepa_tubelet_size
|
num_views_world_model = self.config.jepa_tubelet_size
|
||||||
if batch_videos.shape[1] < num_views_world_model:
|
if batch_videos.shape[1] < num_views_world_model:
|
||||||
num_missing_views = num_views_world_model - batch_videos.shape[1]
|
num_missing_views = num_views_world_model - batch_videos.shape[1]
|
||||||
first_view = np.repeat(batch_videos[:, :1], num_missing_views, axis=1)
|
first_view = batch_videos[:, :1].repeat(1, num_missing_views, 1, 1, 1, 1)
|
||||||
batch_videos = np.concatenate([batch_videos, first_view], axis=1)
|
batch_videos = torch.cat([batch_videos, first_view], dim=1)
|
||||||
elif batch_videos.shape[1] > num_views_world_model:
|
elif batch_videos.shape[1] > num_views_world_model:
|
||||||
batch_videos = batch_videos[:, :num_views_world_model]
|
batch_videos = batch_videos[:, :num_views_world_model]
|
||||||
|
|
||||||
@@ -244,9 +241,15 @@ class VLAJEPAModel(nn.Module):
|
|||||||
b, v, t_frames, c, h_img, w_img = batch_videos.shape
|
b, v, t_frames, c, h_img, w_img = batch_videos.shape
|
||||||
batch_videos_flat = batch_videos.reshape(b * v, t_frames, c, h_img, w_img)
|
batch_videos_flat = batch_videos.reshape(b * v, t_frames, c, h_img, w_img)
|
||||||
|
|
||||||
video_pixels = self.video_processor(videos=list(batch_videos_flat), return_tensors="pt")[
|
# Fast (torchvision) video processor: pass GPU float [0,1] tensors + device so the
|
||||||
"pixel_values_videos"
|
# resize/normalize stays on-device (no GPU->CPU->GPU roundtrip). do_rescale=False
|
||||||
].to(self.video_encoder.device) # [B*V, T, C, H, W]
|
# because the frames already arrive in [0, 1].
|
||||||
|
video_pixels = self.video_processor(
|
||||||
|
videos=list(batch_videos_flat),
|
||||||
|
return_tensors="pt",
|
||||||
|
device=self.video_encoder.device,
|
||||||
|
do_rescale=False,
|
||||||
|
)["pixel_values_videos"] # [B*V, T, C, H, W]
|
||||||
|
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
video_embeddings = self.video_encoder.get_vision_features(pixel_values_videos=video_pixels)
|
video_embeddings = self.video_encoder.get_vision_features(pixel_values_videos=video_pixels)
|
||||||
@@ -286,16 +289,16 @@ class VLAJEPAModel(nn.Module):
|
|||||||
|
|
||||||
# ---- Step 4: Action Head ----
|
# ---- Step 4: Action Head ----
|
||||||
with torch.autocast(device_type=device_type, dtype=torch.float32):
|
with torch.autocast(device_type=device_type, dtype=torch.float32):
|
||||||
actions_tensor = torch.tensor(
|
actions_tensor = torch.stack(actions).to(
|
||||||
np.array(actions), device=last_hidden.device, dtype=torch.float32
|
device=last_hidden.device, dtype=torch.float32
|
||||||
) # [B, T_full, action_dim]
|
) # [B, T_full, action_dim]
|
||||||
action_horizon = self.config.chunk_size
|
action_horizon = self.config.chunk_size
|
||||||
actions_target = actions_tensor[:, -action_horizon:, :]
|
actions_target = actions_tensor[:, -action_horizon:, :]
|
||||||
|
|
||||||
state_tensor = None
|
state_tensor = None
|
||||||
if state is not None:
|
if state is not None:
|
||||||
state_tensor = torch.tensor(
|
state_tensor = torch.stack(state).to(
|
||||||
np.array(state), device=last_hidden.device, dtype=last_hidden.dtype
|
device=last_hidden.device, dtype=last_hidden.dtype
|
||||||
) # [B, 1, state_dim]
|
) # [B, 1, state_dim]
|
||||||
|
|
||||||
repeated_diffusion_steps = self.config.repeated_diffusion_steps
|
repeated_diffusion_steps = self.config.repeated_diffusion_steps
|
||||||
@@ -307,12 +310,7 @@ class VLAJEPAModel(nn.Module):
|
|||||||
action_is_pad_rep = None
|
action_is_pad_rep = None
|
||||||
if action_is_pad is not None:
|
if action_is_pad is not None:
|
||||||
pad_tensor = torch.stack(
|
pad_tensor = torch.stack(
|
||||||
[
|
[p.to(actions_target.device) for p in action_is_pad]
|
||||||
p.to(actions_target.device)
|
|
||||||
if isinstance(p, Tensor)
|
|
||||||
else torch.tensor(p, device=actions_target.device)
|
|
||||||
for p in action_is_pad
|
|
||||||
]
|
|
||||||
) # [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(repeated_diffusion_steps, 1) # [B*R, action_horizon]
|
action_is_pad_rep = pad_tensor.repeat(repeated_diffusion_steps, 1) # [B*R, action_horizon]
|
||||||
@@ -328,26 +326,30 @@ class VLAJEPAModel(nn.Module):
|
|||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
def predict_action(
|
def predict_action(
|
||||||
self,
|
self,
|
||||||
batch_images: list[list[Image.Image]],
|
batch_images: list[list[Tensor]],
|
||||||
instructions: list[str],
|
instructions: list[str],
|
||||||
state: np.ndarray | None = None,
|
state: Tensor | None = None,
|
||||||
) -> np.ndarray:
|
) -> Tensor:
|
||||||
"""
|
"""
|
||||||
Native action prediction following original VLA_JEPA.predict_action.
|
Native action prediction following original VLA_JEPA.predict_action.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
batch_images: List of samples; each is List[PIL.Image] (multi-view).
|
batch_images: List of samples; each is List[Tensor [C, H, W]] (float [0,1], multi-view).
|
||||||
instructions: Task instructions, one per sample.
|
instructions: Task instructions, one per sample.
|
||||||
state: Optional [B, state_dim] numpy array.
|
state: Optional [B, state_dim] tensor.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
np.ndarray [B, action_horizon, action_dim] — predicted actions.
|
Tensor [B, action_horizon, action_dim] — predicted actions (on the model device).
|
||||||
"""
|
"""
|
||||||
if self.config.resize_images_to is not None:
|
if self.config.resize_images_to is not None:
|
||||||
height, width = self.config.resize_images_to
|
height, width = self.config.resize_images_to
|
||||||
resampling = getattr(Image, "Resampling", Image).BOX
|
# PIL BOX resampling ~= area-averaging downsample; F.interpolate(mode="area")
|
||||||
|
# is the on-GPU equivalent. Images stay float [0,1] (do_rescale=False downstream).
|
||||||
batch_images = [
|
batch_images = [
|
||||||
[image.resize((width, height), resample=resampling) for image in sample_images]
|
[
|
||||||
|
F.interpolate(image[None], size=(height, width), mode="area")[0]
|
||||||
|
for image in sample_images
|
||||||
|
]
|
||||||
for sample_images in batch_images
|
for sample_images in batch_images
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -370,15 +372,13 @@ class VLAJEPAModel(nn.Module):
|
|||||||
|
|
||||||
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 = state.to(device=last_hidden.device, dtype=last_hidden.dtype)
|
||||||
device=last_hidden.device, dtype=last_hidden.dtype
|
|
||||||
)
|
|
||||||
|
|
||||||
pred_actions = self.action_model.predict_action(
|
pred_actions = self.action_model.predict_action(
|
||||||
embodied_action_tokens.float(), state_tensor.float() if state_tensor is not None else None
|
embodied_action_tokens.float(), state_tensor.float() if state_tensor is not None else None
|
||||||
) # [B, action_horizon, action_dim]
|
) # [B, action_horizon, action_dim]
|
||||||
|
|
||||||
return pred_actions.detach().cpu().numpy()
|
return pred_actions
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -431,13 +431,13 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
"task": str | List[str], (optional instruction)
|
"task": str | List[str], (optional instruction)
|
||||||
}
|
}
|
||||||
|
|
||||||
Native format (List[dict]):
|
Native format (List[dict]), all tensors kept on the batch device:
|
||||||
{
|
{
|
||||||
"image": List[PIL.Image], # multi-view images per sample
|
"image": List[Tensor [C, H, W]] (float [0,1]), # multi-view images per sample
|
||||||
"video": np.ndarray [V, T, H, W, 3],
|
"video": Tensor [V, T, C, H, W] (float [0,1]),
|
||||||
"lang": str, # task instruction
|
"lang": str, # task instruction
|
||||||
"action": np.ndarray [T, action_dim], # optional
|
"action": Tensor [T, action_dim], # optional
|
||||||
"state": np.ndarray [1, state_dim], # optional
|
"state": Tensor [1, state_dim], # optional
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
# Determine batch size from the first image feature
|
# Determine batch size from the first image feature
|
||||||
@@ -449,8 +449,8 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
batch_size = first_tensor.shape[0]
|
batch_size = first_tensor.shape[0]
|
||||||
|
|
||||||
# ---- Collect images per sample ----
|
# ---- Collect images per sample ----
|
||||||
# images_per_sample[b][v] = PIL.Image for view v
|
# images_per_sample[b][v] = float [0,1] Tensor [C, H, W] for view v (kept on-device)
|
||||||
images_per_sample: list[list[Image.Image]] = [[] for _ in range(batch_size)]
|
images_per_sample: list[list[Tensor]] = [[] for _ in range(batch_size)]
|
||||||
for key in image_keys:
|
for key in image_keys:
|
||||||
tensor = batch[key] # [B, C, H, W] or [B, T, C, H, W]
|
tensor = batch[key] # [B, C, H, W] or [B, T, C, H, W]
|
||||||
if tensor.ndim == 5:
|
if tensor.ndim == 5:
|
||||||
@@ -458,20 +458,10 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
# index 0 is the current observation (delta=0)
|
# index 0 is the current observation (delta=0)
|
||||||
tensor = tensor[:, 0]
|
tensor = tensor[:, 0]
|
||||||
for b in range(batch_size):
|
for b in range(batch_size):
|
||||||
images_per_sample[b].append(self.model.qwen.tensor_to_pil(tensor[b]))
|
images_per_sample[b].append(self.model.qwen.to_pixel_values(tensor[b]))
|
||||||
|
|
||||||
# ---- Collect videos per sample ----
|
# ---- Collect videos per sample ----
|
||||||
# Build video arrays: for each sample, stack views as [V, T, H, W, 3]
|
# Build video tensors: for each sample, stack views as [V, T, C, H, W] (float [0,1], on-device)
|
||||||
# Check whether any image feature has a time dimension
|
|
||||||
video_source = None
|
|
||||||
for k in image_keys:
|
|
||||||
if k in batch:
|
|
||||||
video_source = batch[k] # Use first available for shape inspection
|
|
||||||
break
|
|
||||||
|
|
||||||
if video_source is None:
|
|
||||||
raise ValueError("No image data found in batch for video construction.")
|
|
||||||
|
|
||||||
videos_per_sample = []
|
videos_per_sample = []
|
||||||
for b in range(batch_size):
|
for b in range(batch_size):
|
||||||
sample_views = []
|
sample_views = []
|
||||||
@@ -479,15 +469,9 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
t = batch[k][b] # [C, H, W] or [T, C, H, W]
|
t = batch[k][b] # [C, H, W] or [T, C, H, W]
|
||||||
if t.ndim == 3:
|
if t.ndim == 3:
|
||||||
t = t.unsqueeze(0) # [1, C, H, W]
|
t = t.unsqueeze(0) # [1, C, H, W]
|
||||||
# Convert to [T, H, W, 3] numpy
|
sample_views.append(self.model.qwen.to_pixel_values(t))
|
||||||
t_np = t.permute(0, 2, 3, 1).detach().cpu().float().numpy()
|
# Stack views: [V, T, C, H, W]
|
||||||
# Clamp to [0, 255]
|
videos_per_sample.append(torch.stack(sample_views, dim=0))
|
||||||
if t_np.max() <= 1.0:
|
|
||||||
t_np = t_np * 255.0
|
|
||||||
t_np = np.rint(t_np.clip(0, 255)).astype(np.uint8)
|
|
||||||
sample_views.append(t_np)
|
|
||||||
# Stack views: [V, T, H, W, 3]
|
|
||||||
videos_per_sample.append(np.stack(sample_views, axis=0))
|
|
||||||
|
|
||||||
# ---- Collect instructions ----
|
# ---- Collect instructions ----
|
||||||
tasks = batch.get("task")
|
tasks = batch.get("task")
|
||||||
@@ -505,10 +489,10 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
if actions_tensor is not None:
|
if actions_tensor is not None:
|
||||||
if actions_tensor.ndim == 2:
|
if actions_tensor.ndim == 2:
|
||||||
actions_tensor = actions_tensor.unsqueeze(1)
|
actions_tensor = actions_tensor.unsqueeze(1)
|
||||||
actions_list = [actions_tensor[b].detach().cpu().float().numpy() for b in range(batch_size)]
|
actions_list = [actions_tensor[b].detach().float() for b in range(batch_size)]
|
||||||
action_is_pad_tensor = batch.get("action_is_pad")
|
action_is_pad_tensor = batch.get("action_is_pad")
|
||||||
if action_is_pad_tensor is not None:
|
if action_is_pad_tensor is not None:
|
||||||
action_is_pad_list = [action_is_pad_tensor[b].detach().cpu() for b in range(batch_size)]
|
action_is_pad_list = [action_is_pad_tensor[b].detach() for b in range(batch_size)]
|
||||||
|
|
||||||
# ---- Collect state ----
|
# ---- Collect state ----
|
||||||
state_list = None
|
state_list = None
|
||||||
@@ -518,7 +502,7 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
state_tensor = state_tensor[:, -1, :]
|
state_tensor = state_tensor[:, -1, :]
|
||||||
if state_tensor.ndim == 2:
|
if state_tensor.ndim == 2:
|
||||||
state_tensor = state_tensor.unsqueeze(1) # [B, 1, state_dim]
|
state_tensor = state_tensor.unsqueeze(1) # [B, 1, state_dim]
|
||||||
state_list = [state_tensor[b].detach().cpu().float().numpy() for b in range(batch_size)]
|
state_list = [state_tensor[b].detach().float() for b in range(batch_size)]
|
||||||
|
|
||||||
# ---- Assemble native examples ----
|
# ---- Assemble native examples ----
|
||||||
examples = []
|
examples = []
|
||||||
@@ -565,12 +549,12 @@ class VLAJEPAPolicy(PreTrainedPolicy):
|
|||||||
batch_images = [ex["image"] for ex in examples]
|
batch_images = [ex["image"] for ex in examples]
|
||||||
instructions = [ex["lang"] for ex in examples]
|
instructions = [ex["lang"] for ex in examples]
|
||||||
|
|
||||||
state_np = None
|
state = None
|
||||||
if "state" in examples[0] and examples[0]["state"] is not None:
|
if "state" in examples[0] and examples[0]["state"] is not None:
|
||||||
state_np = np.stack([ex["state"] for ex in examples])
|
state = torch.stack([ex["state"] for ex in examples])
|
||||||
|
|
||||||
actions_np = self.model.predict_action(batch_images, instructions, state_np)
|
actions = self.model.predict_action(batch_images, instructions, state)
|
||||||
return torch.from_numpy(actions_np).to(device=self.config.device, dtype=torch.float32)
|
return actions.to(device=self.config.device, dtype=torch.float32)
|
||||||
|
|
||||||
@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:
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
import torch
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from lerobot.utils.import_utils import _transformers_available
|
from lerobot.utils.import_utils import _transformers_available
|
||||||
|
|
||||||
@@ -78,7 +76,7 @@ class Qwen3VLInterface(torch.nn.Module):
|
|||||||
|
|
||||||
def build_inputs(
|
def build_inputs(
|
||||||
self,
|
self,
|
||||||
images: Sequence[Sequence[Image.Image]],
|
images: Sequence[Sequence[torch.Tensor]],
|
||||||
instructions: Sequence[str],
|
instructions: Sequence[str],
|
||||||
action_prompt: str,
|
action_prompt: str,
|
||||||
embodied_prompt: str,
|
embodied_prompt: str,
|
||||||
@@ -94,24 +92,37 @@ class Qwen3VLInterface(torch.nn.Module):
|
|||||||
content.append({"type": "text", "text": prompt})
|
content.append({"type": "text", "text": prompt})
|
||||||
messages.append([{"role": "user", "content": content}])
|
messages.append([{"role": "user", "content": content}])
|
||||||
|
|
||||||
|
# The Qwen image processor is a torchvision-backed fast processor: passing the
|
||||||
|
# images as GPU tensors (with `device`) keeps the whole vision pipeline on-device
|
||||||
|
# and avoids a GPU->CPU->GPU roundtrip. The image tensors are forwarded through
|
||||||
|
# apply_chat_template untouched into Qwen3VLProcessor.__call__.
|
||||||
|
# do_rescale=False: images already arrive as float in [0, 1] (the dataset decoder
|
||||||
|
# yields float32/255 and VISUAL normalization is IDENTITY), so we skip the
|
||||||
|
# processor's /255 rescale instead of round-tripping through uint8.
|
||||||
batch_inputs = self.processor.apply_chat_template(
|
batch_inputs = self.processor.apply_chat_template(
|
||||||
messages,
|
messages,
|
||||||
tokenize=True,
|
tokenize=True,
|
||||||
add_generation_prompt=True,
|
add_generation_prompt=True,
|
||||||
return_dict=True,
|
return_dict=True,
|
||||||
processor_kwargs={"padding": True, "return_tensors": "pt"},
|
processor_kwargs={
|
||||||
|
"padding": True,
|
||||||
|
"return_tensors": "pt",
|
||||||
|
"device": self.model.device,
|
||||||
|
"do_rescale": False,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return batch_inputs.to(self.model.device)
|
return batch_inputs.to(self.model.device)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tensor_to_pil(image_tensor: torch.Tensor) -> Image.Image:
|
def to_pixel_values(image_tensor: torch.Tensor) -> torch.Tensor:
|
||||||
image = image_tensor.detach().cpu()
|
"""Prepare an image/video tensor for the fast processors (used with do_rescale=False).
|
||||||
if image.ndim == 3 and image.shape[0] in (1, 3):
|
|
||||||
image = image.permute(1, 2, 0)
|
The dataset decoder yields float32 in [0, 1] (channels-first) and VISUAL
|
||||||
image = image.float()
|
normalization is IDENTITY, so the tensor already arrives in [0, 1]; we pass it
|
||||||
if image.max() <= 1.0:
|
through as float and let the processors normalize (no rescale, no uint8
|
||||||
image = image * 255.0
|
quantization). A single channel is expanded to 3 to match the RGB processors.
|
||||||
image = image.clamp(0, 255).round().to(torch.uint8).numpy()
|
"""
|
||||||
if image.shape[-1] == 1:
|
image = image_tensor.detach().float()
|
||||||
image = np.repeat(image, 3, axis=-1)
|
if image.ndim == 3 and image.shape[0] == 1:
|
||||||
return Image.fromarray(image)
|
image = image.repeat(3, 1, 1)
|
||||||
|
return image
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from types import SimpleNamespace
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
from PIL import Image
|
|
||||||
from torch import Tensor, nn
|
from torch import Tensor, nn
|
||||||
|
|
||||||
from lerobot.configs.types import FeatureType, PolicyFeature
|
from lerobot.configs.types import FeatureType, PolicyFeature
|
||||||
@@ -191,7 +190,7 @@ class _FakeQwenInterface(nn.Module):
|
|||||||
|
|
||||||
def build_inputs(
|
def build_inputs(
|
||||||
self,
|
self,
|
||||||
images: list[list[Image.Image]],
|
images: list[list[Tensor]],
|
||||||
instructions: list[str],
|
instructions: list[str],
|
||||||
action_prompt: str,
|
action_prompt: str,
|
||||||
embodied_prompt: str,
|
embodied_prompt: str,
|
||||||
@@ -214,12 +213,11 @@ class _FakeQwenInterface(nn.Module):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tensor_to_pil(image_tensor: Tensor) -> Image.Image:
|
def to_pixel_values(image_tensor: Tensor) -> Tensor:
|
||||||
image = image_tensor.detach().cpu()
|
image = image_tensor.detach().float()
|
||||||
if image.ndim == 3 and image.shape[0] in (1, 3):
|
if image.ndim == 3 and image.shape[0] == 1:
|
||||||
image = image.permute(1, 2, 0)
|
image = image.repeat(3, 1, 1)
|
||||||
image = (image.float().clamp(0, 1) * 255).to(torch.uint8).numpy()
|
return image
|
||||||
return Image.fromarray(image)
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeVideoEncoder(nn.Module):
|
class _FakeVideoEncoder(nn.Module):
|
||||||
@@ -242,12 +240,14 @@ class _FakeVideoEncoder(nn.Module):
|
|||||||
|
|
||||||
|
|
||||||
class _FakeVideoProcessor:
|
class _FakeVideoProcessor:
|
||||||
def __call__(self, videos, return_tensors: str) -> dict[str, Tensor]:
|
def __call__(self, videos, return_tensors: str, device=None, **kwargs) -> dict[str, Tensor]:
|
||||||
assert return_tensors == "pt"
|
assert return_tensors == "pt"
|
||||||
if isinstance(videos, list):
|
if isinstance(videos, list):
|
||||||
pixel_values = torch.stack([torch.as_tensor(v) for v in videos])
|
pixel_values = torch.stack([torch.as_tensor(v) for v in videos])
|
||||||
else:
|
else:
|
||||||
pixel_values = torch.as_tensor(videos).unsqueeze(0)
|
pixel_values = torch.as_tensor(videos).unsqueeze(0)
|
||||||
|
if device is not None:
|
||||||
|
pixel_values = pixel_values.to(device)
|
||||||
return {"pixel_values_videos": pixel_values}
|
return {"pixel_values_videos": pixel_values}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -211,16 +211,17 @@ def test_reset_clears_action_queue(patch_vla_jepa_external_models: None) -> None
|
|||||||
|
|
||||||
|
|
||||||
def test_prepare_model_inputs_training_format(patch_vla_jepa_external_models: None) -> None:
|
def test_prepare_model_inputs_training_format(patch_vla_jepa_external_models: None) -> None:
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
policy = VLAJEPAPolicy(make_config())
|
policy = VLAJEPAPolicy(make_config())
|
||||||
examples = policy._prepare_model_inputs(make_train_batch())
|
examples = policy._prepare_model_inputs(make_train_batch())
|
||||||
|
|
||||||
assert len(examples) == BATCH_SIZE
|
assert len(examples) == BATCH_SIZE
|
||||||
for ex in examples:
|
for ex in examples:
|
||||||
assert set(ex) >= {"image", "video", "lang", "action", "state"}
|
assert set(ex) >= {"image", "video", "lang", "action", "state"}
|
||||||
assert len(ex["image"]) == 1 and isinstance(ex["image"][0], Image.Image)
|
assert len(ex["image"]) == 1
|
||||||
assert ex["video"].ndim == 5 and ex["video"].dtype == np.uint8 # [V,T,H,W,C]
|
assert isinstance(ex["image"][0], torch.Tensor) and ex["image"][0].dtype == torch.float32
|
||||||
|
assert ex["image"][0].ndim == 3 # [C, H, W]
|
||||||
|
assert isinstance(ex["video"], torch.Tensor)
|
||||||
|
assert ex["video"].ndim == 5 and ex["video"].dtype == torch.float32 # [V, T, C, H, W]
|
||||||
assert ex["action"].shape == (ACTION_HORIZON, ACTION_DIM)
|
assert ex["action"].shape == (ACTION_HORIZON, ACTION_DIM)
|
||||||
assert ex["state"].shape == (1, STATE_DIM)
|
assert ex["state"].shape == (1, STATE_DIM)
|
||||||
|
|
||||||
@@ -446,14 +447,14 @@ def test_postprocessor_applied_after_predict_action_chunk(
|
|||||||
"""
|
"""
|
||||||
from lerobot.policies.vla_jepa.processor_vla_jepa import make_vla_jepa_pre_post_processors
|
from lerobot.policies.vla_jepa.processor_vla_jepa import make_vla_jepa_pre_post_processors
|
||||||
|
|
||||||
raw_actions = np.zeros((BATCH_SIZE, ACTION_HORIZON, ACTION_DIM), dtype=np.float32)
|
raw_actions = torch.zeros((BATCH_SIZE, ACTION_HORIZON, ACTION_DIM), dtype=torch.float32)
|
||||||
|
|
||||||
cfg = make_config()
|
cfg = make_config()
|
||||||
cfg.clip_normalized_actions = False
|
cfg.clip_normalized_actions = False
|
||||||
cfg.binarize_gripper_action = False
|
cfg.binarize_gripper_action = False
|
||||||
policy = VLAJEPAPolicy(cfg)
|
policy = VLAJEPAPolicy(cfg)
|
||||||
policy.eval()
|
policy.eval()
|
||||||
monkeypatch.setattr(policy.model, "predict_action", lambda *a, **kw: raw_actions.copy())
|
monkeypatch.setattr(policy.model, "predict_action", lambda *a, **kw: raw_actions.clone())
|
||||||
|
|
||||||
dataset_stats = _make_dataset_stats()
|
dataset_stats = _make_dataset_stats()
|
||||||
_, postprocessor = make_vla_jepa_pre_post_processors(cfg, dataset_stats)
|
_, postprocessor = make_vla_jepa_pre_post_processors(cfg, dataset_stats)
|
||||||
@@ -564,9 +565,9 @@ def test_single_view_is_duplicated_for_world_model(patch_vla_jepa_external_model
|
|||||||
original_processor = policy.model.video_processor
|
original_processor = policy.model.video_processor
|
||||||
|
|
||||||
class _CapturingProcessor:
|
class _CapturingProcessor:
|
||||||
def __call__(self, videos: list, return_tensors: str) -> dict:
|
def __call__(self, videos: list, return_tensors: str, **kwargs) -> dict:
|
||||||
captured_videos.extend(videos)
|
captured_videos.extend(videos)
|
||||||
return original_processor(videos=videos, return_tensors=return_tensors)
|
return original_processor(videos=videos, return_tensors=return_tensors, **kwargs)
|
||||||
|
|
||||||
policy.model.video_processor = _CapturingProcessor()
|
policy.model.video_processor = _CapturingProcessor()
|
||||||
policy.forward(_make_multiview_train_batch(num_views=1))
|
policy.forward(_make_multiview_train_batch(num_views=1))
|
||||||
@@ -587,9 +588,9 @@ def test_excess_views_trimmed_for_world_model(patch_vla_jepa_external_models: No
|
|||||||
original_processor = policy.model.video_processor
|
original_processor = policy.model.video_processor
|
||||||
|
|
||||||
class _CapturingProcessor:
|
class _CapturingProcessor:
|
||||||
def __call__(self, videos: list, return_tensors: str) -> dict:
|
def __call__(self, videos: list, return_tensors: str, **kwargs) -> dict:
|
||||||
captured_videos.extend(videos)
|
captured_videos.extend(videos)
|
||||||
return original_processor(videos=videos, return_tensors=return_tensors)
|
return original_processor(videos=videos, return_tensors=return_tensors, **kwargs)
|
||||||
|
|
||||||
policy.model.video_processor = _CapturingProcessor()
|
policy.model.video_processor = _CapturingProcessor()
|
||||||
policy.forward(_make_multiview_train_batch(num_views=3))
|
policy.forward(_make_multiview_train_batch(num_views=3))
|
||||||
|
|||||||
Reference in New Issue
Block a user