Rate this Page

Source code for torchrl.objectives.multiagent.mappo

# 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.
"""Multi-agent PPO objectives.

Implements :class:`MAPPOLoss` (centralised critic) and :class:`IPPOLoss`
(independent / decentralised critic).

References:
    - Yu, C. et al. *The Surprising Effectiveness of PPO in Cooperative
      Multi-Agent Games.* NeurIPS 2022. https://arxiv.org/abs/2103.01955
    - de Witt, C. S. et al. *Is Independent Learning All You Need in the
      StarCraft Multi-Agent Challenge?* 2020. https://arxiv.org/abs/2011.09533
"""
from __future__ import annotations

from typing import Any

from tensordict.nn import TensorDictModule
from tensordict.nn.probabilistic import ProbabilisticTensorDictSequential

from torchrl.modules.value_norm import ValueNorm
from torchrl.objectives.ppo import ClipPPOLoss
from torchrl.objectives.utils import ValueEstimators


class _MultiAgentPPOMixin:
    """Shared plumbing for :class:`MAPPOLoss` and :class:`IPPOLoss`.

    Two pieces:

    1. Default the value estimator to :class:`MultiAgentGAE` so per-agent
       value outputs broadcast cleanly against team-shared reward / done
       signals.
    2. When a :class:`~torchrl.modules.ValueNorm` is attached, normalise the
       value-target and critic prediction by the running stats *before* the
       MSE / smooth-L1 distance. This stabilises critic-loss magnitude when
       reward scales drift during training (Yu et al. 2022, Table 13).

       The normalisation is plumbed through
       :meth:`~torchrl.objectives.PPOLoss._critic_loss_inputs`, the hook
       :class:`~torchrl.objectives.PPOLoss` exposes for exactly this purpose,
       so all of the parent's other critic-loss machinery (``clip_value``,
       ``separate_losses``, ``log_explained_variance``, ...) continues to
       work alongside ``value_norm``.
    """

    default_value_estimator = ValueEstimators.MAGAE

    # Subclasses (MAPPOLoss / IPPOLoss) wire this up in ``__init__`` —
    # populated here as a type hint only so static checkers know it exists.
    value_norm: ValueNorm | None

    def _critic_loss_inputs(self, target_return, state_value, old_state_value):
        """Override of :meth:`PPOLoss._critic_loss_inputs` for PopArt normalisation.

        Pushes ``target_return`` / ``state_value`` / ``old_state_value``
        through the attached :class:`ValueNorm` if any, leaving the parent's
        ``clip_value`` / ``log_explained_variance`` / ``separate_losses``
        machinery untouched. ``old_state_value`` (used by the PPO value-clip
        path) is normalised with the same stats so the clip radius stays in
        normalised space — the convention the MAPPO paper assumes.

        The running stats are updated *once* per call, on the un-normalised
        ``target_return`` (which is what PopArt expects: the EMA tracks the
        real return distribution, not the normalised one).
        """
        value_norm = self.value_norm
        if value_norm is None:
            return target_return, state_value, old_state_value
        value_norm.update(target_return)
        normalised_target = value_norm.normalize(target_return.detach())
        normalised_pred = value_norm.normalize(state_value)
        normalised_old = (
            value_norm.normalize(old_state_value)
            if old_state_value is not None
            else None
        )
        return normalised_target, normalised_pred, normalised_old


[docs] class MAPPOLoss(_MultiAgentPPOMixin, ClipPPOLoss): """Multi-Agent PPO loss with a centralised critic (Yu et al. 2022). MAPPO trains a *decentralised actor* (each agent's policy conditions only on its local observation) together with a *centralised critic* (single value function that conditions on the full team state or concatenated observations). The decentralised actor lets policies run independently at execution time, while the centralised critic reduces variance during training by giving every agent the same value baseline derived from full state information. This class is a thin specialisation of :class:`ClipPPOLoss`. The differences: - The default value estimator is :class:`~torchrl.objectives.value.MultiAgentGAE`, which broadcasts team-shared rewards / done flags along the agent dimension before computing returns. - ``normalize_advantage_exclude_dims`` defaults to ``(-2,)`` so the agent dim is excluded when standardising advantages. - An optional :class:`~torchrl.modules.ValueNorm` can be supplied via ``value_norm=PopArtValueNorm(shape=1)`` to stabilise the critic loss; the MAPPO paper reports this is load-bearing on SMAC (their Table 13). :class:`~torchrl.modules.RunningValueNorm` is a no-decay alternative for stationary reward scales. Args: actor_network (ProbabilisticTensorDictSequential): per-agent policy operator. Conventionally built with :class:`~torchrl.modules.MultiAgentMLP` using ``centralized=False, share_params=True`` for cooperative homogeneous teams. critic_network (TensorDictModule): centralised value operator. Build this with :class:`~torchrl.modules.MultiAgentMLP` and ``centralized=True, share_params=True``, or with any module that consumes a global ``"state"`` key and returns ``("agents", "state_value")`` of shape ``[*B, n_agents, 1]``. Keyword Args: value_norm (ValueNorm, optional): if supplied, the critic target and prediction are normalised by this running normaliser before the MSE / smooth-L1 distance. Composes correctly with ``clip_value`` (the clip radius is applied in normalised space). Defaults to ``None`` (no value norm). clip_epsilon (float): PPO ratio clip. Defaults to ``0.2``. entropy_coeff (float): entropy bonus weight. Defaults to ``0.01`` (MAPPO default). critic_coef (float, optional): critic loss weight. Defaults to ``1.0``. normalize_advantage (bool): whether to standardise the advantage. Defaults to ``True`` (MAPPO default; differs from base :class:`ClipPPOLoss` which defaults to ``False``). normalize_advantage_exclude_dims (tuple of int): dimensions to exclude from advantage standardisation. Defaults to ``(-2,)`` (the agent dim). **kwargs: forwarded to :class:`ClipPPOLoss`. The expected tensordict layout follows the torchrl multi-agent convention (see :class:`~torchrl.envs.libs.vmas.VmasEnv`, :class:`~torchrl.envs.libs.pettingzoo.PettingZooEnv`): - ``("agents", "observation")``: ``[*B, T, n_agents, obs_dim]`` - ``("agents", "action")``: ``[*B, T, n_agents, action_dim]`` - Optional ``"state"`` at the root for centralised critics - Team-shared ``("next", "reward")``, ``("next", "done")``, ``("next", "terminated")`` of shape ``[*B, T, 1]`` (or per-agent under ``("next", "agents", "reward")`` for competitive settings). Example: >>> import torch >>> from tensordict.nn import TensorDictModule >>> from torchrl.modules import ( ... MultiAgentMLP, PopArtValueNorm, ProbabilisticActor, ... ) >>> from torchrl.modules.distributions import NormalParamExtractor, TanhNormal >>> from torchrl.objectives.multiagent import MAPPOLoss >>> n_agents, obs_dim, action_dim = 3, 6, 2 >>> actor_net = torch.nn.Sequential( ... MultiAgentMLP( ... n_agent_inputs=obs_dim, n_agent_outputs=2 * action_dim, ... n_agents=n_agents, centralized=False, share_params=True, ... ), ... NormalParamExtractor(), ... ) >>> actor_module = TensorDictModule( ... actor_net, ... in_keys=[("agents", "observation")], ... out_keys=[("agents", "loc"), ("agents", "scale")], ... ) >>> actor = ProbabilisticActor( ... module=actor_module, ... in_keys=[("agents", "loc"), ("agents", "scale")], ... out_keys=[("agents", "action")], ... distribution_class=TanhNormal, ... ) >>> critic = TensorDictModule( ... MultiAgentMLP( ... n_agent_inputs=obs_dim, n_agent_outputs=1, ... n_agents=n_agents, centralized=True, share_params=True, ... ), ... in_keys=[("agents", "observation")], ... out_keys=[("agents", "state_value")], ... ) >>> loss = MAPPOLoss(actor, critic, value_norm=PopArtValueNorm(shape=1)) >>> loss.set_keys(value=("agents", "state_value"), action=("agents", "action")) """ def __init__( self, actor_network: ProbabilisticTensorDictSequential | None = None, critic_network: TensorDictModule | None = None, *, value_norm: ValueNorm | None = None, entropy_coeff: float | dict[str, float] = 0.01, normalize_advantage: bool = True, normalize_advantage_exclude_dims: tuple[int, ...] = (-2,), **kwargs: Any, ) -> None: super().__init__( actor_network, critic_network, entropy_coeff=entropy_coeff, normalize_advantage=normalize_advantage, normalize_advantage_exclude_dims=normalize_advantage_exclude_dims, **kwargs, ) # ``nn.Module.__setattr__`` registers the ``ValueNorm`` as a child # module automatically, so ``.to(device)`` / ``state_dict()`` / etc. # pick it up without us calling ``add_module`` a second time. self.value_norm = value_norm
[docs] class IPPOLoss(MAPPOLoss): """Independent PPO loss (de Witt et al. 2020). IPPO is the decentralised counterpart of MAPPO: each agent has its *own* value function that conditions only on its local observation. There is no centralised critic and no global state required. Surprisingly competitive with MAPPO on many SMAC scenarios (the de Witt et al. paper is titled *Is Independent Learning All You Need...*). Structurally this loss is identical to :class:`MAPPOLoss`; the difference lives entirely in the critic the user passes in. We expose it as a separate class so the API is self-documenting: when you import ``IPPOLoss`` it is unambiguous which algorithm you are running, and the docstring spells out the critic-construction recipe. Args: actor_network (ProbabilisticTensorDictSequential): per-agent policy. Build with ``MultiAgentMLP(centralized=False, share_params=True)``. critic_network (TensorDictModule): per-agent value operator. Build with ``MultiAgentMLP(centralized=False, share_params=True)`` so each agent values its own observation. Keyword Args: value_norm (ValueNorm, optional): rarely used with IPPO; defaults to ``None``. entropy_coeff (float): defaults to ``0.01``. normalize_advantage (bool): defaults to ``True``. normalize_advantage_exclude_dims (tuple of int): defaults to ``(-2,)``. **kwargs: forwarded to :class:`ClipPPOLoss`. """