Instructions to use AlexWortega/tinyvla with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use AlexWortega/tinyvla with LeRobot:
- Notebooks
- Google Colab
- Kaggle
File size: 8,393 Bytes
5a2e445 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | #!/usr/bin/env python
"""Closed-loop LIBERO evaluation for canonical-schema TinyVLA checkpoints.
Adapts env observations to the canonical schema the policy was trained on
(cam0/cam1, padded normalized state, embodiment_id) and unnormalizes the
predicted actions with the LIBERO dataset stats.
Usage:
python scripts/eval_libero.py --checkpoint outputs/libero_ft/final \
--suite libero_spatial --episodes 20 [--refresh-s 1.0]
"""
from __future__ import annotations
import argparse
from pathlib import Path
import torch
def make_normalizer(repo_id="HuggingFaceVLA/libero"):
from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata
meta = LeRobotDatasetMetadata(repo_id)
stats = meta.stats
def norm(key, x):
s = stats[key]
mean = torch.as_tensor(s["mean"], dtype=torch.float32, device=x.device)
std = torch.as_tensor(s["std"], dtype=torch.float32, device=x.device).clamp(min=1e-6)
return (x - mean) / std
def unnorm_action(x):
s = stats["action"]
mean = torch.as_tensor(s["mean"], dtype=torch.float32, device=x.device)
std = torch.as_tensor(s["std"], dtype=torch.float32, device=x.device)
return x * std + mean
return norm, unnorm_action, meta
@torch.no_grad()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--checkpoint", type=Path, required=True)
parser.add_argument("--suite", default="libero_spatial",
choices=["libero_spatial", "libero_object", "libero_goal", "libero_10", "libero_90"])
parser.add_argument("--episodes", type=int, default=20)
parser.add_argument("--embodiment-id", type=int, default=2)
parser.add_argument("--refresh-s", type=float, default=None,
help="if set, refresh the semantic latent only every N seconds (dual-rate mode)")
parser.add_argument("--max-steps", type=int, default=520)
parser.add_argument("--n-action-steps", type=int, default=None,
help="execute only first N actions of each chunk before re-planning")
parser.add_argument("--action-repeat", type=int, default=1,
help="env steps per predicted action (dataset 10fps vs env 20Hz -> 2)")
parser.add_argument("--save-video-dir", type=Path, default=None,
help="save per-episode mp4s of the agentview camera here")
args = parser.parse_args()
import numpy as np
from lerobot.envs.factory import make_env, make_env_config
from transformers import AutoTokenizer
from tinyvla.modeling_tinyvla import TinyVLAPolicy
policy = TinyVLAPolicy.from_pretrained(args.checkpoint).cuda().eval()
cfg = policy.config
if args.n_action_steps:
cfg.n_action_steps = args.n_action_steps
tok = AutoTokenizer.from_pretrained(cfg.lm_model_name)
norm, unnorm_action, meta = make_normalizer()
fps = meta.fps
env_cfg = make_env_config("libero", task=args.suite)
envs_dict = make_env(env_cfg, n_envs=1)
task_envs = envs_dict[args.suite] # {task_id: vec_env}
from scipy.spatial.transform import Rotation
def to_canonical(obs, task_text, latent=None):
imgs = {}
for slot, key in (("cam0", "image"), ("cam1", "image2")):
x = torch.as_tensor(np.asarray(obs["pixels"][key]))
if x.dim() == 4: # (1, H, W, C)
x = x[0]
# robosuite renders 180-degree rotated relative to the recorded dataset
x = x.flip(0).flip(1)
x = x.permute(2, 0, 1).float() / 255.0
if x.shape[-2:] != (cfg.image_size, cfg.image_size):
x = torch.nn.functional.interpolate(
x[None], size=(cfg.image_size, cfg.image_size), mode="bilinear", align_corners=False
)[0]
imgs[slot] = x
# dataset convention (OpenVLA-style): [eef pos(3), eef axis-angle(3), gripper qpos(2)]
rs = obs["robot_state"]
pos = np.asarray(rs["eef"]["pos"]).flatten()
quat = np.asarray(rs["eef"]["quat"]).flatten() # robosuite: (x, y, z, w)
rotvec = Rotation.from_quat(quat).as_rotvec()
# canonicalize antipodal representation to match dataset convention
# (dataset uses rotvec with positive x-component, ~+pi for downward gripper)
if rotvec[0] < 0:
theta = np.linalg.norm(rotvec)
if theta > 1e-6:
rotvec = rotvec * (theta - 2 * np.pi) / theta
grip = np.asarray(rs["gripper"]["qpos"]).flatten()
state = torch.tensor(np.concatenate([pos, rotvec, grip]), dtype=torch.float32)
state = norm("observation.state", state)
state = torch.nn.functional.pad(state, (0, cfg.max_state_dim - state.shape[-1]))
t = tok([task_text], padding=True, truncation=True,
max_length=cfg.tokenizer_max_length, return_tensors="pt")
batch = {
"observation.images.cam0": imgs["cam0"][None].cuda(),
"observation.images.cam1": imgs["cam1"][None].cuda(),
"observation.state": state[None].cuda(),
"observation.language.tokens": t["input_ids"].cuda(),
"observation.language.attention_mask": t["attention_mask"].bool().cuda(),
"embodiment_id": torch.tensor([args.embodiment_id], device="cuda"),
}
if latent is not None:
batch["semantic_latent"] = latent
return batch
action_dim = meta.features["action"]["shape"][0]
total, succ = 0, 0
per_task = {}
task_ids = sorted(task_envs.keys())
ep_plan = [(tid, i) for i in range((args.episodes + len(task_ids) - 1) // len(task_ids)) for tid in task_ids]
ep_plan = ep_plan[: args.episodes]
if args.save_video_dir:
args.save_video_dir.mkdir(parents=True, exist_ok=True)
for ep, (tid, rep) in enumerate(ep_plan):
env = task_envs[tid]
obs, info = env.reset(seed=1000 + rep)
frames = [] if args.save_video_dir else None
try:
task_text = env.get_attr("task_description")[0]
except Exception:
task_text = getattr(getattr(env, "envs", [None])[0], "task_description", "")
policy.reset()
latent = None
last_refresh = -1e9
done = False
step_i = 0
ep_succ = False
while not done and step_i < args.max_steps:
t_now = step_i / fps
batch = to_canonical(obs, task_text)
if args.refresh_s is not None:
if t_now - last_refresh >= args.refresh_s:
latent = policy._semantic_latent(batch)
last_refresh = t_now
batch["semantic_latent"] = latent
act_norm = policy.select_action(batch) # (1, max_action_dim) normalized padded
act = unnorm_action(act_norm[0, :action_dim].cpu()).clamp(-1, 1)
for _ in range(args.action_repeat):
try:
obs, reward, terminated, truncated, info = env.step(act.numpy()[None])
except ValueError: # stepped into env's internal horizon
done = True
break
done = bool(terminated[0] or truncated[0])
if info.get("is_success") is not None:
ep_succ = ep_succ or bool(np.asarray(info["is_success"]).flatten()[0])
if frames is not None:
frames.append(np.asarray(obs["pixels"]["image"])[0][::-1, ::-1])
step_i += 1
if done or step_i >= args.max_steps:
done = done or step_i >= args.max_steps
break
total += 1
succ += int(ep_succ)
per_task.setdefault(task_text[:50], []).append(int(ep_succ))
print(f"ep {ep}: {'SUCCESS' if ep_succ else 'fail'} ({step_i} steps) | {task_text[:60]}")
if frames:
import imageio.v2 as imageio
tag = "succ" if ep_succ else "fail"
path = args.save_video_dir / f"ep{ep:02d}_task{tid}_{tag}.mp4"
imageio.mimwrite(path, frames, fps=20, quality=7)
print(f"\n=== {args.suite} | {args.checkpoint} | refresh={args.refresh_s} ===")
print(f"success rate: {succ}/{total} = {succ/total:.1%}")
for t, v in per_task.items():
print(f" {sum(v)}/{len(v)} {t}")
if __name__ == "__main__":
main()
|