Fix EVO1 LIBERO eval action postprocessing

This commit is contained in:
javadcc_mac
2026-06-13 10:18:34 +08:00
parent f9b8f297b4
commit fa984990c0
4 changed files with 82 additions and 8 deletions
+7 -3
View File
@@ -159,14 +159,18 @@ pixel embeddings, VLM fused tokens, normalized actions, and denormalized actions
(`max_abs_diff=0.0`). (`max_abs_diff=0.0`).
The published checkpoint expects the raw LIBERO camera feature names The published checkpoint expects the raw LIBERO camera feature names
`observation.images.agentview_image` and `observation.images.robot0_eye_in_hand_image`. To run the converted `observation.images.agentview_image` and `observation.images.robot0_eye_in_hand_image`. The official EVO1 LIBERO
checkpoint with LeRobot LIBERO evaluation for the same one-episode-per-task setting, keep those camera names rollout protocol also replans every 14 actions and binarizes the gripper command before stepping the simulator.
instead of the default `image`/`image2` mapping: The LIBERO environment postprocessor applies the gripper binarization automatically for EVO1 policies. To run the
converted checkpoint with LeRobot LIBERO evaluation for the same one-episode-per-task setting, keep the raw camera
names instead of the default `image`/`image2` mapping and override `policy.n_action_steps` to 14:
```bash ```bash
lerobot-eval \ lerobot-eval \
--policy.path=javadcc/evo1-libero-lerobot \ --policy.path=javadcc/evo1-libero-lerobot \
--policy.vlm_model_name=OpenGVLab/InternVL3-1B \
--policy.device=cuda \ --policy.device=cuda \
--policy.n_action_steps=14 \
--env.type=libero \ --env.type=libero \
--env.task=libero_object \ --env.task=libero_object \
--env.camera_name_mapping="{agentview_image: agentview_image, robot0_eye_in_hand_image: robot0_eye_in_hand_image}" \ --env.camera_name_mapping="{agentview_image: agentview_image, robot0_eye_in_hand_image: robot0_eye_in_hand_image}" \
+12 -2
View File
@@ -357,6 +357,7 @@ class LiberoEnv(EnvConfig):
} }
) )
control_mode: str = "relative" # or "absolute" control_mode: str = "relative" # or "absolute"
binarize_gripper: bool | None = None
def __post_init__(self): def __post_init__(self):
if self.obs_type == "pixels": if self.obs_type == "pixels":
@@ -442,12 +443,21 @@ class LiberoEnv(EnvConfig):
) )
def get_env_processors(self, policy_cfg: Any | None = None): def get_env_processors(self, policy_cfg: Any | None = None):
max_state_dim = getattr(policy_cfg, "max_state_dim", None) if getattr(policy_cfg, "type", None) == "evo1" else None is_evo1 = getattr(policy_cfg, "type", None) == "evo1"
max_state_dim = getattr(policy_cfg, "max_state_dim", None) if is_evo1 else None
action_feature = self.features.get(ACTION) action_feature = self.features.get(ACTION)
action_dim = int(action_feature.shape[0]) if action_feature is not None else 7 action_dim = int(action_feature.shape[0]) if action_feature is not None else 7
binarize_gripper = is_evo1 if self.binarize_gripper is None else self.binarize_gripper
return ( return (
PolicyProcessorPipeline(steps=[LiberoProcessorStep(max_state_dim=max_state_dim)]), PolicyProcessorPipeline(steps=[LiberoProcessorStep(max_state_dim=max_state_dim)]),
PolicyProcessorPipeline(steps=[LiberoActionProcessorStep(action_dim=action_dim)]), PolicyProcessorPipeline(
steps=[
LiberoActionProcessorStep(
action_dim=action_dim,
binarize_gripper=binarize_gripper,
)
]
),
) )
+38 -2
View File
@@ -173,13 +173,42 @@ class LiberoActionProcessorStep(ActionProcessorStep):
"""Slices padded policy actions back to the executable LIBERO action space.""" """Slices padded policy actions back to the executable LIBERO action space."""
action_dim: int = 7 action_dim: int = 7
binarize_gripper: bool = False
gripper_index: int = 6
gripper_threshold: float = 0.5
gripper_below_threshold_value: float = 1.0
gripper_above_threshold_value: float = -1.0
def action(self, action): def action(self, action):
if action.shape[-1] < self.action_dim: if action.shape[-1] < self.action_dim:
raise ValueError( raise ValueError(
f"LIBERO action has {action.shape[-1]} dims, which is smaller than action_dim={self.action_dim}." f"LIBERO action has {action.shape[-1]} dims, which is smaller than action_dim={self.action_dim}."
) )
return action[..., : self.action_dim] action = action[..., : self.action_dim]
if not self.binarize_gripper:
return action
if not 0 <= self.gripper_index < self.action_dim:
raise ValueError(
f"gripper_index={self.gripper_index} must be within sliced action_dim={self.action_dim}."
)
action = action.clone()
below = torch.as_tensor(
self.gripper_below_threshold_value,
dtype=action.dtype,
device=action.device,
)
above = torch.as_tensor(
self.gripper_above_threshold_value,
dtype=action.dtype,
device=action.device,
)
action[..., self.gripper_index] = torch.where(
action[..., self.gripper_index] > self.gripper_threshold,
above,
below,
)
return action
def transform_features( def transform_features(
self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]] self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]]
@@ -190,7 +219,14 @@ class LiberoActionProcessorStep(ActionProcessorStep):
return new_features return new_features
def get_config(self) -> dict: def get_config(self) -> dict:
return {"action_dim": self.action_dim} return {
"action_dim": self.action_dim,
"binarize_gripper": self.binarize_gripper,
"gripper_index": self.gripper_index,
"gripper_threshold": self.gripper_threshold,
"gripper_below_threshold_value": self.gripper_below_threshold_value,
"gripper_above_threshold_value": self.gripper_above_threshold_value,
}
@dataclass @dataclass
+25 -1
View File
@@ -99,12 +99,18 @@ def test_libero_evo1_processors_use_padded_state_and_env_action_dim():
assert pre.steps[0].max_state_dim == 24 assert pre.steps[0].max_state_dim == 24
assert isinstance(post.steps[0], LiberoActionProcessorStep) assert isinstance(post.steps[0], LiberoActionProcessorStep)
assert post.steps[0].action_dim == cfg.features["action"].shape[0] == 7 assert post.steps[0].action_dim == cfg.features["action"].shape[0] == 7
assert post.steps[0].binarize_gripper is True
class _OtherConfig: class _OtherConfig:
type = "other" type = "other"
pre_other, _ = make_env_pre_post_processors(cfg, policy_cfg=_OtherConfig()) pre_other, post_other = make_env_pre_post_processors(cfg, policy_cfg=_OtherConfig())
assert pre_other.steps[0].max_state_dim is None assert pre_other.steps[0].max_state_dim is None
assert post_other.steps[0].binarize_gripper is False
cfg.binarize_gripper = False
_, post_disabled = make_env_pre_post_processors(cfg, policy_cfg=_Evo1Config())
assert post_disabled.steps[0].binarize_gripper is False
def test_libero_processor_pads_state_to_max_dim(): def test_libero_processor_pads_state_to_max_dim():
@@ -138,6 +144,24 @@ def test_libero_action_processor_slices_padded_action():
step.action(torch.zeros(2, 6)) step.action(torch.zeros(2, 6))
def test_libero_action_processor_can_binarize_gripper():
step = LiberoActionProcessorStep(action_dim=7, binarize_gripper=True)
action = torch.tensor(
[
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 0.5, 7.0],
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 0.6, 7.0],
],
dtype=torch.float32,
)
processed = step.action(action)
assert processed.shape == (2, 7)
assert torch.equal(processed[:, :6], action[:, :6])
assert torch.equal(processed[:, 6], torch.tensor([1.0, -1.0]))
assert torch.equal(action[:, 6], torch.tensor([0.5, 0.6]))
def test_base_create_envs(): def test_base_create_envs():
"""Base class create_envs() should build a single-task VectorEnv via gym.make().""" """Base class create_envs() should build a single-task VectorEnv via gym.make()."""
gym_id = "_dispatch_test/CartPole-v99" gym_id = "_dispatch_test/CartPole-v99"