| |
| """Evaluate a released WorldDiT checkpoint on LIBERO.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import copy |
| import contextlib |
| import json |
| import math |
| import os |
| import random |
| import subprocess |
| import sys |
| from collections import deque |
| from pathlib import Path |
|
|
| import clip |
| import numpy as np |
| import torch |
| from PIL import Image |
| from scipy.spatial.transform import Rotation |
| from torch.nn.parallel import DistributedDataParallel as DDP |
| from tqdm.auto import tqdm |
|
|
| from inference import ACTION_HORIZON, CONTEXT_STEPS, SUITES, load_model |
|
|
|
|
| def rank(): |
| return int(os.environ.get("RANK", "0")) |
|
|
|
|
| def world_size(): |
| return int(os.environ.get("WORLD_SIZE", "1")) |
|
|
|
|
| def launch(gpus: int): |
| if not 1 <= gpus <= torch.cuda.device_count(): |
| raise ValueError(f"--gpus must be between 1 and {torch.cuda.device_count()}") |
| command = [ |
| sys.executable, |
| "-m", |
| "torch.distributed.run", |
| "--standalone", |
| f"--nproc_per_node={gpus}", |
| str(Path(__file__).resolve()), |
| *sys.argv[1:], |
| ] |
| environment = os.environ.copy() |
| environment.setdefault("OMP_NUM_THREADS", "1") |
| return subprocess.run(command, check=False, env=environment).returncode |
|
|
|
|
| def configure_libero(libero_root: Path, output: Path): |
| package = libero_root / "libero" / "libero" |
| required = (package / "bddl_files", package / "init_files", package / "assets") |
| if any(not path.is_dir() for path in required): |
| raise FileNotFoundError(f"invalid LIBERO checkout: {libero_root}") |
| config_dir = output / "libero_config" |
| if rank() == 0: |
| output.mkdir(parents=True, exist_ok=False) |
| config_dir.mkdir() |
| paths = { |
| "assets": str(package / "assets"), |
| "bddl_files": str(package / "bddl_files"), |
| "benchmark_root": str(package), |
| "datasets": str(package.parent / "datasets"), |
| "init_states": str(package / "init_files"), |
| } |
| (config_dir / "config.yaml").write_text(json.dumps(paths), encoding="utf-8") |
| torch.distributed.barrier() |
| os.environ["LIBERO_CONFIG_PATH"] = str(config_dir) |
| sys.path.insert(0, str(libero_root)) |
|
|
|
|
| def orientation(quaternion): |
| quaternion = np.asarray(quaternion, dtype=np.float64).copy() |
| quaternion[3] = np.clip(quaternion[3], -1.0, 1.0) |
| denominator = np.sqrt(1.0 - quaternion[3] ** 2) |
| axisangle = ( |
| np.zeros(3) |
| if math.isclose(denominator, 0.0) |
| else quaternion[:3] * 2.0 * math.acos(quaternion[3]) / denominator |
| ) |
| return Rotation.from_euler("xyz", axisangle).as_euler("xyz") |
|
|
|
|
| def finish_action(action: torch.Tensor): |
| action = action.detach().cpu().numpy().copy() |
| action[-1] = 1.0 if action[-1] > 0.5 else -1.0 |
| return action |
|
|
|
|
| class PolicyRunner: |
| def __init__( |
| self, model, temperature: float, execution_horizon: int, max_steps: int |
| ): |
| self.model = model |
| self.processor = getattr(model, "module", model).image_processor |
| self.temperature = temperature |
| self.execution_horizon = execution_horizon |
| self.max_steps = max_steps |
|
|
| def reset(self): |
| self.primary = deque(maxlen=CONTEXT_STEPS) |
| self.wrist = deque(maxlen=CONTEXT_STEPS) |
| self.state = deque(maxlen=CONTEXT_STEPS) |
| self.pending = deque() |
| self.predictions = torch.zeros( |
| self.max_steps, self.max_steps + ACTION_HORIZON, 7, device="cuda" |
| ) |
| self.valid = torch.zeros( |
| self.max_steps, |
| self.max_steps + ACTION_HORIZON, |
| dtype=torch.bool, |
| device="cuda", |
| ) |
|
|
| def observe(self, observation): |
| primary = Image.fromarray(observation["agentview_image"][::-1]) |
| wrist = Image.fromarray(observation["robot0_eye_in_hand_image"]) |
| self.primary.append(self.processor(primary).unsqueeze(0).unsqueeze(0)) |
| self.wrist.append(self.processor(wrist).unsqueeze(0).unsqueeze(0)) |
| state = np.concatenate( |
| ( |
| observation["robot0_eef_pos"], |
| orientation(observation["robot0_eef_quat"]), |
| observation["robot0_gripper_qpos"], |
| ) |
| ) |
| self.state.append(torch.from_numpy(state).float().view(1, 1, -1)) |
|
|
| def prefill(self, observations): |
| for observation in observations[-CONTEXT_STEPS:-1]: |
| self.observe(observation) |
|
|
| def ensemble(self, chunk: torch.Tensor, timestep: int): |
| self.predictions[timestep, timestep : timestep + ACTION_HORIZON] = chunk |
| self.valid[timestep, timestep : timestep + ACTION_HORIZON] = True |
| selected = [] |
| for target in range(timestep, timestep + self.execution_horizon): |
| mask = self.valid[: timestep + 1, target] |
| actions = self.predictions[: timestep + 1, target][mask] |
| weights = np.exp(-self.temperature * np.arange(len(actions))) |
| weights = torch.as_tensor(weights / weights.sum(), device="cuda").unsqueeze( |
| 1 |
| ) |
| selected.append(finish_action((actions * weights).sum(0))) |
| self.pending.extend(selected[1:]) |
| return selected[0] |
|
|
| @torch.inference_mode() |
| def act(self, observation, instruction: str, timestep: int): |
| self.observe(observation) |
| if self.pending: |
| return self.pending.popleft() |
| if len(self.primary) != CONTEXT_STEPS: |
| raise RuntimeError("evaluation requires three real context observations") |
| primary = torch.cat(tuple(self.primary), dim=1).cuda() |
| wrist = torch.cat(tuple(self.wrist), dim=1).cuda() |
| state = torch.cat(tuple(self.state), dim=1).cuda() |
| text = ( |
| clip.tokenize([instruction] * CONTEXT_STEPS, truncate=True) |
| .view(1, CONTEXT_STEPS, -1) |
| .cuda() |
| ) |
| chunk = self.model(primary, wrist, state, text)[0, -1] |
| return self.ensemble(chunk, timestep) |
|
|
|
|
| def evaluate(args, model): |
| with ( |
| open(os.devnull, "w") as quiet, |
| contextlib.redirect_stdout(quiet), |
| contextlib.redirect_stderr(quiet), |
| ): |
| from libero.libero import benchmark |
| from libero.libero.envs import OffScreenRenderEnv |
|
|
| suite = benchmark.get_benchmark_dict()[args.suite]() |
| runner = PolicyRunner( |
| model, args.temperature, args.execution_horizon, args.max_steps |
| ) |
| total = args.tasks * args.episodes |
| assigned = list(range(total))[rank() :: world_size()] |
| local_results = [] |
| progress = tqdm( |
| assigned, |
| desc=f"GPU {rank()}", |
| position=rank(), |
| dynamic_ncols=True, |
| leave=True, |
| ) |
| for evaluation_id in progress: |
| task_id, episode_index = divmod(evaluation_id, args.episodes) |
| episode_id = args.episode_offset + episode_index |
| task = suite.get_task(task_id) |
| bddl = ( |
| args.libero_path |
| / "libero" |
| / "libero" |
| / "bddl_files" |
| / task.problem_folder |
| / task.bddl_file |
| ) |
| environment = OffScreenRenderEnv( |
| bddl_file_name=str(bddl), |
| camera_heights=128, |
| camera_widths=128, |
| render_gpu_device_id=int(os.environ["LOCAL_RANK"]), |
| ) |
| try: |
| environment.reset() |
| environment.seed(66) |
| initial_states = torch.load( |
| args.libero_path |
| / "libero" |
| / "libero" |
| / "init_files" |
| / task.problem_folder |
| / task.init_states_file, |
| weights_only=False, |
| ) |
| if episode_id >= len(initial_states): |
| raise IndexError( |
| f"episode {episode_id} is unavailable for task {task_id}" |
| ) |
| observation = environment.set_init_state(initial_states[episode_id]) |
| warmup = [] |
| for _ in range(5): |
| observation, _, _, _ = environment.step(np.zeros(7)) |
| warmup.append(copy.deepcopy(observation)) |
| runner.reset() |
| runner.prefill(warmup) |
| observation = warmup[-1] |
| success = 0 |
| for steps in range(1, args.max_steps + 1): |
| action = runner.act(observation, task.language, steps - 1) |
| observation, _, done, _ = environment.step(action) |
| if done: |
| success = 1 |
| break |
| local_results.append((evaluation_id, task_id, episode_id, success, steps)) |
| progress.set_postfix(successes=sum(item[3] for item in local_results)) |
| finally: |
| environment.close() |
|
|
| gathered = [None] * world_size() if rank() == 0 else None |
| torch.distributed.gather_object(local_results, gathered, dst=0) |
| if rank() != 0: |
| return |
| results = sorted((item for group in gathered for item in group), key=lambda x: x[0]) |
| per_task = [] |
| print() |
| for task_id in range(args.tasks): |
| values = [item[3] for item in results if item[1] == task_id] |
| rate = float(np.mean(values)) |
| per_task.append(rate) |
| print(f"Task {task_id}: {sum(values)}/{len(values)} ({rate:.1%})") |
| successes = sum(item[3] for item in results) |
| print(f"Overall: {successes}/{len(results)} ({successes / len(results):.1%})") |
| report = { |
| "suite": args.suite, |
| "gpus": world_size(), |
| "episodes": len(results), |
| "successes": successes, |
| "success_rate": successes / len(results), |
| "per_task_success_rate": per_task, |
| "results": [ |
| {"task": item[1], "episode": item[2], "success": item[3], "steps": item[4]} |
| for item in results |
| ], |
| } |
| (args.output_dir / "results.json").write_text( |
| json.dumps(report, indent=2) + "\n", encoding="utf-8" |
| ) |
|
|
|
|
| def parser(): |
| value = argparse.ArgumentParser(description=__doc__) |
| value.add_argument("--suite", required=True, choices=SUITES) |
| value.add_argument("--gpus", type=int, default=1) |
| value.add_argument( |
| "--model-root", type=Path, default=Path(__file__).resolve().parent |
| ) |
| value.add_argument( |
| "--libero-path", type=Path, default=Path("~/LIBERO").expanduser() |
| ) |
| value.add_argument("--output-dir", type=Path, required=True) |
| value.add_argument("--tasks", type=int, default=10) |
| value.add_argument("--episodes", type=int, default=50) |
| value.add_argument("--episode-offset", type=int, default=0) |
| value.add_argument("--max-steps", type=int, default=600) |
| value.add_argument("--execution-horizon", type=int, choices=(1, 3), default=3) |
| value.add_argument("--temperature", type=float, default=0.01) |
| return value |
|
|
|
|
| def main(): |
| args = parser().parse_args() |
| if "RANK" not in os.environ: |
| return launch(args.gpus) |
| if args.gpus != world_size(): |
| raise ValueError( |
| f"--gpus={args.gpus} but torchrun started {world_size()} workers" |
| ) |
| args.model_root = args.model_root.expanduser().resolve() |
| args.libero_path = args.libero_path.expanduser().resolve() |
| args.output_dir = args.output_dir.expanduser().resolve() |
| os.environ.update(MUJOCO_GL="egl", PYOPENGL_PLATFORM="egl") |
| os.environ.setdefault("NCCL_IB_DISABLE", "1") |
| os.environ.setdefault("NCCL_P2P_DISABLE", "1") |
| os.environ.setdefault("NCCL_CUMEM_ENABLE", "0") |
| os.environ.setdefault("TORCH_NCCL_BLOCKING_WAIT", "1") |
| torch.cuda.set_device(int(os.environ["LOCAL_RANK"])) |
| torch.distributed.init_process_group( |
| backend="nccl", device_id=torch.device("cuda", int(os.environ["LOCAL_RANK"])) |
| ) |
| try: |
| configure_libero(args.libero_path, args.output_dir) |
| model = load_model(args.model_root, args.suite, torch.device("cuda")) |
| seed = 66 + rank() |
| random.seed(seed) |
| np.random.seed(seed) |
| torch.manual_seed(seed) |
| model = DDP( |
| model, |
| device_ids=[int(os.environ["LOCAL_RANK"])], |
| find_unused_parameters=True, |
| ) |
| model.eval() |
| evaluate(args, model) |
| torch.distributed.barrier() |
| finally: |
| torch.distributed.destroy_process_group() |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|