Rate this Page
★ ★ ★ ★ ★

Source code for torchrl.trainers.algorithms.configs.modules

# 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 collections.abc import Sequence
from dataclasses import dataclass, field
from functools import partial
from typing import Any

import torch
import torch.nn as nn
from hydra.utils import instantiate
from omegaconf import MISSING
from tensordict.nn import TensorDictModule, TensorDictSequential
from torchrl.modules import (
    AdditiveGaussianModule,
    DreamerV3DiscreteActor,
    LowLevelController,
    MLP,
    QValueActor,
    RSSMStateEstimatorV3,
    SimplicialNormalization,
    TanhModule,
    TdMpc2Planner,
    TdMpc2QEnsemble,
    ValueOperator,
    WorldModel,
)
from torchrl.modules.models.tdmpc2 import _TdMpc2PolicyPrior
from torchrl.trainers.algorithms.configs.common import (
    _normalize_hydra_key,
    _normalize_hydra_keys,
    ConfigBase,
)


@dataclass
class ActivationConfig(ConfigBase):
    """A class to configure an activation function.

    Defaults to :class:`torch.nn.Tanh`.

    .. seealso:: :class:`torch.nn.Tanh`
    """

    _target_: str = "torch.nn.Tanh"
    _partial_: bool = False

    def __post_init__(self) -> None:
        """Post-initialization hook for activation configurations."""


@dataclass
class LayerConfig(ConfigBase):
    """A class to configure a layer.

    Defaults to :class:`torch.nn.Linear`.

    .. seealso:: :class:`torch.nn.Linear`
    """

    _target_: str = "torch.nn.Linear"
    _partial_: bool = False

    def __post_init__(self) -> None:
        """Post-initialization hook for layer configurations."""


[docs] @dataclass class NetworkConfig(ConfigBase): """Parent class to configure a network.""" _partial_: bool = False def __post_init__(self) -> None: """Post-initialization hook for network configurations."""
@dataclass class LowLevelControllerConfig(NetworkConfig): """Hydra configuration for :class:`~torchrl.modules.LowLevelController`. Pass the pretrained policy to Hydra's instantiate and declare the unbatched Composite decision spec with a Hydra target. Adapter and nested keys can also be configured in YAML. """ policy: Any = MISSING decision_spec: Any = MISSING adapter: Any = None group_key: Any = None state_key: Any = "controller" policy_action_key: Any = "action" action_key: Any = "action" reset_key: Any = None _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_low_level_controller" ) def _make_low_level_controller(**kwargs: Any) -> LowLevelController: """Convert Hydra lists to nested TensorDict keys before construction.""" return LowLevelController( policy=kwargs.pop("policy"), decision_spec=kwargs.pop("decision_spec"), adapter=kwargs.pop("adapter", None), group_key=_normalize_hydra_key(kwargs.pop("group_key", None)), state_key=_normalize_hydra_key(kwargs.pop("state_key", "controller")), policy_action_key=_normalize_hydra_key( kwargs.pop("policy_action_key", "action") ), action_key=_normalize_hydra_key(kwargs.pop("action_key", "action")), reset_key=_normalize_hydra_key(kwargs.pop("reset_key", None)), **kwargs, )
[docs] @dataclass class MLPConfig(NetworkConfig): """A class to configure a multi-layer perceptron. Example: >>> cfg = MLPConfig(in_features=10, out_features=5, depth=2, num_cells=32) >>> net = instantiate(cfg) >>> y = net(torch.randn(1, 10)) >>> assert y.shape == (1, 5) .. seealso:: :class:`torchrl.modules.MLP` """ in_features: int | None = None out_features: Any = None depth: int | None = None num_cells: Any = None activation_class: ActivationConfig = field( default_factory=partial( ActivationConfig, _target_="torch.nn.Tanh", _partial_=True ) ) activation_kwargs: Any = None norm_class: Any = None norm_kwargs: Any = None dropout: float | None = None bias_last_layer: bool = True single_bias_last_layer: bool = False layer_class: LayerConfig = field( default_factory=partial(LayerConfig, _target_="torch.nn.Linear", _partial_=True) ) layer_kwargs: dict | None = None activate_last_layer: bool = False device: Any = None _target_: str = "torchrl.modules.MLP" def __post_init__(self): if isinstance(self.activation_class, str): self.activation_class = ActivationConfig( _target_=self.activation_class, _partial_=True ) if isinstance(self.layer_class, str): self.layer_class = LayerConfig(_target_=self.layer_class, _partial_=True)
[docs] @dataclass class DreamerV3MLPConfig(NetworkConfig): """A class to configure a DreamerV3 multilayer perceptron. Example: >>> import torch >>> from hydra.utils import instantiate >>> from torchrl.trainers.algorithms.configs import DreamerV3MLPConfig >>> cfg = DreamerV3MLPConfig( ... in_features=6, out_features=4, depth=2, num_cells=8 ... ) >>> net = instantiate(cfg) >>> y = net(torch.randn(3, 2), torch.randn(3, 4)) >>> assert y.shape == (3, 4) .. seealso:: :class:`~torchrl.modules.DreamerV3MLP` """ in_features: int = MISSING out_features: int | None = MISSING depth: int = 3 num_cells: int = 1024 outscale: float = 1.0 norm_eps: float = 1e-4 device: Any = None _target_: str = "torchrl.modules.DreamerV3MLP"
[docs] @dataclass class TdMpc2MLPConfig(NetworkConfig): """A class to configure a TDMPC2 multilayer perceptron. Example: >>> import torch >>> from hydra.utils import instantiate >>> from torchrl.trainers.algorithms.configs import TdMpc2MLPConfig >>> cfg = TdMpc2MLPConfig( ... in_features=6, out_features=4, depth=2, num_cells=8 ... ) >>> net = instantiate(cfg) >>> y = net(torch.randn(3, 6)) >>> assert y.shape == (3, 4) .. seealso:: :class:`~torchrl.modules.MLP` """ in_features: int = MISSING out_features: int = MISSING depth: int | None = None num_cells: Any = MISSING output_activation: Any = None dropout: float = 0.0 device: Any = None _target_: str = "torchrl.trainers.algorithms.configs.modules._make_tdmpc2_mlp"
def _make_tdmpc2_mlp( *, in_features: int, out_features: int, depth: int | None, num_cells: int | Sequence[int], output_activation: nn.Module | None = None, dropout: float = 0.0, device: Any = None, ) -> MLP: """Helper function to create a TDMPC2 MLP.""" if isinstance(num_cells, int): hidden_sizes = [num_cells] * (depth or 0) else: hidden_sizes = list(num_cells) network = MLP( in_features=in_features, out_features=out_features, depth=depth, num_cells=num_cells, activation_class=nn.Mish, activation_kwargs={"inplace": False}, norm_class=nn.LayerNorm, norm_kwargs=[{"normalized_shape": size} for size in hidden_sizes], dropout=dropout or None, activate_last_layer=False, device=device, ) # TDMPC2 only applies dropout after the first hidden layer. # MLP applies it to all layers, so we disable all after the first. if dropout: dropout_layers = [ module for module in network.children() if isinstance(module, nn.Dropout) ] for module in dropout_layers[1:]: module.p = 0.0 if output_activation is not None: network.append(nn.LayerNorm(out_features, device=device)) network.append( output_activation.to(device=device) if device is not None else output_activation ) for module in network.modules(): if isinstance(module, nn.Linear): nn.init.trunc_normal_(module.weight, std=0.02) if module.bias is not None: nn.init.zeros_(module.bias) return network
[docs] @dataclass class DreamerV3ImageEncoderConfig(NetworkConfig): """Hydra configuration for :class:`~torchrl.modules.DreamerV3ImageEncoder`. Example: >>> import torch >>> from hydra.utils import instantiate >>> from torchrl.trainers.algorithms.configs import DreamerV3ImageEncoderConfig >>> cfg = DreamerV3ImageEncoderConfig(depth=8, mults=[1, 2]) >>> net = instantiate(cfg) >>> image = torch.randint(0, 256, (4, 3, 16, 16), dtype=torch.uint8) >>> assert net(image).shape == (4, 256) .. seealso:: :class:`~torchrl.modules.DreamerV3ImageEncoder` """ in_channels: int = 3 depth: int = 64 mults: list[int] = field(default_factory=partial(list, (2, 3, 4, 4))) kernel_size: int = 5 norm_eps: float = 1e-4 device: Any = None _target_: str = "torchrl.modules.DreamerV3ImageEncoder"
[docs] @dataclass class DreamerV3ImageDecoderConfig(NetworkConfig): """Hydra configuration for :class:`~torchrl.modules.DreamerV3ImageDecoder`. Example: >>> import torch >>> from hydra.utils import instantiate >>> from torchrl.trainers.algorithms.configs import DreamerV3ImageDecoderConfig >>> cfg = DreamerV3ImageDecoderConfig( ... in_features=12, image_shape=[3, 16, 16], depth=8, mults=[1, 2], num_blocks=2 ... ) >>> net = instantiate(cfg) >>> assert net(torch.randn(4, 12)).shape == (4, 3, 16, 16) .. seealso:: :class:`~torchrl.modules.DreamerV3ImageDecoder` """ in_features: int = MISSING image_shape: list[int] = field(default_factory=partial(list, (3, 64, 64))) depth: int = 64 mults: list[int] = field(default_factory=partial(list, (2, 3, 4, 4))) kernel_size: int = 5 num_blocks: int = 8 norm_eps: float = 1e-4 device: Any = None _target_: str = "torchrl.modules.DreamerV3ImageDecoder"
[docs] @dataclass class DreamerV3DiscreteActorConfig(NetworkConfig): """Hydra configuration for :class:`~torchrl.modules.DreamerV3DiscreteActor`. Examples: >>> import torch >>> from hydra.utils import instantiate >>> from tensordict import TensorDict >>> from torchrl.trainers.algorithms.configs import DreamerV3DiscreteActorConfig >>> actor = instantiate(DreamerV3DiscreteActorConfig(in_features=12, out_features=3)) >>> data = TensorDict({"state": torch.randn(4, 8), "belief": torch.randn(4, 4)}, [4]) >>> actor(data)["action"].shape torch.Size([4, 3]) """ in_features: int = MISSING out_features: int = MISSING depth: int = 3 num_cells: int = 1024 norm_eps: float = 1e-4 unimix: float = 0.01 in_keys: Any = None action_key: Any = "action" logits_key: Any = "logits" log_prob_key: Any = "action_log_prob" device: Any = None _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_dreamer_v3_discrete_actor" )
def _make_dreamer_v3_discrete_actor(**kwargs) -> DreamerV3DiscreteActor: """Normalize Hydra's nested key lists before constructing the actor.""" in_keys = _normalize_hydra_keys(kwargs.pop("in_keys", None)) for key in ("action_key", "logits_key", "log_prob_key"): if key in kwargs: kwargs[key] = _normalize_hydra_key(kwargs[key]) return DreamerV3DiscreteActor(in_keys=in_keys, **kwargs)
[docs] @dataclass class DreamerV3SeededPolicyConfig(NetworkConfig): """Hydra configuration for :class:`~torchrl.modules.DreamerV3SeededPolicy`. Examples: >>> from hydra.utils import instantiate >>> from torchrl.trainers.algorithms.configs import DreamerV3DiscreteActorConfig, DreamerV3SeededPolicyConfig >>> config = DreamerV3SeededPolicyConfig( ... module=DreamerV3DiscreteActorConfig(in_features=6, out_features=3), seed=7, ... ) >>> policy = instantiate(config) >>> policy.get_extra_state() {'seed': 7, 'counter': 0} """ module: Any = MISSING seed: int = MISSING _target_: str = "torchrl.modules.DreamerV3SeededPolicy"
@dataclass class RSSMStateEstimatorV3Config(NetworkConfig): """Hydra configuration for :class:`~torchrl.modules.RSSMStateEstimatorV3`. Examples: Given the shared prior and posterior in the estimator's example: >>> from hydra.utils import instantiate >>> from torchrl.trainers.algorithms.configs import RSSMStateEstimatorV3Config >>> estimator = instantiate( # doctest: +SKIP ... RSSMStateEstimatorV3Config(), prior=prior, posterior=posterior, ... ) """ prior: Any = MISSING posterior: Any = MISSING in_keys: Any = None out_keys: Any = None _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_rssm_state_estimator_v3" ) def _make_rssm_state_estimator_v3(**kwargs) -> RSSMStateEstimatorV3: """Normalize configured nested keys before constructing the estimator.""" in_keys = _normalize_hydra_keys(kwargs.pop("in_keys", None)) out_keys = _normalize_hydra_keys(kwargs.pop("out_keys", None)) return RSSMStateEstimatorV3(in_keys=in_keys, out_keys=out_keys, **kwargs) @dataclass class NormConfig(ConfigBase): """A class to configure a normalization layer. Defaults to :class:`torch.nn.BatchNorm1d`. .. seealso:: :class:`torch.nn.BatchNorm1d` """ _target_: str = "torch.nn.BatchNorm1d" _partial_: bool = False def __post_init__(self) -> None: """Post-initialization hook for normalization configurations.""" @dataclass class AggregatorConfig(ConfigBase): """A class to configure an aggregator layer. Defaults to :class:`torchrl.modules.models.utils.SquashDims`. .. seealso:: :class:`torchrl.modules.models.utils.SquashDims` """ _target_: str = "torchrl.modules.models.utils.SquashDims" _partial_: bool = False def __post_init__(self) -> None: """Post-initialization hook for aggregator configurations."""
[docs] @dataclass class ConvNetConfig(NetworkConfig): """A class to configure a convolutional network. Defaults to :class:`torchrl.modules.ConvNet`. Example: >>> cfg = ConvNetConfig(in_features=3, depth=2, num_cells=[32, 64], kernel_sizes=[3, 5], strides=[1, 2], paddings=[1, 2]) >>> net = instantiate(cfg) >>> y = net(torch.randn(1, 3, 32, 32)) >>> assert y.shape == (1, 64) .. seealso:: :class:`torchrl.modules.ConvNet` """ in_features: int | None = None depth: int | None = None num_cells: Any = None kernel_sizes: Any = 3 strides: Any = 1 paddings: Any = 0 activation_class: ActivationConfig = field( default_factory=partial( ActivationConfig, _target_="torch.nn.ELU", _partial_=True ) ) activation_kwargs: Any = None norm_class: NormConfig | None = None norm_kwargs: Any = None bias_last_layer: bool = True aggregator_class: AggregatorConfig = field( default_factory=partial( AggregatorConfig, _target_="torchrl.modules.models.utils.SquashDims", _partial_=True, ) ) aggregator_kwargs: dict | None = None squeeze_output: bool = False device: Any = None _target_: str = "torchrl.modules.ConvNet" def __post_init__(self): if self.activation_class is None and isinstance(self.activation_class, str): self.activation_class = ActivationConfig( _target_=self.activation_class, _partial_=True ) if self.norm_class is None and isinstance(self.norm_class, str): self.norm_class = NormConfig(_target_=self.norm_class, _partial_=True) if self.aggregator_class is None and isinstance(self.aggregator_class, str): self.aggregator_class = AggregatorConfig( _target_=self.aggregator_class, _partial_=True )
@dataclass class QMixerNetworkConfig(NetworkConfig): """A class to configure a QMIX mixer network. .. seealso:: :class:`torchrl.modules.models.multiagent.QMixer` """ state_shape: Any = MISSING mixing_embed_dim: int = 32 n_agents: int = MISSING device: Any = None _target_: str = "torchrl.modules.models.multiagent.QMixer" def __post_init__(self) -> None: super().__post_init__() @dataclass class VDNMixerNetworkConfig(NetworkConfig): """A class to configure a VDN mixer network. .. seealso:: :class:`torchrl.modules.models.multiagent.VDNMixer` """ n_agents: int = MISSING device: Any = None _target_: str = "torchrl.modules.models.multiagent.VDNMixer" def __post_init__(self) -> None: super().__post_init__()
[docs] @dataclass class ModelConfig(ConfigBase): """Parent class to configure a model. A model can be made of several networks. It is always a :class:`~tensordict.nn.TensorDictModuleBase` instance. .. seealso:: :class:`TanhNormalModelConfig`, :class:`ValueModelConfig` """ _partial_: bool = False in_keys: Any = None out_keys: Any = None shared: bool = False def __post_init__(self) -> None: """Post-initialization hook for model configurations."""
[docs] @dataclass class TdMpc2WorldModelConfig(ConfigBase): """Configuration for a TD-MPC2 world model. The resulting :class:`~torchrl.modules.WorldModel` encodes observations, predicts the next latent state from the current latent state and action, and predicts distributional rewards from the current latent state and action. TensorDict routing is configured with the specialized key fields below. Set ``shared=True`` to place the constructed module in shared memory. .. seealso:: :class:`~torchrl.modules.WorldModel` """ observation_dim: int = MISSING action_dim: int = MISSING latent_dim: int = MISSING encoder_dim: int = 256 encoder_depth: int = 1 mlp_dim: int = 512 simnorm_dim: int = 8 num_bins: int = 101 observation_key: Any = "observation" action_key: Any = "action" latent_key: Any = "latent" reward_logits_key: Any = "reward_logits" device: Any = None shared: bool = False _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_tdmpc2_world_model" ) def __post_init__(self) -> None: """Post-initialization hook for the TD-MPC2 world model config."""
def _next_key(key: Any) -> Any: if isinstance(key, tuple): return ("next", *key) return ("next", key) def _make_tdmpc2_world_model( *, observation_dim: int, action_dim: int, latent_dim: int, encoder_dim: int = 256, encoder_depth: int = 1, mlp_dim: int = 512, simnorm_dim: int = 8, num_bins: int = 101, observation_key: Any = "observation", action_key: Any = "action", latent_key: Any = "latent", reward_logits_key: Any = "reward_logits", device: Any = None, shared: bool = False, ) -> WorldModel: """Build the state-based encoder, dynamics, and reward components of TD-MPC2.""" if observation_dim <= 0 or action_dim <= 0 or latent_dim <= 0: raise ValueError("observation_dim, action_dim, and latent_dim must be positive") if encoder_dim <= 0 or mlp_dim <= 0: raise ValueError("encoder_dim and mlp_dim must be positive") if simnorm_dim <= 0 or latent_dim % simnorm_dim: raise ValueError("simnorm_dim must be a positive divisor of latent_dim") if num_bins <= 1: raise ValueError("num_bins must be greater than 1") observation_key = _normalize_hydra_key(observation_key) action_key = _normalize_hydra_key(action_key) latent_key = _normalize_hydra_key(latent_key) reward_logits_key = _normalize_hydra_key(reward_logits_key) encoder = TensorDictModule( _make_tdmpc2_mlp( in_features=observation_dim, out_features=latent_dim, depth=encoder_depth, num_cells=encoder_dim, output_activation=SimplicialNormalization(simnorm_dim), device=device, ), in_keys=[observation_key], out_keys=[latent_key], ) dynamics = TensorDictModule( _make_tdmpc2_mlp( in_features=latent_dim + action_dim, out_features=latent_dim, depth=2, num_cells=mlp_dim, output_activation=SimplicialNormalization(simnorm_dim), device=device, ), in_keys=[latent_key, action_key], out_keys=[_next_key(latent_key)], ) reward = TensorDictModule( _make_tdmpc2_mlp( in_features=latent_dim + action_dim, out_features=max(num_bins, 1), depth=2, num_cells=mlp_dim, device=device, ), in_keys=[latent_key, action_key], out_keys=[_next_key(reward_logits_key)], ) with torch.no_grad(): reward.module[-1].weight.zero_() world_model = WorldModel(encoder, dynamics, reward) if shared: world_model = world_model.share_memory() return world_model
[docs] @dataclass class TdMpc2QEnsembleConfig(ModelConfig): """Configuration for a TD-MPC2 Q-function ensemble. The resulting :class:`~torchrl.modules.TdMpc2QEnsemble` maps a latent state and action to distributional Q-function logits. Its :meth:`forward` method writes all ensemble logits, while :meth:`~torchrl.modules.TdMpc2QEnsemble.reduce` decodes two randomly selected Q-functions and writes a reduced value. Args: latent_dim: Size of the latent state input. action_dim: Number of action dimensions. mlp_dim: Width of each hidden layer in every Q-function. num_q: Number of Q-functions in the ensemble. num_bins: Number of categorical bins for the Q-function output. Must be greater than 1. vmin: Minimum value of the symlog-space categorical support. vmax: Maximum value of the symlog-space categorical support. dropout: Dropout probability applied in the Q-functions. q_value_key: TensorDict key written by the ensemble reduction. device: Device on which to construct the Q-functions. Example: >>> import torch >>> from hydra.utils import instantiate >>> from tensordict import TensorDict >>> from torchrl.trainers.algorithms.configs import TdMpc2QEnsembleConfig >>> cfg = TdMpc2QEnsembleConfig(latent_dim=8, action_dim=2, mlp_dim=16) >>> q_ensemble = instantiate(cfg) >>> data = TensorDict( ... {"latent": torch.randn(3, 8), "action": torch.randn(3, 2)}, ... batch_size=[3], ... ) >>> q_ensemble(data)["q_logits"].shape torch.Size([3, 5, 101]) .. seealso:: :class:`~torchrl.modules.TdMpc2QEnsemble` """ latent_dim: int = MISSING action_dim: int = MISSING mlp_dim: int = 512 num_q: int = 5 num_bins: int = 101 vmin: float = -10.0 vmax: float = 10.0 dropout: float = 0.01 q_value_key: Any = "q_value" device: Any = None _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_tdmpc2_q_ensemble" ) def __post_init__(self) -> None: super().__post_init__() if self.in_keys is None: self.in_keys = ["latent", "action"] if self.out_keys is None: self.out_keys = ["q_logits"]
def _make_tdmpc2_q_ensemble( *, latent_dim: int, action_dim: int, mlp_dim: int = 512, num_q: int = 5, num_bins: int = 101, vmin: float = -10.0, vmax: float = 10.0, dropout: float = 0.01, q_value_key: Any = "q_value", device: Any = None, in_keys: Any = None, out_keys: Any = None, shared: bool = False, ) -> TdMpc2QEnsemble: """Build the TD-MPC2 Q-function ensemble.""" if latent_dim <= 0 or action_dim <= 0 or mlp_dim <= 0: raise ValueError("latent_dim, action_dim, and mlp_dim must be positive") if num_bins <= 1: raise ValueError("num_bins must be greater than 1") if not 0 <= dropout <= 1: raise ValueError("dropout must be in the interval [0, 1]") if in_keys is None: in_keys = ["latent", "action"] if out_keys is None: out_keys = ["q_logits"] in_keys = _normalize_hydra_keys(in_keys) out_keys = _normalize_hydra_keys(out_keys) q_value_key = _normalize_hydra_key(q_value_key) q_networks = [ _make_tdmpc2_mlp( in_features=latent_dim + action_dim, out_features=num_bins, depth=2, num_cells=mlp_dim, dropout=dropout, device=device, ) for _ in range(num_q) ] with torch.no_grad(): for q_network in q_networks: q_network[-1].weight.zero_() q_ensemble = TdMpc2QEnsemble( q_networks, num_bins=num_bins, vmin=vmin, vmax=vmax, in_keys=in_keys, out_keys=out_keys, q_value_key=q_value_key, ) if shared: q_ensemble = q_ensemble.share_memory() return q_ensemble
[docs] @dataclass class TdMpc2PolicyPriorConfig(ModelConfig): """Configuration for a TD-MPC2 policy prior. The policy prior maps a latent state to a sampled action and the distribution statistics used by the TD-MPC2 objective. By default, the module reads ``"latent"`` and writes ``"action"``, ``"mean"`, ``"log_std"``, ``"entropy"``, and ``"scaled_entropy"``. Example: >>> import torch >>> from hydra.utils import instantiate >>> from tensordict import TensorDict >>> from torchrl.trainers.algorithms.configs import TdMpc2PolicyPriorConfig >>> cfg = TdMpc2PolicyPriorConfig(latent_dim=8, action_dim=2, mlp_dim=16) >>> policy = instantiate(cfg) >>> td = TensorDict({"latent": torch.randn(3, 8)}, batch_size=[3]) >>> assert policy(td)["action"].shape == (3, 2) """ latent_dim: int = MISSING action_dim: int = MISSING mlp_dim: int = 512 log_std_min: float = -10.0 log_std_max: float = 2.0 device: Any = None _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_tdmpc2_policy_prior" ) def __post_init__(self) -> None: super().__post_init__() if self.in_keys is None: self.in_keys = ["latent"] if self.out_keys is None: self.out_keys = [ "action", "mean", "log_std", "entropy", "scaled_entropy", ]
def _make_tdmpc2_policy_prior( *, latent_dim: int, action_dim: int, mlp_dim: int = 512, log_std_min: float = -10.0, log_std_max: float = 2.0, device: Any = None, in_keys: Any = None, out_keys: Any = None, shared: bool = False, ) -> TensorDictModule: """Build a TD-MPC2 policy prior module.""" if latent_dim <= 0 or action_dim <= 0 or mlp_dim <= 0: raise ValueError("latent_dim, action_dim, and mlp_dim must be positive") if log_std_max <= log_std_min: raise ValueError("log_std_max must be greater than log_std_min") if in_keys is None: in_keys = ["latent"] if out_keys is None: out_keys = [ "action", "mean", "log_std", "entropy", "scaled_entropy", ] in_keys = _normalize_hydra_keys(in_keys) out_keys = _normalize_hydra_keys(out_keys) if len(in_keys) != 1: raise ValueError("TdMpc2PolicyPrior requires exactly one input key") if len(out_keys) != 5: raise ValueError( "TdMpc2PolicyPrior requires five output keys: action, mean, " "log_std, entropy, and scaled_entropy" ) network = _make_tdmpc2_mlp( in_features=latent_dim, out_features=2 * action_dim, depth=2, num_cells=mlp_dim, device=device, ) policy_prior = TensorDictModule( _TdMpc2PolicyPrior( network, log_std_min=log_std_min, log_std_max=log_std_max, device=device, ), in_keys=in_keys, out_keys=out_keys, ) if shared: policy_prior = policy_prior.share_memory() return policy_prior
[docs] @dataclass class TdMpc2PlannerConfig(ConfigBase): """Configuration for the TD-MPC2 action planner.""" world_model: Any = None policy_prior: Any = None q_ensemble: Any = None horizon: int | None = None discount: float | None = None num_samples: int = 512 num_elites: int = 64 num_pi_trajs: int = 24 iterations: int = 6 min_std: float = 0.05 max_std: float = 2.0 temperature: float = 0.5 observation_key: Any = None action_key: Any = None is_init_key: Any = "is_init" prev_mean_key: Any = "_tdmpc2_prev_mean" action_dim: int | None = None _partial_: bool = False _target_: str = "torchrl.trainers.algorithms.configs.modules._make_tdmpc2_planner" def __post_init__(self) -> None: if self.horizon is not None and self.horizon <= 0: raise ValueError(f"horizon must be positive, got {self.horizon}.") if self.discount is not None and not 0 <= self.discount <= 1: raise ValueError(f"discount must be in [0, 1], got {self.discount}.")
def _make_tdmpc2_planner( *, world_model: Any, policy_prior: Any, q_ensemble: Any, horizon: int | None = None, discount: float | None = None, num_samples: int = 512, num_elites: int = 64, num_pi_trajs: int = 24, iterations: int = 6, min_std: float = 0.05, max_std: float = 2.0, temperature: float = 0.5, observation_key: Any = None, action_key: Any = None, is_init_key: Any = "is_init", prev_mean_key: Any = "_tdmpc2_prev_mean", action_dim: int | None = None, ) -> TdMpc2Planner: """Build a TD-MPC2 planner from shared live learner components.""" components = { "world_model": world_model, "policy_prior": policy_prior, "q_ensemble": q_ensemble, } for name, component in components.items(): if not isinstance(component, torch.nn.Module) and hasattr( component, "_target_" ): components[name] = instantiate(component) missing = [name for name, component in components.items() if component is None] if missing: raise TypeError( "TdMpc2PlannerConfig requires live component modules for " + ", ".join(missing) + "; the trainer factory should inject these automatically." ) if horizon is None: horizon = 3 if discount is None: discount = 0.99 return TdMpc2Planner( **components, horizon=horizon, discount=discount, num_samples=num_samples, num_elites=num_elites, num_pi_trajs=num_pi_trajs, iterations=iterations, min_std=min_std, max_std=max_std, temperature=temperature, observation_key=( None if observation_key is None else _normalize_hydra_key(observation_key) ), action_key=(None if action_key is None else _normalize_hydra_key(action_key)), is_init_key=_normalize_hydra_key(is_init_key), prev_mean_key=_normalize_hydra_key(prev_mean_key), action_dim=action_dim, )
[docs] @dataclass class TensorDictModuleConfig(ModelConfig): """A class to configure a TensorDictModule. Example: >>> cfg = TensorDictModuleConfig(module=MLPConfig(in_features=10, out_features=10, depth=2, num_cells=32), in_keys=["observation"], out_keys=["action"]) >>> module = instantiate(cfg) >>> assert isinstance(module, TensorDictModule) >>> assert module(observation=torch.randn(10, 10)).shape == (10, 10) .. seealso:: :class:`tensordict.nn.TensorDictModule` """ module: MLPConfig = MISSING _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_tensordict_module" ) _partial_: bool = False def __post_init__(self) -> None: """Post-initialization hook for TensorDict module configurations.""" return super().__post_init__()
[docs] @dataclass class TensorDictSequentialConfig(ModelConfig): """A class to configure a TensorDictSequential. Example: >>> cfg = TensorDictSequentialConfig( ... modules=[ ... TensorDictModuleConfig(module=MLPConfig(in_features=10, out_features=10, depth=2, num_cells=32), in_keys=["observation"], out_keys=["hidden"]), ... TensorDictModuleConfig(module=MLPConfig(in_features=10, out_features=5, depth=2, num_cells=32), in_keys=["hidden"], out_keys=["action"]) ... ] ... ) >>> seq = instantiate(cfg) >>> assert isinstance(seq, TensorDictSequential) .. seealso:: :class:`tensordict.nn.TensorDictSequential` """ modules: Any | None = None partial_tolerant: bool = False selected_out_keys: Any | None = None inplace: bool | str | None = None _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_tensordict_sequential" ) _partial_: bool = False def __post_init__(self) -> None: return super().__post_init__()
[docs] @dataclass class TanhNormalModelConfig(ModelConfig): """A class to configure a TanhNormal model. Example: >>> cfg = TanhNormalModelConfig(network=MLPConfig(in_features=10, out_features=5, depth=2, num_cells=32)) >>> net = instantiate(cfg) >>> y = net(torch.randn(1, 10)) >>> assert y.shape == (1, 5) Args: low: lower bound of the action support handed to :class:`~torchrl.modules.TanhNormal` (a scalar or a per-dimension sequence). Defaults to ``None``, i.e. the distribution default of ``-1``. high: upper bound of the action support. Defaults to ``None``, i.e. ``1``. tanh_loc: if ``True``, the location is squashed to ``[-upscale, upscale]`` before the tanh transform, which keeps the log-probability of actions at the bounds finite (see :class:`~torchrl.modules.TanhNormal`). Defaults to ``False``. .. seealso:: :class:`torchrl.modules.TanhNormal` """ network: MLPConfig = MISSING eval_mode: bool = False extract_normal_params: bool = True scale_mapping: str = "biased_softplus_1.0" scale_lb: float = 1e-4 low: Any = None high: Any = None tanh_loc: bool = False param_keys: Any = None exploration_type: Any = "RANDOM" return_log_prob: bool = False _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_tanh_normal_model" ) def __post_init__(self): """Post-initialization hook for TanhNormal model configurations.""" super().__post_init__() if self.in_keys is None: self.in_keys = ["observation"] if self.param_keys is None: self.param_keys = ["loc", "scale"] if self.out_keys is None: self.out_keys = ["action"]
[docs] @dataclass class ValueModelConfig(ModelConfig): """A class to configure a Value model. Example: >>> cfg = ValueModelConfig(network=MLPConfig(in_features=10, out_features=5, depth=2, num_cells=32)) >>> net = instantiate(cfg) >>> y = net(torch.randn(1, 10)) >>> assert y.shape == (1, 5) .. seealso:: :class:`torchrl.modules.ValueOperator` """ _target_: str = "torchrl.trainers.algorithms.configs.modules._make_value_model" network: NetworkConfig = MISSING def __post_init__(self) -> None: """Post-initialization hook for value model configurations.""" super().__post_init__()
[docs] @dataclass class TanhModuleConfig(ModelConfig): """A class to configure a TanhModule. Example: >>> cfg = TanhModuleConfig(in_keys=["action"], out_keys=["action"], low=-1.0, high=1.0) >>> module = instantiate(cfg) >>> assert isinstance(module, TanhModule) .. seealso:: :class:`torchrl.modules.TanhModule` """ spec: Any = None low: Any = None high: Any = None clamp: bool = False _target_: str = "torchrl.trainers.algorithms.configs.modules._make_tanh_module" def __post_init__(self) -> None: """Post-initialization hook for TanhModule configurations.""" super().__post_init__()
[docs] @dataclass class AdditiveGaussianModuleConfig(ModelConfig): """A class to configure an AdditiveGaussianModule. Example: >>> cfg = AdditiveGaussianModuleConfig( ... spec=None, ... sigma_init=1.0, ... sigma_end=0.1, ... mean=0.0, ... std=1.0, ... action_key="action", ... ) >>> module = instantiate(cfg) >>> assert isinstance(module, AdditiveGaussianModule) .. seealso:: :class:`torchrl.modules.AdditiveGaussianModule` """ spec: Any = None sigma_init: float = 1.0 sigma_end: float = 0.1 annealing_num_steps: int = 1000 mean: float = 0.0 std: float = 1.0 action_key: Any = "action" safe: bool = False device: Any = None _target_: str = ( "torchrl.trainers.algorithms.configs.modules._make_additive_gaussian_module" ) _partial_: bool = False def __post_init__(self) -> None: super().__post_init__()
def _make_tensordict_module(*args, **kwargs) -> TensorDictModule: """Helper function to create a TensorDictModule.""" from hydra.utils import instantiate module = kwargs.pop("module") shared = kwargs.pop("shared", False) for key in ("in_keys", "out_keys"): if key in kwargs: kwargs[key] = _normalize_hydra_keys(kwargs[key]) # Instantiate the module if it's a config if hasattr(module, "_target_"): module = instantiate(module) elif callable(module) and hasattr(module, "func"): # partial function module = module() # Create the TensorDictModule tensordict_module = TensorDictModule(module, **kwargs) # Apply share_memory if needed if shared: tensordict_module = tensordict_module.share_memory() return tensordict_module def _make_tensordict_sequential(*args, **kwargs) -> TensorDictSequential: """Helper function to create a TensorDictSequential.""" from hydra.utils import instantiate from omegaconf import DictConfig, ListConfig modules = kwargs.pop("modules") shared = kwargs.pop("shared", False) partial_tolerant = kwargs.pop("partial_tolerant", False) selected_out_keys = _normalize_hydra_keys(kwargs.pop("selected_out_keys", None)) inplace = kwargs.pop("inplace", None) def _instantiate_module(module): if hasattr(module, "_target_"): return instantiate(module) elif callable(module) and hasattr(module, "func"): return module() else: return module if isinstance(modules, (dict, DictConfig)): instantiated_modules = { key: _instantiate_module(module) for key, module in modules.items() } elif isinstance(modules, (list, ListConfig)): instantiated_modules = [_instantiate_module(module) for module in modules] else: raise ValueError( f"modules must be a dict or list, got {type(modules).__name__}" ) tensordict_sequential = TensorDictSequential( instantiated_modules, partial_tolerant=partial_tolerant, selected_out_keys=selected_out_keys, inplace=inplace, ) if shared: tensordict_sequential = tensordict_sequential.share_memory() return tensordict_sequential def _make_tanh_normal_model(*args, **kwargs): """Helper function to create a TanhNormal model with ProbabilisticTensorDictSequential.""" from hydra.utils import instantiate from tensordict.nn import ( ProbabilisticTensorDictModule, ProbabilisticTensorDictSequential, TensorDictModule, ) from torchrl.modules import NormalParamExtractor, TanhNormal # Extract parameters network = kwargs.pop("network") in_keys = _normalize_hydra_keys(kwargs.pop("in_keys", ["observation"])) param_keys = _normalize_hydra_keys(kwargs.pop("param_keys", ["loc", "scale"])) out_keys = _normalize_hydra_keys(kwargs.pop("out_keys", ["action"])) extract_normal_params = kwargs.pop("extract_normal_params", True) scale_mapping = kwargs.pop("scale_mapping", "biased_softplus_1.0") scale_lb = kwargs.pop("scale_lb", 1e-4) return_log_prob = kwargs.pop("return_log_prob", False) eval_mode = kwargs.pop("eval_mode", False) exploration_type = kwargs.pop("exploration_type", "RANDOM") shared = kwargs.pop("shared", False) distribution_kwargs = dict(kwargs.pop("distribution_kwargs", None) or {}) for bound in ("low", "high"): value = kwargs.pop(bound, None) if value is None: continue # omegaconf hands sequences over as ListConfig if not isinstance(value, (int, float)): value = torch.as_tensor(list(value), dtype=torch.get_default_dtype()) distribution_kwargs[bound] = value if kwargs.pop("tanh_loc", False): distribution_kwargs["tanh_loc"] = True if distribution_kwargs: kwargs["distribution_kwargs"] = distribution_kwargs # Now instantiate the network if hasattr(network, "_target_"): network = instantiate(network) elif callable(network) and hasattr(network, "func"): # partial function network = network() # Create the sequential if extract_normal_params: # Add NormalParamExtractor to split the output network = torch.nn.Sequential( network, NormalParamExtractor(scale_mapping=scale_mapping, scale_lb=scale_lb), ) module = TensorDictModule(network, in_keys=in_keys, out_keys=param_keys) if shared: module = module.share_memory() # Create ProbabilisticTensorDictModule prob_module = ProbabilisticTensorDictModule( in_keys=param_keys, out_keys=out_keys, distribution_class=TanhNormal, return_log_prob=return_log_prob, default_interaction_type=exploration_type, **kwargs, ) result = ProbabilisticTensorDictSequential(module, prob_module) if eval_mode: result.eval() return result def _make_value_model(*args, **kwargs) -> ValueOperator: """Helper function to create a ValueOperator with the given network.""" from hydra.utils import instantiate network = kwargs.pop("network") shared = kwargs.pop("shared", False) for key in ("in_keys", "out_keys"): if key in kwargs: kwargs[key] = _normalize_hydra_keys(kwargs[key]) # Instantiate the network if it's a config if hasattr(network, "_target_"): network = instantiate(network) elif callable(network) and hasattr(network, "func"): # partial function network = network() # Create the ValueOperator value_operator = ValueOperator(network, **kwargs) # Apply share_memory if needed if shared: value_operator = value_operator.share_memory() return value_operator def _make_tanh_module(*args, **kwargs) -> TanhModule: """Helper function to create a TanhModule.""" kwargs.pop("shared", False) if "in_keys" in kwargs: kwargs["in_keys"] = _normalize_hydra_keys(kwargs["in_keys"]) if "out_keys" in kwargs: kwargs["out_keys"] = _normalize_hydra_keys(kwargs["out_keys"]) return TanhModule(**kwargs) def _make_additive_gaussian_module(*args, **kwargs) -> AdditiveGaussianModule: """Helper function to create an AdditiveGaussianModule.""" kwargs.pop("shared", False) kwargs.pop("in_keys", None) kwargs.pop("out_keys", None) if "action_key" in kwargs: kwargs["action_key"] = _normalize_hydra_key(kwargs["action_key"]) return AdditiveGaussianModule(**kwargs)
[docs] @dataclass class QValueModelConfig(ModelConfig): """A class to configure a QValueActor model. .. seealso:: :class:`torchrl.modules.QValueActor` """ _target_: str = "torchrl.trainers.algorithms.configs.modules._make_qvalue_model" network: NetworkConfig = MISSING action_space: Any = None action_key: Any = None action_value_key: Any = None chosen_action_value_key: Any = None action_mask_key: Any = None def __post_init__(self) -> None: super().__post_init__()
def _make_qvalue_model(*args, **kwargs) -> QValueActor: """Helper function to create a QValueActor with the given network.""" from hydra.utils import instantiate network = kwargs.pop("network") shared = kwargs.pop("shared", False) kwargs.pop("out_keys", None) if "in_keys" in kwargs: kwargs["in_keys"] = _normalize_hydra_keys(kwargs["in_keys"]) for key in ( "action_key", "action_value_key", "chosen_action_value_key", "action_mask_key", ): if key in kwargs: kwargs[key] = _normalize_hydra_key(kwargs[key]) if hasattr(network, "_target_"): network = instantiate(network) elif callable(network) and hasattr(network, "func"): network = network() qvalue_actor = QValueActor(network, **kwargs) if shared: qvalue_actor = qvalue_actor.share_memory() return qvalue_actor