chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
from ray.rllib.algorithms.impala.impala import (
IMPALA,
Impala,
IMPALAConfig,
ImpalaConfig,
)
from ray.rllib.algorithms.impala.impala_tf_policy import (
ImpalaTF1Policy,
ImpalaTF2Policy,
)
from ray.rllib.algorithms.impala.impala_torch_policy import ImpalaTorchPolicy
__all__ = [
"IMPALA",
"IMPALAConfig",
# @OldAPIStack
"ImpalaTF1Policy",
"ImpalaTF2Policy",
"ImpalaTorchPolicy",
# Deprecated names (lowercase)
"ImpalaConfig",
"Impala",
]
File diff suppressed because it is too large Load Diff
+632
View File
@@ -0,0 +1,632 @@
import atexit
import logging
import queue
import threading
import weakref
from queue import Queue
from typing import Any, Dict, List
import ray
from ray.rllib.algorithms.impala.impala import LEARNER_RESULTS_CURR_ENTROPY_COEFF_KEY
from ray.rllib.core import COMPONENT_RL_MODULE
from ray.rllib.core.learner.learner import Learner
from ray.rllib.core.learner.training_data import TrainingData
from ray.rllib.core.rl_module.apis import ValueFunctionAPI
from ray.rllib.utils.annotations import (
OverrideToImplementCustomLogic_CallToSuperRecommended,
override,
)
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.lambda_defaultdict import LambdaDefaultDict
from ray.rllib.utils.metrics import (
ALL_MODULES,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.rllib.utils.metrics.metrics_logger import MetricsLogger
from ray.rllib.utils.metrics.ray_metrics import (
DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
TimerAndPrometheusLogger,
)
from ray.rllib.utils.schedules.scheduler import Scheduler
from ray.rllib.utils.typing import ModuleID, ResultDict
from ray.util.metrics import Gauge, Histogram
logger = logging.getLogger(__name__)
torch, _ = try_import_torch()
GPU_LOADER_QUEUE_WAIT_TIMER = "gpu_loader_queue_wait_timer"
GPU_LOADER_LOAD_TO_GPU_TIMER = "gpu_loader_load_to_gpu_timer"
LEARNER_THREAD_IN_QUEUE_WAIT_TIMER = "learner_thread_in_queue_wait_timer"
LEARNER_THREAD_ENV_STEPS_DROPPED = "learner_thread_env_steps_dropped"
LEARNER_THREAD_UPDATE_TIMER = "learner_thread_update_timer"
RAY_GET_EPISODES_TIMER = "ray_get_episodes_timer"
QUEUE_SIZE_GPU_LOADER_QUEUE = "queue_size_gpu_loader_queue"
QUEUE_SIZE_LEARNER_THREAD_QUEUE = "queue_size_learner_thread_queue"
QUEUE_SIZE_RESULTS_QUEUE = "queue_size_results_queue"
# Aggregation cycle size.
BATCHES_PER_AGGREGATION = 10
# Stop sentinel for the `_LearnerThread`
_STOP_SENTINEL = object()
class IMPALALearner(Learner):
@override(Learner)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Ray metrics
self._metrics_learner_impala_update = Histogram(
name="rllib_learner_impala_update_time",
description="Time spent in the 'IMPALALearner.update()' method.",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_learner_impala_update.set_default_tags(
{"rllib": self.__class__.__name__}
)
self._metrics_learner_impala_update_solve_refs = Histogram(
name="rllib_learner_impala_update_solve_refs_time",
description="Time spent on resolving refs in the 'Learner.update()'",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_learner_impala_update_solve_refs.set_default_tags(
{"rllib": self.__class__.__name__}
)
self._metrics_learner_impala_update_make_batch_if_necessary = Histogram(
name="rllib_learner_impala_update_make_batch_if_necessary_time",
description="Time spent on making a batch in the 'Learner.update()'.",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_learner_impala_update_make_batch_if_necessary.set_default_tags(
{"rllib": self.__class__.__name__}
)
self._metrics_learner_impala_get_learner_state_time = Histogram(
name="rllib_learner_impala_get_learner_state_time",
description="Time spent on get_state() in IMPALALearner.update().",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_learner_impala_get_learner_state_time.set_default_tags(
{"rllib": self.__class__.__name__}
)
# Set the aggregation threshold to the broadcast interval. We return
# a state at the same time the metrics are aggregated.
global BATCHES_PER_AGGREGATION
BATCHES_PER_AGGREGATION = self.config.broadcast_interval
@override(Learner)
def build(self) -> None:
super().build()
# APPO/IMPALA require RLock for thread safety around metrics.
self.metrics._threading_lock = threading.RLock()
# Aggregation signaling (replaces condition-variable contention) ---
self._agg_event = threading.Event()
self._submitted_updates = 0 # producer-side counter (update thread(s))
self._num_updates = 0 # learner-side counter
self._num_updates_lock = threading.Lock()
# Set the update kwargs passed in the main thread for use in the learner thread.
self._update_kwargs = {}
self._model_io_lock = threading.RLock()
self._learner_state_queue = Queue(maxsize=1)
self._learner_state_lock = threading.Lock()
self._learner_state = None
# Dict mapping module IDs to entropy Scheduler instances.
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,
)
)
# Create queues as bounded queues to create real back-pressure & stabilize
# GPU memory usage.
# Small loader in-queue to keep threads busy without flooding.
# TODO (simon): Do extensive testing to find an optimal queue size.
loader_qsize = max(2, 10 * self.config.num_gpu_loader_threads)
# Note, we are passing now the timesteps dictionary through the queue.
self._gpu_loader_in_queue: "Queue[tuple[TrainingData, Dict[str, Any]]]" = Queue(
maxsize=loader_qsize
)
# Learner in-queue must be tiny. 1 strictly serializes GPU-resident batches.
# TODO (simon): Add a parameter to define queue size.
if not hasattr(self, "_learner_thread_in_queue"):
self._learner_thread_in_queue: "Queue[tuple[Any, Dict[str, Any]]]" = Queue(
maxsize=self.config.learner_queue_size
)
# Get the rank of this learner, if necessary.
self._rank: int = (
torch.distributed.get_rank() if torch.distributed.is_initialized() else 0
)
# Define the out-queue for the metrics from the `_LearnerThread`.
# TODO (simon): Add types for items.
self._learner_thread_out_queue: "Queue[Dict[str, Any]]" = Queue()
# Create and start `_GPULoaderThread`(s).
if self.config.num_gpus_per_learner > 0:
self._gpu_loader_threads: List[threading.Thread] = [
_GPULoaderThread(
in_queue=self._gpu_loader_in_queue,
out_queue=self._learner_thread_in_queue,
device=self._device,
metrics_logger=self.metrics,
)
for _ in range(self.config.num_gpu_loader_threads)
]
for t in self._gpu_loader_threads:
t.start()
# Create and start the `_LearnerThread`.
self._learner_thread: threading.Thread = _LearnerThread(
update_method=Learner.update,
in_queue=self._learner_thread_in_queue,
out_queue=self._learner_thread_out_queue,
learner=self,
)
self._learner_thread.start()
@override(Learner)
def update(
self,
training_data: TrainingData,
*,
timesteps: Dict[str, Any],
return_state: bool = False,
**kwargs,
) -> ResultDict:
"""
Args:
batch:
timesteps:
return_state: Whether to include one of the Learner worker's state from
after the update step in the returned results dict (under the
`_rl_module_state_after_update` key). Note that after an update, all
Learner workers' states should be identical, so we use the first
Learner's state here. Useful for avoiding an extra `get_weights()` call,
e.g. for synchronizing EnvRunner weights.
**kwargs:
Returns:
"""
# Set the update kwargs passed in the main thread for use in the learner thread.
self._update_kwargs = kwargs
with TimerAndPrometheusLogger(self._metrics_learner_impala_update):
# Get the train batch from the object store.
with TimerAndPrometheusLogger(
self._metrics_learner_impala_update_solve_refs
):
# Resolve object refs and ensure we have a proper batch object.
# TODO (simon): Check, if we can resolve the object references and
# run the pipeline on the GPULoaderThreads.
training_data.solve_refs()
with TimerAndPrometheusLogger(
self._metrics_learner_impala_update_make_batch_if_necessary
):
batch = self._make_batch_if_necessary(training_data=training_data)
assert batch is not None
# Enqeue the batch (bounded backpressure).
if self.config.num_gpus_per_learner > 0:
# Pass timesteps alongside batch (no globals).
self._gpu_loader_in_queue.put((batch, timesteps))
# Only occasionally log loader queue size.
if (self._submitted_updates & 0xFF) == 0:
self.metrics.log_value(
(ALL_MODULES, QUEUE_SIZE_GPU_LOADER_QUEUE),
self._gpu_loader_in_queue.qsize(),
window=1,
)
# TODO (simon): Check, if we want to get here stats from the
# RingBuffer.
else:
# No GPU loader: directly enqueue to learner queue.
_LearnerThread.enqueue(
self._learner_thread_in_queue, (batch, timesteps), self.metrics
)
# Return the module state, if requested and available.
if return_state:
try:
with self._learner_state_lock:
self._learner_state = self._learner_state_queue.get_nowait()
except queue.Empty:
logger.debug("No learner state available in the queue yet.")
# Every 20th block call we submit results. Otherwise we keep the
# thread running without interruption to avoid thread contention.
self._submitted_updates += 1
if (self._submitted_updates % BATCHES_PER_AGGREGATION) != 0:
result = {}
if return_state and self._learner_state:
result["_rl_module_state_after_update"] = self._learner_state
return result
# Result submission: wait until learner finished BATCHES_PER_AGGREGATION updates (blocking).
self._agg_event.wait()
# Reset the aggregation event to keep the `_LearnerThread` running.
self._agg_event.clear()
if self._learner_thread_out_queue:
try:
result = self._learner_thread_out_queue.get(timeout=0.001)
except queue.Empty:
result = {}
# Return the module state, if requested and existent.
if return_state and self._learner_state:
result["_rl_module_state_after_update"] = self._learner_state
return result
@OverrideToImplementCustomLogic_CallToSuperRecommended
def before_gradient_based_update(self, *, timesteps: Dict[str, Any]) -> None:
super().before_gradient_based_update(timesteps=timesteps)
for module_id in self.module.keys():
# 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,
)
@override(Learner)
def remove_module(self, module_id: str):
super().remove_module(module_id)
self.entropy_coeff_schedulers_per_module.pop(module_id)
@override(Learner)
def shutdown(self) -> None:
# Stop the learner thread deterministically: setting the stop event
# and enqueuing a sentinel wakes the consumer if it's blocked on
# `_in_queue.get()`. Then `join` ensures it has fully exited before
# we return, so any subsequent `ray.shutdown()`/interpreter teardown
# can't race with the daemon thread.
thread = getattr(self, "_learner_thread", None)
if thread is not None and thread.is_alive():
thread.request_stop()
thread.join(timeout=5.0)
@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]
ImpalaLearner = IMPALALearner
class _GPULoaderThread(threading.Thread):
def __init__(
self,
*,
in_queue: "Queue[tuple[TrainingData, Dict[str, Any]]]",
out_queue: "Queue[tuple[Any, Dict[str, Any]]]",
device: "torch.device",
metrics_logger: MetricsLogger,
):
super().__init__(name="_GPULoaderThread")
self.daemon = True
self._in_queue = in_queue
self._out_queue = out_queue
self._device = device
self.metrics = metrics_logger
# Use a single CUDA stream for each loader thread.
self._use_cuda_stream = (
torch is not None
and hasattr(torch, "cuda")
and device is not None
and getattr(device, "type", None) == "cuda"
)
self._stream = (
torch.cuda.Stream(device=self._device) if self._use_cuda_stream else None
)
self._metrics_impala_gpu_loader_thread_step_time = Histogram(
name="rllib_learner_impala_gpu_loader_thread_step_time",
description="Time taken in seconds for gpu loader thread _step.",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_impala_gpu_loader_thread_step_time.set_default_tags(
{"rllib": "IMPALA/GPULoaderThread"}
)
self._metrics_impala_gpu_loader_thread_step_in_queue_get_time = Histogram(
name="rllib_learner_impala_gpu_loader_thread_step_get_time",
description="Time taken in seconds for gpu loader thread _step _in_queue.get().",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_impala_gpu_loader_thread_step_in_queue_get_time.set_default_tags(
{"rllib": "IMPALA/GPULoaderThread"}
)
self._metrics_impala_gpu_loader_thread_step_load_to_gpu_time = Histogram(
name="rllib_learner_impala_gpu_loader_thread_step_load_to_gpu_time",
description="Time taken in seconds for GPU loader thread _step to load batch to GPU.",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_impala_gpu_loader_thread_step_load_to_gpu_time.set_default_tags(
{"rllib": "IMPALA/GPULoaderThread"}
)
self._metrics_impala_gpu_loader_thread_in_qsize_beginning_of_step = Gauge(
name="rllib_impala_gpu_loader_thread_in_qsize_beginning_of_step",
description="Size of the _GPULoaderThread in-queue size, at the beginning of the step.",
tag_keys=("rllib",),
)
self._metrics_impala_gpu_loader_thread_in_qsize_beginning_of_step.set_default_tags(
{"rllib": "IMPALA/GPULoaderThread"}
)
# Robust pinned-memory copy: fall back if batch contains CUDA tensors already.
# TODO (simon): Find a more compliant solution.
def _to_device_safe(self, batch):
try:
return batch.to_device(self._device, pin_memory=True)
except RuntimeError as e:
msg = str(e)
if "only dense CPU tensors can be pinned" in msg or "pin_memory" in msg:
return batch.to_device(self._device, pin_memory=False)
raise
def run(self) -> None:
while True:
with TimerAndPrometheusLogger(
self._metrics_impala_gpu_loader_thread_step_time
):
self._step()
def _step(self) -> None:
self._metrics_impala_gpu_loader_thread_in_qsize_beginning_of_step.set(
value=self._in_queue.qsize()
)
# Get a new batch (CPU) and the global timesteps from the loader in--queue (blocking).
with self.metrics.log_time((ALL_MODULES, GPU_LOADER_QUEUE_WAIT_TIMER)):
with TimerAndPrometheusLogger(
self._metrics_impala_gpu_loader_thread_step_in_queue_get_time
):
ma_batch_on_cpu, timesteps = self._in_queue.get()
# Load the batch onto the GPU device; enable pinned memory for async copies.
with self.metrics.log_time((ALL_MODULES, GPU_LOADER_LOAD_TO_GPU_TIMER)):
if self._use_cuda_stream and self._stream is not None:
# Issue copies on a non-default stream so they can overlap with compute.
with torch.cuda.stream(self._stream):
ma_batch_on_gpu = self._to_device_safe(ma_batch_on_cpu)
# TODO (simon): Maybe use the `use_stream` in `convert_to_tensor`.
# No explicit synching here. Consumer will naturally serialize when needed.
else:
ma_batch_on_gpu = self._to_device_safe(ma_batch_on_cpu)
# Enqueue to Learner threads in-queue (GPU-resident batch and global timesteps).
_LearnerThread.enqueue(
self._out_queue, (ma_batch_on_gpu, timesteps), self.metrics
)
class _LearnerThread(threading.Thread):
def __init__(
self,
*,
update_method,
in_queue: "Queue[tuple[Any, Dict[str, Any]]]",
out_queue: "Queue[Dict[str, Any]]",
learner: IMPALALearner,
):
super().__init__(name="_LearnerThread")
self.daemon = True
self.learner = learner
self._update_method = update_method
# Note, we pass now the timesteps dictionary through the queue.
self._in_queue: "queue.Queue[tuple[Any, Dict[str, Any]]]" = in_queue
# TODO (simon): Type hints.
self._out_queue = out_queue
self._stop_event = threading.Event()
# Ray metrics
self._metrics_learner_impala_thread_step = Histogram(
name="rllib_learner_impala_learner_thread_step_time",
description="Time taken in seconds for learner thread _step.",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_learner_impala_thread_step.set_default_tags(
{"rllib": "IMPALA/LearnerThread"}
)
self._metrics_learner_impala_thread_step_update = Histogram(
name="rllib_learner_impala_learner_thread_step_update_time",
description="Time taken in seconds for learner thread _step update.",
boundaries=DEFAULT_HISTOGRAM_BOUNDARIES_SHORT_EVENTS,
tag_keys=("rllib",),
)
self._metrics_learner_impala_thread_step_update.set_default_tags(
{"rllib": "IMPALA/LearnerThread"}
)
# Stop cleanly at interpreter shutdown so the daemon thread doesn't
# get killed mid-call inside an auto_init-wrapped Ray API (which
# would otherwise trigger e.g. `start_reaper` -> `preexec_fn not
# supported at interpreter shutdown`). Use a weakref so this hook
# doesn't pin the thread (and therefore the Learner) alive.
weak_self = weakref.ref(self)
def _request_stop_at_exit():
t = weak_self()
if t is not None:
t.request_stop()
atexit.register(_request_stop_at_exit)
# Keeps compatibility, but thread-safe.
@property
def stopped(self) -> bool:
return self._stop_event.is_set()
# Call this to stop the thread and wake it if it's blocked on .get()
def request_stop(self) -> None:
self._stop_event.set()
# Wake the consumer if it's blocked on an empty queue
try:
self._in_queue.put_nowait(_STOP_SENTINEL)
except queue.Full:
# If the queue is full, the consumer will wake soon anyway.
logger.warning(
"_LearnerThread.request_stop(): in_queue is full; cannot enqueue stop sentinel."
)
def run(self) -> None:
while True:
# Returns always `True` until stop-signal/sentinel is sent.
if not self.step():
break
def step(self) -> bool:
# Get a batch and wait, if the input queue is empty (blocking; no polling).
with self.learner.metrics.log_time(
(ALL_MODULES, LEARNER_THREAD_IN_QUEUE_WAIT_TIMER)
):
item = self._in_queue.get()
# Handle the stop/sentinel signal(s).
# TODO (simon): Check, if we need `None` for belt-and-suspenders/comp.
if item is _STOP_SENTINEL or self.stopped:
try:
self._in_queue.task_done()
except Exception:
logger.warning(
"_LearnerThread._in_queue.task_done() failed during stop handling."
)
# Signal `run` to exit.
return False
# Extract the multi-agent batch and the timesteps dictionary.
ma_batch_on_gpu, timesteps = item
# Update the `RLModule`, but do not reduce metrics.
with self.learner.metrics.log_time((ALL_MODULES, LEARNER_THREAD_UPDATE_TIMER)):
with TimerAndPrometheusLogger(
self._metrics_learner_impala_thread_step_update
):
self._update_method(
self=self.learner,
training_data=TrainingData(batch=ma_batch_on_gpu),
timesteps=timesteps,
_no_metrics_reduce=True,
# Include the learner update kwargs set in the main thread.
**self.learner._update_kwargs,
)
# Signal queue done (unblocks producers put when bounded)
try:
self._in_queue.task_done()
finally:
# Set the Aggregation counter and signal this event (atomic).
with self.learner._num_updates_lock:
self.learner._num_updates += 1
# Check, if we need to aggregate.
do_agg = self.learner._num_updates == BATCHES_PER_AGGREGATION
if do_agg:
# Reset the update counter inside the lock.
self.learner._num_updates = 0
# If we need to aggregate, reduce metrics and queue them.
if do_agg:
# If in multi-learner setup, safeguard state retrieval within barriers.
if torch.distributed.is_initialized():
torch.distributed.barrier()
# Only the first rank retrieves the state.
if self.learner._rank == 0:
with self.learner._model_io_lock, torch.inference_mode():
learner_state = self.learner.get_state(
# Only return the state of those RLModules that are trainable.
components=[
COMPONENT_RL_MODULE + "/" + mid
for mid in self.learner.module.keys()
if self.learner.should_module_be_updated(mid)
],
inference_only=True,
)
learner_state[COMPONENT_RL_MODULE] = ray.put(
learner_state[COMPONENT_RL_MODULE]
)
try:
if (self.learner._submitted_updates & ~0xFF) != (
(self.learner._submitted_updates - BATCHES_PER_AGGREGATION)
& ~0xFF
):
with self.learner._learner_state_lock:
self.learner.metrics.log_value(
(ALL_MODULES, "learner_thread_state_queue_size"),
self.learner._learner_state_queue.qsize(),
window=1,
)
# Remove any old learner state in the queue.
self.learner._learner_state_queue.get_nowait()
except queue.Empty:
logger.debug("No old learner state to remove from the queue.")
# Pass the learner state into the queue to the main process.
self.learner._learner_state_queue.put_nowait(learner_state)
self.learner.metrics.log_value(
(ALL_MODULES, "learner_thread_out_queue_size"),
self._out_queue.qsize(),
window=1,
)
# Reduce metrics and pass them into the queue for the main process.
self._out_queue.put(self.learner.metrics.reduce())
# Notify all listeners that aggregation is done and results can be
# retrieved.
self.learner._agg_event.set()
if torch.distributed.is_initialized():
torch.distributed.barrier()
# Keep running (see `run` method).
return True
@staticmethod
def enqueue(
learner_queue: "queue.Queue[tuple[Any, Dict[str, Any]]]",
batch_with_ts,
metrics: MetricsLogger,
):
# Put the batch into the queue (blocking if thread is updating).
learner_queue.put(batch_with_ts, block=True)
+449
View File
@@ -0,0 +1,449 @@
"""Adapted from A3CTFPolicy to add V-trace.
Keep in sync with changes to A3CTFPolicy and VtraceSurrogatePolicy."""
import logging
from typing import Dict, List, Optional, Type, Union
import gymnasium as gym
import numpy as np
from ray.rllib.algorithms.impala import vtrace_tf as vtrace
from ray.rllib.evaluation.postprocessing import compute_bootstrap_value
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.models.tf.tf_action_dist import Categorical, TFActionDistribution
from ray.rllib.policy.dynamic_tf_policy_v2 import DynamicTFPolicyV2
from ray.rllib.policy.eager_tf_policy_v2 import EagerTFPolicyV2
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.policy.tf_mixins import (
EntropyCoeffSchedule,
GradStatsMixin,
LearningRateSchedule,
ValueNetworkMixin,
)
from ray.rllib.utils import force_list
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf
from ray.rllib.utils.tf_utils import explained_variance
from ray.rllib.utils.typing import (
LocalOptimizer,
ModelGradients,
TensorType,
TFPolicyV2Type,
)
tf1, tf, tfv = try_import_tf()
logger = logging.getLogger(__name__)
class VTraceLoss:
def __init__(
self,
actions,
actions_logp,
actions_entropy,
dones,
behaviour_action_logp,
behaviour_logits,
target_logits,
discount,
rewards,
values,
bootstrap_value,
dist_class,
model,
valid_mask,
config,
vf_loss_coeff=0.5,
entropy_coeff=0.01,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
):
"""Policy gradient loss with vtrace importance weighting.
VTraceLoss takes tensors of shape [T, B, ...], where `B` is the
batch_size. The reason we need to know `B` is for V-trace to properly
handle episode cut boundaries.
Args:
actions: An int|float32 tensor of shape [T, B, ACTION_SPACE].
actions_logp: A float32 tensor of shape [T, B].
actions_entropy: A float32 tensor of shape [T, B].
dones: A bool tensor of shape [T, B].
behaviour_action_logp: Tensor of shape [T, B].
behaviour_logits: A list with length of ACTION_SPACE of float32
tensors of shapes
[T, B, ACTION_SPACE[0]],
...,
[T, B, ACTION_SPACE[-1]]
target_logits: A list with length of ACTION_SPACE of float32
tensors of shapes
[T, B, ACTION_SPACE[0]],
...,
[T, B, ACTION_SPACE[-1]]
discount: A float32 scalar.
rewards: A float32 tensor of shape [T, B].
values: A float32 tensor of shape [T, B].
bootstrap_value: A float32 tensor of shape [B].
dist_class: action distribution class for logits.
valid_mask: A bool tensor of valid RNN input elements (#2992).
config: Algorithm config dict.
"""
# Compute vtrace on the CPU for better performance.
with tf.device("/cpu:0"):
self.vtrace_returns = vtrace.multi_from_logits(
behaviour_action_log_probs=behaviour_action_logp,
behaviour_policy_logits=behaviour_logits,
target_policy_logits=target_logits,
actions=tf.unstack(actions, axis=2),
discounts=tf.cast(~tf.cast(dones, tf.bool), tf.float32) * discount,
rewards=rewards,
values=values,
bootstrap_value=bootstrap_value,
dist_class=dist_class,
model=model,
clip_rho_threshold=tf.cast(clip_rho_threshold, tf.float32),
clip_pg_rho_threshold=tf.cast(clip_pg_rho_threshold, tf.float32),
)
self.value_targets = self.vtrace_returns.vs
# The policy gradients loss.
masked_pi_loss = tf.boolean_mask(
actions_logp * self.vtrace_returns.pg_advantages, valid_mask
)
self.pi_loss = -tf.reduce_sum(masked_pi_loss)
self.mean_pi_loss = -tf.reduce_mean(masked_pi_loss)
# The baseline loss.
delta = tf.boolean_mask(values - self.vtrace_returns.vs, valid_mask)
delta_squarred = tf.math.square(delta)
self.vf_loss = 0.5 * tf.reduce_sum(delta_squarred)
self.mean_vf_loss = 0.5 * tf.reduce_mean(delta_squarred)
# The entropy loss.
masked_entropy = tf.boolean_mask(actions_entropy, valid_mask)
self.entropy = tf.reduce_sum(masked_entropy)
self.mean_entropy = tf.reduce_mean(masked_entropy)
# The summed weighted loss.
self.total_loss = self.pi_loss - self.entropy * entropy_coeff
# Optional vf loss (or in a separate term due to separate
# optimizers/networks).
self.loss_wo_vf = self.total_loss
if not config["_separate_vf_optimizer"]:
self.total_loss += self.vf_loss * vf_loss_coeff
def _make_time_major(policy, seq_lens, tensor):
"""Swaps batch and trajectory axis.
Args:
policy: Policy reference
seq_lens: Sequence lengths if recurrent or None
tensor: A tensor or list of tensors to reshape.
trajectory item.
Returns:
res: A tensor with swapped axes or a list of tensors with
swapped axes.
"""
if isinstance(tensor, list):
return [_make_time_major(policy, seq_lens, t) for t in tensor]
if policy.is_recurrent():
B = tf.shape(seq_lens)[0]
T = tf.shape(tensor)[0] // B
else:
# Important: chop the tensor into batches at known episode cut
# boundaries.
# TODO: (sven) this is kind of a hack and won't work for
# batch_mode=complete_episodes.
T = policy.config["rollout_fragment_length"]
B = tf.shape(tensor)[0] // T
rs = tf.reshape(tensor, tf.concat([[B, T], tf.shape(tensor)[1:]], axis=0))
# swap B and T axes
res = tf.transpose(rs, [1, 0] + list(range(2, 1 + int(tf.shape(tensor).shape[0]))))
return res
class VTraceClipGradients:
"""VTrace version of gradient computation logic."""
def __init__(self):
"""No special initialization required."""
pass
def compute_gradients_fn(
self, optimizer: LocalOptimizer, loss: TensorType
) -> ModelGradients:
# Supporting more than one loss/optimizer.
trainable_variables = self.model.trainable_variables()
if self.config["_tf_policy_handles_more_than_one_loss"]:
optimizers = force_list(optimizer)
losses = force_list(loss)
assert len(optimizers) == len(losses)
clipped_grads_and_vars = []
for optim, loss_ in zip(optimizers, losses):
grads_and_vars = optim.compute_gradients(loss_, trainable_variables)
clipped_g_and_v = []
for g, v in grads_and_vars:
if g is not None:
clipped_g, _ = tf.clip_by_global_norm(
[g], self.config["grad_clip"]
)
clipped_g_and_v.append((clipped_g[0], v))
clipped_grads_and_vars.append(clipped_g_and_v)
self.grads = [g for g_and_v in clipped_grads_and_vars for (g, v) in g_and_v]
# Only one optimizer and and loss term.
else:
grads_and_vars = optimizer.compute_gradients(
loss, self.model.trainable_variables()
)
grads = [g for (g, v) in grads_and_vars]
self.grads, _ = tf.clip_by_global_norm(grads, self.config["grad_clip"])
clipped_grads_and_vars = list(zip(self.grads, trainable_variables))
return clipped_grads_and_vars
class VTraceOptimizer:
"""Optimizer function for VTrace policies."""
def __init__(self):
pass
# TODO: maybe standardize this function, so the choice of optimizers are more
# predictable for common algorithms.
def optimizer(
self,
) -> Union["tf.keras.optimizers.Optimizer", List["tf.keras.optimizers.Optimizer"]]:
config = self.config
if config["opt_type"] == "adam":
if config["framework"] == "tf2":
optim = tf.keras.optimizers.Adam(self.cur_lr)
if config["_separate_vf_optimizer"]:
return optim, tf.keras.optimizers.Adam(config["_lr_vf"])
else:
optim = tf1.train.AdamOptimizer(self.cur_lr)
if config["_separate_vf_optimizer"]:
return optim, tf1.train.AdamOptimizer(config["_lr_vf"])
else:
if config["_separate_vf_optimizer"]:
raise ValueError(
"RMSProp optimizer not supported for separate"
"vf- and policy losses yet! Set `opt_type=adam`"
)
if tfv == 2:
optim = tf.keras.optimizers.RMSprop(
self.cur_lr, config["decay"], config["momentum"], config["epsilon"]
)
else:
optim = tf1.train.RMSPropOptimizer(
self.cur_lr, config["decay"], config["momentum"], config["epsilon"]
)
return optim
# We need this builder function because we want to share the same
# custom logics between TF1 dynamic and TF2 eager policies.
def get_impala_tf_policy(name: str, base: TFPolicyV2Type) -> TFPolicyV2Type:
"""Construct an ImpalaTFPolicy inheriting either dynamic or eager base policies.
Args:
base: Base class for this policy. DynamicTFPolicyV2 or EagerTFPolicyV2.
Returns:
A TF Policy to be used with Impala.
"""
# VTrace mixins are placed in front of more general mixins to make sure
# their functions like optimizer() overrides all the other implementations
# (e.g., LearningRateSchedule.optimizer())
class ImpalaTFPolicy(
VTraceClipGradients,
VTraceOptimizer,
LearningRateSchedule,
EntropyCoeffSchedule,
GradStatsMixin,
ValueNetworkMixin,
base,
):
def __init__(
self,
observation_space,
action_space,
config,
existing_model=None,
existing_inputs=None,
):
# First thing first, enable eager execution if necessary.
base.enable_eager_execution_if_necessary()
# Initialize base class.
base.__init__(
self,
observation_space,
action_space,
config,
existing_inputs=existing_inputs,
existing_model=existing_model,
)
ValueNetworkMixin.__init__(self, config)
# If Learner API is used, we don't need any loss-specific mixins.
# However, we also would like to avoid creating special Policy-subclasses
# for this as the entire Policy concept will soon not be used anymore with
# the new Learner- and RLModule APIs.
GradStatsMixin.__init__(self)
VTraceClipGradients.__init__(self)
VTraceOptimizer.__init__(self)
LearningRateSchedule.__init__(self, config["lr"], config["lr_schedule"])
EntropyCoeffSchedule.__init__(
self, config["entropy_coeff"], config["entropy_coeff_schedule"]
)
# Note: this is a bit ugly, but loss and optimizer initialization must
# happen after all the MixIns are initialized.
self.maybe_initialize_optimizer_and_loss()
@override(base)
def loss(
self,
model: Union[ModelV2, "tf.keras.Model"],
dist_class: Type[TFActionDistribution],
train_batch: SampleBatch,
) -> Union[TensorType, List[TensorType]]:
model_out, _ = model(train_batch)
action_dist = dist_class(model_out, model)
if isinstance(self.action_space, gym.spaces.Discrete):
is_multidiscrete = False
output_hidden_shape = [self.action_space.n]
elif isinstance(self.action_space, gym.spaces.MultiDiscrete):
is_multidiscrete = True
output_hidden_shape = self.action_space.nvec.astype(np.int32)
else:
is_multidiscrete = False
output_hidden_shape = 1
def make_time_major(*args, **kw):
return _make_time_major(
self, train_batch.get(SampleBatch.SEQ_LENS), *args, **kw
)
actions = train_batch[SampleBatch.ACTIONS]
dones = train_batch[SampleBatch.TERMINATEDS]
rewards = train_batch[SampleBatch.REWARDS]
behaviour_action_logp = train_batch[SampleBatch.ACTION_LOGP]
behaviour_logits = train_batch[SampleBatch.ACTION_DIST_INPUTS]
unpacked_behaviour_logits = tf.split(
behaviour_logits, output_hidden_shape, axis=1
)
unpacked_outputs = tf.split(model_out, output_hidden_shape, axis=1)
values = model.value_function()
values_time_major = make_time_major(values)
bootstrap_values_time_major = make_time_major(
train_batch[SampleBatch.VALUES_BOOTSTRAPPED]
)
bootstrap_value = bootstrap_values_time_major[-1]
if self.is_recurrent():
max_seq_len = tf.reduce_max(train_batch[SampleBatch.SEQ_LENS])
mask = tf.sequence_mask(train_batch[SampleBatch.SEQ_LENS], max_seq_len)
mask = tf.reshape(mask, [-1])
else:
mask = tf.ones_like(rewards)
# Prepare actions for loss
loss_actions = (
actions if is_multidiscrete else tf.expand_dims(actions, axis=1)
)
# Inputs are reshaped from [B * T] => [(T|T-1), B] for V-trace calc.
self.vtrace_loss = VTraceLoss(
actions=make_time_major(loss_actions),
actions_logp=make_time_major(action_dist.logp(actions)),
actions_entropy=make_time_major(action_dist.multi_entropy()),
dones=make_time_major(dones),
behaviour_action_logp=make_time_major(behaviour_action_logp),
behaviour_logits=make_time_major(unpacked_behaviour_logits),
target_logits=make_time_major(unpacked_outputs),
discount=self.config["gamma"],
rewards=make_time_major(rewards),
values=values_time_major,
bootstrap_value=bootstrap_value,
dist_class=Categorical if is_multidiscrete else dist_class,
model=model,
valid_mask=make_time_major(mask),
config=self.config,
vf_loss_coeff=self.config["vf_loss_coeff"],
entropy_coeff=self.entropy_coeff,
clip_rho_threshold=self.config["vtrace_clip_rho_threshold"],
clip_pg_rho_threshold=self.config["vtrace_clip_pg_rho_threshold"],
)
if self.config.get("_separate_vf_optimizer"):
return self.vtrace_loss.loss_wo_vf, self.vtrace_loss.vf_loss
else:
return self.vtrace_loss.total_loss
@override(base)
def stats_fn(self, train_batch: SampleBatch) -> Dict[str, TensorType]:
values_batched = _make_time_major(
self,
train_batch.get(SampleBatch.SEQ_LENS),
self.model.value_function(),
)
return {
"cur_lr": tf.cast(self.cur_lr, tf.float64),
"policy_loss": self.vtrace_loss.mean_pi_loss,
"entropy": self.vtrace_loss.mean_entropy,
"entropy_coeff": tf.cast(self.entropy_coeff, tf.float64),
"var_gnorm": tf.linalg.global_norm(self.model.trainable_variables()),
"vf_loss": self.vtrace_loss.mean_vf_loss,
"vf_explained_var": explained_variance(
tf.reshape(self.vtrace_loss.value_targets, [-1]),
tf.reshape(values_batched, [-1]),
),
}
@override(base)
def postprocess_trajectory(
self,
sample_batch: SampleBatch,
other_agent_batches: Optional[SampleBatch] = None,
episode=None,
):
# Call super's postprocess_trajectory first.
# sample_batch = super().postprocess_trajectory(
# sample_batch, other_agent_batches, episode
# )
if self.config["vtrace"]:
# Add the SampleBatch.VALUES_BOOTSTRAPPED column, which we'll need
# inside the loss for vtrace calculations.
sample_batch = compute_bootstrap_value(sample_batch, self)
return sample_batch
@override(base)
def get_batch_divisibility_req(self) -> int:
return self.config["rollout_fragment_length"]
ImpalaTFPolicy.__name__ = name
ImpalaTFPolicy.__qualname__ = name
return ImpalaTFPolicy
ImpalaTF1Policy = get_impala_tf_policy("ImpalaTF1Policy", DynamicTFPolicyV2)
ImpalaTF2Policy = get_impala_tf_policy("ImpalaTF2Policy", EagerTFPolicyV2)
@@ -0,0 +1,425 @@
import logging
from typing import Dict, List, Optional, Type, Union
import gymnasium as gym
import numpy as np
import ray
from ray.rllib.evaluation.postprocessing import compute_bootstrap_value
from ray.rllib.models.action_dist import ActionDistribution
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.models.torch.torch_action_dist import TorchCategorical
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.policy.torch_mixins import (
EntropyCoeffSchedule,
LearningRateSchedule,
ValueNetworkMixin,
)
from ray.rllib.policy.torch_policy_v2 import TorchPolicyV2
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.numpy import convert_to_numpy
from ray.rllib.utils.torch_utils import (
apply_grad_clipping,
explained_variance,
global_norm,
sequence_mask,
)
from ray.rllib.utils.typing import TensorType
torch, nn = try_import_torch()
logger = logging.getLogger(__name__)
class VTraceLoss:
def __init__(
self,
actions,
actions_logp,
actions_entropy,
dones,
behaviour_action_logp,
behaviour_logits,
target_logits,
discount,
rewards,
values,
bootstrap_value,
dist_class,
model,
valid_mask,
config,
vf_loss_coeff=0.5,
entropy_coeff=0.01,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
):
"""Policy gradient loss with vtrace importance weighting.
VTraceLoss takes tensors of shape [T, B, ...], where `B` is the
batch_size. The reason we need to know `B` is for V-trace to properly
handle episode cut boundaries.
Args:
actions: An int|float32 tensor of shape [T, B, ACTION_SPACE].
actions_logp: A float32 tensor of shape [T, B].
actions_entropy: A float32 tensor of shape [T, B].
dones: A bool tensor of shape [T, B].
behaviour_action_logp: Tensor of shape [T, B].
behaviour_logits: A list with length of ACTION_SPACE of float32
tensors of shapes
[T, B, ACTION_SPACE[0]],
...,
[T, B, ACTION_SPACE[-1]]
target_logits: A list with length of ACTION_SPACE of float32
tensors of shapes
[T, B, ACTION_SPACE[0]],
...,
[T, B, ACTION_SPACE[-1]]
discount: A float32 scalar.
rewards: A float32 tensor of shape [T, B].
values: A float32 tensor of shape [T, B].
bootstrap_value: A float32 tensor of shape [B].
dist_class: action distribution class for logits.
valid_mask: A bool tensor of valid RNN input elements (#2992).
config: Algorithm config dict.
"""
import ray.rllib.algorithms.impala.vtrace_torch as vtrace
if valid_mask is None:
valid_mask = torch.ones_like(actions_logp)
# Compute vtrace on the CPU for better perf
# (devices handled inside `vtrace.multi_from_logits`).
device = behaviour_action_logp[0].device
self.vtrace_returns = vtrace.multi_from_logits(
behaviour_action_log_probs=behaviour_action_logp,
behaviour_policy_logits=behaviour_logits,
target_policy_logits=target_logits,
actions=torch.unbind(actions, dim=2),
discounts=(1.0 - dones.float()) * discount,
rewards=rewards,
values=values,
bootstrap_value=bootstrap_value,
dist_class=dist_class,
model=model,
clip_rho_threshold=clip_rho_threshold,
clip_pg_rho_threshold=clip_pg_rho_threshold,
)
# Move v-trace results back to GPU for actual loss computing.
self.value_targets = self.vtrace_returns.vs.to(device)
# The policy gradients loss.
self.pi_loss = -torch.sum(
actions_logp * self.vtrace_returns.pg_advantages.to(device) * valid_mask
)
# The baseline loss.
delta = (values - self.value_targets) * valid_mask
self.vf_loss = 0.5 * torch.sum(torch.pow(delta, 2.0))
# The entropy loss.
self.entropy = torch.sum(actions_entropy * valid_mask)
self.mean_entropy = self.entropy / torch.sum(valid_mask)
# The summed weighted loss.
self.total_loss = self.pi_loss - self.entropy * entropy_coeff
# Optional vf loss (or in a separate term due to separate
# optimizers/networks).
self.loss_wo_vf = self.total_loss
if not config["_separate_vf_optimizer"]:
self.total_loss += self.vf_loss * vf_loss_coeff
def make_time_major(policy, seq_lens, tensor):
"""Swaps batch and trajectory axis.
Args:
policy: Policy reference
seq_lens: Sequence lengths if recurrent or None
tensor: A tensor or list of tensors to reshape.
Returns:
res: A tensor with swapped axes or a list of tensors with
swapped axes.
"""
if isinstance(tensor, (list, tuple)):
return [make_time_major(policy, seq_lens, t) for t in tensor]
if policy.is_recurrent():
B = seq_lens.shape[0]
T = tensor.shape[0] // B
else:
# Important: chop the tensor into batches at known episode cut
# boundaries.
# TODO: (sven) this is kind of a hack and won't work for
# batch_mode=complete_episodes.
T = policy.config["rollout_fragment_length"]
B = tensor.shape[0] // T
rs = torch.reshape(tensor, [B, T] + list(tensor.shape[1:]))
# Swap B and T axes.
res = torch.transpose(rs, 1, 0)
return res
class VTraceOptimizer:
"""Optimizer function for VTrace torch policies."""
def __init__(self):
pass
def optimizer(
self,
) -> Union[List["torch.optim.Optimizer"], "torch.optim.Optimizer"]:
if self.config["_separate_vf_optimizer"]:
# Figure out, which parameters of the model belong to the value
# function (and which to the policy net).
dummy_batch = self._lazy_tensor_dict(
self._get_dummy_batch_from_view_requirements()
)
# Zero out all gradients (set to None)
for param in self.model.parameters():
param.grad = None
# Perform a dummy forward pass (through the policy net, which should be
# separated from the value function in this particular user setup).
out = self.model(dummy_batch)
# Perform a (dummy) backward pass to be able to see, which params have
# gradients and are therefore used for the policy computations (vs vf
# computations).
torch.sum(out[0]).backward() # [0] -> Model returns out and state-outs.
# Collect policy vs value function params separately.
policy_params = []
value_params = []
for param in self.model.parameters():
if param.grad is None:
value_params.append(param)
else:
policy_params.append(param)
if self.config["opt_type"] == "adam":
return (
torch.optim.Adam(params=policy_params, lr=self.cur_lr),
torch.optim.Adam(params=value_params, lr=self.cur_lr2),
)
else:
raise NotImplementedError
if self.config["opt_type"] == "adam":
return torch.optim.Adam(params=self.model.parameters(), lr=self.cur_lr)
else:
return torch.optim.RMSprop(
params=self.model.parameters(),
lr=self.cur_lr,
weight_decay=self.config["decay"],
momentum=self.config["momentum"],
eps=self.config["epsilon"],
)
# VTrace mixins are placed in front of more general mixins to make sure
# their functions like optimizer() overrides all the other implementations
# (e.g., LearningRateSchedule.optimizer())
class ImpalaTorchPolicy(
VTraceOptimizer,
LearningRateSchedule,
EntropyCoeffSchedule,
ValueNetworkMixin,
TorchPolicyV2,
):
"""PyTorch policy class used with IMPALA."""
def __init__(self, observation_space, action_space, config):
config = dict(
ray.rllib.algorithms.impala.impala.IMPALAConfig().to_dict(), **config
)
config["enable_rl_module_and_learner"] = False
config["enable_env_runner_and_connector_v2"] = False
# If Learner API is used, we don't need any loss-specific mixins.
# However, we also would like to avoid creating special Policy-subclasses
# for this as the entire Policy concept will soon not be used anymore with
# the new Learner- and RLModule APIs.
VTraceOptimizer.__init__(self)
# Need to initialize learning rate variable before calling
# TorchPolicyV2.__init__.
lr_schedule_additional_args = []
if config.get("_separate_vf_optimizer"):
lr_schedule_additional_args = (
[config["_lr_vf"][0][1], config["_lr_vf"]]
if isinstance(config["_lr_vf"], (list, tuple))
else [config["_lr_vf"], None]
)
LearningRateSchedule.__init__(
self, config["lr"], config["lr_schedule"], *lr_schedule_additional_args
)
EntropyCoeffSchedule.__init__(
self, config["entropy_coeff"], config["entropy_coeff_schedule"]
)
TorchPolicyV2.__init__(
self,
observation_space,
action_space,
config,
max_seq_len=config["model"]["max_seq_len"],
)
ValueNetworkMixin.__init__(self, config)
self._initialize_loss_from_dummy_batch()
@override(TorchPolicyV2)
def loss(
self,
model: ModelV2,
dist_class: Type[ActionDistribution],
train_batch: SampleBatch,
) -> Union[TensorType, List[TensorType]]:
model_out, _ = model(train_batch)
action_dist = dist_class(model_out, model)
if isinstance(self.action_space, gym.spaces.Discrete):
is_multidiscrete = False
output_hidden_shape = [self.action_space.n]
elif isinstance(self.action_space, gym.spaces.MultiDiscrete):
is_multidiscrete = True
output_hidden_shape = self.action_space.nvec.astype(np.int32)
else:
is_multidiscrete = False
output_hidden_shape = 1
def _make_time_major(*args, **kw):
return make_time_major(
self, train_batch.get(SampleBatch.SEQ_LENS), *args, **kw
)
actions = train_batch[SampleBatch.ACTIONS]
dones = train_batch[SampleBatch.TERMINATEDS]
rewards = train_batch[SampleBatch.REWARDS]
behaviour_action_logp = train_batch[SampleBatch.ACTION_LOGP]
behaviour_logits = train_batch[SampleBatch.ACTION_DIST_INPUTS]
if isinstance(output_hidden_shape, (list, tuple, np.ndarray)):
unpacked_behaviour_logits = torch.split(
behaviour_logits, list(output_hidden_shape), dim=1
)
unpacked_outputs = torch.split(model_out, list(output_hidden_shape), dim=1)
else:
unpacked_behaviour_logits = torch.chunk(
behaviour_logits, output_hidden_shape, dim=1
)
unpacked_outputs = torch.chunk(model_out, output_hidden_shape, dim=1)
values = model.value_function()
values_time_major = _make_time_major(values)
bootstrap_values_time_major = _make_time_major(
train_batch[SampleBatch.VALUES_BOOTSTRAPPED]
)
bootstrap_value = bootstrap_values_time_major[-1]
if self.is_recurrent():
max_seq_len = torch.max(train_batch[SampleBatch.SEQ_LENS])
mask_orig = sequence_mask(train_batch[SampleBatch.SEQ_LENS], max_seq_len)
mask = torch.reshape(mask_orig, [-1])
else:
mask = torch.ones_like(rewards)
# Prepare actions for loss.
loss_actions = actions if is_multidiscrete else torch.unsqueeze(actions, dim=1)
# Inputs are reshaped from [B * T] => [(T|T-1), B] for V-trace calc.
loss = VTraceLoss(
actions=_make_time_major(loss_actions),
actions_logp=_make_time_major(action_dist.logp(actions)),
actions_entropy=_make_time_major(action_dist.entropy()),
dones=_make_time_major(dones),
behaviour_action_logp=_make_time_major(behaviour_action_logp),
behaviour_logits=_make_time_major(unpacked_behaviour_logits),
target_logits=_make_time_major(unpacked_outputs),
discount=self.config["gamma"],
rewards=_make_time_major(rewards),
values=values_time_major,
bootstrap_value=bootstrap_value,
dist_class=TorchCategorical if is_multidiscrete else dist_class,
model=model,
valid_mask=_make_time_major(mask),
config=self.config,
vf_loss_coeff=self.config["vf_loss_coeff"],
entropy_coeff=self.entropy_coeff,
clip_rho_threshold=self.config["vtrace_clip_rho_threshold"],
clip_pg_rho_threshold=self.config["vtrace_clip_pg_rho_threshold"],
)
# Store values for stats function in model (tower), such that for
# multi-GPU, we do not override them during the parallel loss phase.
model.tower_stats["pi_loss"] = loss.pi_loss
model.tower_stats["vf_loss"] = loss.vf_loss
model.tower_stats["entropy"] = loss.entropy
model.tower_stats["mean_entropy"] = loss.mean_entropy
model.tower_stats["total_loss"] = loss.total_loss
values_batched = make_time_major(
self,
train_batch.get(SampleBatch.SEQ_LENS),
values,
)
model.tower_stats["vf_explained_var"] = explained_variance(
torch.reshape(loss.value_targets, [-1]), torch.reshape(values_batched, [-1])
)
if self.config.get("_separate_vf_optimizer"):
return loss.loss_wo_vf, loss.vf_loss
else:
return loss.total_loss
@override(TorchPolicyV2)
def stats_fn(self, train_batch: SampleBatch) -> Dict[str, TensorType]:
return convert_to_numpy(
{
"cur_lr": self.cur_lr,
"total_loss": torch.mean(
torch.stack(self.get_tower_stats("total_loss"))
),
"policy_loss": torch.mean(torch.stack(self.get_tower_stats("pi_loss"))),
"entropy": torch.mean(
torch.stack(self.get_tower_stats("mean_entropy"))
),
"entropy_coeff": self.entropy_coeff,
"var_gnorm": global_norm(self.model.trainable_variables()),
"vf_loss": torch.mean(torch.stack(self.get_tower_stats("vf_loss"))),
"vf_explained_var": torch.mean(
torch.stack(self.get_tower_stats("vf_explained_var"))
),
}
)
@override(TorchPolicyV2)
def postprocess_trajectory(
self,
sample_batch: SampleBatch,
other_agent_batches: Optional[SampleBatch] = None,
episode=None,
):
# Call super's postprocess_trajectory first.
# sample_batch = super().postprocess_trajectory(
# sample_batch, other_agent_batches, episode
# )
if self.config["vtrace"]:
# Add the SampleBatch.VALUES_BOOTSTRAPPED column, which we'll need
# inside the loss for vtrace calculations.
sample_batch = compute_bootstrap_value(sample_batch, self)
return sample_batch
@override(TorchPolicyV2)
def extra_grad_process(
self, optimizer: "torch.optim.Optimizer", loss: TensorType
) -> Dict[str, TensorType]:
return apply_grad_clipping(self, optimizer, loss)
@override(TorchPolicyV2)
def get_batch_divisibility_req(self) -> int:
return self.config["rollout_fragment_length"]
@@ -0,0 +1,109 @@
import unittest
import pytest
import ray
import ray.rllib.algorithms.impala as impala
from ray.rllib.policy.sample_batch import DEFAULT_POLICY_ID
from ray.rllib.utils.metrics import LEARNER_RESULTS
from ray.rllib.utils.test_utils import check
class TestIMPALA(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
ray.init()
@classmethod
def tearDownClass(cls) -> None:
ray.shutdown()
def test_impala_minibatch_size_check(self):
config = (
impala.IMPALAConfig()
.environment("CartPole-v1")
.training(minibatch_size=100)
.env_runners(rollout_fragment_length=30)
)
with pytest.raises(
ValueError,
match=r"`minibatch_size` \(100\) must either be None or a multiple of `rollout_fragment_length` \(30\)",
):
config.validate()
def test_impala_lr_schedule(self):
# Test whether we correctly ignore the "lr" setting.
# The first lr should be 0.05.
config = (
impala.IMPALAConfig()
.learners(num_learners=0)
.experimental(_validate_config=False) #
.training(
lr=[
[0, 0.05],
[100000, 0.000001],
],
train_batch_size=100,
)
.env_runners(num_envs_per_env_runner=2)
.environment(env="CartPole-v1")
)
def get_lr(result):
return result[LEARNER_RESULTS][DEFAULT_POLICY_ID][
"default_optimizer_learning_rate"
]
algo = config.build()
optim = algo.learner_group._learner.get_optimizer()
try:
check(optim.param_groups[0]["lr"], 0.05)
for _ in range(1):
r1 = algo.train()
for _ in range(2):
r2 = algo.train()
for _ in range(2):
r3 = algo.train()
# Due to the asynch'ness of IMPALA, learner-stats metrics
# could be delayed by one iteration. Do 3 train() calls here
# and measure guaranteed decrease in lr between 1st and 3rd.
lr1 = get_lr(r1)
lr2 = get_lr(r2)
lr3 = get_lr(r3)
assert lr2 <= lr1, (lr1, lr2)
assert lr3 <= lr2, (lr2, lr3)
assert lr3 < lr1, (lr1, lr3)
finally:
algo.stop()
def test_local_learner_thread_stops_on_algo_stop(self):
# Regression test: `algo.stop()` -> `LearnerGroup.shutdown()` ->
# `IMPALALearner.shutdown()` must stop and join the local IMPALA
# `_LearnerThread`. Otherwise the daemon thread keeps spinning and
# can race against interpreter shutdown inside an auto_init-wrapped
# Ray API.
config = (
impala.IMPALAConfig()
.environment("CartPole-v1")
.learners(num_learners=0)
.env_runners(num_env_runners=0)
)
algo = config.build()
learner_thread = algo.learner_group._learner._learner_thread
self.assertTrue(learner_thread.is_alive())
algo.stop()
# `Learner.shutdown()` joins the thread, so it must be dead by the
# time `algo.stop()` returns — no extra `join()` needed here.
self.assertFalse(learner_thread.is_alive())
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,290 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for V-trace.
For details and theory see:
"IMPALA: Scalable Distributed Deep-RL with
Importance Weighted Actor-Learner Architectures"
by Espeholt, Soyer, Munos et al.
"""
import unittest
import numpy as np
from gymnasium.spaces import Box
from ray.rllib.algorithms.impala import vtrace_torch as vtrace_torch
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.numpy import softmax
from ray.rllib.utils.test_utils import check
torch, nn = try_import_torch()
def _ground_truth_vtrace_calculation(
discounts: np.ndarray,
log_rhos: np.ndarray,
rewards: np.ndarray,
values: np.ndarray,
bootstrap_value: np.ndarray,
clip_rho_threshold: float,
clip_pg_rho_threshold: float,
):
"""Calculates the ground truth for V-trace in Python/Numpy.
NOTE:
The discount, log_rhos, rewards, values, and bootstrap_values are all assumed to
come from trajectories of experience. Typically batches of trajectories could be
thought of as having the shape [B, T] where B is the batch dimension, and T is
the timestep dimension. Computing vtrace returns requires that the data is time
major, meaning that it has the shape [T, B]. One can use a function like
`make_time_major` to properly format their discount, log_rhos, rewards, values,
and bootstrap_values before calling _ground_truth_vtrace_calculation.
Args:
discounts: Array of shape [T*B] of discounts. T is the length of the trajectory
or sequence and B is the batch size.
NOTE: The discount will be equal to gamma, the discount factor when the
timestep that the discount is being applied to is not a terminal timestep.
log_rhos: Array of shape [T*B] of log likelihood ratios of target action
probabilities to behavior action probabilities.
rewards: Array of shape [T*B] of rewards.
values: Array of shape [T*B] of the value function estimated for every timestep
in a batch.
bootstrap_values: Array of shape [B] of the value function estimated at the last
timestep for each trajectory in the batch.
clip_rho_threshold: The threshold for clipping the importance weights.
clip_pg_rho_threshold: The threshold for clipping the importance weights for
the policy gradient loss.
Returns:
The v-trace adjusted values and the policy gradient advantages.
"""
vs = []
seq_len = len(discounts)
rhos = np.exp(log_rhos)
cs = np.minimum(rhos, 1.0)
clipped_rhos = rhos
if clip_rho_threshold:
clipped_rhos = np.minimum(rhos, clip_rho_threshold)
clipped_pg_rhos = rhos
if clip_pg_rho_threshold:
clipped_pg_rhos = np.minimum(rhos, clip_pg_rho_threshold)
# This is a very inefficient way to calculate the V-trace ground truth.
# We calculate it this way because it is close to the mathematical notation
# of
# V-trace.
# v_s = V(x_s)
# + \sum^{T-1}_{t=s} \gamma^{t-s}
# * \prod_{i=s}^{t-1} c_i
# * \rho_t (r_t + \gamma V(x_{t+1}) - V(x_t))
# Note that when we take the product over c_i, we write `s:t` as the
# notation
# of the paper is inclusive of the `t-1`, but Python is exclusive.
# Also note that np.prod([]) == 1.
values_t_plus_1 = np.concatenate([values[1:], bootstrap_value[None, :]], axis=0)
for s in range(seq_len):
v_s = np.copy(values[s]) # Very important copy.
for t in range(s, seq_len):
v_s += (
np.prod(discounts[s:t], axis=0)
* np.prod(cs[s:t], axis=0)
* clipped_rhos[t]
* (rewards[t] + discounts[t] * values_t_plus_1[t] - values[t])
)
vs.append(v_s)
vs = np.stack(vs, axis=0)
pg_advantages = clipped_pg_rhos * (
rewards
+ discounts * np.concatenate([vs[1:], bootstrap_value[None, :]], axis=0)
- values
)
return vs, pg_advantages
class LogProbsFromLogitsAndActionsTest(unittest.TestCase):
def test_log_probs_from_logits_and_actions(self):
"""Tests log_probs_from_logits_and_actions."""
seq_len = 7
num_actions = 3
batch_size = 4
vtrace = vtrace_torch
policy_logits = Box(
-1.0, 1.0, (seq_len, batch_size, num_actions), np.float32
).sample()
actions = np.random.randint(
0, num_actions - 1, size=(seq_len, batch_size), dtype=np.int32
)
action_log_probs_tensor = vtrace.log_probs_from_logits_and_actions(
torch.from_numpy(policy_logits), torch.from_numpy(actions)
)
# Ground Truth
# Using broadcasting to create a mask that indexes action logits
action_index_mask = actions[..., None] == np.arange(num_actions)
def index_with_mask(array, mask):
return array[mask].reshape(*array.shape[:-1])
# Note: Normally log(softmax) is not a good idea because it's not
# numerically stable. However, in this test we have well-behaved
# values.
ground_truth_v = index_with_mask(
np.log(softmax(policy_logits)), action_index_mask
)
check(action_log_probs_tensor, ground_truth_v)
class VtraceTest(unittest.TestCase):
def test_vtrace(self):
"""Tests V-trace against ground truth data calculated in python."""
seq_len = 5
batch_size = 10
# Create log_rhos such that rho will span from near-zero to above the
# clipping thresholds. In particular, calculate log_rhos in
# [-2.5, 2.5),
# so that rho is in approx [0.08, 12.2).
space_w_time = Box(-1.0, 1.0, (seq_len, batch_size), np.float32)
space_only_batch = Box(-1.0, 1.0, (batch_size,), np.float32)
log_rhos = space_w_time.sample() / (batch_size * seq_len)
log_rhos = 5 * (log_rhos - 0.5) # [0.0, 1.0) -> [-2.5, 2.5).
values = {
"log_rhos": log_rhos,
# T, B where B_i: [0.9 / (i+1)] * T
"discounts": np.array(
[[0.9 / (b + 1) for b in range(batch_size)] for _ in range(seq_len)]
),
"rewards": space_w_time.sample(),
"values": space_w_time.sample() / batch_size,
"bootstrap_value": space_only_batch.sample() + 1.0,
"clip_rho_threshold": 3.7,
"clip_pg_rho_threshold": 2.2,
}
vtrace = vtrace_torch
output = vtrace.from_importance_weights(**values)
gt_vs, gt_pg_advantags = _ground_truth_vtrace_calculation(**values)
check(output.vs, gt_vs)
check(output.pg_advantages, gt_pg_advantags)
def test_vtrace_from_logits(self):
"""Tests V-trace calculated from logits."""
seq_len = 5
batch_size = 15
num_actions = 3
clip_rho_threshold = None # No clipping.
clip_pg_rho_threshold = None # No clipping.
space = Box(-1.0, 1.0, (seq_len, batch_size, num_actions))
action_space = Box(
0,
num_actions - 1,
(
seq_len,
batch_size,
),
dtype=np.int32,
)
space_w_time = Box(
-1.0,
1.0,
(
seq_len,
batch_size,
),
)
space_only_batch = Box(-1.0, 1.0, (batch_size,))
inputs_ = {
# T, B, NUM_ACTIONS
"behaviour_policy_logits": space.sample(),
# T, B, NUM_ACTIONS
"target_policy_logits": space.sample(),
"actions": action_space.sample(),
"discounts": space_w_time.sample(),
"rewards": space_w_time.sample(),
"values": space_w_time.sample(),
"bootstrap_value": space_only_batch.sample(),
}
from_logits_output = vtrace_torch.from_logits(
clip_rho_threshold=clip_rho_threshold,
clip_pg_rho_threshold=clip_pg_rho_threshold,
**inputs_
)
target_log_probs = vtrace_torch.log_probs_from_logits_and_actions(
torch.from_numpy(inputs_["target_policy_logits"]),
torch.from_numpy(inputs_["actions"]),
)
behaviour_log_probs = vtrace_torch.log_probs_from_logits_and_actions(
torch.from_numpy(inputs_["behaviour_policy_logits"]),
torch.from_numpy(inputs_["actions"]),
)
log_rhos = target_log_probs - behaviour_log_probs
from_iw = vtrace_torch.from_importance_weights(
log_rhos=log_rhos,
discounts=inputs_["discounts"],
rewards=inputs_["rewards"],
values=inputs_["values"],
bootstrap_value=inputs_["bootstrap_value"],
clip_rho_threshold=clip_rho_threshold,
clip_pg_rho_threshold=clip_pg_rho_threshold,
)
check(from_iw.vs, from_logits_output.vs)
check(from_iw.pg_advantages, from_logits_output.pg_advantages)
check(behaviour_log_probs, from_logits_output.behaviour_action_log_probs)
check(target_log_probs, from_logits_output.target_action_log_probs)
check(log_rhos, from_logits_output.log_rhos)
def test_higher_rank_inputs_for_importance_weights(self):
"""Checks support for additional dimensions in inputs."""
inputs_ = {
"log_rhos": Box(-1.0, 1.0, (8, 10, 1)).sample(),
"discounts": Box(-1.0, 1.0, (8, 10, 1)).sample(),
"rewards": Box(-1.0, 1.0, (8, 10, 42)).sample(),
"values": Box(-1.0, 1.0, (8, 10, 42)).sample(),
"bootstrap_value": Box(-1.0, 1.0, (10, 42)).sample(),
}
output = vtrace_torch.from_importance_weights(**inputs_)
check(int(output.vs.shape[-1]), 42)
def test_inconsistent_rank_inputs_for_importance_weights(self):
"""Test one of many possible errors in shape of inputs."""
inputs_ = {
"log_rhos": Box(-1.0, 1.0, (7, 15, 1)).sample(),
"discounts": Box(-1.0, 1.0, (7, 15, 1)).sample(),
"rewards": Box(-1.0, 1.0, (7, 15, 42)).sample(),
"values": Box(-1.0, 1.0, (7, 15, 42)).sample(),
# Should be [15, 42].
"bootstrap_value": Box(-1.0, 1.0, (7,)).sample(),
}
with self.assertRaisesRegex((ValueError, AssertionError), "must have rank 2"):
vtrace_torch.from_importance_weights(**inputs_)
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,154 @@
import unittest
import numpy as np
from gymnasium.spaces import Box, Discrete
from ray.rllib.algorithms.impala.tests.test_vtrace_old_api_stack import (
_ground_truth_vtrace_calculation,
)
from ray.rllib.algorithms.impala.torch.vtrace_torch_v2 import (
make_time_major,
vtrace_torch,
)
from ray.rllib.core.distribution.torch.torch_distribution import TorchCategorical
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.test_utils import check
from ray.rllib.utils.torch_utils import convert_to_torch_tensor
torch, _ = try_import_torch()
def flatten_batch_and_time_dim(t):
if not torch.is_tensor(t):
t = torch.from_numpy(t)
new_shape = [-1] + list(t.shape[2:])
return torch.reshape(t, new_shape)
class TestVtraceRLModule(unittest.TestCase):
"""Tests V-trace-v2 against ground truth data calculated.
There is a ground truth implementation that we used to test our original
implementation against. This test checks that the new implementation still
matches the ground truth test from our first implementation of V-Trace.
"""
@classmethod
def setUpClass(cls):
"""Sets up inputs for V-Trace and calculate ground truth.
We use tf operations here to compile the inputs but convert to numpy arrays to
calculate the ground truth (and the other v-trace outputs in the
framework-specific tests).
"""
# we can test against any trajectory length or batch size and it won't matter
trajectory_len = 5
batch_size = 10
action_space = Discrete(10)
action_logit_space = Box(-1.0, 1.0, (action_space.n,), np.float32)
behavior_action_logits = torch.from_numpy(
np.array([action_logit_space.sample()], dtype=np.float32)
)
target_action_logits = torch.from_numpy(
np.array([action_logit_space.sample()], dtype=np.float32)
)
behavior_dist = TorchCategorical(logits=behavior_action_logits)
target_dist = TorchCategorical(logits=target_action_logits)
dummy_action_batch = [
[action_space.sample() for _ in range(trajectory_len)]
for _ in range(batch_size)
]
behavior_log_probs = torch.stack(
[
torch.squeeze(
behavior_dist.logp(torch.from_numpy(np.array(v, np.uint8)))
)
for v in dummy_action_batch
]
)
target_log_probs = torch.stack(
[
torch.squeeze(target_dist.logp(torch.from_numpy(np.array(v, np.uint8))))
for v in dummy_action_batch
]
)
# target_log_probs = torch.stack(
# tree.map_structure(
# lambda v: torch.squeeze(target_dist.logp(v)), dummy_action_batch
# )
# )
value_fn_space_w_time = Box(-1.0, 1.0, (batch_size, trajectory_len), np.float32)
value_fn_space = Box(-1.0, 1.0, (batch_size,), np.float32)
# using randomly sampled values in lieu of actual values sampled from a value fn
values = value_fn_space_w_time.sample()
# this is supposed to be the value function at the last timestep of each
# trajectory in the batch. In IMPALA its bootstrapped at training time
cls.bootstrap_values = np.array(value_fn_space.sample() + 1.0)
# discount factor used at all of the timesteps
discounts = torch.from_numpy(
np.array([0.9 for _ in range(trajectory_len * batch_size)])
)
rewards = value_fn_space_w_time.sample()
cls.clip_rho_threshold = 3.7
cls.clip_pg_rho_threshold = 2.2
# convert to time major dimension
cls.behavior_log_probs_time_major = make_time_major(
flatten_batch_and_time_dim(behavior_log_probs),
trajectory_len=trajectory_len,
).numpy()
cls.target_log_probs_time_major = make_time_major(
flatten_batch_and_time_dim(target_log_probs), trajectory_len=trajectory_len
).numpy()
cls.discounts_time_major = make_time_major(
flatten_batch_and_time_dim(discounts), trajectory_len=trajectory_len
).numpy()
cls.rewards_time_major = make_time_major(
flatten_batch_and_time_dim(rewards), trajectory_len=trajectory_len
).numpy()
cls.values_time_major = make_time_major(
flatten_batch_and_time_dim(values), trajectory_len=trajectory_len
).numpy()
log_rhos = cls.target_log_probs_time_major - cls.behavior_log_probs_time_major
cls.ground_truth_v = _ground_truth_vtrace_calculation(
discounts=cls.discounts_time_major,
log_rhos=log_rhos,
rewards=cls.rewards_time_major,
values=cls.values_time_major,
bootstrap_value=cls.bootstrap_values,
clip_rho_threshold=cls.clip_rho_threshold,
clip_pg_rho_threshold=cls.clip_pg_rho_threshold,
)
def test_vtrace_torch(self):
output_torch_vtrace = vtrace_torch(
behaviour_action_log_probs=convert_to_torch_tensor(
self.behavior_log_probs_time_major
),
target_action_log_probs=convert_to_torch_tensor(
self.target_log_probs_time_major
),
discounts=convert_to_torch_tensor(self.discounts_time_major),
rewards=convert_to_torch_tensor(self.rewards_time_major),
values=convert_to_torch_tensor(self.values_time_major),
bootstrap_values=convert_to_torch_tensor(self.bootstrap_values),
clip_rho_threshold=self.clip_rho_threshold,
clip_pg_rho_threshold=self.clip_pg_rho_threshold,
)
check(output_torch_vtrace, self.ground_truth_v)
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,353 @@
import contextlib
from typing import Dict
from ray.rllib.algorithms.impala.impala import IMPALAConfig
from ray.rllib.algorithms.impala.impala_learner import IMPALALearner
from ray.rllib.algorithms.impala.torch.vtrace_torch_v2 import (
make_time_major,
vtrace_torch,
)
from ray.rllib.core.columns import Columns
from ray.rllib.core.learner.learner import ENTROPY_KEY
from ray.rllib.core.learner.torch.torch_learner import TorchLearner
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.typing import ModuleID, ParamDict, TensorType
torch, nn = try_import_torch()
class IMPALATorchLearner(IMPALALearner, TorchLearner):
"""Implements the IMPALA loss function in torch."""
@override(TorchLearner)
def compute_loss_for_module(
self,
*,
module_id: ModuleID,
config: IMPALAConfig,
batch: Dict,
fwd_out: Dict[str, TensorType],
) -> TensorType:
module = self.module[module_id].unwrapped()
# TODO (sven): Now that we do the +1ts trick to be less vulnerable about
# bootstrap values at the end of rollouts in the new stack, we might make
# this a more flexible, configurable parameter for users, e.g.
# `v_trace_seq_len` (independent of `rollout_fragment_length`). Separation
# of concerns (sampling vs learning).
rollout_frag_or_episode_len = config.get_rollout_fragment_length()
recurrent_seq_len = batch.get("seq_lens")
loss_mask = batch[Columns.LOSS_MASK].float()
loss_mask_time_major = make_time_major(
loss_mask,
trajectory_len=rollout_frag_or_episode_len,
recurrent_seq_len=recurrent_seq_len,
)
size_loss_mask = torch.sum(loss_mask)
# Behavior actions logp and target actions logp.
behaviour_actions_logp = batch[Columns.ACTION_LOGP]
target_policy_dist = module.get_train_action_dist_cls().from_logits(
fwd_out[Columns.ACTION_DIST_INPUTS]
)
target_actions_logp = target_policy_dist.logp(batch[Columns.ACTIONS])
# Values and bootstrap values.
values = module.compute_values(
batch, embeddings=fwd_out.get(Columns.EMBEDDINGS)
)
values_time_major = make_time_major(
values,
trajectory_len=rollout_frag_or_episode_len,
recurrent_seq_len=recurrent_seq_len,
)
assert Columns.VALUES_BOOTSTRAPPED not in batch
# Use as bootstrap values the vf-preds in the next "batch row", except
# for the very last row (which doesn't have a next row), for which the
# bootstrap value does not matter b/c it has a +1ts value at its end
# anyways. So we chose an arbitrary item (for simplicity of not having to
# move new data to the device).
bootstrap_values = torch.cat(
[
values_time_major[0][1:], # 0th ts values from "next row"
values_time_major[0][0:1], # <- can use any arbitrary value here
],
dim=0,
)
# TODO(Artur): In the old impala code, actions were unsqueezed if they were
# multi_discrete. Find out why and if we need to do the same here.
# actions = actions if is_multidiscrete else torch.unsqueeze(actions, dim=1)
target_actions_logp_time_major = make_time_major(
target_actions_logp,
trajectory_len=rollout_frag_or_episode_len,
recurrent_seq_len=recurrent_seq_len,
)
behaviour_actions_logp_time_major = make_time_major(
behaviour_actions_logp,
trajectory_len=rollout_frag_or_episode_len,
recurrent_seq_len=recurrent_seq_len,
)
rewards_time_major = make_time_major(
batch[Columns.REWARDS],
trajectory_len=rollout_frag_or_episode_len,
recurrent_seq_len=recurrent_seq_len,
)
# Discount = gamma * (1 - terminated) * loss_mask.
# - The (1 - terminated) factor implements the Bellman gating: no
# bootstrap from t -> t+1 across a terminal step.
# - The loss_mask factor zeros out the discount at the appended bootstrap
# timestep (loss_mask=False there). Without it, the bootstrap-ts delta
# (which references `bootstrap_values` from a neighbouring trajectory)
# would leak into the V-trace recursion of the last real step. The
# loss_mask gating is equivalent to the legacy convention of marking
# the bootstrap ts as `terminated=True`, but keeps `terminateds`
# meaning only "Gymnasium terminal state reached".
discounts_time_major = (
(
1.0
- make_time_major(
batch[Columns.TERMINATEDS],
trajectory_len=rollout_frag_or_episode_len,
recurrent_seq_len=recurrent_seq_len,
).type(dtype=torch.float32)
)
* config.gamma
* loss_mask_time_major
)
# Note that vtrace will compute the main loop on the CPU for better performance.
vtrace_adjusted_target_values, pg_advantages = vtrace_torch(
target_action_log_probs=target_actions_logp_time_major,
behaviour_action_log_probs=behaviour_actions_logp_time_major,
discounts=discounts_time_major,
rewards=rewards_time_major,
values=values_time_major,
bootstrap_values=bootstrap_values,
clip_rho_threshold=config.vtrace_clip_rho_threshold,
clip_pg_rho_threshold=config.vtrace_clip_pg_rho_threshold,
)
# The policy gradients loss.
pi_loss = -torch.sum(
target_actions_logp_time_major * pg_advantages * loss_mask_time_major
)
mean_pi_loss = pi_loss / size_loss_mask
# The baseline loss.
delta = values_time_major - vtrace_adjusted_target_values
vf_loss = 0.5 * torch.sum(torch.pow(delta, 2.0) * loss_mask_time_major)
mean_vf_loss = vf_loss / size_loss_mask
# The entropy loss.
entropy_loss = -torch.sum(target_policy_dist.entropy() * loss_mask)
mean_entropy_loss = entropy_loss / size_loss_mask
# The summed weighted loss.
total_loss = (
mean_pi_loss
+ mean_vf_loss * config.vf_loss_coeff
+ (
mean_entropy_loss
* self.entropy_coeff_schedulers_per_module[
module_id
].get_current_value()
)
)
# Log important loss stats.
self.metrics.log_dict(
{
"pi_loss": pi_loss,
"mean_pi_loss": mean_pi_loss,
"vf_loss": vf_loss,
"mean_vf_loss": mean_vf_loss,
ENTROPY_KEY: -mean_entropy_loss,
},
key=module_id,
window=1, # <- single items (should not be mean/ema-reduced over time).
)
# Return the total loss.
return total_loss
@override(TorchLearner)
def _uncompiled_update(
self,
batch: Dict,
**kwargs,
):
"""Performs a single update given a batch of data.
This override is critical to prevent DDP (DistributedDataParallel)
deadlocks caused by the unique properties of APPO's asynchronous,
multi-agent data pipeline.
**The Problem: Asymmetric Graph Deadlock**
1. APPO's asynchronous `EnvRunners` send data to each Learner (DDP rank)
independently.
2. This means that ranks will receive sometimes **asymmetric batches**
(e.g., Rank 0 gets `{'p0', 'p1'}`, while Rank 1 gets just `{'p0'}`).
3. The default DDP update path (using automatic, hook-based
synchronization) fails in this scenario. When `backward()` is
called, Rank 1 has no computation graph for `p1`'s parameters,
so its DDP hooks for `p1` never fire.
4. Rank 0, which *does* have a graph for `p1`, waits forever for
`p1` gradients from Rank 1, causing a **permanent deadlock**.
The solution is manual synchronization. This function replaces DDP's
fragile, hook-based communication with a robust, manual, three-stage process:
1. **Disable Hooks (The `no_sync` context):**
The *entire* `forward_train`, `compute_losses`, and
`compute_gradients` (which calls `backward()`) chain is wrapped
in a `mod.no_sync()` context. This is the most critical step, as
DDP hooks are attached during the *forward pass*. This correctly
prevents DDP's automatic communication from firing.
2. **Synchronize `backward()` (avoid GPU race condition):**
A call to `torch.cuda.synchronize()` is added *after*
`total_loss.backward()` (inside `compute_gradients`). On GPU,
`backward()` is asynchronous. This `synchronize()` call forces
the CPU to wait for the GPU to *actually finish* computing the
gradients before we proceed to the next step. This prevents a
race condition where we try to `all_reduce` a `.grad` attribute
that is still `None` because the GPU is lagging.
3. **Manual `all_reduce` (zero-padding):**
After the `no_sync` block, we manually `all_reduce` all
gradients. To prevent a deadlock here, we iterate over all
parameters. If `param.grad is None` (which happens on the
"incomplete" rank for policy `p1`), we zero-pad. This "zero-padding"
ensures that *all* ranks participate in the `all_reduce` call for
*all* parameters, making the manual synchronization robust.
"""
# For single-learner setups, call the super's update.
if self.config.num_learners < 2 or not self.config.is_multi_agent:
return super()._uncompiled_update(batch=batch, **kwargs)
# Compute the off-policyness of the batch.
self._compute_off_policyness(batch)
# These must be defined outside the scope of the `with` block
fwd_out = {}
loss_per_module = {}
gradients = {}
# 1. Enter no_sync() for the forward and backward pass.
with contextlib.ExitStack() as stack:
for mod in self.module.values():
if isinstance(mod, torch.nn.Module):
stack.enter_context(mod.no_sync())
# All Torch DDP-affected computation must be inside to avoid firing
# the hooks on policy gradients that are only trained on some ranks.
# 2. Forward pass (now inside no_sync)
fwd_out = self.module.forward_train(batch)
# 3. Loss computation (now inside no_sync)
loss_per_module = self.compute_losses(fwd_out=fwd_out, batch=batch)
# 4. Compute gradients LOCALLY (backward() is called inside)
gradients = self.compute_gradients(loss_per_module)
# 5. Manually All-Reduce gradients (outside no_sync).
# We iterate over all known parameters (`self._params`). This is important
# to ensure the `all_reduce`` calls are made on all ranks for all params.
for param in self._params.values():
# Is the parameter present on this rank?
present = 1
if param.grad is None:
# Parameter is not present on this rank. Keep track of that
# for averaging later.
present = 0
# This parameter was not used (e.g., p1 on Rank 0).
# Create a zero-gradient to participate in the all_reduce.
param.grad = torch.zeros_like(param)
# Now, all ranks have a valid `param.grad` tensor, i.e. all ranks will call
# all_reduce. No deadlock.
torch.distributed.all_reduce(param.grad, op=torch.distributed.ReduceOp.SUM)
# Scale the gradients accordingly.
denom = torch.tensor(present, device=param.device, dtype=param.dtype)
# Receive the number of participating ranks for this param.
torch.distributed.all_reduce(denom, op=torch.distributed.ReduceOp.SUM)
# Average the summed gradients.
param.grad.data.div_(denom.clamp(min=1.0))
# 6. Collect the gradients for all modules to update the modules synchronously.
gradients = {pid: p.grad for pid, p in self._params.items()}
# 7. Postprocess gradients, e.g. clipping.
postprocessed_gradients = self.postprocess_gradients(gradients)
# 8. Apply the post-processed gradients to the weigths.
self.apply_gradients(postprocessed_gradients)
return fwd_out, loss_per_module, {}
@override(TorchLearner)
def compute_gradients(
self, loss_per_module: Dict[ModuleID, TensorType], **kwargs
) -> ParamDict:
"""Computes the gradients by running `backward`.
This method is a core part of the manual synchronization logic
in `_uncompiled_update`. It performs the `backward()` pass locally
without DDP's automatic communication.
Key implementation details:
1.**GPU Synchronization:** It includes a `torch.cuda.synchronize()`
call after `total_loss.backward()`. This is critical for GPU
training, as `backward()` is asynchronous. This sync
prevents a race condition where the calling function
(`_uncompiled_update`) would try to access `param.grad`
before the GPU has finished computing it (mistakenly reading `None`).
2. **Asymmetric Gradients:** On an incomplete batch, parameters for
missing modules will correctly have a `None` gradient. This is
expected and handled by the zero-padding logic in
`_uncompiled_update`'s `all_reduce` loop.
3. If the loss is zero (i.e., no modules were trained on this rank),
this method returns an empty dict.
"""
# If a single learner is used, fall back to the super's method.
if self.config.num_learners < 2 or not self.config.is_multi_agent:
return super().compute_gradients(loss_per_module=loss_per_module, **kwargs)
for optim in self._optimizer_parameters:
# `set_to_none=True` is a faster way to zero out the gradients.
optim.zero_grad(set_to_none=True)
if self._grad_scalers is not None:
total_loss = sum(
self._grad_scalers[mid].scale(loss)
for mid, loss in loss_per_module.items()
)
else:
total_loss = sum(loss_per_module.values())
# If we don't have any loss computations, `sum` returns 0.
if isinstance(total_loss, int):
assert total_loss == 0
return {}
# This backward() call is inside no_sync(). It will be a clean,
# local-only operation.
total_loss.backward()
# We must force the CPU to wait for the async `backward()` call to
# finish on the GPU. Otherwise, the `param.grad is None` check in
# `_uncompiled_update` will race against the GPU and fail.
if torch.cuda.is_available():
torch.cuda.synchronize()
# This line is now safe. `grads` will have `None` for unused params
# (e.g., p1 on Rank 0). This is expected and handled by `_uncompiled_update`'s
# all_reduce loop.
grads = {pid: p.grad for pid, p in self._params.items()}
return grads
ImpalaTorchLearner = IMPALATorchLearner
@@ -0,0 +1,169 @@
from typing import List, Union
from ray.rllib.utils.framework import try_import_torch
torch, nn = try_import_torch()
def make_time_major(
tensor: Union["torch.Tensor", List["torch.Tensor"]],
*,
trajectory_len: int = None,
recurrent_seq_len: int = None,
):
"""Swaps batch and trajectory axis.
Args:
tensor: A tensor or list of tensors to swap the axis of.
NOTE: Each tensor must have the shape [B * T] where B is the batch size and
T is the trajectory length.
trajectory_len: The length of each trajectory being transformed.
If None then `recurrent_seq_len` must be set.
recurrent_seq_len: Sequence lengths if recurrent.
If None then `trajectory_len` must be set.
Returns:
res: A tensor with swapped axes or a list of tensors with
swapped axes.
"""
if isinstance(tensor, (list, tuple)):
return [
make_time_major(_tensor, trajectory_len, recurrent_seq_len)
for _tensor in tensor
]
assert (
trajectory_len is not None or recurrent_seq_len is not None
), "Either trajectory_len or recurrent_seq_len must be set."
# Figure out the sizes of the final B and T axes.
if recurrent_seq_len is not None:
assert len(tensor.shape) == 2
# Swap B and T axes.
tensor = torch.transpose(tensor, 1, 0)
return tensor
else:
T = trajectory_len
# Zero-pad, if necessary.
tensor_0 = tensor.shape[0]
B = tensor_0 // T
if B != (tensor_0 / T):
assert len(tensor.shape) == 1
tensor = torch.cat(
[
tensor,
torch.zeros(
trajectory_len - tensor_0 % T,
dtype=tensor.dtype,
device=tensor.device,
),
]
)
B += 1
# Reshape tensor (break up B axis into 2 axes: B and T).
tensor = torch.reshape(tensor, [B, T] + list(tensor.shape[1:]))
# Swap B and T axes.
tensor = torch.transpose(tensor, 1, 0)
return tensor
def vtrace_torch(
*,
target_action_log_probs: "torch.Tensor",
behaviour_action_log_probs: "torch.Tensor",
discounts: "torch.Tensor",
rewards: "torch.Tensor",
values: "torch.Tensor",
bootstrap_values: "torch.Tensor",
clip_rho_threshold: Union[float, "torch.Tensor"] = 1.0,
clip_pg_rho_threshold: Union[float, "torch.Tensor"] = 1.0,
):
r"""V-trace for softmax policies implemented with torch.
Calculates V-trace actor critic targets for softmax polices as described in
"IMPALA: Scalable Distributed Deep-RL with Importance Weighted Actor-Learner
Architectures" by Espeholt, Soyer, Munos et al. (https://arxiv.org/abs/1802.01561)
The V-trace implementation used here closely resembles the one found in the
scalable-agent repository by Google DeepMind, available at
https://github.com/deepmind/scalable_agent. This version has been optimized to
minimize the number of floating-point operations required per V-Trace
calculation, achieved through the use of dynamic programming techniques. It's
important to note that the mathematical expressions used in this implementation
may appear quite different from those presented in the IMPALA paper.
The following terminology applies:
- `target policy` refers to the policy we are interested in improving.
- `behaviour policy` refers to the policy that generated the given
rewards and actions.
- `T` refers to the time dimension. This is usually either the length of the
trajectory or the length of the sequence if recurrent.
- `B` refers to the batch size.
Args:
target_action_log_probs: Action log probs from the target policy. A float32
tensor of shape [T, B].
behaviour_action_log_probs: Action log probs from the behaviour policy. A
float32 tensor of shape [T, B].
discounts: A float32 tensor of shape [T, B] with the discount encountered when
following the behaviour policy. This will be 0 for terminal timesteps
(done=True) and gamma (the discount factor) otherwise.
rewards: A float32 tensor of shape [T, B] with the rewards generated by
following the behaviour policy.
values: A float32 tensor of shape [T, B] with the value function estimates
wrt. the target policy.
bootstrap_values: A float32 of shape [B] with the value function estimate at
time T.
clip_rho_threshold: A scalar float32 tensor with the clipping threshold for
importance weights (rho) when calculating the baseline targets (vs).
rho^bar in the paper.
clip_pg_rho_threshold: A scalar float32 tensor with the clipping threshold
on rho_s in \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)).
"""
log_rhos = target_action_log_probs - behaviour_action_log_probs
rhos = torch.exp(log_rhos)
if clip_rho_threshold is not None:
clipped_rhos = torch.clamp(rhos, max=clip_rho_threshold)
else:
clipped_rhos = rhos
cs = torch.clamp(rhos, max=1.0)
# Append bootstrapped value to get [v1, ..., v_t+1]
values_t_plus_1 = torch.cat(
[values[1:], torch.unsqueeze(bootstrap_values, 0)], axis=0
)
deltas = clipped_rhos * (rewards + discounts * values_t_plus_1 - values)
# Note: The original IMPALA code (and paper) suggested to perform the following
# v-trace for-loop on the CPU, due to its sequential nature. However, modern GPUs
# are quite optimized for these shorted for-loops, which is why it should be faster
# nowadays to leave these operations on the GPU to avoid the GPU<>CPU transfer
# penalty. This penalty can actually be quite massive on the LEarner actors, given
# all other code is already well optimized.
vs_minus_v_xs = [torch.zeros_like(bootstrap_values, device=deltas.device)]
for i in reversed(range(len(discounts))):
discount_t, c_t, delta_t = discounts[i], cs[i], deltas[i]
vs_minus_v_xs.append(delta_t + discount_t * c_t * vs_minus_v_xs[-1])
vs_minus_v_xs = torch.stack(vs_minus_v_xs[1:])
# Reverse the results back to original order.
vs_minus_v_xs = torch.flip(vs_minus_v_xs, dims=[0])
# Add V(x_s) to get v_s.
vs = torch.add(vs_minus_v_xs, values)
# Advantage for policy gradient.
vs_t_plus_1 = torch.cat([vs[1:], torch.unsqueeze(bootstrap_values, 0)], axis=0)
if clip_pg_rho_threshold is not None:
clipped_pg_rhos = torch.clamp(rhos, max=clip_pg_rho_threshold)
else:
clipped_pg_rhos = rhos
pg_advantages = clipped_pg_rhos * (rewards + discounts * vs_t_plus_1 - values)
# Make sure no gradients backpropagated through the returned values.
return torch.detach(vs), torch.detach(pg_advantages)
+96
View File
@@ -0,0 +1,96 @@
from collections import defaultdict, deque
import numpy as np
class _SleepTimeController:
def __init__(self):
self.L = 0.0
self.H = 0.4
self._recompute_candidates()
# Defaultdict mapping.
self.results = defaultdict(lambda: deque(maxlen=3))
self.iteration = 0
def _recompute_candidates(self):
self.center = (self.L + self.H) / 2
self.low = (self.L + self.center) / 2
self.high = (self.H + self.center) / 2
# Expand a little if range becomes too narrow to avoid
# overoptimization.
if self.H - self.L < 0.00001:
self.L = max(self.center - 0.1, 0.0)
self.H = min(self.center + 0.1, 1.0)
self._recompute_candidates()
# Reduce results, just in case it has grown too much.
c, l, h = (
self.results[self.center],
self.results[self.low],
self.results[self.high],
)
self.results = defaultdict(lambda: deque(maxlen=3))
self.results[self.center] = c
self.results[self.low] = l
self.results[self.high] = h
@property
def current(self):
if len(self.results[self.center]) < 3:
return self.center
elif len(self.results[self.low]) < 3:
return self.low
else:
return self.high
def log_result(self, performance):
self.iteration += 1
# Skip first 2 iterations for ignoring warm-up effect.
if self.iteration < 2:
return
self.results[self.current].append(performance)
# If all candidates have at least 3 results logged, re-evaluate
# and compute new L and H.
center, low, high = self.center, self.low, self.high
if (
len(self.results[center]) == 3
and len(self.results[low]) == 3
and len(self.results[high]) == 3
):
perf_center = np.mean(self.results[center])
perf_low = np.mean(self.results[low])
perf_high = np.mean(self.results[high])
# Case: `center` is best.
if perf_center > perf_low and perf_center > perf_high:
self.L = low
self.H = high
# Erase low/high results: We'll not use these again.
self.results.pop(low, None)
self.results.pop(high, None)
# Case: `low` is best.
elif perf_low > perf_center and perf_low > perf_high:
self.H = center
# Erase center/high results: We'll not use these again.
self.results.pop(center, None)
self.results.pop(high, None)
# Case: `high` is best.
else:
self.L = center
# Erase center/low results: We'll not use these again.
self.results.pop(center, None)
self.results.pop(low, None)
self._recompute_candidates()
if __name__ == "__main__":
controller = _SleepTimeController()
for _ in range(1000):
performance = np.random.random()
controller.log_result(performance)
+425
View File
@@ -0,0 +1,425 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Functions to compute V-trace off-policy actor critic targets.
For details and theory see:
"IMPALA: Scalable Distributed Deep-RL with
Importance Weighted Actor-Learner Architectures"
by Espeholt, Soyer, Munos et al.
See https://arxiv.org/abs/1802.01561 for the full paper.
In addition to the original paper's code, changes have been made
to support MultiDiscrete action spaces. behaviour_policy_logits,
target_policy_logits and actions parameters in the entry point
multi_from_logits method accepts lists of tensors instead of just
tensors.
"""
import collections
from ray.rllib.models.tf.tf_action_dist import Categorical
from ray.rllib.utils.framework import try_import_tf
tf1, tf, tfv = try_import_tf()
VTraceFromLogitsReturns = collections.namedtuple(
"VTraceFromLogitsReturns",
[
"vs",
"pg_advantages",
"log_rhos",
"behaviour_action_log_probs",
"target_action_log_probs",
],
)
VTraceReturns = collections.namedtuple("VTraceReturns", "vs pg_advantages")
def log_probs_from_logits_and_actions(
policy_logits, actions, dist_class=Categorical, model=None
):
return multi_log_probs_from_logits_and_actions(
[policy_logits], [actions], dist_class, model
)[0]
def multi_log_probs_from_logits_and_actions(policy_logits, actions, dist_class, model):
"""Computes action log-probs from policy logits and actions.
In the notation used throughout documentation and comments, T refers to the
time dimension ranging from 0 to T-1. B refers to the batch size and
ACTION_SPACE refers to the list of numbers each representing a number of
actions.
Args:
policy_logits: A list with length of ACTION_SPACE of float32
tensors of shapes [T, B, ACTION_SPACE[0]], ...,
[T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities
parameterizing a softmax policy.
actions: A list with length of ACTION_SPACE of tensors of shapes
[T, B, ...], ..., [T, B, ...]
with actions.
dist_class: Python class of the action distribution.
Returns:
A list with length of ACTION_SPACE of float32 tensors of shapes
[T, B], ..., [T, B] corresponding to the sampling log probability
of the chosen action w.r.t. the policy.
"""
log_probs = []
for i in range(len(policy_logits)):
p_shape = tf.shape(policy_logits[i])
a_shape = tf.shape(actions[i])
policy_logits_flat = tf.reshape(
policy_logits[i], tf.concat([[-1], p_shape[2:]], axis=0)
)
actions_flat = tf.reshape(actions[i], tf.concat([[-1], a_shape[2:]], axis=0))
log_probs.append(
tf.reshape(
dist_class(policy_logits_flat, model).logp(actions_flat), a_shape[:2]
)
)
return log_probs
def from_logits(
behaviour_policy_logits,
target_policy_logits,
actions,
discounts,
rewards,
values,
bootstrap_value,
dist_class=Categorical,
model=None,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
name="vtrace_from_logits",
):
"""multi_from_logits wrapper used only for tests"""
res = multi_from_logits(
[behaviour_policy_logits],
[target_policy_logits],
[actions],
discounts,
rewards,
values,
bootstrap_value,
dist_class,
model,
clip_rho_threshold=clip_rho_threshold,
clip_pg_rho_threshold=clip_pg_rho_threshold,
name=name,
)
return VTraceFromLogitsReturns(
vs=res.vs,
pg_advantages=res.pg_advantages,
log_rhos=res.log_rhos,
behaviour_action_log_probs=tf.squeeze(res.behaviour_action_log_probs, axis=0),
target_action_log_probs=tf.squeeze(res.target_action_log_probs, axis=0),
)
def multi_from_logits(
behaviour_policy_logits,
target_policy_logits,
actions,
discounts,
rewards,
values,
bootstrap_value,
dist_class,
model,
behaviour_action_log_probs=None,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
name="vtrace_from_logits",
):
r"""V-trace for softmax policies.
Calculates V-trace actor critic targets for softmax polices as described in
"IMPALA: Scalable Distributed Deep-RL with
Importance Weighted Actor-Learner Architectures"
by Espeholt, Soyer, Munos et al.
Target policy refers to the policy we are interested in improving and
behaviour policy refers to the policy that generated the given
rewards and actions.
In the notation used throughout documentation and comments, T refers to the
time dimension ranging from 0 to T-1. B refers to the batch size and
ACTION_SPACE refers to the list of numbers each representing a number of
actions.
Args:
behaviour_policy_logits: A list with length of ACTION_SPACE of float32
tensors of shapes
[T, B, ACTION_SPACE[0]],
...,
[T, B, ACTION_SPACE[-1]]
with un-normalized log-probabilities parameterizing the softmax behaviour
policy.
target_policy_logits: A list with length of ACTION_SPACE of float32
tensors of shapes
[T, B, ACTION_SPACE[0]],
...,
[T, B, ACTION_SPACE[-1]]
with un-normalized log-probabilities parameterizing the softmax target
policy.
actions: A list with length of ACTION_SPACE of
tensors of shapes
[T, B, ...],
...,
[T, B, ...]
with actions sampled from the behaviour policy.
discounts: A float32 tensor of shape [T, B] with the discount encountered
when following the behaviour policy.
rewards: A float32 tensor of shape [T, B] with the rewards generated by
following the behaviour policy.
values: A float32 tensor of shape [T, B] with the value function estimates
wrt. the target policy.
bootstrap_value: A float32 of shape [B] with the value function estimate at
time T.
dist_class: action distribution class for the logits.
model: backing ModelV2 instance
behaviour_action_log_probs: precalculated values of the behaviour actions
clip_rho_threshold: A scalar float32 tensor with the clipping threshold for
importance weights (rho) when calculating the baseline targets (vs).
rho^bar in the paper.
clip_pg_rho_threshold: A scalar float32 tensor with the clipping threshold
on rho_s in \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)).
name: The name scope that all V-trace operations will be created in.
Returns:
A `VTraceFromLogitsReturns` namedtuple with the following fields:
vs: A float32 tensor of shape [T, B]. Can be used as target to train a
baseline (V(x_t) - vs_t)^2.
pg_advantages: A float 32 tensor of shape [T, B]. Can be used as an
estimate of the advantage in the calculation of policy gradients.
log_rhos: A float32 tensor of shape [T, B] containing the log importance
sampling weights (log rhos).
behaviour_action_log_probs: A float32 tensor of shape [T, B] containing
behaviour policy action log probabilities (log \mu(a_t)).
target_action_log_probs: A float32 tensor of shape [T, B] containing
target policy action probabilities (log \pi(a_t)).
"""
for i in range(len(behaviour_policy_logits)):
behaviour_policy_logits[i] = tf.convert_to_tensor(
behaviour_policy_logits[i], dtype=tf.float32
)
target_policy_logits[i] = tf.convert_to_tensor(
target_policy_logits[i], dtype=tf.float32
)
# Make sure tensor ranks are as expected.
# The rest will be checked by from_action_log_probs.
behaviour_policy_logits[i].shape.assert_has_rank(3)
target_policy_logits[i].shape.assert_has_rank(3)
with tf1.name_scope(
name,
values=[
behaviour_policy_logits,
target_policy_logits,
actions,
discounts,
rewards,
values,
bootstrap_value,
],
):
target_action_log_probs = multi_log_probs_from_logits_and_actions(
target_policy_logits, actions, dist_class, model
)
if len(behaviour_policy_logits) > 1 or behaviour_action_log_probs is None:
# can't use precalculated values, recompute them. Note that
# recomputing won't work well for autoregressive action dists
# which may have variables not captured by 'logits'
behaviour_action_log_probs = multi_log_probs_from_logits_and_actions(
behaviour_policy_logits, actions, dist_class, model
)
log_rhos = get_log_rhos(target_action_log_probs, behaviour_action_log_probs)
vtrace_returns = from_importance_weights(
log_rhos=log_rhos,
discounts=discounts,
rewards=rewards,
values=values,
bootstrap_value=bootstrap_value,
clip_rho_threshold=clip_rho_threshold,
clip_pg_rho_threshold=clip_pg_rho_threshold,
)
return VTraceFromLogitsReturns(
log_rhos=log_rhos,
behaviour_action_log_probs=behaviour_action_log_probs,
target_action_log_probs=target_action_log_probs,
**vtrace_returns._asdict()
)
def from_importance_weights(
log_rhos,
discounts,
rewards,
values,
bootstrap_value,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
name="vtrace_from_importance_weights",
):
r"""V-trace from log importance weights.
Calculates V-trace actor critic targets as described in
"IMPALA: Scalable Distributed Deep-RL with
Importance Weighted Actor-Learner Architectures"
by Espeholt, Soyer, Munos et al.
In the notation used throughout documentation and comments, T refers to the
time dimension ranging from 0 to T-1. B refers to the batch size. This code
also supports the case where all tensors have the same number of additional
dimensions, e.g., `rewards` is [T, B, C], `values` is [T, B, C],
`bootstrap_value` is [B, C].
Args:
log_rhos: A float32 tensor of shape [T, B] representing the
log importance sampling weights, i.e.
log(target_policy(a) / behaviour_policy(a)). V-trace performs operations
on rhos in log-space for numerical stability.
discounts: A float32 tensor of shape [T, B] with discounts encountered when
following the behaviour policy.
rewards: A float32 tensor of shape [T, B] containing rewards generated by
following the behaviour policy.
values: A float32 tensor of shape [T, B] with the value function estimates
wrt. the target policy.
bootstrap_value: A float32 of shape [B] with the value function estimate at
time T.
clip_rho_threshold: A scalar float32 tensor with the clipping threshold for
importance weights (rho) when calculating the baseline targets (vs).
rho^bar in the paper. If None, no clipping is applied.
clip_pg_rho_threshold: A scalar float32 tensor with the clipping threshold
on rho_s in \rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)). If
None, no clipping is applied.
name: The name scope that all V-trace operations will be created in.
Returns:
A VTraceReturns namedtuple (vs, pg_advantages) where:
vs: A float32 tensor of shape [T, B]. Can be used as target to
train a baseline (V(x_t) - vs_t)^2.
pg_advantages: A float32 tensor of shape [T, B]. Can be used as the
advantage in the calculation of policy gradients.
"""
log_rhos = tf.convert_to_tensor(log_rhos, dtype=tf.float32)
discounts = tf.convert_to_tensor(discounts, dtype=tf.float32)
rewards = tf.convert_to_tensor(rewards, dtype=tf.float32)
values = tf.convert_to_tensor(values, dtype=tf.float32)
bootstrap_value = tf.convert_to_tensor(bootstrap_value, dtype=tf.float32)
if clip_rho_threshold is not None:
clip_rho_threshold = tf.convert_to_tensor(clip_rho_threshold, dtype=tf.float32)
if clip_pg_rho_threshold is not None:
clip_pg_rho_threshold = tf.convert_to_tensor(
clip_pg_rho_threshold, dtype=tf.float32
)
# Make sure tensor ranks are consistent.
rho_rank = log_rhos.shape.ndims # Usually 2.
values.shape.assert_has_rank(rho_rank)
bootstrap_value.shape.assert_has_rank(rho_rank - 1)
discounts.shape.assert_has_rank(rho_rank)
rewards.shape.assert_has_rank(rho_rank)
if clip_rho_threshold is not None:
clip_rho_threshold.shape.assert_has_rank(0)
if clip_pg_rho_threshold is not None:
clip_pg_rho_threshold.shape.assert_has_rank(0)
with tf1.name_scope(
name, values=[log_rhos, discounts, rewards, values, bootstrap_value]
):
rhos = tf.math.exp(log_rhos)
if clip_rho_threshold is not None:
clipped_rhos = tf.minimum(clip_rho_threshold, rhos, name="clipped_rhos")
else:
clipped_rhos = rhos
cs = tf.minimum(1.0, rhos, name="cs")
# Append bootstrapped value to get [v1, ..., v_t+1]
values_t_plus_1 = tf.concat(
[values[1:], tf.expand_dims(bootstrap_value, 0)], axis=0
)
deltas = clipped_rhos * (rewards + discounts * values_t_plus_1 - values)
# All sequences are reversed, computation starts from the back.
sequences = (
tf.reverse(discounts, axis=[0]),
tf.reverse(cs, axis=[0]),
tf.reverse(deltas, axis=[0]),
)
# V-trace vs are calculated through a scan from the back to the
# beginning of the given trajectory.
def scanfunc(acc, sequence_item):
discount_t, c_t, delta_t = sequence_item
return delta_t + discount_t * c_t * acc
initial_values = tf.zeros_like(bootstrap_value)
vs_minus_v_xs = tf.nest.map_structure(
tf.stop_gradient,
tf.scan(
fn=scanfunc,
elems=sequences,
initializer=initial_values,
parallel_iterations=1,
name="scan",
),
)
# Reverse the results back to original order.
vs_minus_v_xs = tf.reverse(vs_minus_v_xs, [0], name="vs_minus_v_xs")
# Add V(x_s) to get v_s.
vs = tf.add(vs_minus_v_xs, values, name="vs")
# Advantage for policy gradient.
vs_t_plus_1 = tf.concat([vs[1:], tf.expand_dims(bootstrap_value, 0)], axis=0)
if clip_pg_rho_threshold is not None:
clipped_pg_rhos = tf.minimum(
clip_pg_rho_threshold, rhos, name="clipped_pg_rhos"
)
else:
clipped_pg_rhos = rhos
pg_advantages = clipped_pg_rhos * (rewards + discounts * vs_t_plus_1 - values)
# Make sure no gradients backpropagated through the returned values.
return VTraceReturns(
vs=tf.stop_gradient(vs), pg_advantages=tf.stop_gradient(pg_advantages)
)
def get_log_rhos(target_action_log_probs, behaviour_action_log_probs):
"""With the selected log_probs for multi-discrete actions of behaviour
and target policies we compute the log_rhos for calculating the vtrace."""
t = tf.stack(target_action_log_probs)
b = tf.stack(behaviour_action_log_probs)
log_rhos = tf.reduce_sum(t - b, axis=0)
return log_rhos
+359
View File
@@ -0,0 +1,359 @@
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""PyTorch version of the functions to compute V-trace off-policy actor critic
targets.
For details and theory see:
"IMPALA: Scalable Distributed Deep-RL with
Importance Weighted Actor-Learner Architectures"
by Espeholt, Soyer, Munos et al.
See https://arxiv.org/abs/1802.01561 for the full paper.
In addition to the original paper's code, changes have been made
to support MultiDiscrete action spaces. behaviour_policy_logits,
target_policy_logits and actions parameters in the entry point
multi_from_logits method accepts lists of tensors instead of just
tensors.
"""
from ray.rllib.algorithms.impala.vtrace_tf import VTraceFromLogitsReturns, VTraceReturns
from ray.rllib.models.torch.torch_action_dist import TorchCategorical
from ray.rllib.utils import force_list
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.torch_utils import convert_to_torch_tensor
torch, nn = try_import_torch()
def log_probs_from_logits_and_actions(
policy_logits, actions, dist_class=TorchCategorical, model=None
):
return multi_log_probs_from_logits_and_actions(
[policy_logits], [actions], dist_class, model
)[0]
def multi_log_probs_from_logits_and_actions(policy_logits, actions, dist_class, model):
"""Computes action log-probs from policy logits and actions.
In the notation used throughout documentation and comments, T refers to the
time dimension ranging from 0 to T-1. B refers to the batch size and
ACTION_SPACE refers to the list of numbers each representing a number of
actions.
Args:
policy_logits: A list with length of ACTION_SPACE of float32
tensors of shapes [T, B, ACTION_SPACE[0]], ...,
[T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities
parameterizing a softmax policy.
actions: A list with length of ACTION_SPACE of tensors of shapes
[T, B, ...], ..., [T, B, ...]
with actions.
dist_class: Python class of the action distribution.
Returns:
A list with length of ACTION_SPACE of float32 tensors of shapes
[T, B], ..., [T, B] corresponding to the sampling log probability
of the chosen action w.r.t. the policy.
"""
log_probs = []
for i in range(len(policy_logits)):
p_shape = policy_logits[i].shape
a_shape = actions[i].shape
policy_logits_flat = torch.reshape(policy_logits[i], (-1,) + tuple(p_shape[2:]))
actions_flat = torch.reshape(actions[i], (-1,) + tuple(a_shape[2:]))
log_probs.append(
torch.reshape(
dist_class(policy_logits_flat, model).logp(actions_flat), a_shape[:2]
)
)
return log_probs
def from_logits(
behaviour_policy_logits,
target_policy_logits,
actions,
discounts,
rewards,
values,
bootstrap_value,
dist_class=TorchCategorical,
model=None,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
):
"""multi_from_logits wrapper used only for tests"""
res = multi_from_logits(
[behaviour_policy_logits],
[target_policy_logits],
[actions],
discounts,
rewards,
values,
bootstrap_value,
dist_class,
model,
clip_rho_threshold=clip_rho_threshold,
clip_pg_rho_threshold=clip_pg_rho_threshold,
)
assert len(res.behaviour_action_log_probs) == 1
assert len(res.target_action_log_probs) == 1
return VTraceFromLogitsReturns(
vs=res.vs,
pg_advantages=res.pg_advantages,
log_rhos=res.log_rhos,
behaviour_action_log_probs=res.behaviour_action_log_probs[0],
target_action_log_probs=res.target_action_log_probs[0],
)
def multi_from_logits(
behaviour_policy_logits,
target_policy_logits,
actions,
discounts,
rewards,
values,
bootstrap_value,
dist_class,
model,
behaviour_action_log_probs=None,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
):
r"""V-trace for softmax policies.
Calculates V-trace actor critic targets for softmax polices as described in
"IMPALA: Scalable Distributed Deep-RL with
Importance Weighted Actor-Learner Architectures"
by Espeholt, Soyer, Munos et al.
Target policy refers to the policy we are interested in improving and
behaviour policy refers to the policy that generated the given
rewards and actions.
In the notation used throughout documentation and comments, T refers to the
time dimension ranging from 0 to T-1. B refers to the batch size and
ACTION_SPACE refers to the list of numbers each representing a number of
actions.
Args:
behaviour_policy_logits: A list with length of ACTION_SPACE of float32
tensors of shapes [T, B, ACTION_SPACE[0]], ...,
[T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities
parameterizing the softmax behavior policy.
target_policy_logits: A list with length of ACTION_SPACE of float32
tensors of shapes [T, B, ACTION_SPACE[0]], ...,
[T, B, ACTION_SPACE[-1]] with un-normalized log-probabilities
parameterizing the softmax target policy.
actions: A list with length of ACTION_SPACE of tensors of shapes
[T, B, ...], ..., [T, B, ...]
with actions sampled from the behavior policy.
discounts: A float32 tensor of shape [T, B] with the discount
encountered when following the behavior policy.
rewards: A float32 tensor of shape [T, B] with the rewards generated by
following the behavior policy.
values: A float32 tensor of shape [T, B] with the value function
estimates wrt. the target policy.
bootstrap_value: A float32 of shape [B] with the value function
estimate at time T.
dist_class: action distribution class for the logits.
model: backing ModelV2 instance
behaviour_action_log_probs: Precalculated values of the behavior
actions.
clip_rho_threshold: A scalar float32 tensor with the clipping threshold
for importance weights (rho) when calculating the baseline targets
(vs). rho^bar in the paper.
clip_pg_rho_threshold: A scalar float32 tensor with the clipping
threshold on rho_s in:
\rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)).
Returns:
A `VTraceFromLogitsReturns` namedtuple with the following fields:
vs: A float32 tensor of shape [T, B]. Can be used as target to train a
baseline (V(x_t) - vs_t)^2.
pg_advantages: A float 32 tensor of shape [T, B]. Can be used as an
estimate of the advantage in the calculation of policy gradients.
log_rhos: A float32 tensor of shape [T, B] containing the log
importance sampling weights (log rhos).
behaviour_action_log_probs: A float32 tensor of shape [T, B] containing
behaviour policy action log probabilities (log \mu(a_t)).
target_action_log_probs: A float32 tensor of shape [T, B] containing
target policy action probabilities (log \pi(a_t)).
"""
behaviour_policy_logits = convert_to_torch_tensor(
behaviour_policy_logits, device="cpu"
)
target_policy_logits = convert_to_torch_tensor(target_policy_logits, device="cpu")
actions = convert_to_torch_tensor(actions, device="cpu")
# Make sure tensor ranks are as expected.
# The rest will be checked by from_action_log_probs.
for i in range(len(behaviour_policy_logits)):
assert len(behaviour_policy_logits[i].size()) == 3
assert len(target_policy_logits[i].size()) == 3
target_action_log_probs = multi_log_probs_from_logits_and_actions(
target_policy_logits, actions, dist_class, model
)
if len(behaviour_policy_logits) > 1 or behaviour_action_log_probs is None:
# can't use precalculated values, recompute them. Note that
# recomputing won't work well for autoregressive action dists
# which may have variables not captured by 'logits'
behaviour_action_log_probs = multi_log_probs_from_logits_and_actions(
behaviour_policy_logits, actions, dist_class, model
)
behaviour_action_log_probs = convert_to_torch_tensor(
behaviour_action_log_probs, device="cpu"
)
behaviour_action_log_probs = force_list(behaviour_action_log_probs)
# log_rhos = target_logp - behavior_logp
log_rhos = get_log_rhos(target_action_log_probs, behaviour_action_log_probs)
vtrace_returns = from_importance_weights(
log_rhos=log_rhos,
discounts=discounts,
rewards=rewards,
values=values,
bootstrap_value=bootstrap_value,
clip_rho_threshold=clip_rho_threshold,
clip_pg_rho_threshold=clip_pg_rho_threshold,
)
return VTraceFromLogitsReturns(
log_rhos=log_rhos,
behaviour_action_log_probs=behaviour_action_log_probs,
target_action_log_probs=target_action_log_probs,
**vtrace_returns._asdict()
)
def from_importance_weights(
log_rhos,
discounts,
rewards,
values,
bootstrap_value,
clip_rho_threshold=1.0,
clip_pg_rho_threshold=1.0,
):
r"""V-trace from log importance weights.
Calculates V-trace actor critic targets as described in
"IMPALA: Scalable Distributed Deep-RL with
Importance Weighted Actor-Learner Architectures"
by Espeholt, Soyer, Munos et al.
In the notation used throughout documentation and comments, T refers to the
time dimension ranging from 0 to T-1. B refers to the batch size. This code
also supports the case where all tensors have the same number of additional
dimensions, e.g., `rewards` is [T, B, C], `values` is [T, B, C],
`bootstrap_value` is [B, C].
Args:
log_rhos: A float32 tensor of shape [T, B] representing the log
importance sampling weights, i.e.
log(target_policy(a) / behaviour_policy(a)). V-trace performs
operations on rhos in log-space for numerical stability.
discounts: A float32 tensor of shape [T, B] with discounts encountered
when following the behaviour policy.
rewards: A float32 tensor of shape [T, B] containing rewards generated
by following the behaviour policy.
values: A float32 tensor of shape [T, B] with the value function
estimates wrt. the target policy.
bootstrap_value: A float32 of shape [B] with the value function
estimate at time T.
clip_rho_threshold: A scalar float32 tensor with the clipping threshold
for importance weights (rho) when calculating the baseline targets
(vs). rho^bar in the paper. If None, no clipping is applied.
clip_pg_rho_threshold: A scalar float32 tensor with the clipping
threshold on rho_s in
\rho_s \delta log \pi(a|x) (r + \gamma v_{s+1} - V(x_s)).
If None, no clipping is applied.
Returns:
A VTraceReturns namedtuple (vs, pg_advantages) where:
vs: A float32 tensor of shape [T, B]. Can be used as target to
train a baseline (V(x_t) - vs_t)^2.
pg_advantages: A float32 tensor of shape [T, B]. Can be used as the
advantage in the calculation of policy gradients.
"""
log_rhos = convert_to_torch_tensor(log_rhos, device="cpu")
discounts = convert_to_torch_tensor(discounts, device="cpu")
rewards = convert_to_torch_tensor(rewards, device="cpu")
values = convert_to_torch_tensor(values, device="cpu")
bootstrap_value = convert_to_torch_tensor(bootstrap_value, device="cpu")
# Make sure tensor ranks are consistent.
rho_rank = len(log_rhos.size()) # Usually 2.
assert rho_rank == len(values.size())
assert rho_rank - 1 == len(bootstrap_value.size()), "must have rank {}".format(
rho_rank - 1
)
assert rho_rank == len(discounts.size())
assert rho_rank == len(rewards.size())
rhos = torch.exp(log_rhos)
if clip_rho_threshold is not None:
clipped_rhos = torch.clamp_max(rhos, clip_rho_threshold)
else:
clipped_rhos = rhos
cs = torch.clamp_max(rhos, 1.0)
# Append bootstrapped value to get [v1, ..., v_t+1]
values_t_plus_1 = torch.cat(
[values[1:], torch.unsqueeze(bootstrap_value, 0)], dim=0
)
deltas = clipped_rhos * (rewards + discounts * values_t_plus_1 - values)
vs_minus_v_xs = [torch.zeros_like(bootstrap_value)]
for i in reversed(range(len(discounts))):
discount_t, c_t, delta_t = discounts[i], cs[i], deltas[i]
vs_minus_v_xs.append(delta_t + discount_t * c_t * vs_minus_v_xs[-1])
vs_minus_v_xs = torch.stack(vs_minus_v_xs[1:])
# Reverse the results back to original order.
vs_minus_v_xs = torch.flip(vs_minus_v_xs, dims=[0])
# Add V(x_s) to get v_s.
vs = vs_minus_v_xs + values
# Advantage for policy gradient.
vs_t_plus_1 = torch.cat([vs[1:], torch.unsqueeze(bootstrap_value, 0)], dim=0)
if clip_pg_rho_threshold is not None:
clipped_pg_rhos = torch.clamp_max(rhos, clip_pg_rho_threshold)
else:
clipped_pg_rhos = rhos
pg_advantages = clipped_pg_rhos * (rewards + discounts * vs_t_plus_1 - values)
# Make sure no gradients backpropagated through the returned values.
return VTraceReturns(vs=vs.detach(), pg_advantages=pg_advantages.detach())
def get_log_rhos(target_action_log_probs, behaviour_action_log_probs):
"""With the selected log_probs for multi-discrete actions of behavior
and target policies we compute the log_rhos for calculating the vtrace."""
t = torch.stack(target_action_log_probs)
b = torch.stack(behaviour_action_log_probs)
log_rhos = torch.sum(t - b, dim=0)
return log_rhos