Rate this Page

Source code for torchrl.data.replay_buffers.writers.base

# 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.
from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Sequence
from copy import copy
from typing import Any

import torch


try:
    from torch.utils._pytree import tree_leaves
except ImportError:
    from torch.utils._pytree import tree_flatten

    def tree_leaves(data):  # noqa: D103
        tree_flat, _ = tree_flatten(data)
        return tree_flat


from torchrl.data.replay_buffers.storages import Storage
from torchrl.data.replay_buffers.utils import _is_int

# Generation buffers for storages up to this many slots are allocated in one
# shot, so their shape is stable and the ``torch.compile`` extend/sample path
# does not recompile. Larger (or effectively unbounded -- ``ListStorage`` with
# no ``max_size`` reports ``torch.iinfo(torch.int64).max``) capacities grow
# geometrically on demand instead of trying to allocate the whole thing.
_GENERATION_EAGER_ALLOC_LIMIT = 2**20
_GENERATION_MIN_ALLOC = 1024

# Attribute under which the per-slot generation buffer is stored *on the
# storage*. It belongs to the storage, not to the writer: two buffers sharing
# one storage overwrite each other's slots, so a per-writer counter would let
# buffer A's handles look live after buffer B overwrote the slot -- exactly the
# staleness the feature exists to detect.
_SLOT_GENERATIONS_ATTR = "_slot_generations"


class Writer(ABC):
    """A ReplayBuffer base Writer class."""

    _storage: Storage
    _rng: torch.Generator | None = None

    def __init__(self, compilable: bool = False) -> None:
        self._storage = None
        self._compilable = compilable

    #: Whether this writer stamps storage slots with a reuse generation. Always
    #: ``False`` unless the writer both supports generation tracking and was
    #: constructed with it enabled (see
    #: :class:`~torchrl.data.RoundRobinWriter`).
    tracks_generations: bool = False

    def register_storage(self, storage: Storage) -> None:
        self._storage = storage

[docs] def generations_of(self, index: int | torch.Tensor) -> torch.Tensor: """Returns the generation stamp for each physical slot in ``index``. A slot's stamp advances once per write it receives, so a single ``extend`` that wraps the storage advances a reused slot once per write. Comparing a stamp captured at sampling time against the current stamp tells you whether the slot still holds the data you sampled. Writers that do not track slot reuse -- and writers constructed with ``track_generations=False``, which is the default -- report ``-1`` everywhere. Never-written slots also report ``-1``, so ``-1`` means "no usable stamp" rather than "generation zero". Args: index (int or torch.Tensor): dim-0 slot indices. A 1-D tensor is always read as a batch of slot indices; for a storage with ``ndim > 1``, pass a ``tuple`` of per-dimension indices (as :meth:`~torchrl.data.ReplayBuffer.extend` returns) to identify a single cell -- only its dim-0 component is used, since a generation stamps a whole dim-0 slot. Returns: torch.Tensor: ``int64`` stamps shaped like the dim-0 component of ``index``, on ``index``'s device. """ index = torch.as_tensor(index) return torch.full(index.shape, -1, dtype=torch.int64, device=index.device)
[docs] @abstractmethod def add(self, data: Any) -> int: """Inserts one piece of data at an appropriate index, and returns that index.""" ...
[docs] @abstractmethod def extend(self, data: Sequence) -> torch.Tensor: """Inserts a series of data points at appropriate indices, and returns a tensor containing the indices.""" ...
@abstractmethod def _empty(self, empty_write_count: bool = True) -> None: ... @abstractmethod def dumps(self, path): ... @abstractmethod def loads(self, path): ... @abstractmethod def state_dict(self) -> dict[str, Any]: ... @abstractmethod def load_state_dict(self, state_dict: dict[str, Any]) -> None: ... def _replicate_index(self, index): # replicates the index in a non-zero format to have as many indices as # elements truly written when the storage is multidim if self._storage.ndim == 1: return index device = ( index.device if isinstance(index, torch.Tensor) else torch.device("cpu") ) mesh = torch.stack( torch.meshgrid( *(torch.arange(dim, device=device) for dim in self._storage.shape[1:]), indexing="ij", ), -1, ).flatten(0, -2) if _is_int(index): index0 = torch.as_tensor(int(index)).expand(mesh.shape[0], 1) return torch.cat([index0, mesh], 1) return torch.cat( [ index.repeat_interleave(mesh.shape[0]).unsqueeze(1), mesh.repeat(index.numel(), 1), ], 1, ) def __repr__(self): return f"{self.__class__.__name__}()" def __getstate__(self): state = copy(self.__dict__) state["_rng"] = None return state class ImmutableDatasetWriter(Writer): """A blocking writer for immutable datasets.""" WRITING_ERR = "This dataset doesn't allow writing."
[docs] def add(self, data: Any) -> int: raise RuntimeError(self.WRITING_ERR)
[docs] def extend(self, data: Sequence) -> torch.Tensor: raise RuntimeError(self.WRITING_ERR)
def _empty(self, empty_write_count: bool = True) -> None: raise RuntimeError(self.WRITING_ERR) def dumps(self, path): ... def loads(self, path): ... def state_dict(self) -> dict[str, Any]: return {} def load_state_dict(self, state_dict: dict[str, Any]) -> None: return