# 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 {}