147 lines
5.5 KiB
Python
147 lines
5.5 KiB
Python
import abc
|
|
from typing import Any, Dict
|
|
|
|
from ray.rllib.algorithms.ppo.ppo import (
|
|
LEARNER_RESULTS_CURR_ENTROPY_COEFF_KEY,
|
|
LEARNER_RESULTS_KL_KEY,
|
|
PPOConfig,
|
|
)
|
|
from ray.rllib.connectors.learner import (
|
|
AddOneTsToEpisodesAndTruncate,
|
|
GeneralAdvantageEstimation,
|
|
)
|
|
from ray.rllib.core.learner.learner import Learner
|
|
from ray.rllib.core.rl_module.apis.value_function_api import ValueFunctionAPI
|
|
from ray.rllib.utils.annotations import (
|
|
OverrideToImplementCustomLogic_CallToSuperRecommended,
|
|
override,
|
|
)
|
|
from ray.rllib.utils.lambda_defaultdict import LambdaDefaultDict
|
|
from ray.rllib.utils.metrics import (
|
|
NUM_ENV_STEPS_SAMPLED_LIFETIME,
|
|
)
|
|
from ray.rllib.utils.numpy import convert_to_numpy
|
|
from ray.rllib.utils.schedules.scheduler import Scheduler
|
|
from ray.rllib.utils.typing import ModuleID, TensorType
|
|
|
|
|
|
class PPOLearner(Learner):
|
|
@override(Learner)
|
|
def build(self) -> None:
|
|
super().build()
|
|
|
|
# Dict mapping module IDs to the respective entropy Scheduler instance.
|
|
self.entropy_coeff_schedulers_per_module: Dict[
|
|
ModuleID, Scheduler
|
|
] = LambdaDefaultDict(
|
|
lambda module_id: Scheduler(
|
|
fixed_value_or_schedule=(
|
|
self.config.get_config_for_module(module_id).entropy_coeff
|
|
),
|
|
framework=self.framework,
|
|
device=self._device,
|
|
)
|
|
)
|
|
|
|
# Set up KL coefficient variables (per module).
|
|
# Note that the KL coeff is not controlled by a Scheduler, but seeks
|
|
# to stay close to a given kl_target value.
|
|
self.curr_kl_coeffs_per_module: Dict[ModuleID, TensorType] = LambdaDefaultDict(
|
|
lambda module_id: self._get_tensor_variable(
|
|
self.config.get_config_for_module(module_id).kl_coeff
|
|
)
|
|
)
|
|
|
|
# Extend all episodes by one artificial timestep to allow the value function net
|
|
# to compute the bootstrap values (and add a mask to the batch to know, which
|
|
# slots to mask out).
|
|
if (
|
|
self._learner_connector is not None
|
|
and self.config.add_default_connectors_to_learner_pipeline
|
|
):
|
|
# Before anything, add one ts to each episode (and record this in the loss
|
|
# mask, so that the computations at this extra ts are not used to compute
|
|
# the loss).
|
|
self._learner_connector.prepend(AddOneTsToEpisodesAndTruncate())
|
|
# At the end of the pipeline (when the batch is already completed), add the
|
|
# GAE connector, which performs a vf forward pass, then computes the GAE
|
|
# computations, and puts the results of this (advantages, value targets)
|
|
# directly back in the batch. This is then the batch used for
|
|
# `forward_train` and `compute_losses`.
|
|
self._learner_connector.append(
|
|
GeneralAdvantageEstimation(
|
|
gamma=self.config.gamma, lambda_=self.config.lambda_
|
|
)
|
|
)
|
|
|
|
@override(Learner)
|
|
def remove_module(self, module_id: ModuleID, **kwargs):
|
|
marl_spec = super().remove_module(module_id, **kwargs)
|
|
|
|
self.entropy_coeff_schedulers_per_module.pop(module_id, None)
|
|
self.curr_kl_coeffs_per_module.pop(module_id, None)
|
|
|
|
return marl_spec
|
|
|
|
@OverrideToImplementCustomLogic_CallToSuperRecommended
|
|
@override(Learner)
|
|
def after_gradient_based_update(
|
|
self,
|
|
*,
|
|
timesteps: Dict[str, Any],
|
|
) -> None:
|
|
super().after_gradient_based_update(timesteps=timesteps)
|
|
|
|
for module_id, module in self.module._rl_modules.items():
|
|
config = self.config.get_config_for_module(module_id)
|
|
|
|
# Update entropy coefficient via our Scheduler.
|
|
new_entropy_coeff = self.entropy_coeff_schedulers_per_module[
|
|
module_id
|
|
].update(timestep=timesteps.get(NUM_ENV_STEPS_SAMPLED_LIFETIME, 0))
|
|
self.metrics.log_value(
|
|
(module_id, LEARNER_RESULTS_CURR_ENTROPY_COEFF_KEY),
|
|
new_entropy_coeff,
|
|
window=1,
|
|
)
|
|
if (
|
|
config.use_kl_loss
|
|
and (module_id, LEARNER_RESULTS_KL_KEY) in self.metrics
|
|
):
|
|
kl_loss = convert_to_numpy(
|
|
self.metrics.peek((module_id, LEARNER_RESULTS_KL_KEY))
|
|
)
|
|
self._update_module_kl_coeff(
|
|
module_id=module_id,
|
|
config=config,
|
|
kl_loss=kl_loss,
|
|
)
|
|
|
|
@classmethod
|
|
@override(Learner)
|
|
def rl_module_required_apis(cls) -> list[type]:
|
|
# In order for a PPOLearner to update an RLModule, it must implement the
|
|
# following APIs:
|
|
return [ValueFunctionAPI]
|
|
|
|
@abc.abstractmethod
|
|
def _update_module_kl_coeff(
|
|
self,
|
|
*,
|
|
module_id: ModuleID,
|
|
config: PPOConfig,
|
|
kl_loss: float,
|
|
) -> None:
|
|
"""Dynamically update the KL loss coefficients of each module.
|
|
|
|
The update is completed using the mean KL divergence between the action
|
|
distributions current policy and old policy of each module. That action
|
|
distribution is computed during the most recent update/call to `compute_loss`.
|
|
|
|
Args:
|
|
module_id: The module whose KL loss coefficient to update.
|
|
config: The AlgorithmConfig specific to the given `module_id`.
|
|
kl_loss: The mean KL loss of the module, computed inside
|
|
`compute_loss_for_module()`.
|
|
"""
|