Files
vllm-omni/vllm_omni/diffusion/models/gr00t/policy.py
T
2026-06-20 11:45:26 +08:00

435 lines
20 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from pathlib import Path
from typing import Any
import numpy as np
import torch
from transformers import AutoModel, AutoProcessor
from vllm_omni.diffusion.models.gr00t.dataio.embodiment_tags import FINETUNE_ONLY_TAGS, POSTTRAIN_TAGS, EmbodimentTag
from vllm_omni.diffusion.models.gr00t.dataio.types import MessageType, VLAStepData
from vllm_omni.diffusion.models.gr00t.modeling.gr00t_n1d7 import Gr00tN1d7
from vllm_omni.diffusion.models.gr00t.modeling.processing_gr00t_n1d7 import Gr00tN1d7Processor
def _rec_to_dtype(value: Any, dtype: torch.dtype) -> Any:
"""Recursively convert floating-point tensors in nested collator output."""
if isinstance(value, torch.Tensor):
return value.to(dtype=dtype) if torch.is_floating_point(value) else value
if isinstance(value, dict) or hasattr(value, "items"):
return {key: _rec_to_dtype(item, dtype) for key, item in value.items()}
if isinstance(value, list):
return [_rec_to_dtype(item, dtype) for item in value]
return value
class Gr00tPolicy:
"""Core policy class for Gr00t model inference.
This policy handles the end-to-end inference pipeline:
1. Validates input observations
2. Processes observations with pretrained VLA processor
3. Runs model inference
4. Decodes and returns actions
The policy expects observations with specific modalities (video, state, language)
and returns actions in the format defined by the model's modality configuration.
"""
def __init__(
self,
embodiment_tag: EmbodimentTag | str,
model_path: str,
*,
device: int | str,
strict: bool = True,
):
"""Initialize the Gr00t Policy.
Args:
embodiment_tag: The embodiment tag defining the robot/environment type.
Accepts an EmbodimentTag enum or a string (resolved case-insensitively).
model_path: Path to the pretrained model checkpoint directory
device: Device to run the model on (e.g., 'cuda:0', 0, 'cpu')
strict: Whether to enforce strict input validation (default: True)
"""
self.strict = strict
if isinstance(embodiment_tag, str):
embodiment_tag = EmbodimentTag.resolve(embodiment_tag)
model_dir = Path(model_path)
# Load directly into bfloat16 to avoid a transient fp32 copy at load time.
self.model: Gr00tN1d7 = AutoModel.from_pretrained(model_dir, torch_dtype=torch.bfloat16)
self.model.eval()
self.model.to(device=device)
# Training saves processor files under a "processor/" subdirectory, but
# AutoProcessor expects them at the model root. Fall back to the
# subdirectory when the root lacks a processor_config.json.
processor_dir = (
model_dir / "processor"
if (model_dir / "processor").is_dir() and not (model_dir / "processor_config.json").exists()
else model_dir
)
self.processor: Gr00tN1d7Processor = AutoProcessor.from_pretrained(processor_dir)
# Store embodiment-specific configurations
self.embodiment_tag = embodiment_tag
all_modality_configs = self.processor.modality_configs
if self.embodiment_tag.value not in all_modality_configs:
# Map raw checkpoint tag values to user-friendly enum names where possible.
supported_lines = []
for tag_value in sorted(all_modality_configs.keys()):
enum_name = EmbodimentTag.reverse_lookup(tag_value)
if enum_name != tag_value:
supported_lines.append(f" {enum_name:30s} (--embodiment-tag {enum_name})")
else:
supported_lines.append(f" {tag_value:30s} (internal, no public enum)")
supported_str = "\n".join(supported_lines)
hint = ""
if self.embodiment_tag in POSTTRAIN_TAGS:
hint = (
f"\n\nHint: '{self.embodiment_tag.name}' is a posttrain tag that requires "
f"a finetuned checkpoint, not the base model. "
f"See the example READMEs for how to finetune and download checkpoints."
)
elif self.embodiment_tag in FINETUNE_ONLY_TAGS:
hint = (
f"\n\nHint: '{self.embodiment_tag.name}' is for finetuning custom robots. "
f"Use it with launch_finetune.py, not with the base model directly."
)
raise ValueError(
f"Embodiment tag '{self.embodiment_tag.name}' "
f"(value='{self.embodiment_tag.value}') is not supported "
f"by this checkpoint.\n\n"
f"Supported tags in this checkpoint:\n{supported_str}"
f"{hint}"
)
self.modality_configs = {
k: v for k, v in all_modality_configs[self.embodiment_tag.value].items() if k != "rl_info"
}
self.collate_fn = self.processor.collator
# Extract and validate language configuration
# Embodiments may define multiple language keys (e.g. paraphrases); use only the first at inference.
language_keys = self.modality_configs["language"].modality_keys
language_delta_indices = self.modality_configs["language"].delta_indices
assert len(language_keys) >= 1, "At least one language key is required"
assert len(language_delta_indices) == 1, "Only one language delta index is supported"
self.language_key = language_keys[0]
def _unbatch_observation(self, value: dict[str, Any]) -> list[dict[str, Any]]:
"""Unbatch a batched observation into a list of single observations.
Args:
value: Batched observation with shape (B, ...) for each modality
Returns:
List of B observations, each with the batch dimension removed
"""
unbatched_obs = []
# Infer batch size from the first video key
batch_size = value["video"][list(value["video"].keys())[0]].shape[0]
# Split each modality along the batch dimension
for i in range(batch_size):
unbatched_value = {
"video": {k: v[i] for k, v in value["video"].items()},
"state": {k: v[i] for k, v in value["state"].items()},
"language": {k: v[i] for k, v in value["language"].items()},
}
unbatched_obs.append(unbatched_value)
return unbatched_obs
def _to_vla_step_data(self, observation: dict[str, Any]) -> VLAStepData:
"""Convert a single observation into a VLAStepData object for processing.
Args:
observation: Single observation dict with video, state, and language
Returns:
VLAStepData object ready for processor input
"""
return VLAStepData(
images=observation["video"],
states=observation["state"],
actions={}, # No ground truth actions during inference
text=observation["language"][self.language_key][0],
embodiment=self.embodiment_tag,
)
def check_observation(self, observation: dict[str, Any]) -> None:
"""Validate that the observation has the correct structure and types.
This method ensures that all required modalities are present and that their
data types, shapes, and dimensions match the model's expectations.
Expected observation structure:
- video: dict[str, np.ndarray[np.uint8, (B, T, H, W, C)]]
- B: batch size
- T: temporal horizon (number of frames)
- H, W: image height and width
- C: number of channels (must be 3 for RGB)
- state: dict[str, np.ndarray[np.float32, (B, T, D)]]
- B: batch size
- T: temporal horizon (number of state observations)
- D: state dimension
- language: dict[str, list[list[str]]]
- Shape: (B, T) where each element is a string
- T: temporal horizon (typically 1 for language)
Args:
observation: Dictionary containing video, state, and language modalities
Raises:
AssertionError: If any validation check fails
"""
# Check that observation contains all required top-level modality keys
for modality in ["video", "state", "language"]:
assert modality in observation, f"Observation must contain a '{modality}' key"
assert isinstance(observation[modality], dict), (
f"Observation '{modality}' must be a dictionary. "
f"Got {type(observation[modality])}: {observation[modality]}"
)
# Track batch size across modalities to ensure consistency
bs = -1
for video_key in self.modality_configs["video"].modality_keys:
assert video_key in observation["video"], f"Video key '{video_key}' must be in observation"
# Set or verify batch size consistency across all video keys
if bs == -1:
bs = len(observation["video"][video_key])
else:
assert len(observation["video"][video_key]) == bs, (
f"Video key '{video_key}' must have batch size {bs}. Got {len(observation['video'][video_key])}"
)
batched_video = observation["video"][video_key]
# Verify data type is numpy array
assert isinstance(batched_video, np.ndarray), (
f"Video key '{video_key}' must be a numpy array. Got {type(batched_video)}"
)
# Verify dtype is uint8 (standard for image data, range 0-255)
assert batched_video.dtype == np.uint8, (
f"Video key '{video_key}' must be a numpy array of type np.uint8. Got {batched_video.dtype}"
)
# Verify shape has 5 dimensions: (B, T, H, W, C)
assert batched_video.ndim == 5, (
f"Video key '{video_key}' must be a numpy array of shape (B, T, H, W, C), got {batched_video.shape}"
)
# Verify temporal dimension matches the expected horizon from config
assert batched_video.shape[1] == len(self.modality_configs["video"].delta_indices), (
f"Video key '{video_key}'s horizon must be "
f"{len(self.modality_configs['video'].delta_indices)}. Got {batched_video.shape[1]}"
)
# Verify channel dimension is 3 (RGB images)
assert batched_video.shape[-1] == 3, (
f"Video key '{video_key}'s channel 'C' must be 3. Got {batched_video.shape[-1]}"
)
for state_key in self.modality_configs["state"].modality_keys:
# Check that the expected state key exists in the observation
# Must happen before indexing; see video validation above.
assert state_key in observation["state"], f"State key '{state_key}' must be in observation"
# Set or verify batch size consistency across all state keys
if bs == -1:
bs = len(observation["state"][state_key])
else:
assert len(observation["state"][state_key]) == bs, (
f"State key '{state_key}' must have batch size {bs}. Got {len(observation['state'][state_key])}"
)
batched_state = observation["state"][state_key]
# Verify data type is numpy array
assert isinstance(batched_state, np.ndarray), (
f"State key '{state_key}' must be a numpy array. Got {type(batched_state)}"
)
# Verify dtype is float32 (standard for continuous state values)
assert batched_state.dtype == np.float32, (
f"State key '{state_key}' must be a numpy array of type np.float32. Got {batched_state.dtype}"
)
# Verify shape has 3 dimensions: (B, T, D)
assert batched_state.ndim == 3, (
f"State key '{state_key}' must be a numpy array of shape (B, T, D), got {batched_state.shape}"
)
# Verify temporal dimension matches the expected horizon from config
assert batched_state.shape[1] == len(self.modality_configs["state"].delta_indices), (
f"State key '{state_key}'s horizon must be "
f"{len(self.modality_configs['state'].delta_indices)}. Got {batched_state.shape[1]}"
)
for language_key in self.modality_configs["language"].modality_keys:
# Check that the expected language key exists in the observation
# Must happen before indexing; see video validation above.
assert language_key in observation["language"], f"Language key '{language_key}' must be in observation"
# Set or verify batch size consistency (language uses len instead of .shape)
if bs == -1:
bs = len(observation["language"][language_key])
else:
assert len(observation["language"][language_key]) == bs, (
f"Language key '{language_key}' must have batch size {bs}. "
f"Got {len(observation['language'][language_key])}"
)
batched_language: list[list[str]] = observation["language"][language_key]
# Verify outer structure is a list (batch dimension)
assert isinstance(batched_language, list), (
f"Language key '{language_key}' must be a list. Got {type(batched_language)}"
)
# Validate each batch item
for batch_item in batched_language:
# Verify temporal dimension matches expected horizon
assert len(batch_item) == len(self.modality_configs["language"].delta_indices), (
f"Language key '{language_key}'s horizon must be "
f"{len(self.modality_configs['language'].delta_indices)}. Got {len(batched_language)}"
)
# Verify inner structure is also a list (temporal dimension)
assert isinstance(batch_item, list), f"Language batch item must be a list. Got {type(batch_item)}"
# Current implementation expects exactly one language instruction per timestep
assert len(batch_item) == 1, f"Language batch item must have exactly one item. Got {len(batch_item)}"
# Verify the instruction itself is a string
assert isinstance(batch_item[0], str), (
f"Language batch item must be a string. Got {type(batch_item[0])}"
)
def _get_action(
self, observation: dict[str, Any], options: dict[str, Any] | None = None
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Internal method to compute actions from observations.
Pipeline:
1. Unbatch observations into individual samples
2. Convert each to VLAStepData and process
3. Collate into model input batch
4. Run model inference
5. Decode and unnormalize actions
Args:
observation: Batched observation dictionary
options: Optional parameters (currently unused)
Returns:
Tuple of (actions_dict, info_dict)
"""
# Step 1: Split batched observation into individual observations
unbatched_observations = self._unbatch_observation(observation)
processed_inputs = []
# Step 2: Process each observation through the VLA processor
states = []
for obs in unbatched_observations:
vla_step_data = self._to_vla_step_data(obs)
states.append(vla_step_data.states) # dict[str, np.ndarray[np.float32, (T, D)]]
messages = [{"type": MessageType.EPISODE_STEP.value, "content": vla_step_data}]
processed_inputs.append(self.processor(messages))
# Step 3: Collate processed inputs into a single batch for model
collated_inputs = self.collate_fn(processed_inputs)
collated_inputs = _rec_to_dtype(collated_inputs, dtype=torch.bfloat16)
# Step 4: Run model inference to predict actions
with torch.inference_mode():
model_pred = self.model.get_action(**collated_inputs)
normalized_action = model_pred["action_pred"].float()
# Step 5: Decode actions from normalized space back to physical units
batched_states = {}
for k in self.modality_configs["state"].modality_keys:
batched_states[k] = np.stack([s[k] for s in states], axis=0) # (B, T, D)
unnormalized_action = self.processor.decode_action(
normalized_action.cpu().numpy(), self.embodiment_tag, batched_states
)
# Cast all actions to float32 for consistency
casted_action = {key: value.astype(np.float32) for key, value in unnormalized_action.items()}
return casted_action, {}
def check_action(self, action: dict[str, Any]) -> None:
"""Validate that the action has the correct structure and types.
This method ensures that all required action keys are present and that their
data types, shapes, and dimensions match the model's action space.
Expected action structure:
- action: dict[str, np.ndarray[np.float32, (B, T, D)]]
- B: batch size
- T: action horizon (number of future action steps)
- D: action dimension (e.g., joint positions, velocities, gripper state)
Args:
action: Dictionary containing action arrays for each action key
Raises:
AssertionError: If any validation check fails
"""
# Validate each action key defined in the modality config
for action_key in self.modality_configs["action"].modality_keys:
# Check that the expected action key exists
assert action_key in action, f"Action key '{action_key}' must be in action"
action_arr = action[action_key]
# Verify data type is numpy array
assert isinstance(action_arr, np.ndarray), (
f"Action key '{action_key}' must be a numpy array. Got {type(action_arr)}"
)
# Verify dtype is float32 (standard for continuous actions)
assert action_arr.dtype == np.float32, (
f"Action key '{action_key}' must be a numpy array of type np.float32. Got {action_arr.dtype}"
)
# Verify shape has 3 dimensions: (B, T, D)
assert action_arr.ndim == 3, (
f"Action key '{action_key}' must be a numpy array of shape (B, T, D), got {action_arr.shape}"
)
# Verify action horizon matches the expected temporal dimension from config
assert action_arr.shape[1] == len(self.modality_configs["action"].delta_indices), (
f"Action key '{action_key}'s horizon must be "
f"{len(self.modality_configs['action'].delta_indices)}. Got {action_arr.shape[1]}"
)
def get_action(
self, observation: dict[str, Any], options: dict[str, Any] | None = None
) -> tuple[dict[str, Any], dict[str, Any]]:
if self.strict:
self.check_observation(observation)
action, info = self._get_action(observation, options)
if self.strict:
self.check_action(action)
return action, info
def reset(self, options: dict[str, Any] | None = None) -> dict[str, Any]:
"""Reset the policy to its initial state.
Args:
options: Dictionary containing the options for the reset
Returns:
Dictionary containing the info after resetting the policy
"""
return {}