# Partially based on NVLabs ProtoMotions (Apache 2.0): # https://github.com/NVlabs/ProtoMotions/blob/94059259ba2b596bf908828cc04e8fc6ff901114/phys_anim/agents/utils/data_utils.py """Data-management utilities for on-policy PPO rollouts. Provides :class:`RolloutStorage`, a flexible ``nn.Module``-based buffer that stores arbitrary per-transition tensors (observations, actions, rewards, values, …) and exposes them as randomised mini-batches for PPO training. """ import torch from torch import Tensor, nn class RolloutStorage(nn.Module): """On-policy rollout buffer for PPO with dynamic key registration. Stores up to ``num_transitions_per_env`` steps for each of the ``num_envs`` parallel environments. Keys (e.g. "obs", "actions", "rewards") are registered on demand via :meth:`register_key`, which allocates a zero-filled ``nn.Module`` buffer of the appropriate shape so that the storage follows ``.to(device)`` calls. Args: num_envs: Number of parallel simulation environments. num_transitions_per_env: Maximum rollout length (horizon). device: PyTorch device string for all allocated buffers. """ def __init__(self, num_envs, num_transitions_per_env, device="cpu"): super().__init__() self.device = device self.num_transitions_per_env = num_transitions_per_env self.num_envs = num_envs # rnn # self.saved_hidden_states_a = None # self.saved_hidden_states_c = None self.step = 0 self.stored_keys = [] def register_key(self, key: str, shape=(), dtype=torch.float): """Allocate and register a new storage buffer for the given key. The buffer has shape ``(num_transitions_per_env, num_envs, *shape)`` and is registered as a non-persistent ``nn.Module`` buffer so it moves with ``.to(device)`` but is not saved in ``state_dict``. Args: key: Unique name for the data field (e.g. ``"obs"``, ``"rewards"``). shape: Per-transition, per-environment shape tuple. dtype: Tensor dtype for the buffer. Raises: AssertionError: If ``key`` is already registered or ``shape`` is not a list or tuple. """ # This class was partially copied from https://github.com/NVlabs/ProtoMotions/blob/94059259ba2b596bf908828cc04e8fc6ff901114/phys_anim/agents/utils/data_utils.py assert not hasattr(self, key), key assert isinstance(shape, list | tuple), f"shape must be a list or tuple, got {type(shape)}" buffer = torch.zeros( (self.num_transitions_per_env, self.num_envs) + shape, dtype=dtype, device=self.device ) self.register_buffer(key, buffer, persistent=False) self.stored_keys.append(key) def increment_step(self): """Advance the write cursor by one transition step.""" self.step += 1 def update_key(self, key: str, data: Tensor): """Write ``data`` into the buffer at the current step for ``key``. Args: key: Previously registered buffer key. data: Tensor of shape ``(num_envs, *key_shape)``. Must not require gradients. Raises: AssertionError: If ``data.requires_grad`` is True or the buffer is full (step >= num_transitions_per_env). """ # This class was partially copied from https://github.com/NVlabs/ProtoMotions/blob/94059259ba2b596bf908828cc04e8fc6ff901114/phys_anim/agents/utils/data_utils.py assert not data.requires_grad assert self.step < self.num_transitions_per_env, "Rollout buffer overflow" getattr(self, key)[self.step].copy_(data) def batch_update_data(self, key: str, data: Tensor): """Overwrite the entire buffer for ``key`` with ``data``. Useful for writing pre-computed values (e.g. advantages, returns) after a full rollout has been collected. Args: key: Previously registered buffer key. data: Tensor of shape ``(num_transitions_per_env, num_envs, *key_shape)``. Must not require gradients. """ # This class was partially copied from https://github.com/NVlabs/ProtoMotions/blob/94059259ba2b596bf908828cc04e8fc6ff901114/phys_anim/agents/utils/data_utils.py assert not data.requires_grad getattr(self, key)[:] = data # self.store_dict[key] += self.total_sum() def _save_hidden_states(self, hidden_states): assert NotImplementedError if hidden_states is None or hidden_states == (None, None): return # make a tuple out of GRU hidden state sto match the LSTM format hid_a = hidden_states[0] if isinstance(hidden_states[0], tuple) else (hidden_states[0],) hid_c = hidden_states[1] if isinstance(hidden_states[1], tuple) else (hidden_states[1],) # initialize if needed if self.saved_hidden_states_a is None: self.saved_hidden_states_a = [ torch.zeros(self.observations.shape[0], *hid_a[i].shape, device=self.device) for i in range(len(hid_a)) ] self.saved_hidden_states_c = [ torch.zeros(self.observations.shape[0], *hid_c[i].shape, device=self.device) for i in range(len(hid_c)) ] # copy the states for i in range(len(hid_a)): self.saved_hidden_states_a[i][self.step].copy_(hid_a[i]) self.saved_hidden_states_c[i][self.step].copy_(hid_c[i]) def clear(self): """Reset the write cursor to the start of the buffer.""" self.step = 0 def get_statistics(self): """Return buffer statistics (not implemented).""" raise NotImplementedError def query_key(self, key: str): """Return the full buffer tensor for ``key``. Args: key: Previously registered buffer key. Returns: Tensor of shape ``(num_transitions_per_env, num_envs, *key_shape)``. Raises: AssertionError: If ``key`` has not been registered. """ assert hasattr(self, key), key return getattr(self, key) def mini_batch_generator(self, num_mini_batches, num_epochs=8): """Yield randomly shuffled mini-batches over all stored transitions. The full buffer (flattened across transitions and environments) is shuffled once per call and then sliced into ``num_mini_batches`` equal-sized chunks, repeated ``num_epochs`` times. Args: num_mini_batches: Number of mini-batches per epoch. num_epochs: Number of times to iterate over the shuffled data. Yields: Dict mapping each registered key to a mini-batch tensor of shape ``(mini_batch_size, *key_shape)``. """ batch_size = self.num_envs * self.num_transitions_per_env mini_batch_size = batch_size // num_mini_batches indices = torch.randperm( num_mini_batches * mini_batch_size, requires_grad=False, device=self.device ) _buffer_dict = {key: getattr(self, key)[:].flatten(0, 1) for key in self.stored_keys} for epoch in range(num_epochs): for i in range(num_mini_batches): start = i * mini_batch_size end = (i + 1) * mini_batch_size batch_idx = indices[start:end] _batch_buffer_dict = {key: _buffer_dict[key][batch_idx] for key in self.stored_keys} yield _batch_buffer_dict