# 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 dataclasses import dataclass
from typing import Any
from torchrl.objectives import (
A2CLoss,
ClipPPOLoss,
CQLLoss,
DDPGLoss,
DQNLoss,
IQLLoss,
KLPENPPOLoss,
PPOLoss,
QMixerLoss,
ReinforceLoss,
SACLoss,
TD3Loss,
)
from torchrl.objectives.iql import DiscreteIQLLoss
from torchrl.objectives.sac import DiscreteSACLoss
from torchrl.objectives.value import GAE
from torchrl.trainers.algorithms.configs.common import _normalize_hydra_key, ConfigBase
[docs]
@dataclass
class LossConfig(ConfigBase):
"""A class to configure a loss.
Args:
loss_type: The type of loss to use.
"""
_partial_: bool = False
def __post_init__(self) -> None:
"""Post-initialization hook for loss configurations."""
[docs]
@dataclass
class SACLossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.SACLoss` (and :class:`~torchrl.objectives.sac.DiscreteSACLoss` when ``discrete=True``).
Every kwarg accepted by ``SACLoss.__init__`` is exposed as a field here. The
``discrete``/``action_space``/``num_actions``/``target_entropy_weight`` fields
apply only when the discrete variant is selected, and the
``value_network``/``action_spec``/``delay_actor``/``delay_value`` fields only
when it is not.
"""
actor_network: Any = None
qvalue_network: Any = None
value_network: Any = None
discrete: bool = False
action_space: Any = None
num_actions: int | None = None
num_qvalue_nets: int = 2
loss_function: str = "smooth_l1"
alpha_init: float = 1.0
min_alpha: float | None = None
max_alpha: float | None = None
action_spec: Any = None
fixed_alpha: bool = False
target_entropy: str | float = "auto"
target_entropy_weight: float = 0.98
delay_actor: bool = False
delay_qvalue: bool = True
delay_value: bool = True
gamma: float | None = None
priority_key: str | None = None
separate_losses: bool = False
reduction: str | None = None
skip_done_states: bool = False
deactivate_vmap: bool = False
use_prioritized_weights: str | bool = "auto"
scalar_output_mode: str | None = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_sac_loss"
def __post_init__(self) -> None:
"""Post-initialization hook for SAC loss configurations."""
super().__post_init__()
def _make_sac_loss(*args, **kwargs) -> SACLoss:
discrete_loss_type = kwargs.pop("discrete", False)
gamma = kwargs.pop("gamma", None)
# Instantiate networks if they are config objects
actor_network = kwargs.get("actor_network")
qvalue_network = kwargs.get("qvalue_network")
value_network = kwargs.get("value_network")
if actor_network is not None and hasattr(actor_network, "_target_"):
kwargs["actor_network"] = actor_network()
if qvalue_network is not None and hasattr(qvalue_network, "_target_"):
kwargs["qvalue_network"] = qvalue_network()
if value_network is not None and hasattr(value_network, "_target_"):
kwargs["value_network"] = value_network()
if discrete_loss_type:
# DiscreteSACLoss has no value network, action spec or delayed actor/value.
for key in ("value_network", "action_spec", "delay_actor", "delay_value"):
kwargs.pop(key, None)
loss = DiscreteSACLoss(*args, **kwargs)
else:
# SACLoss has no `action_space`, `num_actions` or `target_entropy_weight` kwarg.
for key in ("action_space", "num_actions", "target_entropy_weight"):
kwargs.pop(key, None)
loss = SACLoss(*args, **kwargs)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
# Keep these fields in TQCLoss constructor order so Hydra exposes every option.
@dataclass
class TQCLossConfig(LossConfig):
"""Hydra fields for :class:`~torchrl.objectives.TQCLoss`."""
actor_network: Any = None
qvalue_network: Any = None
num_qvalue_nets: int = 5
top_quantiles_to_drop_per_net: int = 2
alpha_init: float = 1.0
min_alpha: float | None = None
max_alpha: float | None = None
action_spec: Any = None
fixed_alpha: bool = False
target_entropy: str | float = "auto"
delay_qvalue: bool = True
separate_losses: bool = False
reduction: str | None = None
deactivate_vmap: bool = False
skip_done_states: bool = False
use_prioritized_weights: str | bool = "auto"
scalar_output_mode: str | None = None
_target_: str = "torchrl.objectives.TQCLoss"
[docs]
@dataclass
class PPOLossConfig(LossConfig):
"""Hydra configuration for the PPO loss family.
Dispatches between :class:`~torchrl.objectives.ClipPPOLoss` (``loss_type='clip'``),
:class:`~torchrl.objectives.KLPENPPOLoss` (``loss_type='kl'``) and
:class:`~torchrl.objectives.PPOLoss` (``loss_type='ppo'``). Every kwarg
accepted by any of those classes is exposed here; only the kwargs relevant
to the selected ``loss_type`` are forwarded.
"""
actor_network: Any = None
critic_network: Any = None
loss_type: str = "clip"
entropy_bonus: bool = True
samples_mc_entropy: int = 1
entropy_coeff: float | None = None
log_explained_variance: bool = True
critic_coeff: float | None = None
loss_critic_type: str = "smooth_l1"
normalize_advantage: bool = False
normalize_advantage_exclude_dims: tuple = ()
advantage_norm: Any = None
gamma: float | None = None
separate_losses: bool = False
advantage_key: str | None = None
value_target_key: str | None = None
value_key: str | None = None
functional: bool = True
actor: Any = None
critic: Any = None
reduction: str | None = None
clip_value: float | None = None
delay_actor: bool = False
max_importance_ratio: float | None = None
# float for symmetric clipping or a (low, high) pair for DAPO-style
# asymmetric clipping (ClipPPOLoss only)
clip_epsilon: Any = 0.2
dtarg: float = 0.01
beta: float = 1.0
increment: float = 2.0
decrement: float = 0.5
samples_mc_kl: int = 1
device: Any = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_ppo_loss"
def __post_init__(self) -> None:
"""Post-initialization hook for PPO loss configurations."""
super().__post_init__()
def _make_ppo_loss(*args, **kwargs) -> PPOLoss:
loss_type = kwargs.pop("loss_type", "clip")
gamma = kwargs.pop("gamma", None)
# Instantiate the advantage normaliser if it is a config object
advantage_norm = kwargs.get("advantage_norm")
if advantage_norm is not None and hasattr(advantage_norm, "_target_"):
kwargs["advantage_norm"] = advantage_norm()
# Drop kwargs that don't apply to the chosen loss flavor so each class
# receives only what its __init__ accepts.
clip_only = {"clip_epsilon"}
kl_only = {"dtarg", "beta", "increment", "decrement", "samples_mc_kl"}
ppo_only = {
"log_explained_variance",
"advantage_key",
"value_target_key",
"value_key",
"functional",
"actor",
"critic",
}
if loss_type == "clip":
for k in kl_only | ppo_only:
kwargs.pop(k, None)
# a (low, high) clip_epsilon pair arrives as an omegaconf ListConfig,
# which the loss does not recognize as a tuple
clip_epsilon = kwargs.get("clip_epsilon")
if clip_epsilon is not None and not isinstance(clip_epsilon, (int, float)):
kwargs["clip_epsilon"] = tuple(clip_epsilon)
loss = ClipPPOLoss(*args, **kwargs)
elif loss_type == "kl":
for k in clip_only | ppo_only:
kwargs.pop(k, None)
loss = KLPENPPOLoss(*args, **kwargs)
elif loss_type == "ppo":
for k in clip_only | kl_only:
kwargs.pop(k, None)
loss = PPOLoss(*args, **kwargs)
else:
raise ValueError(f"Invalid loss type: {loss_type}")
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
[docs]
@dataclass
class A2CLossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.A2CLoss`.
Every kwarg accepted by ``A2CLoss.__init__`` is exposed as a field here.
``gamma``, ``advantage_key`` and ``value_target_key`` are handled by the
factory (via ``make_value_estimator`` and ``set_keys``) rather than being
forwarded to the constructor, which rejects them.
"""
actor_network: Any = None
critic_network: Any = None
entropy_bonus: bool = True
samples_mc_entropy: int = 1
entropy_coeff: float | None = None
critic_coeff: float = 1.0
loss_critic_type: str = "smooth_l1"
advantage_norm: Any = None
gamma: float | None = None
separate_losses: bool = False
advantage_key: Any = None
value_target_key: Any = None
functional: bool = True
actor: Any = None
critic: Any = None
reduction: str | None = None
clip_value: float | None = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_a2c_loss"
def __post_init__(self) -> None:
"""Post-initialization hook for A2C loss configurations."""
super().__post_init__()
def _make_onpolicy_loss(loss_cls, *args, **kwargs):
# gamma / advantage_key / value_target_key are rejected by the loss
# constructors: route them through make_value_estimator / set_keys instead.
gamma = kwargs.pop("gamma", None)
advantage_key = _normalize_hydra_key(kwargs.pop("advantage_key", None))
value_target_key = _normalize_hydra_key(kwargs.pop("value_target_key", None))
actor_network = kwargs.get("actor_network")
critic_network = kwargs.get("critic_network")
if actor_network is not None and hasattr(actor_network, "_target_"):
kwargs["actor_network"] = actor_network()
if critic_network is not None and hasattr(critic_network, "_target_"):
kwargs["critic_network"] = critic_network()
advantage_norm = kwargs.get("advantage_norm")
if advantage_norm is not None and hasattr(advantage_norm, "_target_"):
kwargs["advantage_norm"] = advantage_norm()
loss = loss_cls(*args, **kwargs)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
keys = {}
if advantage_key is not None:
keys["advantage"] = advantage_key
if value_target_key is not None:
keys["value_target"] = value_target_key
if keys:
loss.set_keys(**keys)
return loss
def _make_a2c_loss(*args, **kwargs) -> A2CLoss:
return _make_onpolicy_loss(A2CLoss, *args, **kwargs)
[docs]
@dataclass
class ReinforceLossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.ReinforceLoss`.
Every kwarg accepted by ``ReinforceLoss.__init__`` is exposed as a field
here. ``gamma``, ``advantage_key`` and ``value_target_key`` are handled by
the factory (via ``make_value_estimator`` and ``set_keys``) rather than
being forwarded to the constructor, which rejects them.
"""
actor_network: Any = None
critic_network: Any = None
delay_value: bool = False
loss_critic_type: str = "smooth_l1"
gamma: float | None = None
advantage_key: Any = None
value_target_key: Any = None
separate_losses: bool = False
functional: bool = True
actor: Any = None
critic: Any = None
reduction: str | None = None
clip_value: float | None = None
_target_: str = (
"torchrl.trainers.algorithms.configs.objectives._make_reinforce_loss"
)
def __post_init__(self) -> None:
"""Post-initialization hook for REINFORCE loss configurations."""
super().__post_init__()
def _make_reinforce_loss(*args, **kwargs) -> ReinforceLoss:
return _make_onpolicy_loss(ReinforceLoss, *args, **kwargs)
[docs]
@dataclass
class TD3LossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.TD3Loss`.
Every kwarg accepted by ``TD3Loss.__init__`` is exposed as a field here.
"""
actor_network: Any = None
qvalue_network: Any = None
action_spec: Any = None
bounds: tuple[float] | None = None
num_qvalue_nets: int = 2
policy_noise: float = 0.2
noise_clip: float = 0.5
loss_function: str = "smooth_l1"
delay_actor: bool = True
delay_qvalue: bool = True
gamma: float | None = None
priority_key: str | None = None
separate_losses: bool = False
reduction: str | None = None
deactivate_vmap: bool = False
use_prioritized_weights: str | bool = "auto"
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_td3_loss"
def _make_td3_loss(*args, **kwargs) -> TD3Loss:
gamma = kwargs.pop("gamma", None)
# Instantiate networks if they are config objects
actor_network = kwargs.get("actor_network")
qvalue_network = kwargs.get("qvalue_network")
if actor_network is not None and hasattr(actor_network, "_target_"):
kwargs["actor_network"] = actor_network()
if qvalue_network is not None and hasattr(qvalue_network, "_target_"):
kwargs["qvalue_network"] = qvalue_network()
loss = TD3Loss(*args, **kwargs)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
[docs]
@dataclass
class TargetNetUpdaterConfig:
"""An abstract class to configure target net updaters."""
loss_module: Any
_partial_: bool = True
[docs]
@dataclass
class SoftUpdateConfig(TargetNetUpdaterConfig):
"""A class for soft update instantiation."""
_target_: str = "torchrl.objectives.utils.SoftUpdate"
eps: float | None = None # noqa # type-ignore
tau: float | None = 0.001 # noqa # type-ignore
[docs]
@dataclass
class HardUpdateConfig(TargetNetUpdaterConfig):
"""A class for hard update instantiation."""
_target_: str = "torchrl.objectives.utils.HardUpdate"
value_network_update_interval: int = 1000
def _make_gae(*args, **kwargs) -> GAE:
group_key = _normalize_hydra_key(kwargs.pop("group_key", None))
valid_key = _normalize_hydra_key(kwargs.pop("valid_key", None))
gae = GAE(*args, group_key=group_key, **kwargs)
if valid_key is not None:
gae.set_keys(valid=valid_key)
return gae
[docs]
@dataclass
class GAEConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.value.GAE`.
Every kwarg accepted by ``GAE.__init__`` is exposed as a field here. The
``valid_key`` field is applied through
:meth:`~torchrl.objectives.value.GAE.set_keys` by the factory.
"""
gamma: float | None = None
lmbda: float | None = None
value_network: Any = None
average_gae: bool = True
differentiable: bool = False
vectorized: bool | None = None
skip_existing: bool | None = None
advantage_key: str | None = None
value_target_key: str | None = None
value_key: str | None = None
shifted: bool = False
device: Any = None
time_dim: int | None = None
auto_reset_env: bool = False
deactivate_vmap: bool = False
value_chunk_size: int | None = None
num_chunks: int | None = None
num_chunk: int | None = None
value_chunk_dim: int = 0
shifted_budget: int = 1
group_key: Any = None
valid_key: Any = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_gae"
_partial_: bool = False
def __post_init__(self) -> None:
"""Post-initialization hook for GAELoss configurations."""
super().__post_init__()
[docs]
@dataclass
class DQNLossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.DQNLoss`.
Every kwarg accepted by ``DQNLoss.__init__`` is exposed as a field here.
"""
value_network: Any = None
loss_function: str = "l2"
delay_value: bool = True
double_dqn: bool = False
action_space: Any = None
gamma: float | None = None
priority_key: str | None = None
reduction: str | None = None
use_prioritized_weights: str | bool = "auto"
action_key: Any = None
action_value_key: Any = None
value_key: Any = None
reward_key: Any = None
done_key: Any = None
terminated_key: Any = None
priority_key: Any = None
priority_weight_key: Any = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_dqn_loss"
def __post_init__(self) -> None:
super().__post_init__()
def _make_dqn_loss(*args, **kwargs) -> DQNLoss:
tensor_keys = {}
for key in (
"action_key",
"action_value_key",
"value_key",
"reward_key",
"done_key",
"terminated_key",
"priority_key",
"priority_weight_key",
):
if key in kwargs:
value = kwargs.pop(key)
if value is not None:
tensor_keys[key.removesuffix("_key")] = _normalize_hydra_key(value)
value_network = kwargs.get("value_network")
gamma = kwargs.pop("gamma", None)
if value_network is not None and hasattr(value_network, "_target_"):
kwargs["value_network"] = value_network()
loss = DQNLoss(*args, **kwargs)
if tensor_keys:
loss.set_keys(**tensor_keys)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
@dataclass
class QMixerLossConfig(LossConfig):
"""A class to configure a QMixer loss."""
local_value_network: Any = None
mixer_network: Any = None
loss_function: str = "l2"
delay_value: bool = True
action_space: Any = None
gamma: float | None = None
priority_key: str | None = None
action_key: Any = None
action_value_key: Any = None
local_value_key: Any = None
global_value_key: Any = None
reward_key: Any = None
done_key: Any = None
terminated_key: Any = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_qmixer_loss"
def __post_init__(self) -> None:
super().__post_init__()
def _make_qmixer_loss(*args, **kwargs) -> QMixerLoss:
tensor_keys = {}
for key in (
"action_key",
"action_value_key",
"local_value_key",
"global_value_key",
"reward_key",
"done_key",
"terminated_key",
"priority_key",
):
if key in kwargs:
value = kwargs.pop(key)
if value is not None:
tensor_keys[key.removesuffix("_key")] = _normalize_hydra_key(value)
local_value_network = kwargs.get("local_value_network")
mixer_network = kwargs.get("mixer_network")
gamma = kwargs.pop("gamma", None)
if local_value_network is not None and hasattr(local_value_network, "_target_"):
kwargs["local_value_network"] = local_value_network()
if mixer_network is not None and hasattr(mixer_network, "_target_"):
kwargs["mixer_network"] = mixer_network()
loss = QMixerLoss(*args, **kwargs)
loss.set_keys(**tensor_keys)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
[docs]
@dataclass
class DDPGLossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.DDPGLoss`.
Every kwarg accepted by ``DDPGLoss.__init__`` is exposed as a field here.
"""
actor_network: Any = None
value_network: Any = None
loss_function: str = "l2"
delay_actor: bool = False
delay_value: bool = True
gamma: float | None = None
separate_losses: bool = False
reduction: str | None = None
use_prioritized_weights: str | bool = "auto"
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_ddpg_loss"
def __post_init__(self) -> None:
super().__post_init__()
def _make_ddpg_loss(*args, **kwargs) -> DDPGLoss:
gamma = kwargs.pop("gamma", None)
actor_network = kwargs.get("actor_network")
value_network = kwargs.get("value_network")
if actor_network is not None and hasattr(actor_network, "_target_"):
kwargs["actor_network"] = actor_network()
if value_network is not None and hasattr(value_network, "_target_"):
kwargs["value_network"] = value_network()
loss = DDPGLoss(*args, **kwargs)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
[docs]
@dataclass
class IQLLossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.IQLLoss` (and :class:`~torchrl.objectives.iql.DiscreteIQLLoss` when ``discrete=True``).
Every kwarg accepted by ``IQLLoss.__init__`` is exposed as a field here. The
``discrete``/``action_space`` fields apply only when the discrete variant is
selected.
"""
actor_network: Any = None
qvalue_network: Any = None
value_network: Any = None
discrete: bool = False
action_space: Any = None
num_qvalue_nets: int = 2
loss_function: str = "smooth_l1"
temperature: float = 1.0
expectile: float = 0.5
gamma: float | None = None
priority_key: str | None = None
separate_losses: bool = False
reduction: str | None = None
deactivate_vmap: bool = False
scalar_output_mode: str | None = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_iql_loss"
def __post_init__(self) -> None:
super().__post_init__()
def _make_iql_loss(*args, **kwargs) -> IQLLoss:
discrete_loss_type = kwargs.pop("discrete", False)
gamma = kwargs.pop("gamma", None)
actor_network = kwargs.get("actor_network")
qvalue_network = kwargs.get("qvalue_network")
value_network = kwargs.get("value_network")
if actor_network is not None and hasattr(actor_network, "_target_"):
kwargs["actor_network"] = actor_network()
if qvalue_network is not None and hasattr(qvalue_network, "_target_"):
kwargs["qvalue_network"] = qvalue_network()
if value_network is not None and hasattr(value_network, "_target_"):
kwargs["value_network"] = value_network()
if discrete_loss_type:
# DiscreteIQLLoss has no `deactivate_vmap` kwarg.
kwargs.pop("deactivate_vmap", None)
loss = DiscreteIQLLoss(*args, **kwargs)
else:
# IQLLoss has no `action_space` kwarg.
kwargs.pop("action_space", None)
loss = IQLLoss(*args, **kwargs)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
[docs]
@dataclass
class CQLLossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.CQLLoss`.
Every kwarg accepted by ``CQLLoss.__init__`` is exposed as a field here.
"""
actor_network: Any = None
qvalue_network: Any = None
loss_function: str = "smooth_l1"
alpha_init: float = 1.0
min_alpha: float | None = None
max_alpha: float | None = None
action_spec: Any = None
fixed_alpha: bool = False
target_entropy: str | float = "auto"
delay_actor: bool = False
delay_qvalue: bool = True
gamma: float | None = None
temperature: float = 1.0
min_q_weight: float = 1.0
max_q_backup: bool = False
deterministic_backup: bool = True
num_random: int = 10
with_lagrange: bool = False
lagrange_thresh: float = 0.0
reduction: str | None = None
deactivate_vmap: bool = False
scalar_output_mode: str | None = None
_target_: str = "torchrl.trainers.algorithms.configs.objectives._make_cql_loss"
def __post_init__(self) -> None:
super().__post_init__()
def _make_cql_loss(*args, **kwargs) -> CQLLoss:
gamma = kwargs.pop("gamma", None)
actor_network = kwargs.get("actor_network")
qvalue_network = kwargs.get("qvalue_network")
if actor_network is not None and hasattr(actor_network, "_target_"):
kwargs["actor_network"] = actor_network()
if qvalue_network is not None and hasattr(qvalue_network, "_target_"):
kwargs["qvalue_network"] = qvalue_network()
loss = CQLLoss(*args, **kwargs)
if gamma is not None:
loss.make_value_estimator(gamma=gamma)
return loss
[docs]
@dataclass
class DreamerV3LossConfig(LossConfig):
"""Hydra configuration for :class:`~torchrl.objectives.DreamerV3Loss`.
Examples:
With the component losses from the ``DreamerV3Loss`` example:
>>> from hydra.utils import instantiate
>>> from torchrl.trainers.algorithms.configs import DreamerV3LossConfig
>>> configured_loss = instantiate(
... DreamerV3LossConfig(), model_loss=model_loss,
... actor_loss=actor_loss, value_loss=value_loss,
... )
>>> losses = configured_loss(sample)
>>> assert not losses["replay_context", "state"].requires_grad
"""
model_loss: Any = None
actor_loss: Any = None
value_loss: Any = None
replay_value_loss_weight: float = 0.3
continuation_horizon: float = 333.0
lmbda: float = 0.95
_target_: str = "torchrl.objectives.DreamerV3Loss"