Rate this Page

Source code for torchrl.modules.vla.models

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
"""A small reference Vision-Language-Action policy for tests and tutorials."""
from __future__ import annotations

import hashlib

import torch
from tensordict import TensorDictBase
from tensordict.nn import InteractionType
from torch import nn

from torchrl.data.utils import DEVICE_TYPING
from torchrl.data.vla import ActionTokenizerBase
from torchrl.modules.models.models import ConvNet, MLP

from torchrl.modules.vla.common import (
    ActionHead,
    InputMode,
    LogProbsMode,
    OutputMode,
    SamplingMode,
    VLAWrapperBase,
)

__all__ = ["TinyVLA"]


[docs] class TinyVLA(VLAWrapperBase): """A tiny, dependency-free reference VLA policy for CI and tutorials. ``TinyVLA`` fuses a small convolutional image encoder, an optional proprioceptive-state MLP, and a hashed language-instruction embedding into a trunk that feeds either a continuous action-chunk head or a discrete action-token head (see :class:`~torchrl.modules.vla.VLAWrapperBase`). It is intentionally small and CPU-friendly -- a stand-in to exercise the VLA data pipeline, losses and collectors end-to-end, **not** a competitive policy. The language instruction is embedded by hashing the instruction string to an embedding-table index (a deterministic, tokenizer-free stand-in), so the policy is genuinely language-conditioned without any external dependency. .. note:: ``TinyVLA`` expects observations with a single leading batch dimension (``image`` shaped ``[B, C, H, W]``). When training on chunked windows, flatten the time dimension into the batch first. Keyword Args: action_dim (int): the dimensionality of a single action. chunk_size (int): the action-chunk horizon ``H``. action_head (str): ``"continuous"`` (default) or ``"tokens"``. vocab_size (int): action-token bins per dimension (token head). Defaults to ``256``. use_state (bool): whether to read the proprioceptive state. Defaults to ``True``. hidden_dim (int): width of the fused trunk. Defaults to ``128``. text_vocab (int): size of the hashed instruction embedding table. Defaults to ``256``. text_dim (int): instruction-embedding dimension. Defaults to ``32``. default_interaction_type (InteractionType): token-head readout when no exploration context is active (``RANDOM`` samples, else argmax); the forward otherwise follows the ambient :func:`~torchrl.envs.utils.exploration_type`. Defaults to ``InteractionType.DETERMINISTIC``. See :class:`~torchrl.modules.vla.VLAWrapperBase`. mode (str, optional): backward-compatible alias for ``default_interaction_type``. Defaults to ``None``. device (DEVICE_TYPING, optional): device to move the parameters to. return_vla_action_container (bool): whether to write the structured VLAAction container in addition to its TensorDict fields. Defaults to ``True``. Examples: >>> import torch >>> from tensordict import NonTensorStack, TensorDict >>> from torchrl.modules.vla import TinyVLA >>> policy = TinyVLA(action_dim=7, chunk_size=4) >>> td = TensorDict( ... { ... "observation": { ... "image": torch.zeros(2, 3, 16, 16, dtype=torch.uint8), ... "state": torch.zeros(2, 5), ... }, ... "language_instruction": NonTensorStack("pick", "place"), ... }, ... batch_size=[2], ... ) >>> policy(td)["vla_action", "chunk"].shape torch.Size([2, 4, 7]) """ def __init__( self, *, action_dim: int, chunk_size: int, action_head: ActionHead = "continuous", vocab_size: int = 256, use_state: bool = True, hidden_dim: int = 128, text_vocab: int = 256, text_dim: int = 32, default_interaction_type: InteractionType = InteractionType.DETERMINISTIC, log_probs_mode: LogProbsMode = "sequence", mode: SamplingMode | None = None, device: DEVICE_TYPING | None = None, input_mode: InputMode = "canonical", output_mode: OutputMode | None = None, return_vla_action_container: bool = True, action_tokenizer: ActionTokenizerBase | None = None, return_log_probs: bool | None = None, return_logits: bool = False, logits_only: bool = False, inplace: bool | str | None = True, num_samples: int | None = None, ) -> None: super().__init__( action_dim=action_dim, chunk_size=chunk_size, action_head=action_head, vocab_size=vocab_size, use_state=use_state, input_mode=input_mode, output_mode=output_mode, return_vla_action_container=return_vla_action_container, action_tokenizer=action_tokenizer, default_interaction_type=default_interaction_type, log_probs_mode=log_probs_mode, return_log_probs=return_log_probs, return_logits=return_logits, logits_only=logits_only, inplace=inplace, num_samples=num_samples, mode=mode, device=device, ) self.hidden_dim = int(hidden_dim) self.text_vocab = int(text_vocab) self.image_encoder = nn.Sequential( ConvNet(num_cells=[16, 32], kernel_sizes=3, strides=2, paddings=1), nn.LazyLinear(hidden_dim), nn.ReLU(), ) self.state_encoder = ( MLP(out_features=hidden_dim, num_cells=[hidden_dim]) if use_state else None ) self.text_embedding = nn.Embedding(text_vocab, text_dim) self.trunk = MLP(out_features=hidden_dim, num_cells=[hidden_dim]) out_features = chunk_size * action_dim if action_head == "tokens": out_features *= vocab_size self.head = nn.LazyLinear(out_features) if device is not None: self.to(device) def _hash_text(self, strings: list[str], device: torch.device) -> torch.Tensor: indices = [ int(hashlib.sha1(s.encode("utf-8")).hexdigest(), 16) % self.text_vocab for s in strings ] return torch.tensor(indices, dtype=torch.long, device=device) def _flatten_instruction_strings(self, data) -> list[str]: if isinstance(data, str): return [data] if isinstance(data, list): result = [] for item in data: result.extend(self._flatten_instruction_strings(item)) return result return [str(data)] def _instruction_strings(self, tensordict: TensorDictBase, batch: int) -> list[str]: instruction = self._get_instruction(tensordict) data = getattr(instruction, "tolist", lambda: instruction)() if isinstance(data, str): data = [data] * batch else: data = self._flatten_instruction_strings(data) if len(data) == 1 and batch != 1: data = data * batch return data def _features(self, tensordict: TensorDictBase) -> torch.Tensor: image = self._get_image(tensordict) if image.dtype == torch.uint8: image = image.float() / 255.0 else: image = image.float() batch_shape = image.shape[:-3] batch = int(torch.tensor(batch_shape).prod().item()) image = image.reshape(batch, *image.shape[-3:]) feats = [self.image_encoder(image).reshape(*batch_shape, -1)] if self.use_state: state = self._get_state(tensordict).float() state = state.reshape(batch, state.shape[-1]) feats.append(self.state_encoder(state).reshape(*batch_shape, -1)) strings = self._instruction_strings(tensordict, batch) text = self.text_embedding(self._hash_text(strings, image.device)) feats.append(text.reshape(*batch_shape, -1)) return self.trunk(torch.cat(feats, dim=-1)) def _predict(self, tensordict: TensorDictBase) -> torch.Tensor: return self.head(self._features(tensordict))