| import dataclasses |
| import logging |
| import socket |
| import asyncio |
| import os |
| import http |
| import logging |
| import time |
| import traceback |
| import torch |
| import tyro |
| from einops import rearrange |
| import datetime |
|
|
| from groot.vla.model.n1_5.sim_policy import GrootSimPolicy |
| from groot.vla.data.schema import EmbodimentTag |
| import imageio |
| import numpy as np |
|
|
| from openpi_client import base_policy as _base_policy |
| from openpi_client import msgpack_numpy |
| import websockets.asyncio.server as _server |
| import websockets.frames |
| from tianshou.data import Batch |
| import torch.distributed as dist |
| from torch.distributed.device_mesh import DeviceMesh, init_device_mesh |
|
|
| |
| from eval_utils.policy_server import WebsocketPolicyServer as RoboarenaServer |
| from eval_utils.policy_server import PolicyServerConfig |
|
|
| logger = logging.getLogger(__name__) |
|
|
| @dataclasses.dataclass |
| class Args: |
| port: int = 8000 |
| timeout_seconds: int = 50000 |
| model_path: str = "./checkpoints/dreamzero" |
| enable_dit_cache: bool = False |
| index: int = 0 |
| max_chunk_size: int | None = None |
|
|
|
|
| class ARDroidRoboarenaPolicy: |
| """Wrapper policy that implements roboarena.policy.BasePolicy interface for AR_droid. |
| |
| Handles: |
| - Observation format conversion (roboarena -> AR_droid format) |
| - Frame accumulation across calls (roboarena sends single frames, AR_droid expects multi-frame video) |
| - Action format conversion (AR_droid dict -> roboarena array format) |
| - Distributed inference coordination |
| """ |
| |
| |
| FRAMES_PER_CHUNK = 4 |
| |
| def __init__( |
| self, |
| groot_policy: GrootSimPolicy, |
| signal_group: dist.ProcessGroup, |
| output_dir: str | None = None, |
| ) -> None: |
| self._policy = groot_policy |
| self._signal_group = signal_group |
| self._output_dir = output_dir |
| |
| |
| self._frame_buffers: dict[str, list[np.ndarray]] = { |
| "video.exterior_image_1_left": [], |
| "video.exterior_image_2_left": [], |
| "video.wrist_image_left": [], |
| } |
| self._call_count = 0 |
| self._is_first_call = True |
| |
| |
| self._current_session_id: str | None = None |
| |
| |
| self.video_across_time = [] |
| self._msg_index = 0 |
| |
| |
| if self._output_dir: |
| os.makedirs(self._output_dir, exist_ok=True) |
| |
| def _convert_observation(self, obs: dict) -> dict: |
| """Convert roboarena observation format to AR_droid format. |
| |
| Roboarena format: |
| - observation/exterior_image_0_left: (H, W, 3) single frame |
| - observation/exterior_image_1_left: (H, W, 3) single frame |
| - observation/wrist_image_left: (H, W, 3) single frame |
| - observation/joint_position: (7,) |
| - observation/gripper_position: (1,) |
| - prompt: str |
| |
| AR_droid format: |
| - video.exterior_image_1_left: (T, H, W, 3) multi-frame |
| - video.exterior_image_2_left: (T, H, W, 3) multi-frame |
| - video.wrist_image_left: (T, H, W, 3) multi-frame |
| - state.joint_position: (1, 7) |
| - state.gripper_position: (1, 1) |
| - annotation.language.action_text: str |
| """ |
| converted = {} |
| |
| |
| image_key_mapping = { |
| "observation/exterior_image_0_left": "video.exterior_image_1_left", |
| "observation/exterior_image_1_left": "video.exterior_image_2_left", |
| "observation/wrist_image_left": "video.wrist_image_left", |
| } |
| |
| |
| for roboarena_key, droid_key in image_key_mapping.items(): |
| if roboarena_key in obs: |
| data = obs[roboarena_key] |
| if isinstance(data, np.ndarray): |
| if data.ndim == 4: |
| |
| self._frame_buffers[droid_key].extend(list(data)) |
| else: |
| |
| self._frame_buffers[droid_key].append(data) |
|
|
| |
| if self._is_first_call: |
| |
| num_frames = 1 |
| else: |
| |
| num_frames = self.FRAMES_PER_CHUNK |
| |
| |
| for droid_key, buffer in self._frame_buffers.items(): |
| if len(buffer) > 0: |
| if len(buffer) >= num_frames: |
| |
| frames_to_use = buffer[-num_frames:] |
| else: |
| |
| frames_to_use = buffer.copy() |
| while len(frames_to_use) < num_frames: |
| |
| frames_to_use.insert(0, buffer[0]) |
| |
| video = np.stack(frames_to_use, axis=0) |
| converted[droid_key] = video |
| |
| |
| if "observation/joint_position" in obs: |
| joint_pos = obs["observation/joint_position"] |
| |
| if joint_pos.ndim == 1: |
| joint_pos = joint_pos.reshape(1, -1) |
| converted["state.joint_position"] = joint_pos.astype(np.float64) |
| else: |
| converted["state.joint_position"] = np.zeros((1, 7), dtype=np.float64) |
| |
| if "observation/gripper_position" in obs: |
| gripper_pos = obs["observation/gripper_position"] |
| |
| if gripper_pos.ndim == 1: |
| gripper_pos = gripper_pos.reshape(1, -1) |
| converted["state.gripper_position"] = gripper_pos.astype(np.float64) |
| else: |
| converted["state.gripper_position"] = np.zeros((1, 1), dtype=np.float64) |
| |
| |
| if "prompt" in obs: |
| converted["annotation.language.action_text"] = obs["prompt"] |
| else: |
| converted["annotation.language.action_text"] = "" |
| |
| return converted |
| |
| def _convert_action(self, action_dict: dict) -> np.ndarray: |
| """Convert AR_droid action dict to roboarena action array. |
| |
| AR_droid format: |
| - action.joint_position: (N, 7) |
| - action.gripper_position: (N,) or (N, 1) |
| |
| Roboarena format: |
| - action: (N, 8) - 7 joint positions + 1 gripper |
| """ |
| joint_action = None |
| gripper_action = None |
| |
| |
| for key, value in action_dict.items(): |
| if "joint_position" in key: |
| joint_action = value |
| elif "gripper_position" in key or "gripper" in key: |
| gripper_action = value |
| |
| if joint_action is None: |
| |
| return np.zeros((1, 8), dtype=np.float32) |
| |
| |
| if isinstance(joint_action, torch.Tensor): |
| joint_action = joint_action.cpu().numpy() |
| |
| |
| if joint_action.ndim == 1: |
| joint_action = joint_action.reshape(1, -1) |
| |
| N = joint_action.shape[0] |
| |
| |
| if gripper_action is not None: |
| if isinstance(gripper_action, torch.Tensor): |
| gripper_action = gripper_action.cpu().numpy() |
| |
| if gripper_action.ndim == 1: |
| gripper_action = gripper_action.reshape(-1, 1) |
| elif gripper_action.ndim == 0: |
| gripper_action = gripper_action.reshape(1, 1) |
| else: |
| gripper_action = np.zeros((N, 1), dtype=np.float32) |
| |
| |
| action = np.concatenate([joint_action, gripper_action], axis=-1).astype(np.float32) |
| |
| return action |
| |
| def _broadcast_batch_to_workers(self, obs: dict) -> None: |
| """Broadcast batch data from rank 0 to all other ranks.""" |
| import pickle |
| |
| |
| serialized = pickle.dumps(obs) |
| data_size = len(serialized) |
| |
| |
| size_tensor = torch.tensor([data_size], dtype=torch.int64, device='cuda') |
| dist.broadcast(size_tensor, src=0) |
| |
| |
| data_tensor = torch.frombuffer(serialized, dtype=torch.uint8).cuda() |
| dist.broadcast(data_tensor, src=0) |
| |
| def infer(self, obs: dict) -> np.ndarray: |
| """Infer actions from observations. |
| |
| Args: |
| obs: Observation dict in roboarena format |
| |
| Returns: |
| action: (N, 8) action array |
| """ |
| |
| session_id = obs.get("session_id", None) |
| if session_id is not None and session_id != self._current_session_id: |
| if self._current_session_id is not None: |
| logger.info(f"Session changed from '{self._current_session_id}' to '{session_id}', resetting state") |
| |
| self._reset_state() |
| else: |
| logger.info(f"New session started: '{session_id}'") |
| self._current_session_id = session_id |
| |
| self._msg_index += 1 |
| self._call_count += 1 |
| |
| |
| converted_obs = self._convert_observation(obs) |
| |
| |
| signal_tensor = torch.zeros(1, dtype=torch.int32, device='cpu') |
| dist.broadcast(signal_tensor, src=0, group=self._signal_group) |
| |
| |
| self._broadcast_batch_to_workers(converted_obs) |
| |
| |
| batch = Batch(obs=converted_obs) |
| |
| |
| dist.barrier() |
| with torch.no_grad(): |
| result_batch, video_pred = self._policy.lazy_joint_forward_causal(batch) |
| dist.barrier() |
| |
| |
| self.video_across_time.append(video_pred) |
| |
| |
| action_chunk_dict = result_batch.act |
| |
| |
| action_dict = {} |
| for k in dir(action_chunk_dict): |
| if k.startswith("action."): |
| action_dict[k] = getattr(action_chunk_dict, k) |
| |
| action = self._convert_action(action_dict) |
| |
| |
| if self._is_first_call: |
| self._is_first_call = False |
| |
| return action |
| |
| def _reset_state(self, save_video: bool = True) -> None: |
| """Internal method to reset policy state. |
| |
| Args: |
| save_video: Whether to save accumulated video before reset. |
| """ |
| |
| if save_video and len(self.video_across_time) > 0 and self._output_dir: |
| try: |
| frame_list = [] |
| video_across_time_cat = torch.cat(self.video_across_time, dim=2) |
| frames = self._policy.trained_model.action_head.vae.decode( |
| video_across_time_cat, |
| tiled=self._policy.trained_model.action_head.tiled, |
| tile_size=(self._policy.trained_model.action_head.tile_size_height, self._policy.trained_model.action_head.tile_size_width), |
| tile_stride=(self._policy.trained_model.action_head.tile_stride_height, self._policy.trained_model.action_head.tile_stride_width), |
| ) |
| frames = rearrange(frames, "B C T H W -> B T H W C") |
| frames = frames[0] |
| frames = ((frames.float() + 1) * 127.5).clip(0, 255).cpu().numpy().astype(np.uint8) |
| for frame in frames: |
| frame_list.append(frame) |
| |
| if len(frame_list) > 0: |
| sample_frame = frame_list[0] |
| if len(sample_frame.shape) == 3 and sample_frame.shape[2] in [1, 3, 4]: |
| save_dir = self._output_dir |
| os.makedirs(save_dir, exist_ok=True) |
| all_mp4_files = [f for f in os.listdir(save_dir) if f.endswith(".mp4")] |
| timestamp = datetime.datetime.now().strftime("%m_%d_%H_%M_%S") |
| num_frames = len(frame_list) |
| n = (num_frames - 1) // 8 |
| output_path = os.path.join(save_dir, f'{len(all_mp4_files):06}_{timestamp}_n{n}.mp4') |
| imageio.mimsave(output_path, frame_list, fps=5, codec='libx264') |
| logger.info(f"Saved video on reset to: {output_path}") |
| except Exception as e: |
| logger.warning(f"Failed to save video on reset: {e}") |
| |
| |
| for key in self._frame_buffers: |
| self._frame_buffers[key] = [] |
| |
| self._call_count = 0 |
| self._is_first_call = True |
| self.video_across_time = [] |
| |
| def reset(self, reset_info: dict) -> None: |
| """Reset the policy state for a new episode. |
| |
| Clears frame buffers and resets call count. |
| """ |
| self._reset_state(save_video=True) |
|
|
|
|
| class WebsocketPolicyServer: |
| """Serves a policy using the websocket protocol. See websocket_client_policy.py for a client implementation. |
| Currently only implements the `load` and `infer` methods. |
| """ |
|
|
| def __init__( |
| self, |
| policy: _base_policy.BasePolicy, |
| host: str = "0.0.0.0", |
| port: int | None = None, |
| metadata: dict | None = None, |
| output_dir: str | None = None, |
| signal_group: dist.ProcessGroup | None = None, |
| ) -> None: |
| self._policy = policy |
| self._host = host |
| self._port = port |
| self._metadata = metadata or {} |
| self._output_dir = output_dir |
| logging.getLogger("websockets.server").setLevel(logging.INFO) |
| self.video_across_time = [] |
| self._msg_index = 0 |
| self._signal_group = signal_group |
| |
| if self._output_dir: |
| os.makedirs(self._output_dir, exist_ok=True) |
| os.makedirs(os.path.join(self._output_dir, "inputs"), exist_ok=True) |
| |
| def _save_input_obs(self, obs: dict) -> None: |
| """Save incoming observation images per message. |
| |
| Expected format: THWC (Time, Height, Width, Channel) with 4 frames. |
| Saves each frame as a separate PNG image: HWC format (uint8). |
| |
| Directory structure: |
| output_dir/inputs/{msg_index:06d}_{timestamp}/{obs_key}/f{frame_idx:02d}.png |
| """ |
| if not self._output_dir: |
| return |
| timestamp = datetime.datetime.now().strftime("%m_%d_%H_%M_%S") |
| base_dir = os.path.join(self._output_dir, "inputs", f"{self._msg_index:06d}_{timestamp}") |
| try: |
| os.makedirs(base_dir, exist_ok=True) |
| except Exception: |
| return |
|
|
| for key in ("video.exterior_image_1_left", "video.exterior_image_2_left", "video.wrist_image_left"): |
| if key not in obs: |
| continue |
| value = obs[key] |
| try: |
| |
| if isinstance(value, torch.Tensor): |
| arr = value.detach().cpu().numpy() |
| else: |
| arr = np.asarray(value) |
| |
| |
| if arr.ndim != 4: |
| logger.warning(f"obs key '{key}' has shape {arr.shape}, expected 4D (T,H,W,C)") |
| continue |
| |
| |
| T, H, W, C = arr.shape |
| |
| |
| if arr.dtype == np.uint8: |
| frames_u8 = arr |
| else: |
| f = arr.astype(np.float32) |
| |
| min_val = float(np.nanmin(f)) |
| max_val = float(np.nanmax(f)) |
| if min_val >= -1.1 and max_val <= 1.1: |
| |
| frames_u8 = ((f + 1.0) * 127.5).clip(0, 255).astype(np.uint8) |
| else: |
| |
| denom = (max_val - min_val) if (max_val - min_val) > 1e-6 else 1.0 |
| frames_u8 = ((f - min_val) / denom * 255.0).clip(0, 255).astype(np.uint8) |
| |
| |
| key_dir = os.path.join(base_dir, key.replace("/", "_")) |
| os.makedirs(key_dir, exist_ok=True) |
| for frame_idx in range(T): |
| frame = frames_u8[frame_idx] |
| |
| if frame.ndim == 2: |
| frame = np.expand_dims(frame, axis=-1) |
| imageio.imwrite(os.path.join(key_dir, f"f{frame_idx:02d}.png"), frame) |
| |
| except Exception as e: |
| logger.warning(f"Failed to save obs key '{key}': {e}") |
| continue |
|
|
|
|
|
|
| def serve_forever(self, rank: int = 0) -> None: |
| asyncio.run(self.run(rank)) |
|
|
| async def run(self, rank: int = 0): |
| if rank == 0: |
| async with _server.serve( |
| self._handler, |
| self._host, |
| self._port, |
| compression=None, |
| max_size=None, |
| process_request=_health_check, |
| ping_interval=None, |
| ) as server: |
| await server.serve_forever() |
| else: |
| |
| await self._worker_loop() |
|
|
| async def _worker_loop(self): |
| """Worker loop for non-rank-0 processes to participate in distributed inference.""" |
| logger.info(f"Worker loop started for rank {dist.get_rank()}") |
| signal_tensor = torch.zeros(1, dtype=torch.int32, device='cpu') |
| while True: |
| try: |
| |
| |
| |
|
|
| dist.broadcast(signal_tensor, src=0, group=self._signal_group) |
|
|
| signal = signal_tensor.item() |
| if signal == 1: |
| logger.info(f"Rank {dist.get_rank()} received shutdown signal") |
| break |
|
|
| |
| elif signal == 2: |
| logger.info(f"Rank {dist.get_rank()} received idle signal. Waiting for next client.") |
| |
| continue |
|
|
| |
| |
| batch = self._receive_batch_from_rank0() |
| |
| dist.barrier() |
| with torch.no_grad(): |
| result_batch, video_pred = self._policy.lazy_joint_forward_causal(batch) |
| dist.barrier() |
|
|
| except Exception as e: |
| logger.error(f"Worker loop error on rank {dist.get_rank()}: {e}") |
| traceback.print_exc() |
| break |
|
|
| def _receive_batch_from_rank0(self): |
| """Receive batch data from rank 0 using torch.distributed primitives.""" |
| import pickle |
|
|
| |
| size_tensor = torch.zeros(1, dtype=torch.int64, device='cuda') |
| dist.broadcast(size_tensor, src=0) |
| data_size = size_tensor.item() |
|
|
| |
| data_tensor = torch.zeros(data_size, dtype=torch.uint8, device='cuda') |
| dist.broadcast(data_tensor, src=0) |
|
|
| |
| obs = pickle.loads(data_tensor.cpu().numpy().tobytes()) |
| return Batch(obs=obs) |
|
|
| def _broadcast_batch_to_workers(self, obs): |
| """Broadcast batch data from rank 0 to all other ranks.""" |
| import pickle |
|
|
| |
| serialized = pickle.dumps(obs) |
| data_size = len(serialized) |
|
|
| |
| size_tensor = torch.tensor([data_size], dtype=torch.int64, device='cuda') |
| dist.broadcast(size_tensor, src=0) |
|
|
| |
| data_tensor = torch.frombuffer(serialized, dtype=torch.uint8).cuda() |
| dist.broadcast(data_tensor, src=0) |
|
|
| async def _handler(self, websocket: _server.ServerConnection): |
| logger.info(f"Connection from {websocket.remote_address} opened") |
| packer = msgpack_numpy.Packer() |
|
|
| await websocket.send(packer.pack(self._metadata)) |
|
|
| prev_total_time = None |
| signal_tensor = torch.zeros(1, dtype=torch.int32, device='cpu') |
| |
| try: |
| while True: |
| try: |
| start_time = time.perf_counter() |
| data = await websocket.recv() |
| recv_done = time.perf_counter() |
| obs = msgpack_numpy.unpackb(data) |
| print(f"Wait Time: {recv_done - start_time:.2f} seconds") |
| self._msg_index += 1 |
|
|
| infer_start_time = time.perf_counter() |
|
|
| |
| signal_tensor.zero_() |
| dist.broadcast(signal_tensor, src=0, group=self._signal_group) |
|
|
| |
| self._broadcast_batch_to_workers(obs) |
| batch = Batch(obs=obs) |
|
|
| |
| dist.barrier() |
| forward_start_time = time.perf_counter() |
| with torch.no_grad(): |
| result_batch, video_pred = self._policy.lazy_joint_forward_causal(batch) |
| dist.barrier() |
| print(f"Forward Time: {time.perf_counter() - forward_start_time:.2f} seconds") |
|
|
| action_chunk_dict = result_batch.act |
| video_chunk = video_pred |
|
|
| print(f"Inference Time: {time.perf_counter() - infer_start_time:.2f} seconds") |
|
|
| self.video_across_time.append(video_chunk) |
|
|
| if len(self.video_across_time) > 10: |
| frame_list = [] |
| video_across_time_cat = torch.cat(self.video_across_time, dim=2) |
| frames = self._policy.trained_model.action_head.vae.decode( |
| video_across_time_cat, |
| tiled=self._policy.trained_model.action_head.tiled, |
| tile_size=(self._policy.trained_model.action_head.tile_size_height, self._policy.trained_model.action_head.tile_size_width), |
| tile_stride=(self._policy.trained_model.action_head.tile_stride_height, self._policy.trained_model.action_head.tile_stride_width), |
| ) |
| frames = rearrange(frames, "B C T H W -> B T H W C") |
| frames = frames[0] |
| frames = ((frames.float() + 1) * 127.5).clip(0, 255).cpu().numpy().astype(np.uint8) |
| |
| for frame in frames: |
| frame_list.append(frame) |
|
|
| sample_frame = frame_list[0] |
| if len(sample_frame.shape) == 3 and sample_frame.shape[2] in [1, 3, 4]: |
| |
| save_dir = self._output_dir if self._output_dir else "." |
| os.makedirs(save_dir, exist_ok=True) |
| all_mp4_files = [f for f in os.listdir(save_dir) if f.endswith(".mp4")] |
| timestamp = datetime.datetime.now().strftime("%m_%d_%H_%M_%S") |
| num_frames = len(frame_list) |
| n = (num_frames - 1) // 8 |
| output_path = os.path.join(save_dir, f'{len(all_mp4_files):06}_{timestamp}_n{n}.mp4') |
| imageio.mimsave(output_path, frame_list, fps=5, codec='libx264') |
| print(f"Saved video to: {output_path}") |
| else: |
| print(f"Warning: Invalid frame shape {sample_frame.shape}. Expected (H, W, C) with C in [1, 3, 4]. Skipping video save.") |
|
|
| self.video_across_time = [] |
| elif self._policy.trained_model.action_head.current_start_frame == 1 + self._policy.trained_model.action_head.num_frame_per_block and len(self.video_across_time) > 1: |
| print("current_start_frame == 1 + num_frame_per_block and len(self.video_across_time) > 1") |
| frame_list = [] |
| video_across_time_cat = torch.cat(self.video_across_time[:-1], dim=2) |
| frames = self._policy.trained_model.action_head.vae.decode( |
| video_across_time_cat, |
| tiled=self._policy.trained_model.action_head.tiled, |
| tile_size=(self._policy.trained_model.action_head.tile_size_height, self._policy.trained_model.action_head.tile_size_width), |
| tile_stride=(self._policy.trained_model.action_head.tile_stride_height, self._policy.trained_model.action_head.tile_stride_width), |
| ) |
| frames = rearrange(frames, "B C T H W -> B T H W C") |
| frames = frames[0] |
| frames = ((frames.float() + 1) * 127.5).clip(0, 255).cpu().numpy().astype(np.uint8) |
| |
| for frame in frames: |
| frame_list.append(frame) |
| sample_frame = frame_list[0] |
| if len(sample_frame.shape) == 3 and sample_frame.shape[2] in [1, 3, 4]: |
| |
| save_dir = self._output_dir if self._output_dir else "." |
| os.makedirs(save_dir, exist_ok=True) |
| all_mp4_files = [f for f in os.listdir(save_dir) if f.endswith(".mp4")] |
| timestamp = datetime.datetime.now().strftime("%m_%d_%H_%M_%S") |
| num_frames = len(frame_list) |
| n = (num_frames - 1) // 8 |
| output_path = os.path.join(save_dir, f'{len(all_mp4_files):06}_{timestamp}_n{n}.mp4') |
| imageio.mimsave(output_path, frame_list, fps=5, codec='libx264') |
| print(f"Saved video to: {output_path}") |
| self.video_across_time = [video_chunk] |
|
|
| |
| def batch_to_dict(batch): |
| out = {} |
| for k in dir(batch): |
| if not k.startswith("action."): |
| continue |
| out[k] = getattr(batch, k) |
| return out |
| action_chunk_dict = batch_to_dict(action_chunk_dict) |
| await websocket.send(packer.pack(action_chunk_dict)) |
|
|
| except websockets.ConnectionClosed: |
| logger.info(f"Connection from {websocket.remote_address} closed") |
| if len(self.video_across_time) > 0: |
| frame_list = [] |
| video_across_time_cat = torch.cat(self.video_across_time, dim=2) |
| frames = self._policy.trained_model.action_head.vae.decode( |
| video_across_time_cat, |
| tiled=self._policy.trained_model.action_head.tiled, |
| tile_size=(self._policy.trained_model.action_head.tile_size_height, self._policy.trained_model.action_head.tile_size_width), |
| tile_stride=(self._policy.trained_model.action_head.tile_stride_height, self._policy.trained_model.action_head.tile_stride_width), |
| ) |
| frames = rearrange(frames, "B C T H W -> B T H W C") |
| frames = frames[0] |
| frames = ((frames.float() + 1) * 127.5).clip(0, 255).cpu().numpy().astype(np.uint8) |
| |
| for frame in frames: |
| frame_list.append(frame) |
|
|
| sample_frame = frame_list[0] |
| if len(sample_frame.shape) == 3 and sample_frame.shape[2] in [1, 3, 4]: |
| |
| save_dir = self._output_dir if self._output_dir else "." |
| os.makedirs(save_dir, exist_ok=True) |
| all_mp4_files = [f for f in os.listdir(save_dir) if f.endswith(".mp4")] |
| timestamp = datetime.datetime.now().strftime("%m_%d_%H_%M_%S") |
| num_frames = len(frame_list) |
| n = (num_frames - 1) // 8 |
| output_path = os.path.join(save_dir, f'{len(all_mp4_files):06}_{timestamp}_n{n}.mp4') |
| imageio.mimsave(output_path, frame_list, fps=5, codec='libx264') |
| print(f"Saved video to: {output_path}") |
| else: |
| print(f"Warning: Invalid frame shape {sample_frame.shape}. Expected (H, W, C) with C in [1, 3, 4]. Skipping video save.") |
|
|
| self.video_across_time = [] |
| break |
| except Exception: |
| await websocket.send(traceback.format_exc()) |
| await websocket.close( |
| code=websockets.frames.CloseCode.INTERNAL_ERROR, |
| reason="Internal server error. Traceback included in previous frame.", |
| ) |
| raise |
| finally: |
| logger.info(f"Rank 0: Client session ended. Sending idle signal (2) to workers.") |
| signal_tensor.fill_(2) |
| dist.broadcast(signal_tensor, src=0, group=self._signal_group) |
| |
| |
|
|
|
|
| def init_mesh() -> DeviceMesh: |
| |
| dist.init_process_group("nccl") |
| rank = dist.get_rank() |
| world_size = dist.get_world_size() |
| print(f"Rank {rank}/{world_size} (PID: {os.getpid()}) setting device to {rank}") |
|
|
| torch.cuda.set_device(rank) |
| device = torch.device(f"cuda:{rank}") |
|
|
| mesh = init_device_mesh( |
| device_type="cuda", |
| mesh_shape=(world_size, ), |
| mesh_dim_names=("ip", ), |
| ) |
| print(f"Rank {rank}/{world_size} (PID: {os.getpid()}) using device {device}") |
|
|
| return mesh |
|
|
| def _health_check(connection: _server.ServerConnection, request: _server.Request) -> _server.Response | None: |
| if request.path == "/healthz": |
| return connection.respond(http.HTTPStatus.OK, "OK\n") |
| |
| return None |
|
|
|
|
| def main(args: Args) -> None: |
| |
| os.environ["ENABLE_DIT_CACHE"] = "true" if args.enable_dit_cache else "false" |
|
|
| |
| os.environ["ATTENTION_BACKEND"] = "TE" |
|
|
| |
| |
| torch._dynamo.config.recompile_limit = 800 |
|
|
| embodiment_tag = "oxe_droid" |
| model_path = args.model_path |
| policy_metadata = { |
| "embodiment": embodiment_tag, |
| "model_name": "dreamzero", |
| "model_path": model_path, |
| } |
|
|
| device_mesh = init_mesh() |
| rank = dist.get_rank() |
|
|
| timeout_delta = datetime.timedelta(seconds=args.timeout_seconds) |
| signal_group = dist.new_group(backend="gloo", timeout=timeout_delta) |
| logger.info(f"Rank {rank} initialized signal_group (gloo)") |
|
|
| policy = GrootSimPolicy( |
| embodiment_tag=EmbodimentTag(embodiment_tag), |
| model_path=model_path, |
| device="cuda" if torch.cuda.is_available() else "cpu", |
| device_mesh=device_mesh, |
| ) |
|
|
| |
| hostname = socket.gethostname() |
| local_ip = socket.gethostbyname(hostname) |
|
|
| if rank == 0: |
| logging.info("Creating server (host: %s, ip: %s)", hostname, local_ip) |
| |
| |
| parent_dir = os.path.dirname(model_path) |
| date_suffix = datetime.datetime.now().strftime("%Y%m%d") |
| checkpoint_name = os.path.basename(model_path) |
| output_dir = os.path.join(parent_dir, f"real_world_eval_gen_{date_suffix}_{args.index}", checkpoint_name) |
| os.makedirs(output_dir, exist_ok=True) |
| logging.info("Videos will be saved to: %s", output_dir) |
| else: |
| output_dir = None |
| logging.info(f"Rank {rank} starting as worker for distributed inference...") |
| |
| |
| wrapper_policy = ARDroidRoboarenaPolicy( |
| groot_policy=policy, |
| signal_group=signal_group, |
| output_dir=output_dir, |
| ) |
| |
| |
| server_config = PolicyServerConfig( |
| image_resolution=(180, 320), |
| needs_wrist_camera=True, |
| n_external_cameras=2, |
| needs_stereo_camera=False, |
| needs_session_id=True, |
| action_space="joint_position", |
| ) |
| |
| if rank == 0: |
| logging.info("Using roboarena policy server interface") |
| logging.info(f"Server config: {server_config}") |
| roboarena_server = RoboarenaServer( |
| policy=wrapper_policy, |
| server_config=server_config, |
| host="0.0.0.0", |
| port=args.port, |
| ) |
| roboarena_server.serve_forever() |
| else: |
| |
| |
| server = WebsocketPolicyServer( |
| policy=policy, |
| host="0.0.0.0", |
| port=args.port, |
| metadata=policy_metadata, |
| output_dir=output_dir, |
| signal_group=signal_group, |
| ) |
| asyncio.run(server._worker_loop()) |
| |
|
|
|
|
| if __name__ == "__main__": |
| logging.basicConfig(level=logging.INFO, force=True) |
| args = tyro.cli(Args) |
| main(args) |