Update Wall-X to 1.1.0 (#104)

This commit is contained in:
Starrick Liu
2026-06-15 11:40:00 +08:00
committed by GitHub
parent e23a586846
commit 72834e7de5
200 changed files with 33916 additions and 16771 deletions
+142
View File
@@ -0,0 +1,142 @@
# Scripts
This directory contains the public Wall-X command-line helpers. Run the examples
below from the repository root, using `python scripts/...` and `bash scripts/...`.
Pass file and directory paths explicitly.
## Inference smoke test
Use `fake_inference.py` to verify that a checkpoint can be loaded and can
produce one action chunk from a synthetic LIBERO-style observation.
```bash
python scripts/fake_inference.py --checkpoint-path /path/to/checkpoint
```
If the training config is not stored next to the checkpoint as `config.yml` or
`config.yaml`, pass it explicitly:
```bash
python scripts/fake_inference.py \
--checkpoint-path /path/to/checkpoint \
--train-config-path /path/to/config.yml
```
## LIBERO evaluation
`run_libero.sh` is a small shell wrapper around `infer_libero.py`. It requires
the optional LIBERO simulator stack:
```bash
pip install -r requirements-libero.txt
mkdir -p third_party
git clone https://github.com/Lifelong-Robot-Learning/LIBERO.git third_party/LIBERO
```
The launcher checks for LIBERO, robosuite, MuJoCo, PyOpenGL, BDDL, Gym, and
h5py before loading the model. If LIBERO is cloned elsewhere, pass
`LIBERO_PATH=/path/to/LIBERO`.
```bash
bash scripts/run_libero.sh /path/to/checkpoint
```
Useful environment variables:
```bash
CHECKPOINT_PATH=/path/to/checkpoint
TRAIN_CONFIG_PATH=/path/to/config.yml
TASK_SUITE_NAME=libero_spatial
TASK_INDICES=0,1,2
NUM_TRIALS_PER_TASK=50
CUDA_ID=0
SMOKE=1
MAX_INFER_TIMES=52
```
`MAX_INFER_TIMES` is optional. When omitted, the launcher uses suite-specific
defaults aligned with the LIBERO evaluator: spatial 22, object 28, goal 30,
libero_10 52, and libero_90 40 action chunks.
For full control, call the Python entry directly:
```bash
python scripts/infer_libero.py \
--checkpoint-path /path/to/checkpoint \
--task-suite-name libero_spatial \
--num-trials-per-task 50 \
--driver-mode in_process
```
You can also pass a complete eval config:
```bash
python scripts/infer_libero.py --config /path/to/eval_config.yml
```
## WebSocket serving
`run_serving.sh` launches the Wall-X WebSocket server through the public
vendored serving runtime. Pass paths explicitly; the script has no built-in
checkpoint path.
```bash
bash scripts/run_serving.sh \
--checkpoint-path /path/to/checkpoint \
--train-config-path /path/to/config.yml \
--port 32195
```
By default the script returns raw model action chunks, which is the expected
mode for open-loop plotting. Pass `--serialize-actions` when your client expects
robot-serialized actions.
Useful options:
```bash
CUDA_ID=0
ACTION_HORIZON=32
IMAGE_PASSING_MODE=base64
MAX_BATCH_SIZE=1
```
Additional `launch_serving.py` arguments can be forwarded after `--`:
```bash
bash scripts/run_serving.sh --checkpoint-path /path/to/checkpoint -- \
--model-config.norm-key libero_all
```
## Open-loop WebSocket evaluation
`draw_openloop_plot.py` compares predicted action chunks from a running
WebSocket server against LeRobot dataset ground truth. `--dataset-root` and
`--train-config` are required and have no built-in default.
```bash
python scripts/draw_openloop_plot.py \
--uri ws://127.0.0.1:32195 \
--dataset-root /path/to/lerobot_dataset \
--train-config /path/to/train_config.yml \
--episode-indices 0,1,2 \
--save-dir ./openloop_plots
```
## Dataset and checkpoint utilities
- `compute_norm_stats.py`: compute action normalization statistics for a
local LeRobot v3 dataset. The script reads state/action parquet columns
directly when available, so image and video columns are not decoded.
- `merge_sharded_weights.py`: merge FSDP sharded checkpoint files into a single
checkpoint directory.
- `merge_tokenizer.py`: merge FAST action tokens into a Qwen2.5-VL processor
tokenizer.
```bash
python scripts/merge_tokenizer.py \
--processor-path /path/to/Qwen2.5-VL-3B-Instruct \
--action-tokenizer-path /path/to/fast_tokenizer \
--output-dir /path/to/merged_processor
```
Most scripts support `--help` for their command-line options.
+704 -134
View File
@@ -1,182 +1,752 @@
#!/usr/bin/env python3
"""Compute LeRobot normalization stats (mean, std, q01, q99) for training.
Writes JSON in the format expected by wall-x training configs::
{"norm_stats": {
"observation.state": {"mean": [...], "std": [...], "q01": [...], "q99": [...]},
"action": {"mean": [...], "std": [...], "q01": [...], "q99": [...]}
}}
When ``--train_config`` is provided, the script reads ``data.lerobot_config.repo_id``,
``norm_stats_path``, ``task.dof_config``, ``task.agent_pos_config``, and
``task.action_horizon`` from the YAML. Per-DOF slices are aggregated separately;
keys ending with ``_relative`` use the same relative-pose logic as the LeRobot loader.
Usage
-----
Recommended: pass a finetune YAML (paths in the config can be placeholders; override
with CLI flags if needed)::
python scripts/compute_norm_stats.py \\
--train_config /path/to/train_config.yml
Multi-task example::
python scripts/compute_norm_stats.py \\
--train_config /path/to/multitask_config.yml
Override dataset or output path from the command line::
python scripts/compute_norm_stats.py \\
--train_config /path/to/train_config.yml \\
--data_root /path/to/repo_id \\
--output_path /path/to/norm_stats_path
Without a train config (global stats only, no per-DOF relative slices)::
python scripts/compute_norm_stats.py \\
--data_root /path/to/lerobot_dataset \\
--output_path /path/to/norm_stats.json
Requirements
------------
- Local LeRobot v3 dataset at ``--data_root`` (or ``data.lerobot_config.repo_id``)
- ``lerobot>=0.3``, ``datasets``, ``pyarrow``, ``numpy``, ``pyyaml``, ``tqdm``
After running, set ``norm_stats_path`` in your training YAML to the generated JSON.
"""
import argparse
import json
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List
from tqdm import tqdm
from typing import Any
import numpy as np
import yaml
from numba import jit, prange
from tqdm import tqdm
from lerobot.datasets.lerobot_dataset import LeRobotDataset
SKIP_DOF_KEYS = frozenset(
{"velocity_decomposed", "height", "head_actions", "action_padding"}
)
_GEOMETRY_FUNCS_LOADED = False
def write_json(path: Path, data: Dict) -> None:
def _ensure_geometry_funcs() -> None:
global _GEOMETRY_FUNCS_LOADED
global canonicalize_euler_zyx_batch_nb
global euler_to_matrix_zyx_batch_nb
global matrix_to_euler_zyx_batch_nb
global so3_to_matrix_batch_nb
if _GEOMETRY_FUNCS_LOADED:
return
try:
from wall_x._vendor.x2robot_utils.geometry import (
canonicalize_euler_zyx_batch_nb,
euler_to_matrix_zyx_batch_nb,
matrix_to_euler_zyx_batch_nb,
so3_to_matrix_batch_nb,
)
except ImportError as exc:
raise RuntimeError(
"compute_norm_stats.py requires the vendored x2robot_utils geometry "
"helpers. Install Wall-X first so wall_x._vendor is available."
) from exc
_GEOMETRY_FUNCS_LOADED = True
@dataclass
class TrainConfigContext:
data_root: Path
output_path: Path
state_key: str
action_key: str
propri_ranges: dict[str, list[int]]
action_ranges: dict[str, list[int]]
action_chunk: int
dof_config: dict[str, int]
agent_pos_config: dict[str, int]
def write_norm_stats(path: Path, norm_stats: dict[str, dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
json.dumps({"norm_stats": norm_stats}, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def compute_action_statistics(
action_data_by_robot: Dict[str, Dict[str, List]]
) -> Dict[str, Dict[str, Dict]]:
def compute_vector_stats(values: np.ndarray) -> dict[str, list[float]]:
if values.ndim == 1:
values = values.reshape(-1, 1)
return {
"mean": np.mean(values, axis=0).tolist(),
"std": np.std(values, axis=0).tolist(),
"q01": np.quantile(values, 0.01, axis=0).tolist(),
"q99": np.quantile(values, 0.99, axis=0).tolist(),
}
def _apply_slice_stats(
full_stats: dict[str, list[float]],
index_range: list[int],
slice_stats: dict[str, list[float]],
) -> None:
start, end = index_range
for field in ("mean", "std", "q01", "q99"):
full_stats[field][start:end] = slice_stats[field]
def config_to_index_ranges(config: dict[str, int]) -> dict[str, list[int]]:
ranges: dict[str, list[int]] = {}
cur = 0
for key, dim in config.items():
if key in SKIP_DOF_KEYS:
continue
ranges[key] = [cur, cur + int(dim)]
cur += int(dim)
return ranges
def load_train_config(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as f:
config = yaml.load(f, Loader=yaml.SafeLoader)
if not isinstance(config, dict):
raise ValueError(f"train config must be a YAML mapping, got {type(config)}")
return config
def parse_train_config(config: dict[str, Any]) -> TrainConfigContext:
task = config.get("task")
if not isinstance(task, dict):
raise ValueError("train config must contain a 'task' section")
dof_config = task.get("dof_config")
agent_pos_config = task.get("agent_pos_config")
if not isinstance(dof_config, dict) or not dof_config:
raise ValueError("task.dof_config is required in train config")
if not isinstance(agent_pos_config, dict) or not agent_pos_config:
raise ValueError("task.agent_pos_config is required in train config")
data_cfg = config.get("data")
if not isinstance(data_cfg, dict):
raise ValueError("train config must contain a 'data' section")
lerobot_config = data_cfg.get("lerobot_config")
if not isinstance(lerobot_config, dict):
raise ValueError("data.lerobot_config is required in train config")
repo_id = lerobot_config.get("repo_id")
if not repo_id:
raise ValueError("data.lerobot_config.repo_id is required in train config")
norm_stats_path = config.get("norm_stats_path") or data_cfg.get("norm_stats_path")
if not norm_stats_path:
raise ValueError(
"norm_stats_path is required in train config " "(top-level or under data)"
)
key_mappings = data_cfg.get("key_mappings")
if not isinstance(key_mappings, dict):
raise ValueError("data.key_mappings is required in train config")
state_key = key_mappings.get("state", "observation.state")
action_key = key_mappings.get("action", "action")
action_chunk = int(
task.get("action_horizon")
or task.get("action_horizon_flow")
or data_cfg.get("action_horizon")
or 32
)
return TrainConfigContext(
data_root=Path(repo_id),
output_path=Path(norm_stats_path),
state_key=state_key,
action_key=action_key,
propri_ranges=config_to_index_ranges(agent_pos_config),
action_ranges=config_to_index_ranges(dof_config),
action_chunk=action_chunk,
dof_config=dof_config,
agent_pos_config=agent_pos_config,
)
def layout_vector_dim(layout_config: dict[str, int]) -> int:
"""Config vector width excluding virtual padding keys."""
return sum(
int(dim) for key, dim in layout_config.items() if key not in SKIP_DOF_KEYS
)
def _prepare_arrays_for_layout(
states: np.ndarray,
actions: np.ndarray,
agent_pos_config: dict[str, int],
dof_config: dict[str, int],
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute statistics (min, q01, q99, max) for each action type and dimension.
Args:
action_data_by_robot: Dict[robot_id][action_type] -> list of arrays/lists
Returns:
Dict[robot_id][action_type] -> {
"min": [min for each dim],
"q01": [quantile 1% for each dim],
"q99": [quantile 99% for each dim],
"max": [max for each dim],
"delta": [max - min for each dim]
"delta_q99_q01": [q99 - q01 for each dim]
}
Match LeRobot training loader: convert 3D Euler slices to 6D when config
uses rotation_6D keys but the dataset stores 14-dim Euler vectors.
"""
stats = {}
from wall_x.data.backends.lerobot.rotation_layout import (
euler_layout_dim,
layout_uses_6d_rotation,
maybe_convert_euler_to_6d,
)
for robot_id, action_data in action_data_by_robot.items():
stats[robot_id] = {}
convert_state = layout_uses_6d_rotation(agent_pos_config)
convert_action = layout_uses_6d_rotation(dof_config)
for action_type, values_list in action_data.items():
if not values_list:
continue
if convert_state:
raw_dim = euler_layout_dim(agent_pos_config)
target_dim = layout_vector_dim(agent_pos_config)
if states.shape[-1] == raw_dim:
states = maybe_convert_euler_to_6d(states, agent_pos_config, True)
logging.info(
"Converted state Euler->6D (%d -> %d dims)", raw_dim, states.shape[-1]
)
elif states.shape[-1] != target_dim:
raise ValueError(
f"State dim {states.shape[-1]} does not match Euler raw dim "
f"{raw_dim} or 6D layout dim {target_dim} from agent_pos_config"
)
# Convert to numpy array: shape (num_samples, num_dims)
try:
values_array = np.array(values_list)
if values_array.size == 0:
continue
if convert_action:
raw_dim = euler_layout_dim(dof_config)
target_dim = layout_vector_dim(dof_config)
if actions.shape[-1] == raw_dim:
actions = maybe_convert_euler_to_6d(actions, dof_config, True)
logging.info(
"Converted action Euler->6D (%d -> %d dims)", raw_dim, actions.shape[-1]
)
elif actions.shape[-1] != target_dim:
raise ValueError(
f"Action dim {actions.shape[-1]} does not match Euler raw dim "
f"{raw_dim} or 6D layout dim {target_dim} from dof_config"
)
# Handle both 1D and 2D cases
if values_array.ndim == 1:
values_array = values_array.reshape(-1, 1)
elif values_array.ndim == 2:
pass
else:
logging.warning(
f"Unexpected shape for {robot_id}/{action_type}: {values_array.shape}"
)
continue
expected_state_dim = layout_vector_dim(agent_pos_config)
expected_action_dim = layout_vector_dim(dof_config)
if states.shape[-1] != expected_state_dim:
raise ValueError(
f"State dim {states.shape[-1]} != expected layout dim {expected_state_dim}"
)
if actions.shape[-1] != expected_action_dim:
raise ValueError(
f"Action dim {actions.shape[-1]} != expected layout dim {expected_action_dim}"
)
return states, actions
# Compute statistics for each dimension
min_vals = np.min(values_array, axis=0).tolist()
max_vals = np.max(values_array, axis=0).tolist()
q01_vals = np.quantile(values_array, 0.01, axis=0).tolist()
q99_vals = np.quantile(values_array, 0.99, axis=0).tolist()
delta_vals = (np.array(max_vals) - np.array(min_vals)).tolist()
delta_q99_q01_vals = (np.array(q99_vals) - np.array(q01_vals)).tolist()
stats[robot_id][action_type] = {
"min": min_vals,
"q01": q01_vals,
"q99": q99_vals,
"max": max_vals,
"delta": delta_vals,
"delta_q99_q01": delta_q99_q01_vals,
}
def resolve_lerobot_dataset_paths(dataset_root: Path) -> tuple[str, Path]:
"""Return ``(repo_id, root)`` for a local LeRobot dataset directory."""
root = dataset_root.expanduser().resolve()
if not root.is_dir():
raise FileNotFoundError(f"LeRobot dataset root not found: {root}")
return root.name, root
except Exception as e:
logging.warning(
f"Error computing statistics for {robot_id}/{action_type}: {e}"
def _load_parquet_state_action_only(
data_root: Path,
state_key: str,
action_key: str,
):
"""
Read only state/action columns from LeRobot v3 parquet.
Does not load image/video columns or decode mp4 files.
"""
import pyarrow.dataset as pa_ds
from datasets import Dataset
root = data_root.expanduser().resolve()
paths = sorted((root / "data").glob("*/*.parquet"))
if not paths:
raise FileNotFoundError(f"No parquet files under {root / 'data'}")
logging.info(
"Reading parquet columns %r, %r only (no video/images)",
state_key,
action_key,
)
arrow_dataset = pa_ds.dataset([str(path) for path in paths], format="parquet")
table = arrow_dataset.to_table(columns=[state_key, action_key])
return Dataset(table)
def _load_state_action_table(
data_root: Path,
state_key: str,
action_key: str,
):
root = data_root.expanduser().resolve()
if root.is_dir() and (root / "meta" / "info.json").is_file():
table = _load_parquet_state_action_only(root, state_key, action_key)
return table, state_key, action_key
try:
from lerobot.datasets.lerobot_dataset import LeRobotDataset
except ImportError as exc:
raise RuntimeError(
"compute_norm_stats.py requires LeRobot. Install it first, for example "
"`pip install lerobot==0.4.4` or follow the repository README."
) from exc
dataset = LeRobotDataset(str(data_root), root=None, video_backend="pyav")
non_image_columns = [
col for col in dataset.features if "image" not in col and col not in {"task"}
]
if state_key not in non_image_columns or action_key not in non_image_columns:
raise ValueError(
f"Expected keys {state_key!r} and {action_key!r} in dataset columns, "
f"got {non_image_columns}"
)
table = dataset.hf_dataset.select_columns([state_key, action_key])
return table, state_key, action_key
def _table_to_arrays(
table,
state_key: str,
action_key: str,
) -> tuple[np.ndarray, np.ndarray]:
"""Load full columns once; avoids O(N*chunk) random row access."""
logging.info("Loading state/action columns into memory...")
try:
states = np.asarray(table[state_key], dtype=np.float32)
actions = np.asarray(table[action_key], dtype=np.float32)
except (KeyError, TypeError, ValueError) as exc:
logging.warning(
"Column-wise load failed (%s); falling back to per-row stack.", exc
)
states = np.stack(
[
np.asarray(table[i][state_key], dtype=np.float32)
for i in range(len(table))
]
)
actions = np.stack(
[
np.asarray(table[i][action_key], dtype=np.float32)
for i in range(len(table))
]
)
if states.ndim == 1:
states = states.reshape(-1, 1)
if actions.ndim == 1:
actions = actions.reshape(-1, 1)
logging.info(
" frames=%d state_dim=%d action_dim=%d",
len(states),
states.shape[1],
actions.shape[1],
)
return states, actions
def _collect_relative_cartesian(
actions: np.ndarray,
states: np.ndarray,
index_range: list[int],
action_chunk: int,
) -> np.ndarray:
start, end = index_range
max_start = max(0, len(actions) - action_chunk)
chunks = []
anchor_states = states[:max_start, start:end]
for offset in range(action_chunk):
chunks.append(actions[offset : offset + max_start, start:end] - anchor_states)
return np.concatenate(chunks, axis=0)
def _compute_delta_from_state_and_abs_rot(
rotations: np.ndarray, state: np.ndarray
) -> np.ndarray:
"""Relative rotation: R_rel = R_abs @ R_state^T (same convention as the loader)."""
_ensure_geometry_funcs()
if rotations.shape[-1] == 3:
rotations_matrix = euler_to_matrix_zyx_batch_nb(rotations)
out_is_euler = True
elif rotations.shape[-1] == 6:
rotations_matrix = so3_to_matrix_batch_nb(rotations)
out_is_euler = False
else:
raise ValueError(
f"Only 3D euler or 6D rotation supported, got {rotations.shape[-1]}D"
)
if state.shape[-1] == 3:
state_matrix = euler_to_matrix_zyx_batch_nb(state[np.newaxis, :])[0]
elif state.shape[-1] == 6:
state_matrix = so3_to_matrix_batch_nb(state[np.newaxis, :])[0]
else:
raise ValueError(
f"Only 3D euler or 6D rotation supported, got {state.shape[-1]}D"
)
return _abs_rot_to_delta(rotations_matrix, state_matrix, out_is_euler)
@jit(nopython=True, parallel=True)
def _abs_rot_to_delta(
rotations_matrix: np.ndarray,
state_matrix: np.ndarray,
out_is_euler: bool,
) -> np.ndarray:
st = np.empty((3, 3), dtype=np.float64)
st[0, 0] = state_matrix[0, 0]
st[0, 1] = state_matrix[1, 0]
st[0, 2] = state_matrix[2, 0]
st[1, 0] = state_matrix[0, 1]
st[1, 1] = state_matrix[1, 1]
st[1, 2] = state_matrix[2, 1]
st[2, 0] = state_matrix[0, 2]
st[2, 1] = state_matrix[1, 2]
st[2, 2] = state_matrix[2, 2]
n = rotations_matrix.shape[0]
r_rel = np.empty((n, 3, 3), dtype=np.float64)
for i in prange(n):
a00 = rotations_matrix[i, 0, 0]
a01 = rotations_matrix[i, 0, 1]
a02 = rotations_matrix[i, 0, 2]
a10 = rotations_matrix[i, 1, 0]
a11 = rotations_matrix[i, 1, 1]
a12 = rotations_matrix[i, 1, 2]
a20 = rotations_matrix[i, 2, 0]
a21 = rotations_matrix[i, 2, 1]
a22 = rotations_matrix[i, 2, 2]
r_rel[i, 0, 0] = a00 * st[0, 0] + a01 * st[1, 0] + a02 * st[2, 0]
r_rel[i, 0, 1] = a00 * st[0, 1] + a01 * st[1, 1] + a02 * st[2, 1]
r_rel[i, 0, 2] = a00 * st[0, 2] + a01 * st[1, 2] + a02 * st[2, 2]
r_rel[i, 1, 0] = a10 * st[0, 0] + a11 * st[1, 0] + a12 * st[2, 0]
r_rel[i, 1, 1] = a10 * st[0, 1] + a11 * st[1, 1] + a12 * st[2, 1]
r_rel[i, 1, 2] = a10 * st[0, 2] + a11 * st[1, 2] + a12 * st[2, 2]
r_rel[i, 2, 0] = a20 * st[0, 0] + a21 * st[1, 0] + a22 * st[2, 0]
r_rel[i, 2, 1] = a20 * st[0, 1] + a21 * st[1, 1] + a22 * st[2, 1]
r_rel[i, 2, 2] = a20 * st[0, 2] + a21 * st[1, 2] + a22 * st[2, 2]
if out_is_euler:
d_euler = matrix_to_euler_zyx_batch_nb(r_rel)
return canonicalize_euler_zyx_batch_nb(d_euler)
out6 = np.empty((n, 6), dtype=np.float64)
for i in prange(n):
out6[i, 0] = r_rel[i, 0, 0]
out6[i, 1] = r_rel[i, 0, 1]
out6[i, 2] = r_rel[i, 0, 2]
out6[i, 3] = r_rel[i, 1, 0]
out6[i, 4] = r_rel[i, 1, 1]
out6[i, 5] = r_rel[i, 1, 2]
return out6
def _collect_relative_rotation(
actions: np.ndarray,
states: np.ndarray,
index_range: list[int],
action_chunk: int,
) -> np.ndarray:
"""
Per-anchor action chunk relative to anchor state (matches lerobot loader).
Unlike cartesian relative, each anchor processes a full [chunk, dim] action
clip against a single proprio rotation at the anchor frame.
"""
start, end = index_range
max_start = max(0, len(actions) - action_chunk)
if max_start == 0:
return np.empty((0, end - start), dtype=np.float32)
chunks = []
for anchor_idx in tqdm(
range(max_start),
desc=" relative rotation anchors",
leave=False,
):
action_clip = actions[anchor_idx : anchor_idx + action_chunk, start:end]
proprio_clip = states[anchor_idx, start:end]
rel = _compute_delta_from_state_and_abs_rot(
action_clip.astype(np.float64), proprio_clip.astype(np.float64)
).astype(np.float32)
chunks.append(rel)
return np.concatenate(chunks, axis=0)
def collect_dof_vectors_from_arrays(
states: np.ndarray,
actions: np.ndarray,
propri_ranges: dict[str, list[int]],
action_ranges: dict[str, list[int]],
action_chunk: int = 32,
) -> dict[str, np.ndarray]:
vectors: dict[str, np.ndarray] = {}
absolute_action_keys = {
key: index_range
for key, index_range in action_ranges.items()
if not key.endswith("_relative")
}
relative_action_keys = {
key: index_range
for key, index_range in action_ranges.items()
if key.endswith("_relative")
}
for sub_key, index_range in propri_ranges.items():
start, end = index_range
vectors[sub_key] = states[:, start:end]
for sub_key, index_range in absolute_action_keys.items():
start, end = index_range
vectors[sub_key] = actions[:, start:end]
if relative_action_keys:
logging.info(
"Computing relative action slices (chunk=%d, anchors=%d)...",
action_chunk,
max(0, len(actions) - action_chunk),
)
for sub_key, index_range in tqdm(
relative_action_keys.items(), desc="Relative action keys"
):
if "rotation" in sub_key:
vectors[sub_key] = _collect_relative_rotation(
actions, states, index_range, action_chunk
)
else:
vectors[sub_key] = _collect_relative_cartesian(
actions, states, index_range, action_chunk
)
continue
return stats
return vectors
def load_lerobot_dataset(
repo_id: str,
trajectory_keys: Dict,
base_dir: Path,
) -> None:
def compute_norm_stats_with_dof_config(
data_root: Path,
output_path: Path,
propri_ranges: dict[str, list[int]],
action_ranges: dict[str, list[int]],
state_key: str = "observation.state",
action_key: str = "action",
action_chunk: int = 32,
dof_config: dict[str, int] | None = None,
agent_pos_config: dict[str, int] | None = None,
) -> dict[str, dict]:
table, state_key, action_key = _load_state_action_table(
data_root, state_key, action_key
)
states, actions = _table_to_arrays(table, state_key, action_key)
if dof_config is not None and agent_pos_config is not None:
states, actions = _prepare_arrays_for_layout(
states, actions, agent_pos_config, dof_config
)
norm_stats = {
state_key: compute_vector_stats(states),
action_key: compute_vector_stats(actions),
}
# Load local or remote dataset
dataset = LeRobotDataset(base_dir)
vectors = collect_dof_vectors_from_arrays(
states=states,
actions=actions,
propri_ranges=propri_ranges,
action_ranges=action_ranges,
action_chunk=action_chunk,
)
# Iterate through all data
frames: Dict[str, Dict[str, List]] = defaultdict(lambda: defaultdict(list))
for sub_key, index_range in propri_ranges.items():
if sub_key not in vectors:
logging.warning("No samples collected for propri key %s, skipping", sub_key)
continue
slice_stats = compute_vector_stats(vectors[sub_key])
_apply_slice_stats(norm_stats[state_key], index_range, slice_stats)
logging.info(" %s (agent_pos): dim=%d", sub_key, len(slice_stats["mean"]))
all_features = dataset.features
non_image_columns = [col for col in all_features if "image" not in col]
for sub_key, index_range in action_ranges.items():
if sub_key not in vectors:
logging.warning("No samples collected for action key %s, skipping", sub_key)
continue
slice_stats = compute_vector_stats(vectors[sub_key])
_apply_slice_stats(norm_stats[action_key], index_range, slice_stats)
mode = "relative" if sub_key.endswith("_relative") else "absolute"
logging.info(" %s (dof, %s): dim=%d", sub_key, mode, len(slice_stats["mean"]))
print(f"Reading the following fields:{non_image_columns}")
fast_dataset = dataset.hf_dataset.select_columns(non_image_columns)
for i in tqdm(range(len(fast_dataset))):
sample = fast_dataset[i]
action = sample["action"] # torch.Tensor
propri = sample["observation.state"]
for key, action_keys in trajectory_keys.items():
for action_key, action_range in action_keys.items():
if key == "action":
frames[repo_id][action_key].append(
action[action_range[0] : action_range[1]].numpy().tolist()
)
else:
frames[repo_id][action_key].append(
propri[action_range[0] : action_range[1]].numpy().tolist()
)
return frames
write_norm_stats(output_path, norm_stats)
return norm_stats
def compute_action_normalizer(
repo_id: str, trajectory_keys: Dict, base_dir: Path, output_dir: Path
) -> None:
"""
Compute action normalizer statistics for all robot_ids.
"""
logging.info("Starting action normalizer computation...")
def load_vectors(
data_root: Path,
state_key: str,
action_key: str,
) -> tuple[np.ndarray, np.ndarray]:
table, state_key, action_key = _load_state_action_table(
data_root, state_key, action_key
)
return _table_to_arrays(table, state_key, action_key)
frames = load_lerobot_dataset(repo_id, trajectory_keys, base_dir)
# Compute statistics
stats = compute_action_statistics(frames)
def compute_norm_stats(
data_root: Path,
output_path: Path,
state_key: str = "observation.state",
action_key: str = "action",
train_ctx: TrainConfigContext | None = None,
) -> dict[str, dict]:
if train_ctx is None:
states, actions = load_vectors(data_root, state_key, action_key)
norm_stats = {
state_key: compute_vector_stats(states),
action_key: compute_vector_stats(actions),
}
write_norm_stats(output_path, norm_stats)
return norm_stats
# Save statistics for each robot_id
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
return compute_norm_stats_with_dof_config(
data_root=data_root,
output_path=output_path,
propri_ranges=train_ctx.propri_ranges,
action_ranges=train_ctx.action_ranges,
state_key=state_key,
action_key=action_key,
action_chunk=train_ctx.action_chunk,
dof_config=train_ctx.dof_config,
agent_pos_config=train_ctx.agent_pos_config,
)
# for robot_id, robot_stats in stats.items():
# output_file = output_dir / f"{robot_id}_action_stats.json"
# write_json(output_file, robot_stats)
# logging.info(f"Saved action statistics for {robot_id} to {output_file}")
# Also save a combined file
combined_output = output_dir / "all_robots_action_stats.json"
write_json(combined_output, stats)
logging.info(f"Saved combined action statistics to {combined_output}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compute norm stats for a local LeRobot v3 dataset.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""\
examples:
%(prog)s --train_config /path/to/train_config.yml
%(prog)s --train_config /path/to/multitask_config.yml \\
--data_root /path/to/repo_id --output_path /path/to/norm_stats_path
%(prog)s --data_root /path/to/lerobot_dataset --output_path /path/to/norm_stats.json
""",
)
parser.add_argument(
"--train_config",
type=str,
default=None,
help=(
"Training YAML config (e.g. cvpr_example.yml). When set, reads "
"data.lerobot_config.repo_id, data.norm_stats_path, task.dof_config, "
"task.agent_pos_config and task.action_horizon. Action keys ending "
"with '_relative' use the same relative-pose logic as lerobot loader."
),
)
parser.add_argument(
"--data_root",
type=str,
default=None,
help="Local LeRobot dataset directory (overrides train config)",
)
parser.add_argument(
"--output_path",
type=str,
default=None,
help="Output json path (overrides train config)",
)
parser.add_argument(
"--state_key",
type=str,
default=None,
help="Dataset column for proprioception (overrides train config)",
)
parser.add_argument(
"--action_key",
type=str,
default=None,
help="Dataset column for action (overrides train config)",
)
return parser.parse_args()
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
args = parse_args()
repo_id = "xxx" # your dataset name
data_root_path = "/path/to/lerobot/dataset"
output_stats_dir = "/path/to/save/action_stats"
trajectory_keys = { # your dataset keys
"propri": {
"follow_right_ee_cartesian_pos": [0, 3],
"follow_right_ee_rotation": [3, 6],
"follow_right_gripper": [6, 7],
},
"action": {
"master_right_ee_cartesian_pos": [0, 3],
"master_right_ee_rotation": [3, 6],
"master_right_gripper": [6, 7],
},
}
train_ctx: TrainConfigContext | None = None
if args.train_config:
config_path = Path(args.train_config)
if not config_path.exists():
raise FileNotFoundError(f"train config not found: {config_path}")
train_ctx = parse_train_config(load_train_config(config_path))
logging.info("train_config: %s", config_path)
logging.info(" dof_config keys: %s", list(train_ctx.action_ranges))
logging.info(" agent_pos_config keys: %s", list(train_ctx.propri_ranges))
logging.info(" action_chunk: %d", train_ctx.action_chunk)
compute_action_normalizer(
repo_id, trajectory_keys, data_root_path, output_stats_dir
data_root = Path(args.data_root or (train_ctx.data_root if train_ctx else ""))
output_path = Path(args.output_path or (train_ctx.output_path if train_ctx else ""))
state_key = args.state_key or (
train_ctx.state_key if train_ctx else "observation.state"
)
logging.info("Action normalizer computation completed.")
action_key = args.action_key or (train_ctx.action_key if train_ctx else "action")
if not data_root.exists():
raise FileNotFoundError(f"Dataset not found: {data_root}")
logging.info("dataset: %s", data_root)
logging.info("output: %s", output_path)
norm_stats = compute_norm_stats(
data_root=data_root,
output_path=output_path,
state_key=state_key,
action_key=action_key,
train_ctx=train_ctx,
)
for key, stats in norm_stats.items():
logging.info(" %s: dim=%d", key, len(stats["mean"]))
logging.info("Saved norm stats to %s", output_path)
if __name__ == "__main__":
File diff suppressed because it is too large Load Diff
Regular → Executable
+109 -70
View File
@@ -1,85 +1,124 @@
import torch
from wall_x.model.qwen2_5_based.modeling_qwen2_5_vl_act import Qwen2_5_VLMoEForAction
#!/usr/bin/env python3
"""Run one Wall-X VLA inference pass through the harrix adapter.
model_path = "/path/to/model"
model = Qwen2_5_VLMoEForAction.from_pretrained(model_path)
model.eval()
This is a lightweight smoke test for the inference path. It builds a synthetic
LIBERO-style observation, loads the checkpoint through harrix, and prints the
predicted action chunk shape.
"""
# Gen Fake data
batch_size = 1
seq_length = 50
from __future__ import annotations
torch.manual_seed(0)
fake_input_ids = torch.randint(
0, len(model.processor.tokenizer), (batch_size, seq_length), dtype=torch.long
)
fake_attention_mask = torch.ones((batch_size, seq_length), dtype=torch.long)
fake_moe_token_types = torch.zeros((batch_size, seq_length), dtype=torch.long)
fake_position_ids = (
torch.arange(seq_length, dtype=torch.long).unsqueeze(0).expand(batch_size, -1)
)
fake_proprioception = torch.randn((batch_size, 1, 20), dtype=torch.float32)
fake_agent_pos_mask = torch.ones((batch_size, 1, 20), dtype=torch.float32)
fake_dof_mask = torch.ones((batch_size, 32, 20), dtype=torch.float32)
fake_dataset_names = ["x2_normal"]
import argparse
import sys
from pathlib import Path
import numpy as np
device = "cuda"
def _ensure_local_harrix_on_path() -> None:
repo_root = Path(__file__).resolve().parents[1]
harrix_python = repo_root / "third_party" / "harrix" / "python"
if harrix_python.is_dir():
sys.path.insert(0, str(harrix_python))
model = model.to(device)
model = model.bfloat16()
fake_input_ids = fake_input_ids.to(device)
fake_attention_mask = fake_attention_mask.to(device)
fake_moe_token_types = fake_moe_token_types.to(device)
fake_position_ids = fake_position_ids.to(device)
fake_proprioception = fake_proprioception.to(device).bfloat16()
fake_agent_pos_mask = fake_agent_pos_mask.to(device).bfloat16()
fake_dof_mask = fake_dof_mask.to(device).bfloat16()
def _right_gripper_dim_from_config(train_config: dict) -> int:
layout = train_config.get("agent_pos_config") or train_config.get("task", {}).get(
"agent_pos_config", {}
)
if not isinstance(layout, dict):
return 1
try:
with torch.no_grad():
outputs = model(
input_ids=fake_input_ids,
attention_mask=fake_attention_mask,
moe_token_types=fake_moe_token_types,
position_ids=fake_position_ids,
proprioception=fake_proprioception,
agent_pos_mask=fake_agent_pos_mask,
dof_mask=fake_dof_mask,
dataset_names=fake_dataset_names,
mode="validate",
)
gripper_dim = 1
for key, dim in layout.items():
bare = key.replace("follow_", "").replace("master_", "")
if bare == "right_gripper":
gripper_dim = int(dim)
break
print("✅ Fake inference test successful!")
print(f"Output logits shape: {outputs.logits.shape}")
print(f"Output logits dtype: {outputs.logits.dtype}")
print(f"Output logits device: {outputs.logits.device}")
norm_dim = int(train_config.get("_libero_proprio_norm_dim") or 0)
real_dim = sum(int(v) for k, v in layout.items() if k != "action_padding")
if norm_dim == real_dim + 1:
gripper_dim += 1
return max(1, gripper_dim)
# Check if output is reasonable
if outputs.logits.shape == (batch_size, seq_length, model.config.vocab_size):
print("✅ Output shape correct")
else:
print("❌ Output shape incorrect")
if not torch.isnan(outputs.logits).any():
print("✅ Output contains no NaN values")
else:
print("❌ Output contains NaN values")
def _build_fake_observation(seed: int, image_size: int, gripper_dim: int) -> dict:
rng = np.random.default_rng(seed)
return {
"eef_pos": rng.normal(size=(3,)).astype(np.float32),
"eef_axisangle": rng.normal(size=(3,)).astype(np.float32),
"gripper": rng.normal(size=(gripper_dim,)).astype(np.float32),
"face_view": rng.integers(0, 256, (image_size, image_size, 3), dtype=np.uint8),
"wrist_view": rng.integers(0, 256, (image_size, image_size, 3), dtype=np.uint8),
}
if not torch.isinf(outputs.logits).any():
print("✅ Output contains no infinity values")
else:
print("❌ Output contains infinity values")
print("Output logits statistics:")
print(f" Min value: {outputs.logits.min().item():.4f}")
print(f" Max value: {outputs.logits.max().item():.4f}")
print(f" Mean: {outputs.logits.mean().item():.4f}")
print(f" Standard deviation: {outputs.logits.std().item():.4f}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--checkpoint-path", required=True, help="Checkpoint directory."
)
parser.add_argument(
"--train-config-path",
default=None,
help="Optional training config path. Defaults to config.yml/config.yaml next to the checkpoint.",
)
parser.add_argument("--norm-key", default="libero_all")
parser.add_argument("--architecture", default="qwen2_5")
parser.add_argument("--action-mode", default="flow")
parser.add_argument(
"--cam-names",
nargs="+",
default=["face_view", "right_wrist_view"],
help="Camera names expected by the checkpoint.",
)
parser.add_argument("--action-horizon", type=int, default=None)
parser.add_argument("--instruction", default="pick up the object")
parser.add_argument("--image-size", type=int, default=128)
parser.add_argument("--seed", type=int, default=0)
return parser.parse_args()
except Exception as e:
print(f"❌ Fake inference test failed: {e}")
import traceback
traceback.print_exc()
def main() -> int:
args = parse_args()
_ensure_local_harrix_on_path()
import wall_x._vendor.harrix.adapters # noqa: F401 register model adapters
from wall_x._vendor.harrix.adapters.registry import build_adapter
from wall_x._vendor.harrix.eval_config import (
EvalConfig,
LiberoEnvParams,
autofill_from_checkpoint,
)
cfg = EvalConfig()
cfg.model.checkpoint_path = args.checkpoint_path
cfg.model.train_config_path = args.train_config_path
cfg.model.norm_key = args.norm_key
cfg.model.cam_names = list(args.cam_names)
cfg.model.action_horizon = args.action_horizon
cfg.model.architecture = args.architecture
cfg.model.action_mode = args.action_mode
cfg.env.libero = LiberoEnvParams(num_trials_per_task=1, task_indices=[0])
cfg = autofill_from_checkpoint(cfg)
adapter = build_adapter(cfg)
gripper_dim = _right_gripper_dim_from_config(getattr(adapter, "_train_config", {}))
payload = {
"observation": _build_fake_observation(args.seed, args.image_size, gripper_dim),
"instruction": args.instruction,
"noise": None,
}
actions = adapter.predict_batch([payload])
action = np.asarray(actions[0])
print("Fake inference succeeded.")
print(f"action shape: {action.shape}")
print(f"action dtype: {action.dtype}")
print(f"action min/max: {float(action.min()):.6f} / {float(action.max()):.6f}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Regular → Executable
+231 -218
View File
@@ -1,222 +1,235 @@
import argparse
import time
import os
#!/usr/bin/env python3
"""Run LIBERO evaluation through harrix.
# from wall_x.utils.baseline_utils import check_baseline_dump, update_baseline
from wall_x.infer.utils_libero import set_seed_everywhere, TaskSuite, TASK_MAX_STEPS
from wall_x.infer.infer_config import InferConfig
from wall_x.infer.env_libero import LiberoRobotEnv
The script accepts either a full harrix EvalConfig YAML or a checkpoint path
plus common command-line overrides. It intentionally bypasses the legacy
Wall-X inference stack.
"""
from __future__ import annotations
import argparse
import sys
import tempfile
from pathlib import Path
import yaml
LIBERO_DEFAULT_MAX_INFER_TIMES = {
"libero_spatial": 22,
"libero_object": 28,
"libero_goal": 30,
"libero_10": 52,
"libero_90": 40,
}
def _ensure_local_harrix_on_path() -> None:
repo_root = Path(__file__).resolve().parents[1]
harrix_python = repo_root / "third_party" / "harrix" / "python"
if harrix_python.is_dir():
sys.path.insert(0, str(harrix_python))
def _parse_task_indices(value: str | None) -> list[int] | None:
if value is None or value.strip() == "":
return None
return [int(x) for x in value.split(",") if x.strip()]
def _resolve_max_infer_times(
task_suite_name: str | None, max_infer_times: int | None
) -> int:
if max_infer_times is not None:
return max_infer_times
suite = task_suite_name or "libero_spatial"
return LIBERO_DEFAULT_MAX_INFER_TIMES.get(suite, 22)
def _load_or_build_raw_config(args: argparse.Namespace) -> dict:
if args.config is not None:
with open(args.config, "r") as f:
raw = yaml.safe_load(f) or {}
model = raw.setdefault("model", {})
env = raw.setdefault("env", {})
libero = env.setdefault("libero", {})
runtime = raw.setdefault("runtime", {})
debug = raw.setdefault("debug", {})
if args.checkpoint_path is not None:
model["checkpoint_path"] = args.checkpoint_path
if args.train_config_path is not None:
model["train_config_path"] = args.train_config_path
task_indices = _parse_task_indices(args.task_indices)
if task_indices is not None:
libero["task_indices"] = task_indices
if args.max_infer_times is not None or libero.get("max_infer_times") is None:
libero["max_infer_times"] = _resolve_max_infer_times(
libero.get("task_suite_name", args.task_suite_name),
args.max_infer_times,
)
if args.smoke:
libero["task_indices"] = [0]
libero["num_trials_per_task"] = 5
runtime["num_workers"] = 1
runtime["max_batch_size"] = 1
if args.deterministic_model:
debug["deterministic_model"] = True
return raw
else:
if args.checkpoint_path is None:
raise ValueError("--checkpoint-path is required when --config is not set")
max_infer_times = _resolve_max_infer_times(
args.task_suite_name, args.max_infer_times
)
raw = {
"model": {
"checkpoint_path": args.checkpoint_path,
"norm_key": args.norm_key,
"cam_names": args.cam_names,
"architecture": args.architecture,
"action_mode": args.action_mode,
},
"env": {
"type": "libero",
"seed": args.seed,
"libero": {
"task_suite_name": args.task_suite_name,
"initial_states_path": args.initial_states_path,
"num_trials_per_task": args.num_trials_per_task,
"max_infer_times": max_infer_times,
"skip_intermediate_render": args.skip_intermediate_render,
},
},
"runtime": {
"num_workers": args.num_workers,
"max_batch_size": args.max_batch_size,
"ws_port": args.ws_port,
"log_dir": args.log_dir,
"driver_mode": args.driver_mode,
},
"debug": {"deterministic_model": args.deterministic_model},
}
model = raw.setdefault("model", {})
env = raw.setdefault("env", {})
libero = env.setdefault("libero", {})
runtime = raw.setdefault("runtime", {})
debug = raw.setdefault("debug", {})
if args.checkpoint_path is not None:
model["checkpoint_path"] = args.checkpoint_path
if args.train_config_path is not None:
model["train_config_path"] = args.train_config_path
if args.norm_key is not None:
model["norm_key"] = args.norm_key
if args.cam_names is not None:
model["cam_names"] = args.cam_names
if args.action_horizon is not None:
model["action_horizon"] = args.action_horizon
if args.architecture is not None:
model["architecture"] = args.architecture
if args.action_mode is not None:
model["action_mode"] = args.action_mode
env["type"] = "libero"
env["seed"] = args.seed
libero["task_suite_name"] = args.task_suite_name
libero["initial_states_path"] = args.initial_states_path
libero["num_trials_per_task"] = args.num_trials_per_task
libero["max_infer_times"] = _resolve_max_infer_times(
args.task_suite_name, args.max_infer_times
)
libero["skip_intermediate_render"] = args.skip_intermediate_render
task_indices = _parse_task_indices(args.task_indices)
if task_indices is not None:
libero["task_indices"] = task_indices
if args.smoke:
libero["task_indices"] = [0]
libero["num_trials_per_task"] = 5
runtime["num_workers"] = 1
runtime["max_batch_size"] = 1
runtime["num_workers"] = (
args.num_workers if not args.smoke else runtime["num_workers"]
)
runtime["max_batch_size"] = (
args.max_batch_size if not args.smoke else runtime["max_batch_size"]
)
runtime["ws_port"] = args.ws_port
runtime["log_dir"] = args.log_dir
runtime["driver_mode"] = args.driver_mode
debug["deterministic_model"] = args.deterministic_model
return raw
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--config", default=None, help="Optional harrix EvalConfig YAML."
)
parser.add_argument("--checkpoint-path", default=None)
parser.add_argument("--train-config-path", default=None)
parser.add_argument("--norm-key", default="libero_all")
parser.add_argument("--architecture", default="qwen2_5")
parser.add_argument("--action-mode", default="flow")
parser.add_argument(
"--cam-names", nargs="+", default=["face_view", "right_wrist_view"]
)
parser.add_argument("--action-horizon", type=int, default=None)
parser.add_argument("--task-suite-name", default="libero_spatial")
parser.add_argument("--initial-states-path", default="DEFAULT")
parser.add_argument("--num-trials-per-task", type=int, default=50)
parser.add_argument(
"--task-indices", default=None, help="Comma-separated task ids."
)
parser.add_argument(
"--max-infer-times",
type=int,
default=None,
help=(
"Number of model action chunks per episode. Defaults are suite-specific "
"and match the internal LIBERO evaluator."
),
)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--num-workers", type=int, default=1)
parser.add_argument("--max-batch-size", type=int, default=1)
parser.add_argument("--ws-port", type=int, default=8765)
parser.add_argument("--log-dir", default="/tmp/harrix_libero_eval")
parser.add_argument("--driver-mode", choices=["in_process"], default="in_process")
parser.add_argument("--smoke", action="store_true")
parser.add_argument("--deterministic-model", action="store_true")
parser.add_argument(
"--skip-intermediate-render",
action=argparse.BooleanOptionalAction,
default=True,
)
return parser.parse_args()
def main() -> int:
args = parse_args()
_ensure_local_harrix_on_path()
from wall_x._vendor.harrix.eval_config import (
autofill_from_checkpoint,
load_eval_config,
)
raw = _load_or_build_raw_config(args)
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as f:
yaml.safe_dump(raw, f, sort_keys=False)
tmp_config = f.name
cfg = autofill_from_checkpoint(load_eval_config(tmp_config))
if cfg.runtime.driver_mode != "in_process":
raise ValueError("Only driver_mode='in_process' is supported")
from wall_x._vendor.harrix.drivers.inproc import run
run(cfg)
return 0
if __name__ == "__main__":
args = argparse.ArgumentParser(description="Wall-X Libero evaluation script")
args.add_argument("--seed", type=int, default=42, help="Random seed")
args.add_argument("--id", type=int, default=None, help="Unique index id")
args.add_argument("--name", type=int, default=None, help="Launch command name")
args.add_argument(
"--baseline_path", type=str, default=None, help="Path to baseline record table"
)
args.add_argument(
"--update_baseline",
type=bool,
default=False,
help="Whether to update baseline table",
)
args.add_argument(
"--mode", type=str, default="flow", choices=["flow", "ar"], help="Running mode"
)
args.add_argument(
"--checkpoint_path", type=str, required=True, help="Model checkpoint path"
)
args.add_argument(
"--train_config_path",
type=str,
required=False,
default=None,
help="Path to training config .yml file",
)
args.add_argument(
"--norm_key",
type=str,
default="physical-intelligence/libero",
help="Key for normalization statistics",
)
args.add_argument(
"--cam_names",
nargs="+",
default=["face_view", "right_wrist_view"],
help="List of camera names (e.g., --cam_names face_view right_wrist_view)",
)
args.add_argument(
"--task_suite_name",
type=str,
default=TaskSuite.LIBERO_SPATIAL,
choices=[e.value for e in TaskSuite],
help="Libero task suite to load",
)
args.add_argument(
"--initial_states_path",
type=str,
default="DEFAULT",
help="Path to initial states .json file, or 'DEFAULT' to use default states.",
)
args.add_argument(
"--num_trials_per_task",
type=int,
default=50,
help="Number of evaluation episodes to run per task",
)
args.add_argument(
"--rollout_dir",
type=str,
default="./rollouts",
help="Directory to save rollout videos",
)
args = args.parse_args()
print(f"Using random seed: {args.seed}")
set_seed_everywhere(args.seed)
print("Initializing InferConfig...")
if args.train_config_path is None:
args.train_config_path = os.path.join(args.checkpoint_path, "config.yml")
config = InferConfig(
checkpoint_path=args.checkpoint_path,
train_config_path=args.train_config_path,
norm_key=args.norm_key,
cam_names=args.cam_names,
)
if args.mode == "flow":
config.action_horizon = config.train_config.get("data", {}).get(
"action_horizon_flow", 10
)
elif args.mode == "ar":
config.action_horizon = config.train_config.get("data", {}).get(
"action_horizon_ar", 10
)
else:
raise ValueError(f"Invalid mode: {args.mode}")
config.model_device = "cuda"
print("Initializing LiberoRobotEnv (Evaluator)...")
config.action_dim = 7
config.pred_horizon = 10
evaluator = LiberoRobotEnv(
config=config,
task_suite_name=args.task_suite_name,
initial_states_path=args.initial_states_path,
rollout_dir=args.rollout_dir,
seed=args.seed,
)
print(f"\n{'='*20} Starting Evaluation {'='*20}")
print(f"Task suite: {args.task_suite_name}")
print(f"Number of tasks: {evaluator.num_tasks}")
print(f"Trials per task: {args.num_trials_per_task}")
print(f"Initial states: {args.initial_states_path}")
print(f"Videos will be saved to: {evaluator.rollout_dir}")
print(f"{'='*50}\n")
total_successes = 0
total_episodes_run = 0
start_time = time.time()
for task_id in range(evaluator.num_tasks):
task_successes = 0
task_episodes_attempted = 0
libero_env_instance = None
task_desc = ""
initial_states = None
max_infer_times = TASK_MAX_STEPS[args.task_suite_name]
print(
f"{args.task_suite_name} TASK_MAX_STEPS: {TASK_MAX_STEPS[args.task_suite_name]}"
)
for ep_idx in range(args.num_trials_per_task):
print(f" > Running trial {ep_idx + 1} / {args.num_trials_per_task}...")
try:
print(f"\nCreating environment for Task {task_id}...")
libero_env_instance, task_desc, initial_states = (
evaluator.create_env_for_task(task_id)
)
print(
f"--- Starting task {task_id + 1} / {evaluator.num_tasks}: {task_desc} ---"
)
except Exception as e:
print(
f"\n[CRITICAL ERROR] Failed to create environment for task {task_id}: {e}. Skipping entire task."
)
continue
task_episodes_attempted += 1
total_episodes_run += 1
success = False
try:
if args.mode == "flow":
success = evaluator.run_infer_flow_action(
env=libero_env_instance,
task_id=task_id,
task_desc=task_desc,
default_initial_states=initial_states,
episode_idx=ep_idx,
max_infer_times=max_infer_times,
)
elif args.mode == "ar":
success = evaluator.run_infer_ar_action(
env=libero_env_instance,
task_id=task_id,
task_desc=task_desc,
default_initial_states=initial_states,
episode_idx=ep_idx,
max_infer_times=max_infer_times,
)
except Exception as e:
print(f" [EXCEPTION] Episode run error: {e}")
if success:
task_successes += 1
total_successes += 1
print(" > Trial result: SUCCESS")
else:
print(" > Trial result: FAILURE")
if task_episodes_attempted > 0:
print(
f" > Task {task_id} current success rate: {task_successes / task_episodes_attempted * 100:.1f}% ({task_successes}/{task_episodes_attempted})"
)
if total_episodes_run > 0:
print(
f" > Overall current success rate: {total_successes / total_episodes_run * 100:.1f}% ({total_successes}/{total_episodes_run})"
)
task_success_rate = (
task_successes / task_episodes_attempted
if task_episodes_attempted > 0
else 0
)
print(f"\n--- Task {task_id} ({task_desc}) Summary ---")
print(
f"Success rate: {task_success_rate * 100:.1f}% ({task_successes}/{task_episodes_attempted})"
)
print(f"{'-'*40}\n")
end_time = time.time()
total_time = end_time - start_time
final_success_rate = (
total_successes / total_episodes_run if total_episodes_run > 0 else 0
)
print(f"\n{'='*20} Final Evaluation Summary {'='*20}")
print(f"Total runtime: {total_time:.2f} seconds ({total_time / 60:.1f} minutes)")
print(f"Total trials run: {total_episodes_run}")
print(f"Total successes: {total_successes}")
print(f"Overall success rate: {final_success_rate * 100:.2f}%")
print(f"{'='*56}")
print("Evaluation completed.")
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
+12 -60
View File
@@ -6,9 +6,10 @@ Works around the StorageMeta compatibility issue between PyTorch versions.
import os
import sys
import torch
from pathlib import Path
from typing import Dict
import torch
from safetensors.torch import save_file
@@ -70,65 +71,16 @@ def load_sharded_checkpoint(checkpoint_dir: str) -> Dict[str, torch.Tensor]:
except AttributeError as e:
if "StorageMeta" in str(e):
print(f"[ERROR] StorageMeta compatibility issue: {e}")
print("[INFO] Attempting alternative loading method...")
return load_checkpoint_alternative(checkpoint_dir)
else:
raise
def load_checkpoint_alternative(checkpoint_dir: str) -> Dict[str, torch.Tensor]:
"""
Alternative method to load checkpoint by directly reading shard files.
Args:
checkpoint_dir: Path to directory containing .distcp files
Returns:
Dictionary of merged model state
"""
checkpoint_path = Path(checkpoint_dir)
# Find all shard files
shard_files = sorted(checkpoint_path.glob("*.distcp"))
if not shard_files:
raise FileNotFoundError(f"No .distcp files found in {checkpoint_dir}")
print(f"[INFO] Found {len(shard_files)} shard files")
# Load all shards
merged_state = {}
for shard_file in shard_files:
print(f"[INFO] Loading shard: {shard_file.name}")
try:
shard_data = torch.load(shard_file, map_location="cpu")
# Merge the shard into the state dict
if isinstance(shard_data, dict):
for key, value in shard_data.items():
if isinstance(value, torch.Tensor):
if key in merged_state:
# Handle duplicates - concatenate or overwrite based on shape
print(f"[WARNING] Duplicate key found: {key}")
merged_state[key] = value
elif isinstance(value, dict):
# Nested dict structure
for subkey, subvalue in value.items():
full_key = f"{key}.{subkey}" if key else subkey
if isinstance(subvalue, torch.Tensor):
merged_state[full_key] = subvalue
except Exception as e:
print(f"[WARNING] Failed to load shard {shard_file.name}: {e}")
continue
if not merged_state:
raise RuntimeError("Failed to load any checkpoint data from shards")
print(f"[INFO] Loaded {len(merged_state)} tensors from shards")
return merged_state
raise RuntimeError(
"Unable to load this DCP checkpoint because its metadata uses "
"StorageMeta from a different PyTorch version. The previous "
"manual .distcp fallback was removed because FSDP shards cannot "
"be reconstructed by directly loading shard files and overwriting "
"duplicate keys. Please run this script with a PyTorch version "
"compatible with the checkpoint writer, or re-save the checkpoint "
"with the current PyTorch version."
) from e
raise
def save_merged_checkpoint(
+101 -20
View File
@@ -1,26 +1,107 @@
from transformers import AutoProcessor
import os
#!/usr/bin/env python3
"""Merge Wall-X action tokens into a Qwen2.5-VL processor tokenizer."""
processor_path = "/path/to/Qwen2.5-VL-3B-Instruct"
action_tokenizer_path = "/path/to/fast"
use_fast_tokenizer = True
from __future__ import annotations
processor = AutoProcessor.from_pretrained(processor_path, use_fast=True)
processor.tokenizer.padding_side = "left"
import argparse
from pathlib import Path
action_tokenizer = AutoProcessor.from_pretrained(
action_tokenizer_path, trust_remote_code=True
)
new_tokens = ["<|propri|>", "<|action|>"]
new_tokens += [f"<|action_token_{i}|>" for i in range(action_tokenizer.vocab_size)]
num_added_tokens = processor.tokenizer.add_tokens(new_tokens)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Create a Wall-X processor directory by adding FAST action tokens "
"to a Qwen2.5-VL processor tokenizer."
)
)
parser.add_argument(
"--processor-path",
required=True,
help="Base Qwen2.5-VL processor directory or Hugging Face repo id.",
)
parser.add_argument(
"--action-tokenizer-path",
required=True,
help="FAST/action tokenizer processor directory or Hugging Face repo id.",
)
parser.add_argument(
"--output-dir",
required=True,
help="Directory where the merged processor will be written.",
)
parser.add_argument(
"--use-fast",
action=argparse.BooleanOptionalAction,
default=True,
help="Use the fast tokenizer implementation when loading the base processor.",
)
parser.add_argument(
"--trust-remote-code",
action="store_true",
help="Allow custom code when loading the action tokenizer processor.",
)
return parser.parse_args()
begin_idx_token = "<|action_token_0|>"
token_id = processor.tokenizer.convert_tokens_to_ids(begin_idx_token)
processor.tokenizer.init_kwargs["action_token_start_index"] = token_id
processor.tokenizer.init_kwargs["action_token_vocab_size"] = action_tokenizer.vocab_size
new_tokenizer_dir = "/path/to/new_tokenizer"
os.makedirs(new_tokenizer_dir, exist_ok=True)
processor.save_pretrained(new_tokenizer_dir)
def _resolve_action_vocab_size(action_processor) -> int:
vocab_size = getattr(action_processor, "vocab_size", None)
if vocab_size is None and hasattr(action_processor, "tokenizer"):
vocab_size = getattr(action_processor.tokenizer, "vocab_size", None)
if vocab_size is None:
raise AttributeError(
"Could not determine action tokenizer vocab size from the loaded processor."
)
return int(vocab_size)
def merge_tokenizer(
*,
processor_path: str,
action_tokenizer_path: str,
output_dir: str,
use_fast: bool,
trust_remote_code: bool,
) -> None:
from transformers import AutoProcessor
processor = AutoProcessor.from_pretrained(processor_path, use_fast=use_fast)
processor.tokenizer.padding_side = "left"
action_processor = AutoProcessor.from_pretrained(
action_tokenizer_path,
trust_remote_code=trust_remote_code,
)
action_vocab_size = _resolve_action_vocab_size(action_processor)
new_tokens = ["<|propri|>", "<|action|>"]
new_tokens += [f"<|action_token_{i}|>" for i in range(action_vocab_size)]
num_added_tokens = processor.tokenizer.add_tokens(new_tokens)
begin_idx_token = "<|action_token_0|>"
token_id = processor.tokenizer.convert_tokens_to_ids(begin_idx_token)
processor.tokenizer.init_kwargs["action_token_start_index"] = token_id
processor.tokenizer.init_kwargs["action_token_vocab_size"] = action_vocab_size
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
processor.save_pretrained(output_path)
print(f"Saved merged processor to {output_path}")
print(f"Added {num_added_tokens} tokenizer tokens")
print(f"action_token_start_index={token_id}")
print(f"action_token_vocab_size={action_vocab_size}")
def main() -> None:
args = parse_args()
merge_tokenizer(
processor_path=args.processor_path,
action_tokenizer_path=args.action_tokenizer_path,
output_dir=args.output_dir,
use_fast=args.use_fast,
trust_remote_code=args.trust_remote_code,
)
if __name__ == "__main__":
main()
-161
View File
@@ -1,161 +0,0 @@
# This file is copied from openpi
import json
import pathlib
import numpy as np
import numpydantic
import pydantic
@pydantic.dataclasses.dataclass
class NormStats:
mean: numpydantic.NDArray
std: numpydantic.NDArray
q01: numpydantic.NDArray | None = None # 1st quantile
q99: numpydantic.NDArray | None = None # 99th quantile
class RunningStats:
"""Compute running statistics of a batch of vectors."""
def __init__(self):
self._count = 0
self._mean = None
self._mean_of_squares = None
self._min = None
self._max = None
self._histograms = None
self._bin_edges = None
self._num_quantile_bins = 5000 # for computing quantiles on the fly
def update(self, batch: np.ndarray) -> None:
"""
Update the running statistics with a batch of vectors.
Args:
vectors (np.ndarray): An array where all dimensions except the last are batch dimensions.
"""
batch = batch.reshape(-1, batch.shape[-1])
num_elements, vector_length = batch.shape
if self._count == 0:
self._mean = np.mean(batch, axis=0)
self._mean_of_squares = np.mean(batch**2, axis=0)
self._min = np.min(batch, axis=0)
self._max = np.max(batch, axis=0)
self._histograms = [
np.zeros(self._num_quantile_bins) for _ in range(vector_length)
]
self._bin_edges = [
np.linspace(
self._min[i] - 1e-10,
self._max[i] + 1e-10,
self._num_quantile_bins + 1,
)
for i in range(vector_length)
]
else:
if vector_length != self._mean.size:
raise ValueError(
"The length of new vectors does not match the initialized vector length."
)
new_max = np.max(batch, axis=0)
new_min = np.min(batch, axis=0)
max_changed = np.any(new_max > self._max)
min_changed = np.any(new_min < self._min)
self._max = np.maximum(self._max, new_max)
self._min = np.minimum(self._min, new_min)
if max_changed or min_changed:
self._adjust_histograms()
self._count += num_elements
batch_mean = np.mean(batch, axis=0)
batch_mean_of_squares = np.mean(batch**2, axis=0)
# Update running mean and mean of squares.
self._mean += (batch_mean - self._mean) * (num_elements / self._count)
self._mean_of_squares += (batch_mean_of_squares - self._mean_of_squares) * (
num_elements / self._count
)
self._update_histograms(batch)
def get_statistics(self) -> NormStats:
"""
Compute and return the statistics of the vectors processed so far.
Returns:
dict: A dictionary containing the computed statistics.
"""
if self._count < 2:
raise ValueError("Cannot compute statistics for less than 2 vectors.")
variance = self._mean_of_squares - self._mean**2
stddev = np.sqrt(np.maximum(0, variance))
q01, q99 = self._compute_quantiles([0.01, 0.99])
return NormStats(mean=self._mean, std=stddev, q01=q01, q99=q99)
def _adjust_histograms(self):
"""Adjust histograms when min or max changes."""
for i in range(len(self._histograms)):
old_edges = self._bin_edges[i]
new_edges = np.linspace(
self._min[i], self._max[i], self._num_quantile_bins + 1
)
# Redistribute the existing histogram counts to the new bins
new_hist, _ = np.histogram(
old_edges[:-1], bins=new_edges, weights=self._histograms[i]
)
self._histograms[i] = new_hist
self._bin_edges[i] = new_edges
def _update_histograms(self, batch: np.ndarray) -> None:
"""Update histograms with new vectors."""
for i in range(batch.shape[1]):
hist, _ = np.histogram(batch[:, i], bins=self._bin_edges[i])
self._histograms[i] += hist
def _compute_quantiles(self, quantiles):
"""Compute quantiles based on histograms."""
results = []
for q in quantiles:
target_count = q * self._count
q_values = []
for hist, edges in zip(self._histograms, self._bin_edges, strict=True):
cumsum = np.cumsum(hist)
idx = np.searchsorted(cumsum, target_count)
q_values.append(edges[idx])
results.append(np.array(q_values))
return results
class _NormStatsDict(pydantic.BaseModel):
norm_stats: dict[str, NormStats]
def serialize_json(norm_stats: dict[str, NormStats]) -> str:
"""Serialize the running statistics to a JSON string."""
return _NormStatsDict(norm_stats=norm_stats).model_dump_json(indent=2)
def deserialize_json(data: str) -> dict[str, NormStats]:
"""Deserialize the running statistics from a JSON string."""
return _NormStatsDict(**json.loads(data)).norm_stats
def save(directory: pathlib.Path | str, norm_stats: dict[str, NormStats]) -> None:
"""Save the normalization stats to a directory."""
path = pathlib.Path(directory) / "norm_stats.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(serialize_json(norm_stats))
def load(directory: pathlib.Path | str) -> dict[str, NormStats]:
"""Load the normalization stats from a directory."""
path = pathlib.Path(directory) / "norm_stats.json"
if not path.exists():
raise FileNotFoundError(f"Norm stats file not found at: {path}")
return deserialize_json(path.read_text())
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env bash
set -euo pipefail
# LIBERO evaluation launcher. Configure with environment variables:
#
# CHECKPOINT_PATH=/path/to/checkpoint bash scripts/run_libero.sh
# bash scripts/run_libero.sh /path/to/checkpoint
# CHECKPOINT_PATH=/path/to/checkpoint SMOKE=1 bash scripts/run_libero.sh
# CONFIG=/path/to/eval.yaml bash scripts/run_libero.sh
#
# Optional knobs:
# LIBERO_PATH=/path/to/LIBERO # optional when not cloned to third_party/LIBERO
# CUDA_ID=0
# DRIVER_MODE=in_process
# NUM_WORKERS=1
# MAX_BATCH_SIZE=1
# ALL_SUITES=1 # run all standard LIBERO suites (40 tasks)
# TASK_SUITES="libero_spatial ..." # custom suite list (space- or comma-separated)
# TASK_SUITE_NAME=libero_spatial # single suite when ALL_SUITES=0 and TASK_SUITES unset
# NUM_TRIALS_PER_TASK=50
# TASK_INDICES=0,1,2 # omit to run every task in the suite
# MAX_INFER_TIMES=52
# NORM_KEY=libero_all
# ROLLOUT_BASE=/path/to/rollout # per-suite logs under ${ROLLOUT_BASE}/${suite}/
# LOG_DIR=/tmp/harrix_libero_eval # overrides ROLLOUT_BASE when set (single suite)
# SKIP_LIBERO_DEP_CHECK=1 # bypass dependency preflight
# export ALL_SUITES=1
# export TASK_SUITE_NAME=libero_10
# export CUDA_ID=0
# export NUM_WORKERS=10
# export NUM_TRIALS_PER_TASK=20
# export CHECKPOINT_PATH=/path/to/checkpoint
# export ROLLOUT_BASE=/path/to/rollout
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
if [[ -d "${SOURCE_ROOT}/third_party/LIBERO" ]]; then
export PYTHONPATH="${SOURCE_ROOT}/third_party/LIBERO:${PYTHONPATH:-}"
fi
if [[ -n "${LIBERO_PATH:-}" ]]; then
export PYTHONPATH="${LIBERO_PATH}:${PYTHONPATH:-}"
fi
if [[ -f "${SCRIPT_DIR}/infer_libero.py" ]]; then
INFER_LIBERO_CMD=("${SCRIPT_DIR}/infer_libero.py")
elif [[ -f "${SOURCE_ROOT}/scripts/infer_libero.py" ]]; then
INFER_LIBERO_CMD=(python "${SOURCE_ROOT}/scripts/infer_libero.py")
elif command -v infer_libero.py >/dev/null 2>&1; then
INFER_LIBERO_CMD=(infer_libero.py)
else
echo "infer_libero.py is not available. Install Wall-X first." >&2
exit 2
fi
DEFAULT_ALL_SUITES=(
libero_spatial
libero_object
libero_goal
libero_10
)
if [[ $# -gt 1 ]]; then
echo "Usage: bash scripts/run_libero.sh [CHECKPOINT_PATH]" >&2
exit 2
fi
if [[ $# -eq 1 ]]; then
CHECKPOINT_PATH="$1"
fi
export CUDA_VISIBLE_DEVICES="${CUDA_ID:-0}"
# MuJoCo offscreen rendering via NVIDIA EGL (required on headless GPU nodes).
export MUJOCO_GL=egl
export PYOPENGL_PLATFORM=egl
# LIBERO init-state files are trusted simulator assets. PyTorch 2.6 changed
# torch.load() defaults in a way that breaks LIBERO's upstream loader.
export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD="${TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD:-1}"
EGL_VENDOR_DIR="${HOME}/.config/glvnd/egl_vendor.d"
EGL_VENDOR_FILE="${EGL_VENDOR_DIR}/10_nvidia.json"
if [[ ! -f "${EGL_VENDOR_FILE}" ]]; then
mkdir -p "${EGL_VENDOR_DIR}"
cat > "${EGL_VENDOR_FILE}" <<'EOF'
{
"file_format_version" : "1.0.0",
"ICD" : {
"library_path" : "libEGL_nvidia.so.0"
}
}
EOF
fi
export __EGL_VENDOR_LIBRARY_FILENAMES="${EGL_VENDOR_FILE}"
export LD_LIBRARY_PATH="/usr/lib/x86_64-linux-gnu:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH:-}"
# After CUDA_VISIBLE_DEVICES remapping, MuJoCo only sees devices from index 0.
export MUJOCO_EGL_DEVICE_ID="${MUJOCO_EGL_DEVICE_ID:-0}"
if [[ -d "${SOURCE_ROOT}/third_party/harrix/python" ]]; then
export PYTHONPATH="${SOURCE_ROOT}/third_party/harrix/python:${PYTHONPATH:-}"
fi
if [[ -d "${SOURCE_ROOT}/wall_x" ]]; then
export PYTHONPATH="${SOURCE_ROOT}:${PYTHONPATH:-}"
fi
check_libero_dependencies() {
local missing
if missing="$(
python - <<'PY'
import importlib.util
checks = [
("libero.libero", "LIBERO"),
("robosuite", "robosuite"),
("mujoco", "mujoco"),
("OpenGL", "PyOpenGL"),
("bddl", "bddl"),
("gym", "gym"),
("h5py", "h5py"),
]
missing = [label for module, label in checks if importlib.util.find_spec(module) is None]
if missing:
print(", ".join(missing))
raise SystemExit(1)
PY
)"; then
return 0
fi
cat >&2 <<EOF
LIBERO simulator dependencies are missing: ${missing}
Install the optional LIBERO simulator stack from the Wall-X repository root:
pip install -r requirements-libero.txt
mkdir -p third_party
git clone https://github.com/Lifelong-Robot-Learning/LIBERO.git third_party/LIBERO
If LIBERO is cloned elsewhere, pass it with:
LIBERO_PATH=/path/to/LIBERO bash scripts/run_libero.sh ...
Set SKIP_LIBERO_DEP_CHECK=1 only if you intentionally manage these dependencies elsewhere.
EOF
exit 2
}
if [[ "${SKIP_LIBERO_DEP_CHECK:-0}" != "1" ]]; then
check_libero_dependencies
fi
resolve_task_suites() {
local suites=()
if [[ "${ALL_SUITES:-0}" == "1" ]]; then
if [[ -n "${TASK_SUITES:-}" ]]; then
TASK_SUITES="${TASK_SUITES//,/ }"
read -r -a suites <<< "${TASK_SUITES}"
else
suites=("${DEFAULT_ALL_SUITES[@]}")
fi
elif [[ -n "${TASK_SUITES:-}" ]]; then
TASK_SUITES="${TASK_SUITES//,/ }"
read -r -a suites <<< "${TASK_SUITES}"
else
suites=("${TASK_SUITE_NAME:-libero_spatial}")
fi
echo "${suites[@]}"
}
resolve_log_dirs() {
local suite="$1"
local multi_suite="$2"
local log_dir rollout_dir
if [[ -n "${LOG_DIR:-}" ]]; then
if [[ "${multi_suite}" == "1" ]]; then
log_dir="${LOG_DIR}/${suite}"
else
log_dir="${LOG_DIR}"
fi
elif [[ -n "${ROLLOUT_BASE:-}" ]]; then
if [[ "${multi_suite}" == "1" ]]; then
log_dir="${ROLLOUT_BASE}/${suite}/json"
else
log_dir="${ROLLOUT_BASE}/json"
fi
else
log_dir="/tmp/harrix_libero_eval"
fi
if [[ -n "${WALLX_ROLLOUT_DIR:-}" ]]; then
if [[ "${multi_suite}" == "1" ]]; then
rollout_dir="${WALLX_ROLLOUT_DIR}/${suite}"
else
rollout_dir="${WALLX_ROLLOUT_DIR}"
fi
elif [[ -n "${ROLLOUT_BASE:-}" ]]; then
if [[ "${multi_suite}" == "1" ]]; then
rollout_dir="${ROLLOUT_BASE}/${suite}/videos"
else
rollout_dir="${ROLLOUT_BASE}/videos"
fi
else
rollout_dir=""
fi
mkdir -p "${log_dir}"
if [[ -n "${rollout_dir}" ]]; then
mkdir -p "${rollout_dir}"
fi
LOG_DIR_RESOLVED="${log_dir}"
WALLX_ROLLOUT_DIR_RESOLVED="${rollout_dir}"
}
run_suite() {
local suite="$1"
local multi_suite="$2"
resolve_log_dirs "${suite}" "${multi_suite}"
if [[ -n "${WALLX_ROLLOUT_DIR_RESOLVED}" ]]; then
export WALLX_ROLLOUT_DIR="${WALLX_ROLLOUT_DIR_RESOLVED}"
else
unset WALLX_ROLLOUT_DIR
fi
local args=()
if [[ -n "${CONFIG:-}" ]]; then
args+=(--config "${CONFIG}")
else
if [[ -z "${CHECKPOINT_PATH:-}" ]]; then
echo "CHECKPOINT_PATH is required when CONFIG is not set." >&2
exit 2
fi
args+=(--checkpoint-path "${CHECKPOINT_PATH}")
fi
if [[ -n "${TRAIN_CONFIG_PATH:-}" ]]; then
args+=(--train-config-path "${TRAIN_CONFIG_PATH}")
fi
if [[ "${SMOKE:-0}" == "1" ]]; then
args+=(--smoke)
fi
if [[ "${DET:-0}" == "1" ]]; then
args+=(--deterministic-model)
fi
if [[ -n "${TASK_INDICES:-}" ]]; then
args+=(--task-indices "${TASK_INDICES}")
fi
if [[ -n "${MAX_INFER_TIMES:-}" ]]; then
args+=(--max-infer-times "${MAX_INFER_TIMES}")
fi
args+=(--driver-mode "${DRIVER_MODE:-in_process}")
args+=(--num-workers "${NUM_WORKERS:-1}")
args+=(--max-batch-size "${MAX_BATCH_SIZE:-1}")
args+=(--task-suite-name "${suite}")
args+=(--num-trials-per-task "${NUM_TRIALS_PER_TASK:-50}")
args+=(--norm-key "${NORM_KEY:-libero_all}")
args+=(--log-dir "${LOG_DIR_RESOLVED}")
echo "=== LIBERO eval: suite=${suite} log_dir=${LOG_DIR_RESOLVED} ==="
"${INFER_LIBERO_CMD[@]}" "${args[@]}"
}
suites=($(resolve_task_suites))
multi_suite=0
if [[ "${#suites[@]}" -gt 1 ]]; then
multi_suite=1
fi
for suite in "${suites[@]}"; do
run_suite "${suite}" "${multi_suite}"
done
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
bash scripts/run_serving.sh --checkpoint-path /path/to/checkpoint [options]
Required:
--checkpoint-path PATH Checkpoint directory or checkpoint file.
Common options:
--train-config-path PATH Training config used by the checkpoint.
--port PORT WebSocket port. Default: 32195.
--host HOST Bind host. Default: 0.0.0.0.
--env X2ROBOT|LIBERO Serving environment. Default: X2ROBOT.
--cuda-id ID Sets CUDA_VISIBLE_DEVICES. Default: 0.
--image-passing-mode MODE base64 or numpy. Default: base64.
--action-horizon N Model action horizon. Default: 32.
--robot-type TYPE desktop, turtle, or ex001. Default: desktop.
--raw-actions Alias for --no-serialize-actions.
--serialize-actions Return robot-serialized actions.
--no-serialize-actions Return raw model action chunks. Default.
--max-batch-size N Enable dynamic batching.
--enable-cuda-graph Enable CUDA graph in the serving runtime.
--enable-experimental-engine Enable the experimental inference engine.
--debug Enable debug logging.
--dry-run Print the command without running it.
Additional arguments after "--" are forwarded to launch_serving.py, for example:
bash scripts/run_serving.sh --checkpoint-path /ckpt -- \
--model-config.norm-key libero_all
Environment variables can also be used, e.g. CHECKPOINT_PATH,
TRAIN_CONFIG_PATH, PORT, CUDA_ID, ACTION_HORIZON, WALLX_ENV.
EOF
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
if [[ -d "${SOURCE_ROOT}/wall_x" ]]; then
export PYTHONPATH="${SOURCE_ROOT}:${PYTHONPATH:-}"
fi
PYTHON_BIN="${PYTHON_BIN:-python}"
CHECKPOINT_PATH="${CHECKPOINT_PATH:-}"
TRAIN_CONFIG_PATH="${TRAIN_CONFIG_PATH:-}"
PORT="${PORT:-32195}"
HOST="${HOST:-0.0.0.0}"
WALLX_ENV="${WALLX_ENV:-X2ROBOT}"
CUDA_ID="${CUDA_ID:-0}"
IMAGE_PASSING_MODE="${IMAGE_PASSING_MODE:-base64}"
ACTION_HORIZON="${ACTION_HORIZON:-32}"
ROBOT_TYPE="${ROBOT_TYPE:-desktop}"
ROBOT_ACTION_INTERPOLATE_MULTIPLIER="${ROBOT_ACTION_INTERPOLATE_MULTIPLIER:-1}"
ROBOT_ACTION_END_RATIO="${ROBOT_ACTION_END_RATIO:-1.0}"
MODEL_DEVICE="${MODEL_DEVICE:-cuda}"
MAX_BATCH_SIZE="${MAX_BATCH_SIZE:-}"
DEFAULT_PROMPT="${DEFAULT_PROMPT:-}"
SERIALIZE_ACTIONS="${SERIALIZE_ACTIONS:-0}"
ENABLE_CUDA_GRAPH="${ENABLE_CUDA_GRAPH:-0}"
ENABLE_EXPERIMENTAL_ENGINE="${ENABLE_EXPERIMENTAL_ENGINE:-0}"
DEBUG="${DEBUG:-0}"
DRY_RUN=0
EXTRA_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--checkpoint-path)
CHECKPOINT_PATH="${2:?missing value for --checkpoint-path}"
shift 2
;;
--checkpoint-path=*)
CHECKPOINT_PATH="${1#*=}"
shift
;;
--train-config-path)
TRAIN_CONFIG_PATH="${2:?missing value for --train-config-path}"
shift 2
;;
--train-config-path=*)
TRAIN_CONFIG_PATH="${1#*=}"
shift
;;
--port)
PORT="${2:?missing value for --port}"
shift 2
;;
--port=*)
PORT="${1#*=}"
shift
;;
--host)
HOST="${2:?missing value for --host}"
shift 2
;;
--host=*)
HOST="${1#*=}"
shift
;;
--env)
WALLX_ENV="${2:?missing value for --env}"
shift 2
;;
--env=*)
WALLX_ENV="${1#*=}"
shift
;;
--cuda-id)
CUDA_ID="${2:?missing value for --cuda-id}"
shift 2
;;
--cuda-id=*)
CUDA_ID="${1#*=}"
shift
;;
--image-passing-mode)
IMAGE_PASSING_MODE="${2:?missing value for --image-passing-mode}"
shift 2
;;
--image-passing-mode=*)
IMAGE_PASSING_MODE="${1#*=}"
shift
;;
--action-horizon)
ACTION_HORIZON="${2:?missing value for --action-horizon}"
shift 2
;;
--action-horizon=*)
ACTION_HORIZON="${1#*=}"
shift
;;
--robot-type)
ROBOT_TYPE="${2:?missing value for --robot-type}"
shift 2
;;
--robot-type=*)
ROBOT_TYPE="${1#*=}"
shift
;;
--robot-action-interpolate-multiplier)
ROBOT_ACTION_INTERPOLATE_MULTIPLIER="${2:?missing value for --robot-action-interpolate-multiplier}"
shift 2
;;
--robot-action-interpolate-multiplier=*)
ROBOT_ACTION_INTERPOLATE_MULTIPLIER="${1#*=}"
shift
;;
--robot-action-end-ratio)
ROBOT_ACTION_END_RATIO="${2:?missing value for --robot-action-end-ratio}"
shift 2
;;
--robot-action-end-ratio=*)
ROBOT_ACTION_END_RATIO="${1#*=}"
shift
;;
--model-device)
MODEL_DEVICE="${2:?missing value for --model-device}"
shift 2
;;
--model-device=*)
MODEL_DEVICE="${1#*=}"
shift
;;
--max-batch-size)
MAX_BATCH_SIZE="${2:?missing value for --max-batch-size}"
shift 2
;;
--max-batch-size=*)
MAX_BATCH_SIZE="${1#*=}"
shift
;;
--default-prompt)
DEFAULT_PROMPT="${2:?missing value for --default-prompt}"
shift 2
;;
--default-prompt=*)
DEFAULT_PROMPT="${1#*=}"
shift
;;
--serialize-actions)
SERIALIZE_ACTIONS=1
shift
;;
--no-serialize-actions|--raw-actions)
SERIALIZE_ACTIONS=0
shift
;;
--enable-cuda-graph)
ENABLE_CUDA_GRAPH=1
shift
;;
--enable-experimental-engine)
ENABLE_EXPERIMENTAL_ENGINE=1
shift
;;
--debug)
DEBUG=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--)
shift
EXTRA_ARGS+=("$@")
break
;;
*)
EXTRA_ARGS+=("$1")
shift
;;
esac
done
if [[ -z "${CHECKPOINT_PATH}" ]]; then
echo "error: --checkpoint-path is required." >&2
echo >&2
usage >&2
exit 2
fi
export CUDA_VISIBLE_DEVICES="${CUDA_ID}"
export ENABLE_FAST_PREPROCESS="${ENABLE_FAST_PREPROCESS:-true}"
CMD=(
"${PYTHON_BIN}" -m wall_x._vendor.harrix.serving.launch_serving
--env "${WALLX_ENV}"
--host "${HOST}"
--port "${PORT}"
--image-passing-mode "${IMAGE_PASSING_MODE}"
)
if [[ "${SERIALIZE_ACTIONS}" == "1" ]]; then
CMD+=(--serialize-actions)
else
CMD+=(--no-serialize-actions)
fi
if [[ -n "${MAX_BATCH_SIZE}" ]]; then
CMD+=(--max-batch-size "${MAX_BATCH_SIZE}")
fi
if [[ -n "${DEFAULT_PROMPT}" ]]; then
CMD+=(--default-prompt "${DEFAULT_PROMPT}")
fi
if [[ "${ENABLE_CUDA_GRAPH}" == "1" ]]; then
CMD+=(--enable-cuda-graph)
fi
if [[ "${ENABLE_EXPERIMENTAL_ENGINE}" == "1" ]]; then
CMD+=(--enable-experimental-engine)
fi
if [[ "${DEBUG}" == "1" ]]; then
CMD+=(--debug)
fi
CMD+=(
model-config:server-model-config
--model-config.checkpoint-path "${CHECKPOINT_PATH}"
--model-config.action-horizon "${ACTION_HORIZON}"
--model-config.robot-type "${ROBOT_TYPE}"
--model-config.robot-action-interpolate-multiplier "${ROBOT_ACTION_INTERPOLATE_MULTIPLIER}"
--model-config.robot-action-end-ratio "${ROBOT_ACTION_END_RATIO}"
--model-config.model-device "${MODEL_DEVICE}"
)
if [[ -n "${TRAIN_CONFIG_PATH}" ]]; then
CMD+=(--model-config.train-config-path "${TRAIN_CONFIG_PATH}")
fi
CMD+=("${EXTRA_ARGS[@]}")
printf 'Launching Wall-X serving:\n'
printf ' %q' "${CMD[@]}"
printf '\n'
if [[ "${DRY_RUN}" == "1" ]]; then
exit 0
fi
exec "${CMD[@]}"
-104
View File
@@ -1,104 +0,0 @@
import torch
from PIL import Image
from transformers import AutoProcessor
import yaml
import os
from wall_x.model.qwen2_5_based.modeling_qwen2_5_vl_act import Qwen2_5_VLMoEForAction
class VQAWrapper(object):
def __init__(self, model_path: str, train_config: dict = None):
self.device = self._setup_device()
if train_config is None:
try:
with open(os.path.join(model_path, "config.yml"), "r") as f:
train_config = yaml.load(f, Loader=yaml.FullLoader)
except Exception as e:
print(f"load train_config.yml fail: {e}")
self.processor = self._load_processor(train_config["processor_path"])
self.model = self._load_model(model_path, train_config)
def _setup_device(self) -> str:
if torch.cuda.is_available():
return "cuda"
else:
return "cpu"
def _load_processor(self, model_path: str) -> AutoProcessor:
return AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
def _load_model(
self, model_path: str, train_config: dict
) -> Qwen2_5_VLMoEForAction:
model = Qwen2_5_VLMoEForAction.from_pretrained(
model_path, train_config=train_config
)
if self.device == "cuda":
model = model.to(self.device, dtype=torch.bfloat16)
else:
model.to(self.device)
model.eval()
return model
def generate(self, image: Image.Image, text: str, **kwargs) -> str:
messages = [
{
"role": "user",
"content": [{"type": "image"}, {"type": "text", "text": text}],
}
]
text_prompt = self.processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = self.processor(text=[text_prompt], images=[image], return_tensors="pt")
inputs = {k: v.to(self.device) for k, v in inputs.items()}
generation_params = {
"max_new_tokens": 1024, # default value, can be overridden by kwargs
"do_sample": False,
"eos_token_id": self.processor.tokenizer.eos_token_id,
"pad_token_id": self.processor.tokenizer.pad_token_id,
**kwargs,
}
with torch.no_grad():
generated_ids = self.model.generate(**inputs, **generation_params)
generated_ids = [
output_ids[len(input_ids) :]
for input_ids, output_ids in zip(inputs["input_ids"], generated_ids)
]
response = self.processor.batch_decode(
generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False
)[0]
return response
if __name__ == "__main__":
MODEL_PATH_FOR_MODULE_TEST = "/path/to/model_path"
train_config_path = "/path/to/model_path/config.yml"
with open(train_config_path, "r") as f:
train_config = yaml.load(f, Loader=yaml.FullLoader)
wrapper = VQAWrapper(
model_path=MODEL_PATH_FOR_MODULE_TEST, train_config=train_config
)
try:
test_question = "To move the red block in the plate with same color, what should you do next? Think step by step."
# Local Image
img = Image.open(
"/x2robot_v2/yangping/github/wall-x/assets/cot_example_frame.png"
).convert("RGB")
# Internet Image
# import requests
# test_image_url = "https://www.ilankelman.org/stopsigns/australia.jpg"
# img = Image.open(requests.get(test_image_url, stream=True).raw).convert("RGB")
answer = wrapper.generate(img, test_question)
print("model answer:", answer)
except Exception as e:
print(f"model answer fail: {e}")