mirror of
https://github.com/wassname/ray.git
synced 2026-07-21 12:50:45 +08:00
[rllib] Remove deprecated policy optimizer package. (#9262)
This commit is contained in:
@@ -209,77 +209,76 @@ In the above section you saw how to compose a simple policy gradient algorithm w
|
||||
name="PPOTrainer",
|
||||
default_config=DEFAULT_CONFIG,
|
||||
default_policy=PPOTFPolicy,
|
||||
make_policy_optimizer=choose_policy_optimizer,
|
||||
validate_config=validate_config,
|
||||
after_optimizer_step=update_kl,
|
||||
before_train_step=warn_about_obs_filter,
|
||||
after_train_result=warn_about_bad_reward_scales)
|
||||
execution_plan=execution_plan)
|
||||
|
||||
Besides some boilerplate for defining the PPO configuration and some warnings, there are two important arguments to take note of here: ``make_policy_optimizer=choose_policy_optimizer``, and ``after_optimizer_step=update_kl``.
|
||||
Besides some boilerplate for defining the PPO configuration and some warnings, the most important argument to take note of is the ``execution_plan``.
|
||||
|
||||
.. warning::
|
||||
|
||||
Policy optimizers are deprecated. This documentation will be updated in the future.
|
||||
|
||||
The ``choose_policy_optimizer`` function chooses which `Policy Optimizer <#policy-optimization>`__ to use for distributed training. You can think of these policy optimizers as coordinating the distributed workflow needed to improve the policy. Depending on the trainer config, PPO can switch between a simple synchronous optimizer, or a multi-GPU optimizer that implements minibatch SGD (the default):
|
||||
The trainer's `execution plan <#execution-plan>`__ defines the distributed training workflow. Depending on the ``simple_optimizer`` trainer config, PPO can switch between a simple synchronous plan, or a multi-GPU plan that implements minibatch SGD (the default):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def choose_policy_optimizer(workers, config):
|
||||
def execution_plan(workers, config):
|
||||
rollouts = ParallelRollouts(workers, mode="bulk_sync")
|
||||
|
||||
# Collect large batches of relevant experiences & standardize.
|
||||
rollouts = rollouts.for_each(
|
||||
SelectExperiences(workers.trainable_policies()))
|
||||
rollouts = rollouts.combine(
|
||||
ConcatBatches(min_batch_size=config["train_batch_size"]))
|
||||
rollouts = rollouts.for_each(StandardizeFields(["advantages"]))
|
||||
|
||||
if config["simple_optimizer"]:
|
||||
return SyncSamplesOptimizer(
|
||||
workers,
|
||||
num_sgd_iter=config["num_sgd_iter"],
|
||||
train_batch_size=config["train_batch_size"])
|
||||
train_op = rollouts.for_each(
|
||||
TrainOneStep(
|
||||
workers,
|
||||
num_sgd_iter=config["num_sgd_iter"],
|
||||
sgd_minibatch_size=config["sgd_minibatch_size"]))
|
||||
else:
|
||||
train_op = rollouts.for_each(
|
||||
TrainTFMultiGPU(
|
||||
workers,
|
||||
sgd_minibatch_size=config["sgd_minibatch_size"],
|
||||
num_sgd_iter=config["num_sgd_iter"],
|
||||
num_gpus=config["num_gpus"],
|
||||
rollout_fragment_length=config["rollout_fragment_length"],
|
||||
num_envs_per_worker=config["num_envs_per_worker"],
|
||||
train_batch_size=config["train_batch_size"],
|
||||
shuffle_sequences=config["shuffle_sequences"],
|
||||
_fake_gpus=config["_fake_gpus"]))
|
||||
|
||||
return LocalMultiGPUOptimizer(
|
||||
workers,
|
||||
sgd_batch_size=config["sgd_minibatch_size"],
|
||||
num_sgd_iter=config["num_sgd_iter"],
|
||||
num_gpus=config["num_gpus"],
|
||||
rollout_fragment_length=config["rollout_fragment_length"],
|
||||
num_envs_per_worker=config["num_envs_per_worker"],
|
||||
train_batch_size=config["train_batch_size"],
|
||||
standardize_fields=["advantages"],
|
||||
straggler_mitigation=config["straggler_mitigation"])
|
||||
# Update KL after each round of training.
|
||||
train_op = train_op.for_each(lambda t: t[1]).for_each(UpdateKL(workers))
|
||||
|
||||
Suppose we want to customize PPO to use an asynchronous-gradient optimization strategy similar to A3C. To do that, we could define a new function that returns ``AsyncGradientsOptimizer`` and override the ``make_policy_optimizer`` component of ``PPOTrainer``.
|
||||
return StandardMetricsReporting(train_op, workers, config) \
|
||||
.for_each(lambda result: warn_about_bad_reward_scales(config, result))
|
||||
|
||||
Suppose we want to customize PPO to use an asynchronous-gradient optimization strategy similar to A3C. To do that, we could swap out its execution plan to that of A3C's:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from ray.rllib.agents.ppo import PPOTrainer
|
||||
from ray.rllib.optimizers import AsyncGradientsOptimizer
|
||||
from ray.rllib.execution.rollout_ops import AsyncGradients
|
||||
from ray.rllib.execution.train_ops import ApplyGradients
|
||||
from ray.rllib.execution.metric_ops import StandardMetricsReporting
|
||||
|
||||
def make_async_optimizer(workers, config):
|
||||
return AsyncGradientsOptimizer(workers, grads_per_step=100)
|
||||
def a3c_execution_plan(workers, config):
|
||||
# For A3C, compute policy gradients remotely on the rollout workers.
|
||||
grads = AsyncGradients(workers)
|
||||
|
||||
# Apply the gradients as they arrive. We set update_all to False so that
|
||||
# only the worker sending the gradient is updated with new weights.
|
||||
train_op = grads.for_each(ApplyGradients(workers, update_all=False))
|
||||
|
||||
return StandardMetricsReporting(train_op, workers, config)
|
||||
|
||||
CustomTrainer = PPOTrainer.with_updates(
|
||||
make_policy_optimizer=make_async_optimizer)
|
||||
execution_plan=a3c_execution_plan)
|
||||
|
||||
|
||||
The ``with_updates`` method that we use here is also available for Torch and TF policies built from templates.
|
||||
|
||||
Now let's take a look at the ``update_kl`` function. This is used to adaptively adjust the KL penalty coefficient on the PPO loss, which bounds the policy change per training step. You'll notice the code handles both single and multi-agent cases (where there are be multiple policies each with different KL coeffs):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def update_kl(trainer, fetches):
|
||||
if "kl" in fetches:
|
||||
# single-agent
|
||||
trainer.workers.local_worker().for_policy(
|
||||
lambda pi: pi.update_kl(fetches["kl"]))
|
||||
else:
|
||||
|
||||
def update(pi, pi_id):
|
||||
if pi_id in fetches:
|
||||
pi.update_kl(fetches[pi_id]["kl"])
|
||||
else:
|
||||
logger.debug("No data for {}, not updating kl".format(pi_id))
|
||||
|
||||
# multi-agent
|
||||
trainer.workers.local_worker().foreach_trainable_policy(update)
|
||||
|
||||
The ``update_kl`` method on the policy is defined in `PPOTFPolicy <https://github.com/ray-project/ray/blob/master/rllib/agents/ppo/ppo_tf_policy.py>`__ via the ``KLCoeffMixin``, along with several other advanced features. Let's look at each new feature used by the policy:
|
||||
Now let's look at each PPO policy definition:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -582,33 +581,12 @@ Here is an example of creating a set of rollout workers and using them gather ex
|
||||
for w in workers.remote_workers():
|
||||
w.set_weights.remote(weights)
|
||||
|
||||
Policy Optimization
|
||||
-------------------
|
||||
Execution Plan
|
||||
--------------
|
||||
|
||||
.. warning::
|
||||
|
||||
Policy optimizers are deprecated. This documentation will be updated in the future.
|
||||
|
||||
Similar to how a `gradient-descent optimizer <https://www.tensorflow.org/api_docs/python/tf/train/GradientDescentOptimizer>`__ can be used to improve a model, RLlib's `policy optimizers <https://github.com/ray-project/ray/tree/master/rllib/optimizers>`__ implement different strategies for improving a policy.
|
||||
|
||||
For example, in A3C you'd want to compute gradients asynchronously on different workers, and apply them to a central policy replica. This strategy is implemented by the `AsyncGradientsOptimizer <https://github.com/ray-project/ray/blob/master/rllib/optimizers/async_gradients_optimizer.py>`__. Another alternative is to gather experiences synchronously in parallel and optimize the model centrally, as in `SyncSamplesOptimizer <https://github.com/ray-project/ray/blob/master/rllib/optimizers/sync_samples_optimizer.py>`__. Policy optimizers abstract these strategies away into reusable modules.
|
||||
|
||||
This is how the example in the previous section looks when written using a policy optimizer:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Same setup as before
|
||||
workers = WorkerSet(
|
||||
policy=CustomPolicy,
|
||||
env_creator=lambda c: gym.make("CartPole-v0"),
|
||||
num_workers=10)
|
||||
|
||||
# this optimizer implements the IMPALA architecture
|
||||
optimizer = AsyncSamplesOptimizer(workers, train_batch_size=500)
|
||||
|
||||
while True:
|
||||
optimizer.step()
|
||||
.. note::
|
||||
|
||||
Policy optimizers have been replaced by the execution plan API. This documentation will be updated in the future.
|
||||
|
||||
Trainers
|
||||
--------
|
||||
|
||||
+5
-39
@@ -18,7 +18,6 @@ from ray.rllib.models import MODEL_DEFAULTS
|
||||
from ray.rllib.policy import Policy
|
||||
from ray.rllib.policy.sample_batch import DEFAULT_POLICY_ID
|
||||
from ray.rllib.evaluation.metrics import collect_metrics
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.evaluation.worker_set import WorkerSet
|
||||
from ray.rllib.utils import FilterManager, deep_update, merge_dicts
|
||||
from ray.rllib.utils.spaces import space_utils
|
||||
@@ -492,14 +491,6 @@ class Trainer(Trainable):
|
||||
def train(self) -> ResultDict:
|
||||
"""Overrides super.train to synchronize global vars."""
|
||||
|
||||
if self._has_policy_optimizer():
|
||||
self.global_vars["timestep"] = self.optimizer.num_steps_sampled
|
||||
self.optimizer.workers.local_worker().set_global_vars(
|
||||
self.global_vars)
|
||||
for w in self.optimizer.workers.remote_workers():
|
||||
w.set_global_vars.remote(self.global_vars)
|
||||
logger.debug("updated global vars: {}".format(self.global_vars))
|
||||
|
||||
result = None
|
||||
for _ in range(1 + MAX_WORKER_FAILURE_RETRIES):
|
||||
try:
|
||||
@@ -526,10 +517,6 @@ class Trainer(Trainable):
|
||||
if hasattr(self, "workers") and isinstance(self.workers, WorkerSet):
|
||||
self._sync_filters_if_needed(self.workers)
|
||||
|
||||
if self._has_policy_optimizer():
|
||||
result["num_healthy_workers"] = len(
|
||||
self.optimizer.workers.remote_workers())
|
||||
|
||||
if self.config["evaluation_interval"] == 1 or (
|
||||
self._iteration > 0 and self.config["evaluation_interval"]
|
||||
and self._iteration % self.config["evaluation_interval"] == 0):
|
||||
@@ -1095,15 +1082,8 @@ class Trainer(Trainable):
|
||||
an error is raised.
|
||||
"""
|
||||
|
||||
if (not self._has_policy_optimizer()
|
||||
and not hasattr(self, "execution_plan")):
|
||||
raise NotImplementedError(
|
||||
"Recovery is not supported for this algorithm")
|
||||
if self._has_policy_optimizer():
|
||||
workers = self.optimizer.workers
|
||||
else:
|
||||
assert hasattr(self, "execution_plan")
|
||||
workers = self.workers
|
||||
assert hasattr(self, "execution_plan")
|
||||
workers = self.workers
|
||||
|
||||
logger.info("Health checking all workers...")
|
||||
checks = []
|
||||
@@ -1129,23 +1109,9 @@ class Trainer(Trainable):
|
||||
raise RuntimeError(
|
||||
"Not enough healthy workers remain to continue.")
|
||||
|
||||
if self._has_policy_optimizer():
|
||||
self.optimizer.reset(healthy_workers)
|
||||
else:
|
||||
assert hasattr(self, "execution_plan")
|
||||
logger.warning("Recreating execution plan after failure")
|
||||
workers.reset(healthy_workers)
|
||||
self.train_exec_impl = self.execution_plan(workers, self.config)
|
||||
|
||||
def _has_policy_optimizer(self):
|
||||
"""Whether this Trainer has a PolicyOptimizer as `optimizer` property.
|
||||
|
||||
Returns:
|
||||
bool: True if this Trainer holds a PolicyOptimizer object in
|
||||
property `self.optimizer`.
|
||||
"""
|
||||
return hasattr(self, "optimizer") and isinstance(
|
||||
self.optimizer, PolicyOptimizer)
|
||||
logger.warning("Recreating execution plan after failure")
|
||||
workers.reset(healthy_workers)
|
||||
self.train_exec_impl = self.execution_plan(workers, self.config)
|
||||
|
||||
@override(Trainable)
|
||||
def _export_model(self, export_formats: List[str],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import Callable, Optional, List, Iterable
|
||||
|
||||
from ray.rllib.agents.trainer import Trainer, COMMON_CONFIG
|
||||
@@ -10,7 +9,6 @@ from ray.rllib.execution.metric_ops import StandardMetricsReporting
|
||||
from ray.rllib.policy import Policy
|
||||
from ray.rllib.utils import add_mixins
|
||||
from ray.rllib.utils.annotations import override, DeveloperAPI
|
||||
from ray.rllib.utils.deprecation import deprecation_warning
|
||||
from ray.rllib.utils.types import TrainerConfigDict, ResultDict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -39,16 +37,9 @@ def build_trainer(
|
||||
*,
|
||||
default_config: TrainerConfigDict = None,
|
||||
validate_config: Callable[[TrainerConfigDict], None] = None,
|
||||
get_initial_state=None, # DEPRECATED
|
||||
get_policy_class: Callable[[TrainerConfigDict], Policy] = None,
|
||||
before_init: Callable[[Trainer], None] = None,
|
||||
make_workers=None, # DEPRECATED
|
||||
make_policy_optimizer=None, # DEPRECATED
|
||||
after_init: Callable[[Trainer], None] = None,
|
||||
before_train_step=None, # DEPRECATED
|
||||
after_optimizer_step=None, # DEPRECATED
|
||||
after_train_result=None, # DEPRECATED
|
||||
collect_metrics_fn=None, # DEPRECATED
|
||||
before_evaluate_fn: Callable[[Trainer], None] = None,
|
||||
mixins: List[type] = None,
|
||||
execution_plan: Callable[[WorkerSet, TrainerConfigDict], Iterable[
|
||||
@@ -100,11 +91,6 @@ def build_trainer(
|
||||
if validate_config:
|
||||
validate_config(config)
|
||||
|
||||
if get_initial_state:
|
||||
deprecation_warning("get_initial_state", "execution_plan")
|
||||
self.state = get_initial_state(self)
|
||||
else:
|
||||
self.state = {}
|
||||
if get_policy_class is None:
|
||||
self._policy = default_policy
|
||||
else:
|
||||
@@ -112,68 +98,15 @@ def build_trainer(
|
||||
if before_init:
|
||||
before_init(self)
|
||||
# Creating all workers (excluding evaluation workers).
|
||||
if make_workers and not execution_plan:
|
||||
deprecation_warning("make_workers", "execution_plan")
|
||||
self.workers = make_workers(self, env_creator, self._policy,
|
||||
config)
|
||||
else:
|
||||
self.workers = self._make_workers(env_creator, self._policy,
|
||||
config,
|
||||
self.config["num_workers"])
|
||||
self.train_exec_impl = None
|
||||
self.optimizer = None
|
||||
self.workers = self._make_workers(
|
||||
env_creator, self._policy, config, self.config["num_workers"])
|
||||
self.execution_plan = execution_plan
|
||||
|
||||
if make_policy_optimizer:
|
||||
deprecation_warning("make_policy_optimizer", "execution_plan")
|
||||
self.optimizer = make_policy_optimizer(self.workers, config)
|
||||
else:
|
||||
assert execution_plan is not None
|
||||
self.train_exec_impl = execution_plan(self.workers, config)
|
||||
self.train_exec_impl = execution_plan(self.workers, config)
|
||||
if after_init:
|
||||
after_init(self)
|
||||
|
||||
@override(Trainer)
|
||||
def step(self):
|
||||
if self.train_exec_impl:
|
||||
return self._train_exec_impl()
|
||||
|
||||
if before_train_step:
|
||||
deprecation_warning("before_train_step", "execution_plan")
|
||||
before_train_step(self)
|
||||
prev_steps = self.optimizer.num_steps_sampled
|
||||
|
||||
start = time.time()
|
||||
optimizer_steps_this_iter = 0
|
||||
while True:
|
||||
fetches = self.optimizer.step()
|
||||
optimizer_steps_this_iter += 1
|
||||
if after_optimizer_step:
|
||||
deprecation_warning("after_optimizer_step",
|
||||
"execution_plan")
|
||||
after_optimizer_step(self, fetches)
|
||||
if (time.time() - start >= self.config["min_iter_time_s"]
|
||||
and self.optimizer.num_steps_sampled - prev_steps >=
|
||||
self.config["timesteps_per_iteration"]):
|
||||
break
|
||||
|
||||
if collect_metrics_fn:
|
||||
deprecation_warning("collect_metrics_fn", "execution_plan")
|
||||
res = collect_metrics_fn(self)
|
||||
else:
|
||||
res = self.collect_metrics()
|
||||
res.update(
|
||||
optimizer_steps_this_iter=optimizer_steps_this_iter,
|
||||
timesteps_this_iter=self.optimizer.num_steps_sampled -
|
||||
prev_steps,
|
||||
info=res.get("info", {}))
|
||||
|
||||
if after_train_result:
|
||||
deprecation_warning("after_train_result", "execution_plan")
|
||||
after_train_result(self, res)
|
||||
return res
|
||||
|
||||
def _train_exec_impl(self):
|
||||
res = next(self.train_exec_impl)
|
||||
return res
|
||||
|
||||
@@ -184,18 +117,14 @@ def build_trainer(
|
||||
|
||||
def __getstate__(self):
|
||||
state = Trainer.__getstate__(self)
|
||||
state["trainer_state"] = self.state.copy()
|
||||
if self.train_exec_impl:
|
||||
state["train_exec_impl"] = (
|
||||
self.train_exec_impl.shared_metrics.get().save())
|
||||
state["train_exec_impl"] = (
|
||||
self.train_exec_impl.shared_metrics.get().save())
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
Trainer.__setstate__(self, state)
|
||||
self.state = state["trainer_state"].copy()
|
||||
if self.train_exec_impl:
|
||||
self.train_exec_impl.shared_metrics.get().restore(
|
||||
state["train_exec_impl"])
|
||||
self.train_exec_impl.shared_metrics.get().restore(
|
||||
state["train_exec_impl"])
|
||||
|
||||
def with_updates(**overrides):
|
||||
"""Build a copy of this trainer with the specified overrides.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
This directory is deprecated; all files in it will be removed in a future release.
|
||||
@@ -1,26 +0,0 @@
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.optimizers.async_replay_optimizer import AsyncReplayOptimizer
|
||||
from ray.rllib.optimizers.async_samples_optimizer import AsyncSamplesOptimizer
|
||||
from ray.rllib.optimizers.async_gradients_optimizer import \
|
||||
AsyncGradientsOptimizer
|
||||
from ray.rllib.optimizers.sync_samples_optimizer import SyncSamplesOptimizer
|
||||
from ray.rllib.optimizers.sync_replay_optimizer import SyncReplayOptimizer
|
||||
from ray.rllib.optimizers.sync_batch_replay_optimizer import \
|
||||
SyncBatchReplayOptimizer
|
||||
from ray.rllib.optimizers.microbatch_optimizer import MicrobatchOptimizer
|
||||
from ray.rllib.optimizers.multi_gpu_optimizer import LocalMultiGPUOptimizer
|
||||
from ray.rllib.optimizers.torch_distributed_data_parallel_optimizer import \
|
||||
TorchDistributedDataParallelOptimizer
|
||||
|
||||
__all__ = [
|
||||
"PolicyOptimizer",
|
||||
"AsyncReplayOptimizer",
|
||||
"AsyncSamplesOptimizer",
|
||||
"AsyncGradientsOptimizer",
|
||||
"MicrobatchOptimizer",
|
||||
"SyncSamplesOptimizer",
|
||||
"SyncReplayOptimizer",
|
||||
"LocalMultiGPUOptimizer",
|
||||
"SyncBatchReplayOptimizer",
|
||||
"TorchDistributedDataParallelOptimizer",
|
||||
]
|
||||
@@ -1,207 +0,0 @@
|
||||
"""Helper class for AsyncSamplesOptimizer."""
|
||||
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
import ray
|
||||
from ray.rllib.utils.actors import TaskPool
|
||||
from ray.rllib.utils.annotations import override
|
||||
|
||||
|
||||
class Aggregator:
|
||||
"""An aggregator collects and processes samples from workers.
|
||||
|
||||
This class is used to abstract away the strategy for sample collection.
|
||||
For example, you may want to use a tree of actors to collect samples. The
|
||||
use of multiple actors can be necessary to offload expensive work such
|
||||
as concatenating and decompressing sample batches.
|
||||
|
||||
Attributes:
|
||||
local_worker: local RolloutWorker copy
|
||||
"""
|
||||
|
||||
def iter_train_batches(self):
|
||||
"""Returns a generator over batches ready to learn on.
|
||||
|
||||
Iterating through this generator will also send out weight updates to
|
||||
remote workers as needed.
|
||||
|
||||
This call may block until results are available.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def broadcast_new_weights(self):
|
||||
"""Broadcast a new set of weights from the local workers."""
|
||||
raise NotImplementedError
|
||||
|
||||
def should_broadcast(self):
|
||||
"""Returns whether broadcast() should be called to update weights."""
|
||||
raise NotImplementedError
|
||||
|
||||
def stats(self):
|
||||
"""Returns runtime statistics for debugging."""
|
||||
raise NotImplementedError
|
||||
|
||||
def reset(self, remote_workers):
|
||||
"""Called to change the set of remote workers being used."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AggregationWorkerBase:
|
||||
"""Aggregators should extend from this class."""
|
||||
|
||||
def __init__(self, initial_weights_obj_id, remote_workers,
|
||||
max_sample_requests_in_flight_per_worker, replay_proportion,
|
||||
replay_buffer_num_slots, train_batch_size,
|
||||
rollout_fragment_length):
|
||||
"""Initialize an aggregator.
|
||||
|
||||
Arguments:
|
||||
initial_weights_obj_id (ObjectID): initial worker weights
|
||||
remote_workers (list): set of remote workers assigned to this agg
|
||||
max_sample_request_in_flight_per_worker (int): max queue size per
|
||||
worker
|
||||
replay_proportion (float): ratio of replay to sampled outputs
|
||||
replay_buffer_num_slots (int): max number of sample batches to
|
||||
store in the replay buffer
|
||||
train_batch_size (int): size of batches to learn on
|
||||
rollout_fragment_length (int): size of batches to sample from
|
||||
workers.
|
||||
"""
|
||||
|
||||
self.broadcasted_weights = initial_weights_obj_id
|
||||
self.remote_workers = remote_workers
|
||||
self.rollout_fragment_length = rollout_fragment_length
|
||||
self.train_batch_size = train_batch_size
|
||||
|
||||
if replay_proportion:
|
||||
if (replay_buffer_num_slots * rollout_fragment_length <=
|
||||
train_batch_size):
|
||||
raise ValueError(
|
||||
"Replay buffer size is too small to produce train, "
|
||||
"please increase replay_buffer_num_slots.",
|
||||
replay_buffer_num_slots, rollout_fragment_length,
|
||||
train_batch_size)
|
||||
|
||||
# Kick off async background sampling
|
||||
self.sample_tasks = TaskPool()
|
||||
for ev in self.remote_workers:
|
||||
ev.set_weights.remote(self.broadcasted_weights)
|
||||
for _ in range(max_sample_requests_in_flight_per_worker):
|
||||
self.sample_tasks.add(ev, ev.sample.remote())
|
||||
|
||||
self.batch_buffer = []
|
||||
|
||||
self.replay_proportion = replay_proportion
|
||||
self.replay_buffer_num_slots = replay_buffer_num_slots
|
||||
self.replay_batches = []
|
||||
self.replay_index = 0
|
||||
self.num_sent_since_broadcast = 0
|
||||
self.num_weight_syncs = 0
|
||||
self.num_replayed = 0
|
||||
|
||||
@override(Aggregator)
|
||||
def iter_train_batches(self, max_yield=999):
|
||||
"""Iterate over train batches.
|
||||
|
||||
Arguments:
|
||||
max_yield (int): Max number of batches to iterate over in this
|
||||
cycle. Setting this avoids iter_train_batches returning too
|
||||
much data at once.
|
||||
"""
|
||||
|
||||
for ev, sample_batch in self._augment_with_replay(
|
||||
self.sample_tasks.completed_prefetch(
|
||||
blocking_wait=True, max_yield=max_yield)):
|
||||
sample_batch.decompress_if_needed()
|
||||
self.batch_buffer.append(sample_batch)
|
||||
if sum(b.count
|
||||
for b in self.batch_buffer) >= self.train_batch_size:
|
||||
if len(self.batch_buffer) == 1:
|
||||
# make a defensive copy to avoid sharing plasma memory
|
||||
# across multiple threads
|
||||
train_batch = self.batch_buffer[0].copy()
|
||||
else:
|
||||
train_batch = self.batch_buffer[0].concat_samples(
|
||||
self.batch_buffer)
|
||||
yield train_batch
|
||||
self.batch_buffer = []
|
||||
|
||||
# If the batch was replayed, skip the update below.
|
||||
if ev is None:
|
||||
continue
|
||||
|
||||
# Put in replay buffer if enabled
|
||||
if self.replay_buffer_num_slots > 0:
|
||||
if len(self.replay_batches) < self.replay_buffer_num_slots:
|
||||
self.replay_batches.append(sample_batch)
|
||||
else:
|
||||
self.replay_batches[self.replay_index] = sample_batch
|
||||
self.replay_index += 1
|
||||
self.replay_index %= self.replay_buffer_num_slots
|
||||
|
||||
ev.set_weights.remote(self.broadcasted_weights)
|
||||
self.num_weight_syncs += 1
|
||||
self.num_sent_since_broadcast += 1
|
||||
|
||||
# Kick off another sample request
|
||||
self.sample_tasks.add(ev, ev.sample.remote())
|
||||
|
||||
@override(Aggregator)
|
||||
def stats(self):
|
||||
return {
|
||||
"num_weight_syncs": self.num_weight_syncs,
|
||||
"num_steps_replayed": self.num_replayed,
|
||||
}
|
||||
|
||||
@override(Aggregator)
|
||||
def reset(self, remote_workers):
|
||||
self.sample_tasks.reset_workers(remote_workers)
|
||||
|
||||
def _augment_with_replay(self, sample_futures):
|
||||
def can_replay():
|
||||
num_needed = int(
|
||||
np.ceil(self.train_batch_size / self.rollout_fragment_length))
|
||||
return len(self.replay_batches) > num_needed
|
||||
|
||||
for ev, sample_batch in sample_futures:
|
||||
sample_batch = ray.get(sample_batch)
|
||||
yield ev, sample_batch
|
||||
|
||||
if can_replay():
|
||||
f = self.replay_proportion
|
||||
while random.random() < f:
|
||||
f -= 1
|
||||
replay_batch = random.choice(self.replay_batches)
|
||||
self.num_replayed += replay_batch.count
|
||||
yield None, replay_batch
|
||||
|
||||
|
||||
class SimpleAggregator(AggregationWorkerBase, Aggregator):
|
||||
"""Simple single-threaded implementation of an Aggregator."""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
max_sample_requests_in_flight_per_worker=2,
|
||||
replay_proportion=0.0,
|
||||
replay_buffer_num_slots=0,
|
||||
train_batch_size=500,
|
||||
rollout_fragment_length=50,
|
||||
broadcast_interval=5):
|
||||
self.workers = workers
|
||||
self.local_worker = workers.local_worker()
|
||||
self.broadcast_interval = broadcast_interval
|
||||
self.broadcast_new_weights()
|
||||
AggregationWorkerBase.__init__(
|
||||
self, self.broadcasted_weights, self.workers.remote_workers(),
|
||||
max_sample_requests_in_flight_per_worker, replay_proportion,
|
||||
replay_buffer_num_slots, train_batch_size, rollout_fragment_length)
|
||||
|
||||
@override(Aggregator)
|
||||
def broadcast_new_weights(self):
|
||||
self.broadcasted_weights = ray.put(self.local_worker.get_weights())
|
||||
self.num_sent_since_broadcast = 0
|
||||
|
||||
@override(Aggregator)
|
||||
def should_broadcast(self):
|
||||
return self.num_sent_since_broadcast >= self.broadcast_interval
|
||||
@@ -1,75 +0,0 @@
|
||||
"""Helper class for AsyncSamplesOptimizer."""
|
||||
|
||||
import threading
|
||||
|
||||
from six.moves import queue
|
||||
|
||||
from ray.rllib.evaluation.metrics import get_learner_stats
|
||||
from ray.rllib.optimizers.aso_minibatch_buffer import MinibatchBuffer
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
from ray.rllib.utils.window_stat import WindowStat
|
||||
|
||||
|
||||
class LearnerThread(threading.Thread):
|
||||
"""Background thread that updates the local model from sample trajectories.
|
||||
|
||||
This is for use with AsyncSamplesOptimizer.
|
||||
|
||||
The learner thread communicates with the main thread through Queues. This
|
||||
is needed since Ray operations can only be run on the main thread. In
|
||||
addition, moving heavyweight gradient ops session runs off the main thread
|
||||
improves overall throughput.
|
||||
"""
|
||||
|
||||
def __init__(self, local_worker, minibatch_buffer_size, num_sgd_iter,
|
||||
learner_queue_size, learner_queue_timeout):
|
||||
"""Initialize the learner thread.
|
||||
|
||||
Arguments:
|
||||
local_worker (RolloutWorker): process local rollout worker holding
|
||||
policies this thread will call learn_on_batch() on
|
||||
minibatch_buffer_size (int): max number of train batches to store
|
||||
in the minibatching buffer
|
||||
num_sgd_iter (int): number of passes to learn on per train batch
|
||||
learner_queue_size (int): max size of queue of inbound
|
||||
train batches to this thread
|
||||
learner_queue_timeout (int): raise an exception if the queue has
|
||||
been empty for this long in seconds
|
||||
"""
|
||||
threading.Thread.__init__(self)
|
||||
self.learner_queue_size = WindowStat("size", 50)
|
||||
self.local_worker = local_worker
|
||||
self.inqueue = queue.Queue(maxsize=learner_queue_size)
|
||||
self.outqueue = queue.Queue()
|
||||
self.minibatch_buffer = MinibatchBuffer(
|
||||
inqueue=self.inqueue,
|
||||
size=minibatch_buffer_size,
|
||||
timeout=learner_queue_timeout,
|
||||
num_passes=num_sgd_iter,
|
||||
init_num_passes=num_sgd_iter)
|
||||
self.queue_timer = TimerStat()
|
||||
self.grad_timer = TimerStat()
|
||||
self.load_timer = TimerStat()
|
||||
self.load_wait_timer = TimerStat()
|
||||
self.daemon = True
|
||||
self.weights_updated = False
|
||||
self.stats = {}
|
||||
self.stopped = False
|
||||
self.num_steps = 0
|
||||
|
||||
def run(self):
|
||||
while not self.stopped:
|
||||
self.step()
|
||||
|
||||
def step(self):
|
||||
with self.queue_timer:
|
||||
batch, _ = self.minibatch_buffer.get()
|
||||
|
||||
with self.grad_timer:
|
||||
fetches = self.local_worker.learn_on_batch(batch)
|
||||
self.weights_updated = True
|
||||
self.stats = get_learner_stats(fetches)
|
||||
|
||||
self.num_steps += 1
|
||||
self.outqueue.put(batch.count)
|
||||
self.learner_queue_size.push(self.inqueue.qsize())
|
||||
@@ -1,47 +0,0 @@
|
||||
"""Helper class for AsyncSamplesOptimizer."""
|
||||
|
||||
|
||||
class MinibatchBuffer:
|
||||
"""Ring buffer of recent data batches for minibatch SGD.
|
||||
|
||||
This is for use with AsyncSamplesOptimizer.
|
||||
"""
|
||||
|
||||
def __init__(self, inqueue, size, timeout, num_passes, init_num_passes=1):
|
||||
"""Initialize a minibatch buffer.
|
||||
|
||||
Arguments:
|
||||
inqueue: Queue to populate the internal ring buffer from.
|
||||
size: Max number of data items to buffer.
|
||||
timeout: Queue timeout
|
||||
num_passes: Max num times each data item should be emitted.
|
||||
init_num_passes: Initial max passes for each data item
|
||||
"""
|
||||
self.inqueue = inqueue
|
||||
self.size = size
|
||||
self.timeout = timeout
|
||||
self.max_ttl = num_passes
|
||||
self.cur_max_ttl = init_num_passes
|
||||
self.buffers = [None] * size
|
||||
self.ttl = [0] * size
|
||||
self.idx = 0
|
||||
|
||||
def get(self):
|
||||
"""Get a new batch from the internal ring buffer.
|
||||
|
||||
Returns:
|
||||
buf: Data item saved from inqueue.
|
||||
released: True if the item is now removed from the ring buffer.
|
||||
"""
|
||||
if self.ttl[self.idx] <= 0:
|
||||
self.buffers[self.idx] = self.inqueue.get(timeout=self.timeout)
|
||||
self.ttl[self.idx] = self.cur_max_ttl
|
||||
if self.cur_max_ttl < self.max_ttl:
|
||||
self.cur_max_ttl += 1
|
||||
buf = self.buffers[self.idx]
|
||||
self.ttl[self.idx] -= 1
|
||||
released = self.ttl[self.idx] <= 0
|
||||
if released:
|
||||
self.buffers[self.idx] = None
|
||||
self.idx = (self.idx + 1) % len(self.buffers)
|
||||
return buf, released
|
||||
@@ -1,174 +0,0 @@
|
||||
"""Helper class for AsyncSamplesOptimizer."""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import math
|
||||
|
||||
from six.moves import queue
|
||||
|
||||
from ray.rllib.evaluation.metrics import get_learner_stats
|
||||
from ray.rllib.policy.sample_batch import DEFAULT_POLICY_ID
|
||||
from ray.rllib.optimizers.aso_learner import LearnerThread
|
||||
from ray.rllib.optimizers.aso_minibatch_buffer import MinibatchBuffer
|
||||
from ray.rllib.optimizers.multi_gpu_impl import LocalSyncParallelOptimizer
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_tf
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
tf1, tf, tfv = try_import_tf()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TFMultiGPULearner(LearnerThread):
|
||||
"""Learner that can use multiple GPUs and parallel loading.
|
||||
|
||||
This is for use with AsyncSamplesOptimizer.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
local_worker,
|
||||
num_gpus=1,
|
||||
lr=0.0005,
|
||||
train_batch_size=500,
|
||||
num_data_loader_buffers=1,
|
||||
minibatch_buffer_size=1,
|
||||
num_sgd_iter=1,
|
||||
learner_queue_size=16,
|
||||
learner_queue_timeout=300,
|
||||
num_data_load_threads=16,
|
||||
_fake_gpus=False):
|
||||
"""Initialize a multi-gpu learner thread.
|
||||
|
||||
Arguments:
|
||||
local_worker (RolloutWorker): process local rollout worker holding
|
||||
policies this thread will call learn_on_batch() on
|
||||
num_gpus (int): number of GPUs to use for data-parallel SGD
|
||||
lr (float): learning rate
|
||||
train_batch_size (int): size of batches to learn on
|
||||
num_data_loader_buffers (int): number of buffers to load data into
|
||||
in parallel. Each buffer is of size of train_batch_size and
|
||||
increases GPU memory usage proportionally.
|
||||
minibatch_buffer_size (int): max number of train batches to store
|
||||
in the minibatching buffer
|
||||
num_sgd_iter (int): number of passes to learn on per train batch
|
||||
learner_queue_size (int): max size of queue of inbound
|
||||
train batches to this thread
|
||||
num_data_loader_threads (int): number of threads to use to load
|
||||
data into GPU memory in parallel
|
||||
"""
|
||||
LearnerThread.__init__(self, local_worker, minibatch_buffer_size,
|
||||
num_sgd_iter, learner_queue_size,
|
||||
learner_queue_timeout)
|
||||
self.lr = lr
|
||||
self.train_batch_size = train_batch_size
|
||||
if not num_gpus:
|
||||
self.devices = ["/cpu:0"]
|
||||
elif _fake_gpus:
|
||||
self.devices = [
|
||||
"/cpu:{}".format(i) for i in range(int(math.ceil(num_gpus)))
|
||||
]
|
||||
else:
|
||||
self.devices = [
|
||||
"/gpu:{}".format(i) for i in range(int(math.ceil(num_gpus)))
|
||||
]
|
||||
logger.info("TFMultiGPULearner devices {}".format(self.devices))
|
||||
assert self.train_batch_size % len(self.devices) == 0
|
||||
assert self.train_batch_size >= len(self.devices), "batch too small"
|
||||
|
||||
if set(self.local_worker.policy_map.keys()) != {DEFAULT_POLICY_ID}:
|
||||
raise NotImplementedError("Multi-gpu mode for multi-agent")
|
||||
self.policy = self.local_worker.policy_map[DEFAULT_POLICY_ID]
|
||||
|
||||
# per-GPU graph copies created below must share vars with the policy
|
||||
# reuse is set to AUTO_REUSE because Adam nodes are created after
|
||||
# all of the device copies are created.
|
||||
self.par_opt = []
|
||||
with self.local_worker.tf_sess.graph.as_default():
|
||||
with self.local_worker.tf_sess.as_default():
|
||||
with tf1.variable_scope(
|
||||
DEFAULT_POLICY_ID, reuse=tf1.AUTO_REUSE):
|
||||
if self.policy._state_inputs:
|
||||
rnn_inputs = self.policy._state_inputs + [
|
||||
self.policy._seq_lens
|
||||
]
|
||||
else:
|
||||
rnn_inputs = []
|
||||
adam = tf1.train.AdamOptimizer(self.lr)
|
||||
for _ in range(num_data_loader_buffers):
|
||||
self.par_opt.append(
|
||||
LocalSyncParallelOptimizer(
|
||||
adam,
|
||||
self.devices,
|
||||
[v for _, v in self.policy._loss_inputs],
|
||||
rnn_inputs,
|
||||
999999, # it will get rounded down
|
||||
self.policy.copy))
|
||||
|
||||
self.sess = self.local_worker.tf_sess
|
||||
self.sess.run(tf1.global_variables_initializer())
|
||||
|
||||
self.idle_optimizers = queue.Queue()
|
||||
self.ready_optimizers = queue.Queue()
|
||||
for opt in self.par_opt:
|
||||
self.idle_optimizers.put(opt)
|
||||
for i in range(num_data_load_threads):
|
||||
self.loader_thread = _LoaderThread(self, share_stats=(i == 0))
|
||||
self.loader_thread.start()
|
||||
|
||||
self.minibatch_buffer = MinibatchBuffer(
|
||||
self.ready_optimizers, minibatch_buffer_size,
|
||||
learner_queue_timeout, num_sgd_iter)
|
||||
|
||||
@override(LearnerThread)
|
||||
def step(self):
|
||||
assert self.loader_thread.is_alive()
|
||||
with self.load_wait_timer:
|
||||
opt, released = self.minibatch_buffer.get()
|
||||
|
||||
with self.grad_timer:
|
||||
fetches = opt.optimize(self.sess, 0)
|
||||
self.weights_updated = True
|
||||
self.stats = get_learner_stats(fetches)
|
||||
|
||||
if released:
|
||||
self.idle_optimizers.put(opt)
|
||||
|
||||
self.outqueue.put(opt.num_tuples_loaded)
|
||||
self.learner_queue_size.push(self.inqueue.qsize())
|
||||
|
||||
|
||||
class _LoaderThread(threading.Thread):
|
||||
def __init__(self, learner, share_stats):
|
||||
threading.Thread.__init__(self)
|
||||
self.learner = learner
|
||||
self.daemon = True
|
||||
if share_stats:
|
||||
self.queue_timer = learner.queue_timer
|
||||
self.load_timer = learner.load_timer
|
||||
else:
|
||||
self.queue_timer = TimerStat()
|
||||
self.load_timer = TimerStat()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self._step()
|
||||
|
||||
def _step(self):
|
||||
s = self.learner
|
||||
with self.queue_timer:
|
||||
batch = s.inqueue.get()
|
||||
|
||||
opt = s.idle_optimizers.get()
|
||||
|
||||
with self.load_timer:
|
||||
tuples = s.policy._get_loss_inputs_dict(batch, shuffle=False)
|
||||
data_keys = [ph for _, ph in s.policy._loss_inputs]
|
||||
if s.policy._state_inputs:
|
||||
state_keys = s.policy._state_inputs + [s.policy._seq_lens]
|
||||
else:
|
||||
state_keys = []
|
||||
opt.load_data(s.sess, [tuples[k] for k in data_keys],
|
||||
[tuples[k] for k in state_keys])
|
||||
|
||||
s.ready_optimizers.put(opt)
|
||||
@@ -1,175 +0,0 @@
|
||||
"""Helper class for AsyncSamplesOptimizer."""
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import platform
|
||||
import time
|
||||
|
||||
import ray
|
||||
from ray.rllib.utils.actors import TaskPool, create_colocated
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.optimizers.aso_aggregator import Aggregator, \
|
||||
AggregationWorkerBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TreeAggregator(Aggregator):
|
||||
"""A hierarchical experiences aggregator.
|
||||
|
||||
The given set of remote workers is divided into subsets and assigned to
|
||||
one of several aggregation workers. These aggregation workers collate
|
||||
experiences into batches of size `train_batch_size` and we collect them
|
||||
in this class when `iter_train_batches` is called.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
num_aggregation_workers,
|
||||
max_sample_requests_in_flight_per_worker=2,
|
||||
replay_proportion=0.0,
|
||||
replay_buffer_num_slots=0,
|
||||
train_batch_size=500,
|
||||
rollout_fragment_length=50,
|
||||
broadcast_interval=5):
|
||||
"""Initialize a tree aggregator.
|
||||
|
||||
Arguments:
|
||||
workers (WorkerSet): set of all workers
|
||||
num_aggregation_workers (int): number of intermediate actors to
|
||||
use for data aggregation
|
||||
max_sample_request_in_flight_per_worker (int): max queue size per
|
||||
worker
|
||||
replay_proportion (float): ratio of replay to sampled outputs
|
||||
replay_buffer_num_slots (int): max number of sample batches to
|
||||
store in the replay buffer
|
||||
train_batch_size (int): size of batches to learn on
|
||||
rollout_fragment_length (int): size of batches to sample from
|
||||
workers.
|
||||
broadcast_interval (int): max number of workers to send the
|
||||
same set of weights to
|
||||
"""
|
||||
self.workers = workers
|
||||
self.num_aggregation_workers = num_aggregation_workers
|
||||
self.max_sample_requests_in_flight_per_worker = \
|
||||
max_sample_requests_in_flight_per_worker
|
||||
self.replay_proportion = replay_proportion
|
||||
self.replay_buffer_num_slots = replay_buffer_num_slots
|
||||
self.rollout_fragment_length = rollout_fragment_length
|
||||
self.train_batch_size = train_batch_size
|
||||
self.broadcast_interval = broadcast_interval
|
||||
self.broadcasted_weights = ray.put(
|
||||
workers.local_worker().get_weights())
|
||||
self.num_batches_processed = 0
|
||||
self.num_broadcasts = 0
|
||||
self.num_sent_since_broadcast = 0
|
||||
self.initialized = False
|
||||
|
||||
def init(self, aggregators):
|
||||
"""Deferred init so that we can pass in previously created workers."""
|
||||
|
||||
assert len(aggregators) == self.num_aggregation_workers, aggregators
|
||||
if len(self.workers.remote_workers()) < self.num_aggregation_workers:
|
||||
raise ValueError(
|
||||
"The number of aggregation workers should not exceed the "
|
||||
"number of total evaluation workers ({} vs {})".format(
|
||||
self.num_aggregation_workers,
|
||||
len(self.workers.remote_workers())))
|
||||
|
||||
assigned_workers = collections.defaultdict(list)
|
||||
for i, ev in enumerate(self.workers.remote_workers()):
|
||||
assigned_workers[i % self.num_aggregation_workers].append(ev)
|
||||
|
||||
self.aggregators = aggregators
|
||||
for i, agg in enumerate(self.aggregators):
|
||||
agg.init.remote(
|
||||
self.broadcasted_weights, assigned_workers[i],
|
||||
self.max_sample_requests_in_flight_per_worker,
|
||||
self.replay_proportion, self.replay_buffer_num_slots,
|
||||
self.train_batch_size, self.rollout_fragment_length)
|
||||
|
||||
self.agg_tasks = TaskPool()
|
||||
for agg in self.aggregators:
|
||||
agg.set_weights.remote(self.broadcasted_weights)
|
||||
self.agg_tasks.add(agg, agg.get_train_batches.remote())
|
||||
|
||||
self.initialized = True
|
||||
|
||||
@override(Aggregator)
|
||||
def iter_train_batches(self):
|
||||
assert self.initialized, "Must call init() before using this class."
|
||||
for agg, batches in self.agg_tasks.completed_prefetch():
|
||||
for b in ray.get(batches):
|
||||
self.num_sent_since_broadcast += 1
|
||||
yield b
|
||||
agg.set_weights.remote(self.broadcasted_weights)
|
||||
self.agg_tasks.add(agg, agg.get_train_batches.remote())
|
||||
self.num_batches_processed += 1
|
||||
|
||||
@override(Aggregator)
|
||||
def broadcast_new_weights(self):
|
||||
self.broadcasted_weights = ray.put(
|
||||
self.workers.local_worker().get_weights())
|
||||
self.num_sent_since_broadcast = 0
|
||||
self.num_broadcasts += 1
|
||||
|
||||
@override(Aggregator)
|
||||
def should_broadcast(self):
|
||||
return self.num_sent_since_broadcast >= self.broadcast_interval
|
||||
|
||||
@override(Aggregator)
|
||||
def stats(self):
|
||||
return {
|
||||
"num_broadcasts": self.num_broadcasts,
|
||||
"num_batches_processed": self.num_batches_processed,
|
||||
}
|
||||
|
||||
@override(Aggregator)
|
||||
def reset(self, remote_workers):
|
||||
raise NotImplementedError("changing number of remote workers")
|
||||
|
||||
@staticmethod
|
||||
def precreate_aggregators(n):
|
||||
return create_colocated(AggregationWorker, [], n)
|
||||
|
||||
|
||||
@ray.remote(num_cpus=1)
|
||||
class AggregationWorker(AggregationWorkerBase):
|
||||
def __init__(self):
|
||||
self.initialized = False
|
||||
|
||||
def init(self, initial_weights_obj_id, remote_workers,
|
||||
max_sample_requests_in_flight_per_worker, replay_proportion,
|
||||
replay_buffer_num_slots, train_batch_size,
|
||||
rollout_fragment_length):
|
||||
"""Deferred init that assigns sub-workers to this aggregator."""
|
||||
|
||||
logger.info("Assigned workers {} to aggregation worker {}".format(
|
||||
remote_workers, self))
|
||||
assert remote_workers
|
||||
AggregationWorkerBase.__init__(
|
||||
self, initial_weights_obj_id, remote_workers,
|
||||
max_sample_requests_in_flight_per_worker, replay_proportion,
|
||||
replay_buffer_num_slots, train_batch_size, rollout_fragment_length)
|
||||
self.initialized = True
|
||||
|
||||
def set_weights(self, weights):
|
||||
self.broadcasted_weights = weights
|
||||
|
||||
def get_train_batches(self):
|
||||
assert self.initialized, "Must call init() before using this class."
|
||||
start = time.time()
|
||||
result = []
|
||||
for batch in self.iter_train_batches(max_yield=5):
|
||||
result.append(batch)
|
||||
while not result:
|
||||
time.sleep(0.01)
|
||||
for batch in self.iter_train_batches(max_yield=5):
|
||||
result.append(batch)
|
||||
logger.debug("Returning {} train batches, {}s".format(
|
||||
len(result),
|
||||
time.time() - start))
|
||||
return result
|
||||
|
||||
def get_host(self):
|
||||
return platform.node()
|
||||
@@ -1,82 +0,0 @@
|
||||
import ray
|
||||
from ray.rllib.evaluation.metrics import get_learner_stats
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
|
||||
class AsyncGradientsOptimizer(PolicyOptimizer):
|
||||
"""An asynchronous RL optimizer, e.g. for implementing A3C.
|
||||
|
||||
This optimizer asynchronously pulls and applies gradients from remote
|
||||
workers, sending updated weights back as needed. This pipelines the
|
||||
gradient computations on the remote workers.
|
||||
"""
|
||||
|
||||
def __init__(self, workers, grads_per_step=100):
|
||||
"""Initialize an async gradients optimizer.
|
||||
|
||||
Arguments:
|
||||
grads_per_step (int): The number of gradients to collect and apply
|
||||
per each call to step(). This number should be sufficiently
|
||||
high to amortize the overhead of calling step().
|
||||
"""
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
self.apply_timer = TimerStat()
|
||||
self.wait_timer = TimerStat()
|
||||
self.dispatch_timer = TimerStat()
|
||||
self.grads_per_step = grads_per_step
|
||||
self.learner_stats = {}
|
||||
if not self.workers.remote_workers():
|
||||
raise ValueError(
|
||||
"Async optimizer requires at least 1 remote workers")
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
weights = ray.put(self.workers.local_worker().get_weights())
|
||||
pending_gradients = {}
|
||||
num_gradients = 0
|
||||
|
||||
# Kick off the first wave of async tasks
|
||||
for e in self.workers.remote_workers():
|
||||
e.set_weights.remote(weights)
|
||||
future = e.compute_gradients.remote(e.sample.remote())
|
||||
pending_gradients[future] = e
|
||||
num_gradients += 1
|
||||
|
||||
while pending_gradients:
|
||||
with self.wait_timer:
|
||||
wait_results = ray.wait(
|
||||
list(pending_gradients.keys()), num_returns=1)
|
||||
ready_list = wait_results[0]
|
||||
future = ready_list[0]
|
||||
|
||||
gradient, info = ray.get(future)
|
||||
e = pending_gradients.pop(future)
|
||||
self.learner_stats = get_learner_stats(info)
|
||||
|
||||
if gradient is not None:
|
||||
with self.apply_timer:
|
||||
self.workers.local_worker().apply_gradients(gradient)
|
||||
self.num_steps_sampled += info["batch_count"]
|
||||
self.num_steps_trained += info["batch_count"]
|
||||
|
||||
if num_gradients < self.grads_per_step:
|
||||
with self.dispatch_timer:
|
||||
e.set_weights.remote(
|
||||
self.workers.local_worker().get_weights())
|
||||
future = e.compute_gradients.remote(e.sample.remote())
|
||||
|
||||
pending_gradients[future] = e
|
||||
num_gradients += 1
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
return dict(
|
||||
PolicyOptimizer.stats(self), **{
|
||||
"wait_time_ms": round(1000 * self.wait_timer.mean, 3),
|
||||
"apply_time_ms": round(1000 * self.apply_timer.mean, 3),
|
||||
"dispatch_time_ms": round(1000 * self.dispatch_timer.mean, 3),
|
||||
"learner": self.learner_stats,
|
||||
})
|
||||
@@ -1,521 +0,0 @@
|
||||
"""Implements Distributed Prioritized Experience Replay.
|
||||
|
||||
https://arxiv.org/abs/1803.00933"""
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import numpy as np
|
||||
import platform
|
||||
import random
|
||||
from six.moves import queue
|
||||
import threading
|
||||
import time
|
||||
|
||||
import ray
|
||||
from ray.exceptions import RayError
|
||||
from ray.util.iter import ParallelIteratorWorker
|
||||
from ray.rllib.evaluation.metrics import get_learner_stats
|
||||
from ray.rllib.policy.policy import LEARNER_STATS_KEY
|
||||
from ray.rllib.policy.sample_batch import SampleBatch, DEFAULT_POLICY_ID, \
|
||||
MultiAgentBatch
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.optimizers.replay_buffer import PrioritizedReplayBuffer
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.actors import TaskPool, create_colocated
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
from ray.rllib.utils.window_stat import WindowStat
|
||||
|
||||
SAMPLE_QUEUE_DEPTH = 2
|
||||
REPLAY_QUEUE_DEPTH = 4
|
||||
LEARNER_QUEUE_MAX_SIZE = 16
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncReplayOptimizer(PolicyOptimizer):
|
||||
"""Main event loop of the Ape-X optimizer (async sampling with replay).
|
||||
|
||||
This class coordinates the data transfers between the learner thread,
|
||||
remote workers (Ape-X actors), and replay buffer actors.
|
||||
|
||||
This has two modes of operation:
|
||||
- normal replay: replays independent samples.
|
||||
- batch replay: simplified mode where entire sample batches are
|
||||
replayed. This supports RNNs, but not prioritization.
|
||||
|
||||
This optimizer requires that rollout workers return an additional
|
||||
"td_error" array in the info return of compute_gradients(). This error
|
||||
term will be used for sample prioritization."""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
learning_starts=1000,
|
||||
buffer_size=10000,
|
||||
prioritized_replay=True,
|
||||
prioritized_replay_alpha=0.6,
|
||||
prioritized_replay_beta=0.4,
|
||||
prioritized_replay_eps=1e-6,
|
||||
train_batch_size=512,
|
||||
rollout_fragment_length=50,
|
||||
num_replay_buffer_shards=1,
|
||||
max_weight_sync_delay=400,
|
||||
debug=False,
|
||||
batch_replay=False):
|
||||
"""Initialize an async replay optimizer.
|
||||
|
||||
Arguments:
|
||||
workers (WorkerSet): all workers
|
||||
learning_starts (int): wait until this many steps have been sampled
|
||||
before starting optimization.
|
||||
buffer_size (int): max size of the replay buffer
|
||||
prioritized_replay (bool): whether to enable prioritized replay
|
||||
prioritized_replay_alpha (float): replay alpha hyperparameter
|
||||
prioritized_replay_beta (float): replay beta hyperparameter
|
||||
prioritized_replay_eps (float): replay eps hyperparameter
|
||||
train_batch_size (int): size of batches to learn on
|
||||
rollout_fragment_length (int): size of batches to sample from
|
||||
workers.
|
||||
num_replay_buffer_shards (int): number of actors to use to store
|
||||
replay samples
|
||||
max_weight_sync_delay (int): update the weights of a rollout worker
|
||||
after collecting this number of timesteps from it
|
||||
debug (bool): return extra debug stats
|
||||
batch_replay (bool): replay entire sequential batches of
|
||||
experiences instead of sampling steps individually
|
||||
"""
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
self.debug = debug
|
||||
self.batch_replay = batch_replay
|
||||
self.replay_starts = learning_starts
|
||||
self.prioritized_replay_beta = prioritized_replay_beta
|
||||
self.prioritized_replay_eps = prioritized_replay_eps
|
||||
self.max_weight_sync_delay = max_weight_sync_delay
|
||||
|
||||
self.learner = LearnerThread(self.workers.local_worker())
|
||||
self.learner.start()
|
||||
|
||||
if self.batch_replay:
|
||||
replay_cls = BatchReplayActor
|
||||
else:
|
||||
replay_cls = ReplayActor
|
||||
self.replay_actors = create_colocated(replay_cls, [
|
||||
num_replay_buffer_shards,
|
||||
learning_starts,
|
||||
buffer_size,
|
||||
train_batch_size,
|
||||
prioritized_replay_alpha,
|
||||
prioritized_replay_beta,
|
||||
prioritized_replay_eps,
|
||||
], num_replay_buffer_shards)
|
||||
|
||||
# Stats
|
||||
self.timers = {
|
||||
k: TimerStat()
|
||||
for k in [
|
||||
"put_weights", "get_samples", "sample_processing",
|
||||
"replay_processing", "update_priorities", "train", "sample"
|
||||
]
|
||||
}
|
||||
self.num_weight_syncs = 0
|
||||
self.num_samples_dropped = 0
|
||||
self.learning_started = False
|
||||
|
||||
# Number of worker steps since the last weight update
|
||||
self.steps_since_update = {}
|
||||
|
||||
# Otherwise kick of replay tasks for local gradient updates
|
||||
self.replay_tasks = TaskPool()
|
||||
for ra in self.replay_actors:
|
||||
for _ in range(REPLAY_QUEUE_DEPTH):
|
||||
self.replay_tasks.add(ra, ra.replay.remote())
|
||||
|
||||
# Kick off async background sampling
|
||||
self.sample_tasks = TaskPool()
|
||||
if self.workers.remote_workers():
|
||||
self._set_workers(self.workers.remote_workers())
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
assert self.learner.is_alive()
|
||||
assert len(self.workers.remote_workers()) > 0
|
||||
start = time.time()
|
||||
sample_timesteps, train_timesteps = self._step()
|
||||
time_delta = time.time() - start
|
||||
self.timers["sample"].push(time_delta)
|
||||
self.timers["sample"].push_units_processed(sample_timesteps)
|
||||
if train_timesteps > 0:
|
||||
self.learning_started = True
|
||||
if self.learning_started:
|
||||
self.timers["train"].push(time_delta)
|
||||
self.timers["train"].push_units_processed(train_timesteps)
|
||||
self.num_steps_sampled += sample_timesteps
|
||||
self.num_steps_trained += train_timesteps
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stop(self):
|
||||
for r in self.replay_actors:
|
||||
r.__ray_terminate__.remote()
|
||||
self.learner.stopped = True
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def reset(self, remote_workers):
|
||||
self.workers.reset(remote_workers)
|
||||
self.sample_tasks.reset_workers(remote_workers)
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
replay_stats = ray.get(self.replay_actors[0].stats.remote(self.debug))
|
||||
timing = {
|
||||
"{}_time_ms".format(k): round(1000 * self.timers[k].mean, 3)
|
||||
for k in self.timers
|
||||
}
|
||||
timing["learner_grad_time_ms"] = round(
|
||||
1000 * self.learner.grad_timer.mean, 3)
|
||||
timing["learner_dequeue_time_ms"] = round(
|
||||
1000 * self.learner.queue_timer.mean, 3)
|
||||
stats = {
|
||||
"sample_throughput": round(self.timers["sample"].mean_throughput,
|
||||
3),
|
||||
"train_throughput": round(self.timers["train"].mean_throughput, 3),
|
||||
"num_weight_syncs": self.num_weight_syncs,
|
||||
"num_samples_dropped": self.num_samples_dropped,
|
||||
"learner_queue": self.learner.learner_queue_size.stats(),
|
||||
"replay_shard_0": replay_stats,
|
||||
}
|
||||
debug_stats = {
|
||||
"timing_breakdown": timing,
|
||||
"pending_sample_tasks": self.sample_tasks.count,
|
||||
"pending_replay_tasks": self.replay_tasks.count,
|
||||
}
|
||||
if self.debug:
|
||||
stats.update(debug_stats)
|
||||
if self.learner.stats:
|
||||
stats["learner"] = self.learner.stats
|
||||
return dict(PolicyOptimizer.stats(self), **stats)
|
||||
|
||||
# For https://github.com/ray-project/ray/issues/2541 only
|
||||
def _set_workers(self, remote_workers):
|
||||
self.workers.reset(remote_workers)
|
||||
weights = self.workers.local_worker().get_weights()
|
||||
for ev in self.workers.remote_workers():
|
||||
ev.set_weights.remote(weights)
|
||||
self.steps_since_update[ev] = 0
|
||||
for _ in range(SAMPLE_QUEUE_DEPTH):
|
||||
self.sample_tasks.add(ev, ev.sample_with_count.remote())
|
||||
|
||||
def _step(self):
|
||||
sample_timesteps, train_timesteps = 0, 0
|
||||
weights = None
|
||||
|
||||
with self.timers["sample_processing"]:
|
||||
completed = list(self.sample_tasks.completed())
|
||||
# First try a batched ray.get().
|
||||
ray_error = None
|
||||
try:
|
||||
counts = {
|
||||
i: v
|
||||
for i, v in enumerate(
|
||||
ray.get([c[1][1] for c in completed]))
|
||||
}
|
||||
# If there are failed workers, try to recover the still good ones
|
||||
# (via non-batched ray.get()) and store the first error (to raise
|
||||
# later).
|
||||
except RayError:
|
||||
counts = {}
|
||||
for i, c in enumerate(completed):
|
||||
try:
|
||||
counts[i] = ray.get(c[1][1])
|
||||
except RayError as e:
|
||||
logger.exception(
|
||||
"Error in completed task: {}".format(e))
|
||||
ray_error = ray_error if ray_error is not None else e
|
||||
|
||||
for i, (ev, (sample_batch, count)) in enumerate(completed):
|
||||
# Skip failed tasks.
|
||||
if i not in counts:
|
||||
continue
|
||||
|
||||
sample_timesteps += counts[i]
|
||||
# Send the data to the replay buffer
|
||||
random.choice(
|
||||
self.replay_actors).add_batch.remote(sample_batch)
|
||||
|
||||
# Update weights if needed.
|
||||
self.steps_since_update[ev] += counts[i]
|
||||
if self.steps_since_update[ev] >= self.max_weight_sync_delay:
|
||||
# Note that it's important to pull new weights once
|
||||
# updated to avoid excessive correlation between actors.
|
||||
if weights is None or self.learner.weights_updated:
|
||||
self.learner.weights_updated = False
|
||||
with self.timers["put_weights"]:
|
||||
weights = ray.put(
|
||||
self.workers.local_worker().get_weights())
|
||||
ev.set_weights.remote(weights)
|
||||
self.num_weight_syncs += 1
|
||||
self.steps_since_update[ev] = 0
|
||||
|
||||
# Kick off another sample request.
|
||||
self.sample_tasks.add(ev, ev.sample_with_count.remote())
|
||||
|
||||
# Now that all still good tasks have been kicked off again,
|
||||
# we can throw the error.
|
||||
if ray_error:
|
||||
raise ray_error
|
||||
|
||||
with self.timers["replay_processing"]:
|
||||
for ra, replay in self.replay_tasks.completed():
|
||||
self.replay_tasks.add(ra, ra.replay.remote())
|
||||
if self.learner.inqueue.full():
|
||||
self.num_samples_dropped += 1
|
||||
else:
|
||||
with self.timers["get_samples"]:
|
||||
samples = ray.get(replay)
|
||||
# Defensive copy against plasma crashes, see #2610 #3452
|
||||
self.learner.inqueue.put((ra, samples and samples.copy()))
|
||||
|
||||
with self.timers["update_priorities"]:
|
||||
while not self.learner.outqueue.empty():
|
||||
ra, prio_dict, count = self.learner.outqueue.get()
|
||||
ra.update_priorities.remote(prio_dict)
|
||||
train_timesteps += count
|
||||
|
||||
return sample_timesteps, train_timesteps
|
||||
|
||||
|
||||
# Visible for testing.
|
||||
_local_replay_buffer = None
|
||||
|
||||
|
||||
# TODO(ekl) move this class to common
|
||||
class LocalReplayBuffer(ParallelIteratorWorker):
|
||||
"""A replay buffer shard.
|
||||
|
||||
Ray actors are single-threaded, so for scalability multiple replay actors
|
||||
may be created to increase parallelism."""
|
||||
|
||||
def __init__(self,
|
||||
num_shards,
|
||||
learning_starts,
|
||||
buffer_size,
|
||||
replay_batch_size,
|
||||
prioritized_replay_alpha=0.6,
|
||||
prioritized_replay_beta=0.4,
|
||||
prioritized_replay_eps=1e-6,
|
||||
multiagent_sync_replay=False):
|
||||
self.replay_starts = learning_starts // num_shards
|
||||
self.buffer_size = buffer_size // num_shards
|
||||
self.replay_batch_size = replay_batch_size
|
||||
self.prioritized_replay_beta = prioritized_replay_beta
|
||||
self.prioritized_replay_eps = prioritized_replay_eps
|
||||
self.multiagent_sync_replay = multiagent_sync_replay
|
||||
|
||||
def gen_replay():
|
||||
while True:
|
||||
yield self.replay()
|
||||
|
||||
ParallelIteratorWorker.__init__(self, gen_replay, False)
|
||||
|
||||
def new_buffer():
|
||||
return PrioritizedReplayBuffer(
|
||||
self.buffer_size, alpha=prioritized_replay_alpha)
|
||||
|
||||
self.replay_buffers = collections.defaultdict(new_buffer)
|
||||
|
||||
# Metrics
|
||||
self.add_batch_timer = TimerStat()
|
||||
self.replay_timer = TimerStat()
|
||||
self.update_priorities_timer = TimerStat()
|
||||
self.num_added = 0
|
||||
|
||||
# Make externally accessible for testing.
|
||||
global _local_replay_buffer
|
||||
_local_replay_buffer = self
|
||||
# If set, return this instead of the usual data for testing.
|
||||
self._fake_batch = None
|
||||
|
||||
@staticmethod
|
||||
def get_instance_for_testing():
|
||||
global _local_replay_buffer
|
||||
return _local_replay_buffer
|
||||
|
||||
def get_host(self):
|
||||
return platform.node()
|
||||
|
||||
def add_batch(self, batch):
|
||||
# Make a copy so the replay buffer doesn't pin plasma memory.
|
||||
batch = batch.copy()
|
||||
# Handle everything as if multiagent
|
||||
if isinstance(batch, SampleBatch):
|
||||
batch = MultiAgentBatch({DEFAULT_POLICY_ID: batch}, batch.count)
|
||||
with self.add_batch_timer:
|
||||
for policy_id, s in batch.policy_batches.items():
|
||||
for row in s.rows():
|
||||
self.replay_buffers[policy_id].add(
|
||||
row["obs"], row["actions"], row["rewards"],
|
||||
row["new_obs"], row["dones"], row["weights"]
|
||||
if "weights" in row else None)
|
||||
self.num_added += batch.count
|
||||
|
||||
def replay(self):
|
||||
if self._fake_batch:
|
||||
fake_batch = SampleBatch(self._fake_batch)
|
||||
return MultiAgentBatch({
|
||||
DEFAULT_POLICY_ID: fake_batch
|
||||
}, fake_batch.count)
|
||||
|
||||
if self.num_added < self.replay_starts:
|
||||
return None
|
||||
|
||||
with self.replay_timer:
|
||||
samples = {}
|
||||
idxes = None
|
||||
for policy_id, replay_buffer in self.replay_buffers.items():
|
||||
if self.multiagent_sync_replay:
|
||||
if idxes is None:
|
||||
idxes = replay_buffer.sample_idxes(
|
||||
self.replay_batch_size)
|
||||
else:
|
||||
idxes = replay_buffer.sample_idxes(self.replay_batch_size)
|
||||
(obses_t, actions, rewards, obses_tp1, dones, weights,
|
||||
batch_indexes) = replay_buffer.sample_with_idxes(
|
||||
idxes, beta=self.prioritized_replay_beta)
|
||||
samples[policy_id] = SampleBatch({
|
||||
"obs": obses_t,
|
||||
"actions": actions,
|
||||
"rewards": rewards,
|
||||
"new_obs": obses_tp1,
|
||||
"dones": dones,
|
||||
"weights": weights,
|
||||
"batch_indexes": batch_indexes
|
||||
})
|
||||
return MultiAgentBatch(samples, self.replay_batch_size)
|
||||
|
||||
def update_priorities(self, prio_dict):
|
||||
with self.update_priorities_timer:
|
||||
for policy_id, (batch_indexes, td_errors) in prio_dict.items():
|
||||
new_priorities = (
|
||||
np.abs(td_errors) + self.prioritized_replay_eps)
|
||||
self.replay_buffers[policy_id].update_priorities(
|
||||
batch_indexes, new_priorities)
|
||||
|
||||
def stats(self, debug=False):
|
||||
stat = {
|
||||
"add_batch_time_ms": round(1000 * self.add_batch_timer.mean, 3),
|
||||
"replay_time_ms": round(1000 * self.replay_timer.mean, 3),
|
||||
"update_priorities_time_ms": round(
|
||||
1000 * self.update_priorities_timer.mean, 3),
|
||||
}
|
||||
for policy_id, replay_buffer in self.replay_buffers.items():
|
||||
stat.update({
|
||||
"policy_{}".format(policy_id): replay_buffer.stats(debug=debug)
|
||||
})
|
||||
return stat
|
||||
|
||||
|
||||
ReplayActor = ray.remote(num_cpus=0)(LocalReplayBuffer)
|
||||
|
||||
|
||||
# TODO(ekl) move this class to common
|
||||
# note: we set num_cpus=0 to avoid failing to create replay actors when
|
||||
# resources are fragmented. This isn't ideal.
|
||||
class LocalBatchReplayBuffer(LocalReplayBuffer):
|
||||
"""The batch replay version of the replay actor.
|
||||
|
||||
This allows for RNN models, but ignores prioritization params.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
num_shards,
|
||||
learning_starts,
|
||||
buffer_size,
|
||||
train_batch_size,
|
||||
prioritized_replay_alpha=0.6,
|
||||
prioritized_replay_beta=0.4,
|
||||
prioritized_replay_eps=1e-6):
|
||||
self.replay_starts = learning_starts // num_shards
|
||||
self.buffer_size = buffer_size // num_shards
|
||||
self.train_batch_size = train_batch_size
|
||||
self.buffer = []
|
||||
|
||||
# Metrics
|
||||
self.num_added = 0
|
||||
self.cur_size = 0
|
||||
|
||||
def add_batch(self, batch):
|
||||
# Handle everything as if multiagent
|
||||
if isinstance(batch, SampleBatch):
|
||||
batch = MultiAgentBatch({DEFAULT_POLICY_ID: batch}, batch.count)
|
||||
self.buffer.append(batch)
|
||||
self.cur_size += batch.count
|
||||
self.num_added += batch.count
|
||||
while self.cur_size > self.buffer_size:
|
||||
self.cur_size -= self.buffer.pop(0).count
|
||||
|
||||
def replay(self):
|
||||
if self.num_added < self.replay_starts:
|
||||
return None
|
||||
return random.choice(self.buffer)
|
||||
|
||||
def update_priorities(self, prio_dict):
|
||||
pass
|
||||
|
||||
def stats(self, debug=False):
|
||||
stat = {
|
||||
"cur_size": self.cur_size,
|
||||
"num_added": self.num_added,
|
||||
}
|
||||
return stat
|
||||
|
||||
|
||||
BatchReplayActor = ray.remote(num_cpus=0)(LocalBatchReplayBuffer)
|
||||
|
||||
|
||||
class LearnerThread(threading.Thread):
|
||||
"""Background thread that updates the local model from replay data.
|
||||
|
||||
The learner thread communicates with the main thread through Queues. This
|
||||
is needed since Ray operations can only be run on the main thread. In
|
||||
addition, moving heavyweight gradient ops session runs off the main thread
|
||||
improves overall throughput.
|
||||
"""
|
||||
|
||||
def __init__(self, local_worker):
|
||||
threading.Thread.__init__(self)
|
||||
self.learner_queue_size = WindowStat("size", 50)
|
||||
self.local_worker = local_worker
|
||||
self.inqueue = queue.Queue(maxsize=LEARNER_QUEUE_MAX_SIZE)
|
||||
self.outqueue = queue.Queue()
|
||||
self.queue_timer = TimerStat()
|
||||
self.grad_timer = TimerStat()
|
||||
self.overall_timer = TimerStat()
|
||||
self.daemon = True
|
||||
self.weights_updated = False
|
||||
self.stopped = False
|
||||
self.stats = {}
|
||||
|
||||
def run(self):
|
||||
while not self.stopped:
|
||||
self.step()
|
||||
|
||||
def step(self):
|
||||
with self.overall_timer:
|
||||
with self.queue_timer:
|
||||
ra, replay = self.inqueue.get()
|
||||
if replay is not None:
|
||||
prio_dict = {}
|
||||
with self.grad_timer:
|
||||
grad_out = self.local_worker.learn_on_batch(replay)
|
||||
for pid, info in grad_out.items():
|
||||
td_error = info.get(
|
||||
"td_error",
|
||||
info[LEARNER_STATS_KEY].get("td_error"))
|
||||
prio_dict[pid] = (replay.policy_batches[pid].data.get(
|
||||
"batch_indexes"), td_error)
|
||||
self.stats[pid] = get_learner_stats(info)
|
||||
self.grad_timer.push_units_processed(replay.count)
|
||||
self.outqueue.put((ra, prio_dict, replay.count))
|
||||
self.learner_queue_size.push(self.inqueue.qsize())
|
||||
self.weights_updated = True
|
||||
self.overall_timer.push_units_processed(replay and replay.count
|
||||
or 0)
|
||||
@@ -1,185 +0,0 @@
|
||||
"""Implements the IMPALA asynchronous sampling architecture.
|
||||
|
||||
https://arxiv.org/abs/1802.01561"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from ray.rllib.optimizers.aso_aggregator import SimpleAggregator
|
||||
from ray.rllib.optimizers.aso_tree_aggregator import TreeAggregator
|
||||
from ray.rllib.optimizers.aso_learner import LearnerThread
|
||||
from ray.rllib.optimizers.aso_multi_gpu_learner import TFMultiGPULearner
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncSamplesOptimizer(PolicyOptimizer):
|
||||
"""Main event loop of the IMPALA architecture.
|
||||
|
||||
This class coordinates the data transfers between the learner thread
|
||||
and remote workers (IMPALA actors).
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
train_batch_size=500,
|
||||
rollout_fragment_length=50,
|
||||
num_envs_per_worker=1,
|
||||
num_gpus=0,
|
||||
lr=0.0005,
|
||||
replay_buffer_num_slots=0,
|
||||
replay_proportion=0.0,
|
||||
num_data_loader_buffers=1,
|
||||
max_sample_requests_in_flight_per_worker=2,
|
||||
broadcast_interval=1,
|
||||
num_sgd_iter=1,
|
||||
minibatch_buffer_size=1,
|
||||
learner_queue_size=16,
|
||||
learner_queue_timeout=300,
|
||||
num_aggregation_workers=0,
|
||||
_fake_gpus=False):
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
self._stats_start_time = time.time()
|
||||
self._last_stats_time = {}
|
||||
self._last_stats_sum = {}
|
||||
|
||||
if num_gpus > 1 or num_data_loader_buffers > 1:
|
||||
logger.info(
|
||||
"Enabling multi-GPU mode, {} GPUs, {} parallel loaders".format(
|
||||
num_gpus, num_data_loader_buffers))
|
||||
if num_data_loader_buffers < minibatch_buffer_size:
|
||||
raise ValueError(
|
||||
"In multi-gpu mode you must have at least as many "
|
||||
"parallel data loader buffers as minibatch buffers: "
|
||||
"{} vs {}".format(num_data_loader_buffers,
|
||||
minibatch_buffer_size))
|
||||
self.learner = TFMultiGPULearner(
|
||||
self.workers.local_worker(),
|
||||
lr=lr,
|
||||
num_gpus=num_gpus,
|
||||
train_batch_size=train_batch_size,
|
||||
num_data_loader_buffers=num_data_loader_buffers,
|
||||
minibatch_buffer_size=minibatch_buffer_size,
|
||||
num_sgd_iter=num_sgd_iter,
|
||||
learner_queue_size=learner_queue_size,
|
||||
learner_queue_timeout=learner_queue_timeout,
|
||||
_fake_gpus=_fake_gpus)
|
||||
else:
|
||||
self.learner = LearnerThread(
|
||||
self.workers.local_worker(),
|
||||
minibatch_buffer_size=minibatch_buffer_size,
|
||||
num_sgd_iter=num_sgd_iter,
|
||||
learner_queue_size=learner_queue_size,
|
||||
learner_queue_timeout=learner_queue_timeout)
|
||||
self.learner.start()
|
||||
|
||||
# Stats
|
||||
self._optimizer_step_timer = TimerStat()
|
||||
self._stats_start_time = time.time()
|
||||
self._last_stats_time = {}
|
||||
|
||||
if num_aggregation_workers > 0:
|
||||
self.aggregator = TreeAggregator(
|
||||
workers,
|
||||
num_aggregation_workers,
|
||||
replay_proportion=replay_proportion,
|
||||
max_sample_requests_in_flight_per_worker=(
|
||||
max_sample_requests_in_flight_per_worker),
|
||||
replay_buffer_num_slots=replay_buffer_num_slots,
|
||||
train_batch_size=train_batch_size,
|
||||
rollout_fragment_length=rollout_fragment_length,
|
||||
broadcast_interval=broadcast_interval)
|
||||
else:
|
||||
self.aggregator = SimpleAggregator(
|
||||
workers,
|
||||
replay_proportion=replay_proportion,
|
||||
max_sample_requests_in_flight_per_worker=(
|
||||
max_sample_requests_in_flight_per_worker),
|
||||
replay_buffer_num_slots=replay_buffer_num_slots,
|
||||
train_batch_size=train_batch_size,
|
||||
rollout_fragment_length=rollout_fragment_length,
|
||||
broadcast_interval=broadcast_interval)
|
||||
|
||||
def add_stat_val(self, key, val):
|
||||
if key not in self._last_stats_sum:
|
||||
self._last_stats_sum[key] = 0
|
||||
self._last_stats_time[key] = self._stats_start_time
|
||||
self._last_stats_sum[key] += val
|
||||
|
||||
def get_mean_stats_and_reset(self):
|
||||
now = time.time()
|
||||
mean_stats = {
|
||||
key: round(val / (now - self._last_stats_time[key]), 3)
|
||||
for key, val in self._last_stats_sum.items()
|
||||
}
|
||||
|
||||
for key in self._last_stats_sum.keys():
|
||||
self._last_stats_sum[key] = 0
|
||||
self._last_stats_time[key] = time.time()
|
||||
|
||||
return mean_stats
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
if len(self.workers.remote_workers()) == 0:
|
||||
raise ValueError("Config num_workers=0 means training will hang!")
|
||||
assert self.learner.is_alive()
|
||||
with self._optimizer_step_timer:
|
||||
sample_timesteps, train_timesteps = self._step()
|
||||
|
||||
if sample_timesteps > 0:
|
||||
self.add_stat_val("sample_throughput", sample_timesteps)
|
||||
if train_timesteps > 0:
|
||||
self.add_stat_val("train_throughput", train_timesteps)
|
||||
|
||||
self.num_steps_sampled += sample_timesteps
|
||||
self.num_steps_trained += train_timesteps
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stop(self):
|
||||
self.learner.stopped = True
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def reset(self, remote_workers):
|
||||
self.workers.reset(remote_workers)
|
||||
self.aggregator.reset(remote_workers)
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
def timer_to_ms(timer):
|
||||
return round(1000 * timer.mean, 3)
|
||||
|
||||
stats = self.aggregator.stats()
|
||||
stats.update(self.get_mean_stats_and_reset())
|
||||
stats["timing_breakdown"] = {
|
||||
"optimizer_step_time_ms": timer_to_ms(self._optimizer_step_timer),
|
||||
"learner_grad_time_ms": timer_to_ms(self.learner.grad_timer),
|
||||
"learner_load_time_ms": timer_to_ms(self.learner.load_timer),
|
||||
"learner_load_wait_time_ms": timer_to_ms(
|
||||
self.learner.load_wait_timer),
|
||||
"learner_dequeue_time_ms": timer_to_ms(self.learner.queue_timer),
|
||||
}
|
||||
stats["learner_queue"] = self.learner.learner_queue_size.stats()
|
||||
if self.learner.stats:
|
||||
stats["learner"] = self.learner.stats
|
||||
return dict(PolicyOptimizer.stats(self), **stats)
|
||||
|
||||
def _step(self):
|
||||
sample_timesteps, train_timesteps = 0, 0
|
||||
|
||||
for train_batch in self.aggregator.iter_train_batches():
|
||||
sample_timesteps += train_batch.count
|
||||
self.learner.inqueue.put(train_batch)
|
||||
if (self.learner.weights_updated
|
||||
and self.aggregator.should_broadcast()):
|
||||
self.aggregator.broadcast_new_weights()
|
||||
|
||||
while not self.learner.outqueue.empty():
|
||||
count = self.learner.outqueue.get()
|
||||
train_timesteps += count
|
||||
|
||||
return sample_timesteps, train_timesteps
|
||||
@@ -1,138 +0,0 @@
|
||||
import logging
|
||||
|
||||
import ray
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.policy.sample_batch import SampleBatch, DEFAULT_POLICY_ID, \
|
||||
MultiAgentBatch
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.filter import RunningStat
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MicrobatchOptimizer(PolicyOptimizer):
|
||||
"""A microbatching synchronous RL optimizer.
|
||||
|
||||
This optimizer pulls sample batches from workers until the target
|
||||
microbatch size is reached. Then, it computes and accumulates the policy
|
||||
gradient in a local buffer. This process is repeated until the number of
|
||||
samples collected equals the train batch size. Then, an accumulated
|
||||
gradient update is made.
|
||||
|
||||
This allows for training with effective batch sizes much larger than can
|
||||
fit in GPU or host memory.
|
||||
"""
|
||||
|
||||
def __init__(self, workers, train_batch_size=10000, microbatch_size=1000):
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
if train_batch_size <= microbatch_size:
|
||||
raise ValueError(
|
||||
"The microbatch size must be smaller than the train batch "
|
||||
"size, got {} vs {}".format(microbatch_size, train_batch_size))
|
||||
|
||||
self.update_weights_timer = TimerStat()
|
||||
self.sample_timer = TimerStat()
|
||||
self.grad_timer = TimerStat()
|
||||
self.throughput = RunningStat()
|
||||
self.train_batch_size = train_batch_size
|
||||
self.microbatch_size = microbatch_size
|
||||
self.learner_stats = {}
|
||||
self.policies = dict(self.workers.local_worker()
|
||||
.foreach_trainable_policy(lambda p, i: (i, p)))
|
||||
logger.debug("Policies to train: {}".format(self.policies))
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
with self.update_weights_timer:
|
||||
if self.workers.remote_workers():
|
||||
weights = ray.put(self.workers.local_worker().get_weights())
|
||||
for e in self.workers.remote_workers():
|
||||
e.set_weights.remote(weights)
|
||||
|
||||
fetches = {}
|
||||
accumulated_gradients = {}
|
||||
samples_so_far = 0
|
||||
|
||||
# Accumulate minibatches.
|
||||
i = 0
|
||||
while samples_so_far < self.train_batch_size:
|
||||
i += 1
|
||||
with self.sample_timer:
|
||||
samples = []
|
||||
while sum(s.count for s in samples) < self.microbatch_size:
|
||||
if self.workers.remote_workers():
|
||||
samples.extend(
|
||||
ray.get([
|
||||
e.sample.remote()
|
||||
for e in self.workers.remote_workers()
|
||||
]))
|
||||
else:
|
||||
samples.append(self.workers.local_worker().sample())
|
||||
samples = SampleBatch.concat_samples(samples)
|
||||
self.sample_timer.push_units_processed(samples.count)
|
||||
samples_so_far += samples.count
|
||||
|
||||
logger.info(
|
||||
"Computing gradients for microbatch {} ({}/{} samples)".format(
|
||||
i, samples_so_far, self.train_batch_size))
|
||||
|
||||
# Handle everything as if multiagent
|
||||
if isinstance(samples, SampleBatch):
|
||||
samples = MultiAgentBatch({
|
||||
DEFAULT_POLICY_ID: samples
|
||||
}, samples.count)
|
||||
|
||||
with self.grad_timer:
|
||||
for policy_id, policy in self.policies.items():
|
||||
if policy_id not in samples.policy_batches:
|
||||
continue
|
||||
batch = samples.policy_batches[policy_id]
|
||||
grad_out, info_out = (
|
||||
self.workers.local_worker().compute_gradients(
|
||||
MultiAgentBatch({
|
||||
policy_id: batch
|
||||
}, batch.count)))
|
||||
grad = grad_out[policy_id]
|
||||
fetches.update(info_out)
|
||||
if policy_id not in accumulated_gradients:
|
||||
accumulated_gradients[policy_id] = grad
|
||||
else:
|
||||
grad_size = len(accumulated_gradients[policy_id])
|
||||
assert grad_size == len(grad), (grad_size, len(grad))
|
||||
c = []
|
||||
for a, b in zip(accumulated_gradients[policy_id],
|
||||
grad):
|
||||
c.append(a + b)
|
||||
accumulated_gradients[policy_id] = c
|
||||
self.grad_timer.push_units_processed(samples.count)
|
||||
|
||||
# Apply the accumulated gradient
|
||||
logger.info("Applying accumulated gradients ({} samples)".format(
|
||||
samples_so_far))
|
||||
self.workers.local_worker().apply_gradients(accumulated_gradients)
|
||||
|
||||
if len(fetches) == 1 and DEFAULT_POLICY_ID in fetches:
|
||||
self.learner_stats = fetches[DEFAULT_POLICY_ID]
|
||||
else:
|
||||
self.learner_stats = fetches
|
||||
self.num_steps_sampled += samples_so_far
|
||||
self.num_steps_trained += samples_so_far
|
||||
return self.learner_stats
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
return dict(
|
||||
PolicyOptimizer.stats(self), **{
|
||||
"sample_time_ms": round(1000 * self.sample_timer.mean, 3),
|
||||
"grad_time_ms": round(1000 * self.grad_timer.mean, 3),
|
||||
"update_time_ms": round(1000 * self.update_weights_timer.mean,
|
||||
3),
|
||||
"opt_peak_throughput": round(self.grad_timer.mean_throughput,
|
||||
3),
|
||||
"sample_peak_throughput": round(
|
||||
self.sample_timer.mean_throughput, 3),
|
||||
"opt_samples": round(self.grad_timer.mean_units_processed, 3),
|
||||
"learner": self.learner_stats,
|
||||
})
|
||||
@@ -1,358 +0,0 @@
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
from ray.util.debug import log_once
|
||||
from ray.rllib.utils.debug import summarize
|
||||
from ray.rllib.utils.framework import try_import_tf
|
||||
|
||||
tf1, tf, tfv = try_import_tf()
|
||||
|
||||
# Variable scope in which created variables will be placed under
|
||||
TOWER_SCOPE_NAME = "tower"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalSyncParallelOptimizer:
|
||||
"""Optimizer that runs in parallel across multiple local devices.
|
||||
|
||||
LocalSyncParallelOptimizer automatically splits up and loads training data
|
||||
onto specified local devices (e.g. GPUs) with `load_data()`. During a call
|
||||
to `optimize()`, the devices compute gradients over slices of the data in
|
||||
parallel. The gradients are then averaged and applied to the shared
|
||||
weights.
|
||||
|
||||
The data loaded is pinned in device memory until the next call to
|
||||
`load_data`, so you can make multiple passes (possibly in randomized order)
|
||||
over the same data once loaded.
|
||||
|
||||
This is similar to tf.train.SyncReplicasOptimizer, but works within a
|
||||
single TensorFlow graph, i.e. implements in-graph replicated training:
|
||||
|
||||
https://www.tensorflow.org/api_docs/python/tf/train/SyncReplicasOptimizer
|
||||
|
||||
Args:
|
||||
optimizer: Delegate TensorFlow optimizer object.
|
||||
devices: List of the names of TensorFlow devices to parallelize over.
|
||||
input_placeholders: List of input_placeholders for the loss function.
|
||||
Tensors of these shapes will be passed to build_graph() in order
|
||||
to define the per-device loss ops.
|
||||
rnn_inputs: Extra input placeholders for RNN inputs. These will have
|
||||
shape [BATCH_SIZE // MAX_SEQ_LEN, ...].
|
||||
max_per_device_batch_size: Number of tuples to optimize over at a time
|
||||
per device. In each call to `optimize()`,
|
||||
`len(devices) * per_device_batch_size` tuples of data will be
|
||||
processed. If this is larger than the total data size, it will be
|
||||
clipped.
|
||||
build_graph: Function that takes the specified inputs and returns a
|
||||
TF Policy instance.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
optimizer,
|
||||
devices,
|
||||
input_placeholders,
|
||||
rnn_inputs,
|
||||
max_per_device_batch_size,
|
||||
build_graph,
|
||||
grad_norm_clipping=None):
|
||||
self.optimizer = optimizer
|
||||
self.devices = devices
|
||||
self.max_per_device_batch_size = max_per_device_batch_size
|
||||
self.loss_inputs = input_placeholders + rnn_inputs
|
||||
self.build_graph = build_graph
|
||||
|
||||
# First initialize the shared loss network
|
||||
with tf1.name_scope(TOWER_SCOPE_NAME):
|
||||
self._shared_loss = build_graph(self.loss_inputs)
|
||||
shared_ops = tf1.get_collection(
|
||||
tf1.GraphKeys.UPDATE_OPS, scope=tf1.get_variable_scope().name)
|
||||
|
||||
# Then setup the per-device loss graphs that use the shared weights
|
||||
self._batch_index = tf1.placeholder(tf.int32, name="batch_index")
|
||||
|
||||
# Dynamic batch size, which may be shrunk if there isn't enough data
|
||||
self._per_device_batch_size = tf1.placeholder(
|
||||
tf.int32, name="per_device_batch_size")
|
||||
self._loaded_per_device_batch_size = max_per_device_batch_size
|
||||
|
||||
# When loading RNN input, we dynamically determine the max seq len
|
||||
self._max_seq_len = tf1.placeholder(tf.int32, name="max_seq_len")
|
||||
self._loaded_max_seq_len = 1
|
||||
|
||||
# Split on the CPU in case the data doesn't fit in GPU memory.
|
||||
with tf.device("/cpu:0"):
|
||||
data_splits = zip(
|
||||
*[tf.split(ph, len(devices)) for ph in self.loss_inputs])
|
||||
|
||||
self._towers = []
|
||||
for device, device_placeholders in zip(self.devices, data_splits):
|
||||
self._towers.append(
|
||||
self._setup_device(device, device_placeholders,
|
||||
len(input_placeholders)))
|
||||
|
||||
avg = average_gradients([t.grads for t in self._towers])
|
||||
if grad_norm_clipping:
|
||||
clipped = []
|
||||
for grad, _ in avg:
|
||||
clipped.append(grad)
|
||||
clipped, _ = tf.clip_by_global_norm(clipped, grad_norm_clipping)
|
||||
for i, (grad, var) in enumerate(avg):
|
||||
avg[i] = (clipped[i], var)
|
||||
|
||||
# gather update ops for any batch norm layers. TODO(ekl) here we will
|
||||
# use all the ops found which won't work for DQN / DDPG, but those
|
||||
# aren't supported with multi-gpu right now anyways.
|
||||
self._update_ops = tf1.get_collection(
|
||||
tf1.GraphKeys.UPDATE_OPS, scope=tf1.get_variable_scope().name)
|
||||
for op in shared_ops:
|
||||
self._update_ops.remove(op) # only care about tower update ops
|
||||
if self._update_ops:
|
||||
logger.debug("Update ops to run on apply gradient: {}".format(
|
||||
self._update_ops))
|
||||
|
||||
with tf1.control_dependencies(self._update_ops):
|
||||
self._train_op = self.optimizer.apply_gradients(avg)
|
||||
|
||||
def load_data(self, sess, inputs, state_inputs):
|
||||
"""Bulk loads the specified inputs into device memory.
|
||||
|
||||
The shape of the inputs must conform to the shapes of the input
|
||||
placeholders this optimizer was constructed with.
|
||||
|
||||
The data is split equally across all the devices. If the data is not
|
||||
evenly divisible by the batch size, excess data will be discarded.
|
||||
|
||||
Args:
|
||||
sess: TensorFlow session.
|
||||
inputs: List of arrays matching the input placeholders, of shape
|
||||
[BATCH_SIZE, ...].
|
||||
state_inputs: List of RNN input arrays. These arrays have size
|
||||
[BATCH_SIZE / MAX_SEQ_LEN, ...].
|
||||
|
||||
Returns:
|
||||
The number of tuples loaded per device.
|
||||
"""
|
||||
|
||||
if log_once("load_data"):
|
||||
logger.info(
|
||||
"Training on concatenated sample batches:\n\n{}\n".format(
|
||||
summarize({
|
||||
"placeholders": self.loss_inputs,
|
||||
"inputs": inputs,
|
||||
"state_inputs": state_inputs
|
||||
})))
|
||||
|
||||
feed_dict = {}
|
||||
assert len(self.loss_inputs) == len(inputs + state_inputs), \
|
||||
(self.loss_inputs, inputs, state_inputs)
|
||||
|
||||
# Let's suppose we have the following input data, and 2 devices:
|
||||
# 1 2 3 4 5 6 7 <- state inputs shape
|
||||
# A A A B B B C C C D D D E E E F F F G G G <- inputs shape
|
||||
# The data is truncated and split across devices as follows:
|
||||
# |---| seq len = 3
|
||||
# |---------------------------------| seq batch size = 6 seqs
|
||||
# |----------------| per device batch size = 9 tuples
|
||||
|
||||
if len(state_inputs) > 0:
|
||||
smallest_array = state_inputs[0]
|
||||
seq_len = len(inputs[0]) // len(state_inputs[0])
|
||||
self._loaded_max_seq_len = seq_len
|
||||
else:
|
||||
smallest_array = inputs[0]
|
||||
self._loaded_max_seq_len = 1
|
||||
|
||||
sequences_per_minibatch = (
|
||||
self.max_per_device_batch_size // self._loaded_max_seq_len * len(
|
||||
self.devices))
|
||||
if sequences_per_minibatch < 1:
|
||||
logger.warning(
|
||||
("Target minibatch size is {}, however the rollout sequence "
|
||||
"length is {}, hence the minibatch size will be raised to "
|
||||
"{}.").format(self.max_per_device_batch_size,
|
||||
self._loaded_max_seq_len,
|
||||
self._loaded_max_seq_len * len(self.devices)))
|
||||
sequences_per_minibatch = 1
|
||||
|
||||
if len(smallest_array) < sequences_per_minibatch:
|
||||
# Dynamically shrink the batch size if insufficient data
|
||||
sequences_per_minibatch = make_divisible_by(
|
||||
len(smallest_array), len(self.devices))
|
||||
|
||||
if log_once("data_slicing"):
|
||||
logger.info(
|
||||
("Divided {} rollout sequences, each of length {}, among "
|
||||
"{} devices.").format(
|
||||
len(smallest_array), self._loaded_max_seq_len,
|
||||
len(self.devices)))
|
||||
|
||||
if sequences_per_minibatch < len(self.devices):
|
||||
raise ValueError(
|
||||
"Must load at least 1 tuple sequence per device. Try "
|
||||
"increasing `sgd_minibatch_size` or reducing `max_seq_len` "
|
||||
"to ensure that at least one sequence fits per device.")
|
||||
self._loaded_per_device_batch_size = (sequences_per_minibatch // len(
|
||||
self.devices) * self._loaded_max_seq_len)
|
||||
|
||||
if len(state_inputs) > 0:
|
||||
# First truncate the RNN state arrays to the sequences_per_minib.
|
||||
state_inputs = [
|
||||
make_divisible_by(arr, sequences_per_minibatch)
|
||||
for arr in state_inputs
|
||||
]
|
||||
# Then truncate the data inputs to match
|
||||
inputs = [arr[:len(state_inputs[0]) * seq_len] for arr in inputs]
|
||||
assert len(state_inputs[0]) * seq_len == len(inputs[0]), \
|
||||
(len(state_inputs[0]), sequences_per_minibatch, seq_len,
|
||||
len(inputs[0]))
|
||||
for ph, arr in zip(self.loss_inputs, inputs + state_inputs):
|
||||
feed_dict[ph] = arr
|
||||
truncated_len = len(inputs[0])
|
||||
else:
|
||||
for ph, arr in zip(self.loss_inputs, inputs + state_inputs):
|
||||
truncated_arr = make_divisible_by(arr, sequences_per_minibatch)
|
||||
feed_dict[ph] = truncated_arr
|
||||
truncated_len = len(truncated_arr)
|
||||
|
||||
sess.run([t.init_op for t in self._towers], feed_dict=feed_dict)
|
||||
|
||||
self.num_tuples_loaded = truncated_len
|
||||
tuples_per_device = truncated_len // len(self.devices)
|
||||
assert tuples_per_device > 0, "No data loaded?"
|
||||
assert tuples_per_device % self._loaded_per_device_batch_size == 0
|
||||
return tuples_per_device
|
||||
|
||||
def optimize(self, sess, batch_index):
|
||||
"""Run a single step of SGD.
|
||||
|
||||
Runs a SGD step over a slice of the preloaded batch with size given by
|
||||
self._loaded_per_device_batch_size and offset given by the batch_index
|
||||
argument.
|
||||
|
||||
Updates shared model weights based on the averaged per-device
|
||||
gradients.
|
||||
|
||||
Args:
|
||||
sess: TensorFlow session.
|
||||
batch_index: Offset into the preloaded data. This value must be
|
||||
between `0` and `tuples_per_device`. The amount of data to
|
||||
process is at most `max_per_device_batch_size`.
|
||||
|
||||
Returns:
|
||||
The outputs of extra_ops evaluated over the batch.
|
||||
"""
|
||||
feed_dict = {
|
||||
self._batch_index: batch_index,
|
||||
self._per_device_batch_size: self._loaded_per_device_batch_size,
|
||||
self._max_seq_len: self._loaded_max_seq_len,
|
||||
}
|
||||
for tower in self._towers:
|
||||
feed_dict.update(tower.loss_graph.extra_compute_grad_feed_dict())
|
||||
|
||||
fetches = {"train": self._train_op}
|
||||
for tower in self._towers:
|
||||
fetches.update(tower.loss_graph._get_grad_and_stats_fetches())
|
||||
|
||||
return sess.run(fetches, feed_dict=feed_dict)
|
||||
|
||||
def get_common_loss(self):
|
||||
return self._shared_loss
|
||||
|
||||
def get_device_losses(self):
|
||||
return [t.loss_graph for t in self._towers]
|
||||
|
||||
def _setup_device(self, device, device_input_placeholders, num_data_in):
|
||||
assert num_data_in <= len(device_input_placeholders)
|
||||
with tf.device(device):
|
||||
with tf1.name_scope(TOWER_SCOPE_NAME):
|
||||
device_input_batches = []
|
||||
device_input_slices = []
|
||||
for i, ph in enumerate(device_input_placeholders):
|
||||
current_batch = tf1.Variable(
|
||||
ph,
|
||||
trainable=False,
|
||||
validate_shape=False,
|
||||
collections=[])
|
||||
device_input_batches.append(current_batch)
|
||||
if i < num_data_in:
|
||||
scale = self._max_seq_len
|
||||
granularity = self._max_seq_len
|
||||
else:
|
||||
scale = self._max_seq_len
|
||||
granularity = 1
|
||||
current_slice = tf.slice(
|
||||
current_batch,
|
||||
([self._batch_index // scale * granularity] +
|
||||
[0] * len(ph.shape[1:])),
|
||||
([self._per_device_batch_size // scale * granularity] +
|
||||
[-1] * len(ph.shape[1:])))
|
||||
current_slice.set_shape(ph.shape)
|
||||
device_input_slices.append(current_slice)
|
||||
graph_obj = self.build_graph(device_input_slices)
|
||||
device_grads = graph_obj.gradients(self.optimizer,
|
||||
graph_obj._loss)
|
||||
return Tower(
|
||||
tf.group(
|
||||
*[batch.initializer for batch in device_input_batches]),
|
||||
device_grads, graph_obj)
|
||||
|
||||
|
||||
# Each tower is a copy of the loss graph pinned to a specific device.
|
||||
Tower = namedtuple("Tower", ["init_op", "grads", "loss_graph"])
|
||||
|
||||
|
||||
def make_divisible_by(a, n):
|
||||
if type(a) is int:
|
||||
return a - a % n
|
||||
return a[0:a.shape[0] - a.shape[0] % n]
|
||||
|
||||
|
||||
def average_gradients(tower_grads):
|
||||
"""Averages gradients across towers.
|
||||
|
||||
Calculate the average gradient for each shared variable across all towers.
|
||||
Note that this function provides a synchronization point across all towers.
|
||||
|
||||
Args:
|
||||
tower_grads: List of lists of (gradient, variable) tuples. The outer
|
||||
list is over individual gradients. The inner list is over the
|
||||
gradient calculation for each tower.
|
||||
|
||||
Returns:
|
||||
List of pairs of (gradient, variable) where the gradient has been
|
||||
averaged across all towers.
|
||||
|
||||
TODO(ekl): We could use NCCL if this becomes a bottleneck.
|
||||
"""
|
||||
|
||||
average_grads = []
|
||||
for grad_and_vars in zip(*tower_grads):
|
||||
|
||||
# Note that each grad_and_vars looks like the following:
|
||||
# ((grad0_gpu0, var0_gpu0), ... , (grad0_gpuN, var0_gpuN))
|
||||
grads = []
|
||||
for g, _ in grad_and_vars:
|
||||
if g is not None:
|
||||
# Add 0 dimension to the gradients to represent the tower.
|
||||
expanded_g = tf.expand_dims(g, 0)
|
||||
|
||||
# Append on a 'tower' dimension which we will average over
|
||||
# below.
|
||||
grads.append(expanded_g)
|
||||
|
||||
if not grads:
|
||||
continue
|
||||
|
||||
# Average over the 'tower' dimension.
|
||||
grad = tf.concat(axis=0, values=grads)
|
||||
grad = tf.reduce_mean(grad, 0)
|
||||
|
||||
# Keep in mind that the Variables are redundant because they are shared
|
||||
# across towers. So .. we will just return the first tower's pointer to
|
||||
# the Variable.
|
||||
v = grad_and_vars[0][1]
|
||||
grad_and_var = (grad, v)
|
||||
average_grads.append(grad_and_var)
|
||||
|
||||
return average_grads
|
||||
@@ -1,234 +0,0 @@
|
||||
import logging
|
||||
import math
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
|
||||
import ray
|
||||
from ray.rllib.evaluation.metrics import LEARNER_STATS_KEY
|
||||
from ray.rllib.policy.tf_policy import TFPolicy
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.optimizers.multi_gpu_impl import LocalSyncParallelOptimizer
|
||||
from ray.rllib.optimizers.rollout import collect_samples
|
||||
from ray.rllib.policy.sample_batch import SampleBatch, DEFAULT_POLICY_ID, \
|
||||
MultiAgentBatch
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.framework import try_import_tf
|
||||
from ray.rllib.utils.sgd import averaged
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
tf1, tf, tfv = try_import_tf()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalMultiGPUOptimizer(PolicyOptimizer):
|
||||
"""A synchronous optimizer that uses multiple local GPUs.
|
||||
|
||||
Samples are pulled synchronously from multiple remote workers,
|
||||
concatenated, and then split across the memory of multiple local GPUs.
|
||||
A number of SGD passes are then taken over the in-memory data. For more
|
||||
details, see `multi_gpu_impl.LocalSyncParallelOptimizer`.
|
||||
|
||||
This optimizer is Tensorflow-specific and requires the underlying
|
||||
Policy to be a TFPolicy instance that implements the `copy()` method
|
||||
for multi-GPU tower generation.
|
||||
|
||||
Note that all replicas of the TFPolicy will merge their
|
||||
extra_compute_grad and apply_grad feed_dicts and fetches. This
|
||||
may result in unexpected behavior.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
sgd_batch_size=128,
|
||||
num_sgd_iter=10,
|
||||
rollout_fragment_length=200,
|
||||
num_envs_per_worker=1,
|
||||
train_batch_size=1024,
|
||||
num_gpus=0,
|
||||
standardize_fields=[],
|
||||
shuffle_sequences=True,
|
||||
_fake_gpus=False):
|
||||
"""Initialize a synchronous multi-gpu optimizer.
|
||||
|
||||
Arguments:
|
||||
workers (WorkerSet): all workers
|
||||
sgd_batch_size (int): SGD minibatch size within train batch size
|
||||
num_sgd_iter (int): number of passes to learn on per train batch
|
||||
rollout_fragment_length (int): size of batches to sample from
|
||||
workers.
|
||||
num_envs_per_worker (int): num envs in each rollout worker
|
||||
train_batch_size (int): size of batches to learn on
|
||||
num_gpus (int): number of GPUs to use for data-parallel SGD
|
||||
standardize_fields (list): list of fields in the training batch
|
||||
to normalize
|
||||
shuffle_sequences (bool): whether to shuffle the train batch prior
|
||||
to SGD to break up correlations
|
||||
_fake_gpus (bool): Whether to use fake-GPUs (CPUs) instead of
|
||||
actual GPUs (should only be used for testing on non-GPU
|
||||
machines).
|
||||
"""
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
self.batch_size = sgd_batch_size
|
||||
self.num_sgd_iter = num_sgd_iter
|
||||
self.num_envs_per_worker = num_envs_per_worker
|
||||
self.rollout_fragment_length = rollout_fragment_length
|
||||
self.train_batch_size = train_batch_size
|
||||
self.shuffle_sequences = shuffle_sequences
|
||||
|
||||
# Collect actual devices to use.
|
||||
if not num_gpus:
|
||||
_fake_gpus = True
|
||||
num_gpus = 1
|
||||
type_ = "cpu" if _fake_gpus else "gpu"
|
||||
self.devices = [
|
||||
"/{}:{}".format(type_, i) for i in range(int(math.ceil(num_gpus)))
|
||||
]
|
||||
|
||||
self.batch_size = int(sgd_batch_size / len(self.devices)) * len(
|
||||
self.devices)
|
||||
assert self.batch_size % len(self.devices) == 0
|
||||
assert self.batch_size >= len(self.devices), "batch size too small"
|
||||
self.per_device_batch_size = int(self.batch_size / len(self.devices))
|
||||
self.sample_timer = TimerStat()
|
||||
self.load_timer = TimerStat()
|
||||
self.grad_timer = TimerStat()
|
||||
self.update_weights_timer = TimerStat()
|
||||
self.standardize_fields = standardize_fields
|
||||
|
||||
logger.info("LocalMultiGPUOptimizer devices {}".format(self.devices))
|
||||
|
||||
self.policies = dict(self.workers.local_worker()
|
||||
.foreach_trainable_policy(lambda p, i: (i, p)))
|
||||
logger.debug("Policies to train: {}".format(self.policies))
|
||||
for policy_id, policy in self.policies.items():
|
||||
if not isinstance(policy, TFPolicy):
|
||||
raise ValueError(
|
||||
"Only TF graph policies are supported with multi-GPU. "
|
||||
"Try setting `simple_optimizer=True` instead.")
|
||||
|
||||
# per-GPU graph copies created below must share vars with the policy
|
||||
# reuse is set to AUTO_REUSE because Adam nodes are created after
|
||||
# all of the device copies are created.
|
||||
self.optimizers = {}
|
||||
with self.workers.local_worker().tf_sess.graph.as_default():
|
||||
with self.workers.local_worker().tf_sess.as_default():
|
||||
for policy_id, policy in self.policies.items():
|
||||
with tf1.variable_scope(policy_id, reuse=tf1.AUTO_REUSE):
|
||||
if policy._state_inputs:
|
||||
rnn_inputs = policy._state_inputs + [
|
||||
policy._seq_lens
|
||||
]
|
||||
else:
|
||||
rnn_inputs = []
|
||||
self.optimizers[policy_id] = (
|
||||
LocalSyncParallelOptimizer(
|
||||
policy._optimizer, self.devices,
|
||||
[v
|
||||
for _, v in policy._loss_inputs], rnn_inputs,
|
||||
self.per_device_batch_size, policy.copy))
|
||||
|
||||
self.sess = self.workers.local_worker().tf_sess
|
||||
self.sess.run(tf1.global_variables_initializer())
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
with self.update_weights_timer:
|
||||
if self.workers.remote_workers():
|
||||
weights = ray.put(self.workers.local_worker().get_weights())
|
||||
for e in self.workers.remote_workers():
|
||||
e.set_weights.remote(weights)
|
||||
|
||||
with self.sample_timer:
|
||||
if self.workers.remote_workers():
|
||||
samples = collect_samples(self.workers.remote_workers(),
|
||||
self.rollout_fragment_length,
|
||||
self.num_envs_per_worker,
|
||||
self.train_batch_size)
|
||||
if samples.count > self.train_batch_size * 2:
|
||||
logger.info(
|
||||
"Collected more training samples than expected "
|
||||
"(actual={}, train_batch_size={}). ".format(
|
||||
samples.count, self.train_batch_size) +
|
||||
"This may be because you have many workers or "
|
||||
"long episodes in 'complete_episodes' batch mode.")
|
||||
else:
|
||||
samples = []
|
||||
while sum(s.count for s in samples) < self.train_batch_size:
|
||||
samples.append(self.workers.local_worker().sample())
|
||||
samples = SampleBatch.concat_samples(samples)
|
||||
|
||||
# Handle everything as if multiagent
|
||||
if isinstance(samples, SampleBatch):
|
||||
samples = MultiAgentBatch({
|
||||
DEFAULT_POLICY_ID: samples
|
||||
}, samples.count)
|
||||
|
||||
for policy_id, policy in self.policies.items():
|
||||
if policy_id not in samples.policy_batches:
|
||||
continue
|
||||
|
||||
batch = samples.policy_batches[policy_id]
|
||||
for field in self.standardize_fields:
|
||||
value = batch[field]
|
||||
standardized = (value - value.mean()) / max(1e-4, value.std())
|
||||
batch[field] = standardized
|
||||
|
||||
num_loaded_tuples = {}
|
||||
with self.load_timer:
|
||||
for policy_id, batch in samples.policy_batches.items():
|
||||
if policy_id not in self.policies:
|
||||
continue
|
||||
|
||||
policy = self.policies[policy_id]
|
||||
policy._debug_vars()
|
||||
tuples = policy._get_loss_inputs_dict(
|
||||
batch, shuffle=self.shuffle_sequences)
|
||||
data_keys = [ph for _, ph in policy._loss_inputs]
|
||||
if policy._state_inputs:
|
||||
state_keys = policy._state_inputs + [policy._seq_lens]
|
||||
else:
|
||||
state_keys = []
|
||||
num_loaded_tuples[policy_id] = (
|
||||
self.optimizers[policy_id].load_data(
|
||||
self.sess, [tuples[k] for k in data_keys],
|
||||
[tuples[k] for k in state_keys]))
|
||||
|
||||
fetches = {}
|
||||
with self.grad_timer:
|
||||
for policy_id, tuples_per_device in num_loaded_tuples.items():
|
||||
optimizer = self.optimizers[policy_id]
|
||||
num_batches = max(
|
||||
1,
|
||||
int(tuples_per_device) // int(self.per_device_batch_size))
|
||||
logger.debug("== sgd epochs for {} ==".format(policy_id))
|
||||
for i in range(self.num_sgd_iter):
|
||||
iter_extra_fetches = defaultdict(list)
|
||||
permutation = np.random.permutation(num_batches)
|
||||
for batch_index in range(num_batches):
|
||||
batch_fetches = optimizer.optimize(
|
||||
self.sess, permutation[batch_index] *
|
||||
self.per_device_batch_size)
|
||||
for k, v in batch_fetches[LEARNER_STATS_KEY].items():
|
||||
iter_extra_fetches[k].append(v)
|
||||
logger.debug("{} {}".format(i,
|
||||
averaged(iter_extra_fetches)))
|
||||
fetches[policy_id] = averaged(iter_extra_fetches)
|
||||
|
||||
self.num_steps_sampled += samples.count
|
||||
self.num_steps_trained += tuples_per_device * len(self.devices)
|
||||
self.learner_stats = fetches
|
||||
return fetches
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
return dict(
|
||||
PolicyOptimizer.stats(self), **{
|
||||
"sample_time_ms": round(1000 * self.sample_timer.mean, 3),
|
||||
"load_time_ms": round(1000 * self.load_timer.mean, 3),
|
||||
"grad_time_ms": round(1000 * self.grad_timer.mean, 3),
|
||||
"update_time_ms": round(1000 * self.update_weights_timer.mean,
|
||||
3),
|
||||
"learner": self.learner_stats,
|
||||
})
|
||||
@@ -1,132 +0,0 @@
|
||||
import logging
|
||||
|
||||
from ray.rllib.utils.annotations import DeveloperAPI
|
||||
from ray.rllib.evaluation.metrics import collect_episodes, summarize_episodes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class PolicyOptimizer:
|
||||
"""Policy optimizers encapsulate distributed RL optimization strategies.
|
||||
|
||||
Policy optimizers serve as the "control plane" of algorithms.
|
||||
|
||||
For example, AsyncOptimizer is used for A3C, and LocalMultiGPUOptimizer is
|
||||
used for PPO. These optimizers are all pluggable, and it is possible
|
||||
to mix and match as needed.
|
||||
|
||||
Attributes:
|
||||
config (dict): The JSON configuration passed to this optimizer.
|
||||
workers (WorkerSet): The set of rollout workers to use.
|
||||
num_steps_trained (int): Number of timesteps trained on so far.
|
||||
num_steps_sampled (int): Number of timesteps sampled so far.
|
||||
"""
|
||||
|
||||
@DeveloperAPI
|
||||
def __init__(self, workers):
|
||||
"""Create an optimizer instance.
|
||||
|
||||
Args:
|
||||
workers (WorkerSet): The set of rollout workers to use.
|
||||
"""
|
||||
self.workers = workers
|
||||
self.episode_history = []
|
||||
self.to_be_collected = []
|
||||
|
||||
# Counters that should be updated by sub-classes
|
||||
self.num_steps_trained = 0
|
||||
self.num_steps_sampled = 0
|
||||
|
||||
@DeveloperAPI
|
||||
def step(self):
|
||||
"""Takes a logical optimization step.
|
||||
|
||||
This should run for long enough to minimize call overheads (i.e., at
|
||||
least a couple seconds), but short enough to return control
|
||||
periodically to callers (i.e., at most a few tens of seconds).
|
||||
|
||||
Returns:
|
||||
fetches (dict|None): Optional fetches from compute grads calls.
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
@DeveloperAPI
|
||||
def stats(self):
|
||||
"""Returns a dictionary of internal performance statistics."""
|
||||
|
||||
return {
|
||||
"num_steps_trained": self.num_steps_trained,
|
||||
"num_steps_sampled": self.num_steps_sampled,
|
||||
}
|
||||
|
||||
@DeveloperAPI
|
||||
def save(self):
|
||||
"""Returns a serializable object representing the optimizer state."""
|
||||
|
||||
return [self.num_steps_trained, self.num_steps_sampled]
|
||||
|
||||
@DeveloperAPI
|
||||
def restore(self, data):
|
||||
"""Restores optimizer state from the given data object."""
|
||||
|
||||
self.num_steps_trained = data[0]
|
||||
self.num_steps_sampled = data[1]
|
||||
|
||||
@DeveloperAPI
|
||||
def stop(self):
|
||||
"""Release any resources used by this optimizer."""
|
||||
pass
|
||||
|
||||
@DeveloperAPI
|
||||
def collect_metrics(self,
|
||||
timeout_seconds,
|
||||
min_history=100,
|
||||
selected_workers=None):
|
||||
"""Returns worker and optimizer stats.
|
||||
|
||||
Arguments:
|
||||
timeout_seconds (int): Max wait time for a worker before
|
||||
dropping its results. This usually indicates a hung worker.
|
||||
min_history (int): Min history length to smooth results over.
|
||||
selected_workers (list): Override the list of remote workers
|
||||
to collect metrics from.
|
||||
|
||||
Returns:
|
||||
res (dict): A training result dict from worker metrics with
|
||||
`info` replaced with stats from self.
|
||||
"""
|
||||
episodes, self.to_be_collected = collect_episodes(
|
||||
self.workers.local_worker(),
|
||||
selected_workers or self.workers.remote_workers(),
|
||||
self.to_be_collected,
|
||||
timeout_seconds=timeout_seconds)
|
||||
orig_episodes = list(episodes)
|
||||
missing = min_history - len(episodes)
|
||||
if missing > 0:
|
||||
episodes.extend(self.episode_history[-missing:])
|
||||
assert len(episodes) <= min_history
|
||||
self.episode_history.extend(orig_episodes)
|
||||
self.episode_history = self.episode_history[-min_history:]
|
||||
res = summarize_episodes(episodes, orig_episodes)
|
||||
res.update(info=self.stats())
|
||||
return res
|
||||
|
||||
@DeveloperAPI
|
||||
def reset(self, remote_workers):
|
||||
"""Called to change the set of remote workers being used."""
|
||||
self.workers.reset(remote_workers)
|
||||
|
||||
@DeveloperAPI
|
||||
def foreach_worker(self, func):
|
||||
"""Apply the given function to each worker instance."""
|
||||
return self.workers.foreach_worker(func)
|
||||
|
||||
@DeveloperAPI
|
||||
def foreach_worker_with_index(self, func):
|
||||
"""Apply the given function to each worker instance.
|
||||
|
||||
The index will be passed as the second arg to the given function.
|
||||
"""
|
||||
return self.workers.foreach_worker_with_index(func)
|
||||
@@ -1,279 +0,0 @@
|
||||
import numpy as np
|
||||
import random
|
||||
import sys
|
||||
|
||||
from ray.rllib.optimizers.segment_tree import SumSegmentTree, MinSegmentTree
|
||||
from ray.rllib.utils.annotations import DeveloperAPI
|
||||
from ray.rllib.utils.compression import unpack_if_needed
|
||||
from ray.rllib.utils.window_stat import WindowStat
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class ReplayBuffer:
|
||||
@DeveloperAPI
|
||||
def __init__(self, size):
|
||||
"""Create Prioritized Replay buffer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size: int
|
||||
Max number of transitions to store in the buffer. When the buffer
|
||||
overflows the old memories are dropped.
|
||||
"""
|
||||
self._storage = []
|
||||
self._maxsize = size
|
||||
self._next_idx = 0
|
||||
self._hit_count = np.zeros(size)
|
||||
self._eviction_started = False
|
||||
self._num_added = 0
|
||||
self._num_sampled = 0
|
||||
self._evicted_hit_stats = WindowStat("evicted_hit", 1000)
|
||||
self._est_size_bytes = 0
|
||||
|
||||
def __len__(self):
|
||||
return len(self._storage)
|
||||
|
||||
@DeveloperAPI
|
||||
def add(self, obs_t, action, reward, obs_tp1, done, weight):
|
||||
data = (obs_t, action, reward, obs_tp1, done)
|
||||
self._num_added += 1
|
||||
|
||||
if self._next_idx >= len(self._storage):
|
||||
self._storage.append(data)
|
||||
self._est_size_bytes += sum(sys.getsizeof(d) for d in data)
|
||||
else:
|
||||
self._storage[self._next_idx] = data
|
||||
if self._next_idx + 1 >= self._maxsize:
|
||||
self._eviction_started = True
|
||||
self._next_idx = (self._next_idx + 1) % self._maxsize
|
||||
if self._eviction_started:
|
||||
self._evicted_hit_stats.push(self._hit_count[self._next_idx])
|
||||
self._hit_count[self._next_idx] = 0
|
||||
|
||||
def _encode_sample(self, idxes):
|
||||
obses_t, actions, rewards, obses_tp1, dones = [], [], [], [], []
|
||||
for i in idxes:
|
||||
data = self._storage[i]
|
||||
obs_t, action, reward, obs_tp1, done = data
|
||||
obses_t.append(np.array(unpack_if_needed(obs_t), copy=False))
|
||||
actions.append(np.array(action, copy=False))
|
||||
rewards.append(reward)
|
||||
obses_tp1.append(np.array(unpack_if_needed(obs_tp1), copy=False))
|
||||
dones.append(done)
|
||||
self._hit_count[i] += 1
|
||||
return (np.array(obses_t), np.array(actions), np.array(rewards),
|
||||
np.array(obses_tp1), np.array(dones))
|
||||
|
||||
@DeveloperAPI
|
||||
def sample_idxes(self, batch_size):
|
||||
return np.random.randint(0, len(self._storage), batch_size)
|
||||
|
||||
@DeveloperAPI
|
||||
def sample_with_idxes(self, idxes):
|
||||
self._num_sampled += len(idxes)
|
||||
return self._encode_sample(idxes)
|
||||
|
||||
@DeveloperAPI
|
||||
def sample(self, batch_size):
|
||||
"""Sample a batch of experiences.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
batch_size: int
|
||||
How many transitions to sample.
|
||||
|
||||
Returns
|
||||
-------
|
||||
obs_batch: np.array
|
||||
batch of observations
|
||||
act_batch: np.array
|
||||
batch of actions executed given obs_batch
|
||||
rew_batch: np.array
|
||||
rewards received as results of executing act_batch
|
||||
next_obs_batch: np.array
|
||||
next set of observations seen after executing act_batch
|
||||
done_mask: np.array
|
||||
done_mask[i] = 1 if executing act_batch[i] resulted in
|
||||
the end of an episode and 0 otherwise.
|
||||
"""
|
||||
idxes = [
|
||||
random.randint(0,
|
||||
len(self._storage) - 1) for _ in range(batch_size)
|
||||
]
|
||||
self._num_sampled += batch_size
|
||||
return self._encode_sample(idxes)
|
||||
|
||||
@DeveloperAPI
|
||||
def stats(self, debug=False):
|
||||
data = {
|
||||
"added_count": self._num_added,
|
||||
"sampled_count": self._num_sampled,
|
||||
"est_size_bytes": self._est_size_bytes,
|
||||
"num_entries": len(self._storage),
|
||||
}
|
||||
if debug:
|
||||
data.update(self._evicted_hit_stats.stats())
|
||||
return data
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
class PrioritizedReplayBuffer(ReplayBuffer):
|
||||
@DeveloperAPI
|
||||
def __init__(self, size, alpha):
|
||||
"""Create Prioritized Replay buffer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
size: int
|
||||
Max number of transitions to store in the buffer. When the buffer
|
||||
overflows the old memories are dropped.
|
||||
alpha: float
|
||||
how much prioritization is used
|
||||
(0 - no prioritization, 1 - full prioritization)
|
||||
|
||||
See Also
|
||||
--------
|
||||
ReplayBuffer.__init__
|
||||
"""
|
||||
super(PrioritizedReplayBuffer, self).__init__(size)
|
||||
assert alpha > 0
|
||||
self._alpha = alpha
|
||||
|
||||
it_capacity = 1
|
||||
while it_capacity < size:
|
||||
it_capacity *= 2
|
||||
|
||||
self._it_sum = SumSegmentTree(it_capacity)
|
||||
self._it_min = MinSegmentTree(it_capacity)
|
||||
self._max_priority = 1.0
|
||||
self._prio_change_stats = WindowStat("reprio", 1000)
|
||||
|
||||
@DeveloperAPI
|
||||
def add(self, obs_t, action, reward, obs_tp1, done, weight):
|
||||
"""See ReplayBuffer.store_effect"""
|
||||
|
||||
idx = self._next_idx
|
||||
super(PrioritizedReplayBuffer, self).add(obs_t, action, reward,
|
||||
obs_tp1, done, weight)
|
||||
if weight is None:
|
||||
weight = self._max_priority
|
||||
self._it_sum[idx] = weight**self._alpha
|
||||
self._it_min[idx] = weight**self._alpha
|
||||
|
||||
def _sample_proportional(self, batch_size):
|
||||
res = []
|
||||
for _ in range(batch_size):
|
||||
# TODO(szymon): should we ensure no repeats?
|
||||
mass = random.random() * self._it_sum.sum(0, len(self._storage))
|
||||
idx = self._it_sum.find_prefixsum_idx(mass)
|
||||
res.append(idx)
|
||||
return res
|
||||
|
||||
@DeveloperAPI
|
||||
def sample_idxes(self, batch_size):
|
||||
return self._sample_proportional(batch_size)
|
||||
|
||||
@DeveloperAPI
|
||||
def sample_with_idxes(self, idxes, beta):
|
||||
assert beta > 0
|
||||
self._num_sampled += len(idxes)
|
||||
|
||||
weights = []
|
||||
p_min = self._it_min.min() / self._it_sum.sum()
|
||||
max_weight = (p_min * len(self._storage))**(-beta)
|
||||
|
||||
for idx in idxes:
|
||||
p_sample = self._it_sum[idx] / self._it_sum.sum()
|
||||
weight = (p_sample * len(self._storage))**(-beta)
|
||||
weights.append(weight / max_weight)
|
||||
weights = np.array(weights)
|
||||
encoded_sample = self._encode_sample(idxes)
|
||||
return tuple(list(encoded_sample) + [weights, idxes])
|
||||
|
||||
@DeveloperAPI
|
||||
def sample(self, batch_size, beta):
|
||||
"""Sample a batch of experiences.
|
||||
|
||||
compared to ReplayBuffer.sample
|
||||
it also returns importance weights and idxes
|
||||
of sampled experiences.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
batch_size: int
|
||||
How many transitions to sample.
|
||||
beta: float
|
||||
To what degree to use importance weights
|
||||
(0 - no corrections, 1 - full correction)
|
||||
|
||||
Returns
|
||||
-------
|
||||
obs_batch: np.array
|
||||
batch of observations
|
||||
act_batch: np.array
|
||||
batch of actions executed given obs_batch
|
||||
rew_batch: np.array
|
||||
rewards received as results of executing act_batch
|
||||
next_obs_batch: np.array
|
||||
next set of observations seen after executing act_batch
|
||||
done_mask: np.array
|
||||
done_mask[i] = 1 if executing act_batch[i] resulted in
|
||||
the end of an episode and 0 otherwise.
|
||||
weights: np.array
|
||||
Array of shape (batch_size,) and dtype np.float32
|
||||
denoting importance weight of each sampled transition
|
||||
idxes: np.array
|
||||
Array of shape (batch_size,) and dtype np.int32
|
||||
idexes in buffer of sampled experiences
|
||||
"""
|
||||
assert beta >= 0.0
|
||||
self._num_sampled += batch_size
|
||||
|
||||
idxes = self._sample_proportional(batch_size)
|
||||
|
||||
weights = []
|
||||
p_min = self._it_min.min() / self._it_sum.sum()
|
||||
max_weight = (p_min * len(self._storage))**(-beta)
|
||||
|
||||
for idx in idxes:
|
||||
p_sample = self._it_sum[idx] / self._it_sum.sum()
|
||||
weight = (p_sample * len(self._storage))**(-beta)
|
||||
weights.append(weight / max_weight)
|
||||
weights = np.array(weights)
|
||||
encoded_sample = self._encode_sample(idxes)
|
||||
return tuple(list(encoded_sample) + [weights, idxes])
|
||||
|
||||
@DeveloperAPI
|
||||
def update_priorities(self, idxes, priorities):
|
||||
"""Update priorities of sampled transitions.
|
||||
|
||||
sets priority of transition at index idxes[i] in buffer
|
||||
to priorities[i].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
idxes: [int]
|
||||
List of idxes of sampled transitions
|
||||
priorities: [float]
|
||||
List of updated priorities corresponding to
|
||||
transitions at the sampled idxes denoted by
|
||||
variable `idxes`.
|
||||
"""
|
||||
assert len(idxes) == len(priorities)
|
||||
for idx, priority in zip(idxes, priorities):
|
||||
assert priority > 0
|
||||
assert 0 <= idx < len(self._storage)
|
||||
delta = priority**self._alpha - self._it_sum[idx]
|
||||
self._prio_change_stats.push(delta)
|
||||
self._it_sum[idx] = priority**self._alpha
|
||||
self._it_min[idx] = priority**self._alpha
|
||||
|
||||
self._max_priority = max(self._max_priority, priority)
|
||||
|
||||
@DeveloperAPI
|
||||
def stats(self, debug=False):
|
||||
parent = ReplayBuffer.stats(self, debug)
|
||||
if debug:
|
||||
parent.update(self._prio_change_stats.stats())
|
||||
return parent
|
||||
@@ -1,35 +0,0 @@
|
||||
import logging
|
||||
|
||||
import ray
|
||||
from ray.rllib.policy.sample_batch import SampleBatch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def collect_samples(agents, rollout_fragment_length, num_envs_per_worker,
|
||||
train_batch_size):
|
||||
"""Collects at least train_batch_size samples, never discarding any."""
|
||||
|
||||
num_timesteps_so_far = 0
|
||||
trajectories = []
|
||||
agent_dict = {}
|
||||
|
||||
for agent in agents:
|
||||
fut_sample = agent.sample.remote()
|
||||
agent_dict[fut_sample] = agent
|
||||
|
||||
while agent_dict:
|
||||
[fut_sample], _ = ray.wait(list(agent_dict))
|
||||
agent = agent_dict.pop(fut_sample)
|
||||
next_sample = ray.get(fut_sample)
|
||||
num_timesteps_so_far += next_sample.count
|
||||
trajectories.append(next_sample)
|
||||
|
||||
# Only launch more tasks if we don't already have enough pending
|
||||
pending = len(
|
||||
agent_dict) * rollout_fragment_length * num_envs_per_worker
|
||||
if num_timesteps_so_far + pending < train_batch_size:
|
||||
fut_sample2 = agent.sample.remote()
|
||||
agent_dict[fut_sample2] = agent
|
||||
|
||||
return SampleBatch.concat_samples(trajectories)
|
||||
@@ -1,196 +0,0 @@
|
||||
import operator
|
||||
|
||||
|
||||
class SegmentTree:
|
||||
"""A Segment Tree data structure.
|
||||
|
||||
https://en.wikipedia.org/wiki/Segment_tree
|
||||
|
||||
Can be used as regular array, but with two important differences:
|
||||
|
||||
a) Setting an item's value is slightly slower. It is O(lg capacity),
|
||||
instead of O(1).
|
||||
b) Offers efficient `reduce` operation which reduces the tree's values
|
||||
over some specified contiguous subsequence of items in the array.
|
||||
Operation could be e.g. min/max/sum.
|
||||
|
||||
The data is stored in a list, where the length is 2 * capacity.
|
||||
The second half of the list stores the actual values for each index, so if
|
||||
capacity=8, values are stored at indices 8 to 15. The first half of the
|
||||
array contains the reduced-values of the different (binary divided)
|
||||
segments, e.g. (capacity=4):
|
||||
0=not used
|
||||
1=reduced-value over all elements (array indices 4 to 7).
|
||||
2=reduced-value over array indices (4 and 5).
|
||||
3=reduced-value over array indices (6 and 7).
|
||||
4-7: values of the tree.
|
||||
NOTE that the values of the tree are accessed by indices starting at 0, so
|
||||
`tree[0]` accesses `internal_array[4]` in the above example.
|
||||
"""
|
||||
|
||||
def __init__(self, capacity, operation, neutral_element=None):
|
||||
"""Initializes a Segment Tree object.
|
||||
|
||||
Args:
|
||||
capacity (int): Total size of the array - must be a power of two.
|
||||
operation (operation): Lambda obj, obj -> obj
|
||||
The operation for combining elements (eg. sum, max).
|
||||
Must be a mathematical group together with the set of
|
||||
possible values for array elements.
|
||||
neutral_element (Optional[obj]): The neutral element for
|
||||
`operation`. Use None for automatically finding a value:
|
||||
max: float("-inf"), min: float("inf"), sum: 0.0.
|
||||
"""
|
||||
|
||||
assert capacity > 0 and capacity & (capacity - 1) == 0, \
|
||||
"Capacity must be positive and a power of 2!"
|
||||
self.capacity = capacity
|
||||
if neutral_element is None:
|
||||
neutral_element = 0.0 if operation is operator.add else \
|
||||
float("-inf") if operation is max else float("inf")
|
||||
self.neutral_element = neutral_element
|
||||
self.value = [self.neutral_element for _ in range(2 * capacity)]
|
||||
self.operation = operation
|
||||
|
||||
def reduce(self, start=0, end=None):
|
||||
"""Applies `self.operation` to subsequence of our values.
|
||||
|
||||
Subsequence is contiguous, includes `start` and excludes `end`.
|
||||
|
||||
self.operation(
|
||||
arr[start], operation(arr[start+1], operation(... arr[end])))
|
||||
|
||||
Args:
|
||||
start (int): Start index to apply reduction to.
|
||||
end (Optional[int]): End index to apply reduction to (excluded).
|
||||
|
||||
Returns:
|
||||
any: The result of reducing self.operation over the specified
|
||||
range of `self._value` elements.
|
||||
"""
|
||||
if end is None:
|
||||
end = self.capacity
|
||||
elif end < 0:
|
||||
end += self.capacity
|
||||
|
||||
# Init result with neutral element.
|
||||
result = self.neutral_element
|
||||
# Map start/end to our actual index space (second half of array).
|
||||
start += self.capacity
|
||||
end += self.capacity
|
||||
|
||||
# Example:
|
||||
# internal-array (first half=sums, second half=actual values):
|
||||
# 0 1 2 3 | 4 5 6 7
|
||||
# - 6 1 5 | 1 0 2 3
|
||||
|
||||
# tree.sum(0, 3) = 3
|
||||
# internally: start=4, end=7 -> sum values 1 0 2 = 3.
|
||||
|
||||
# Iterate over tree starting in the actual-values (second half)
|
||||
# section.
|
||||
# 1) start=4 is even -> do nothing.
|
||||
# 2) end=7 is odd -> end-- -> end=6 -> add value to result: result=2
|
||||
# 3) int-divide start and end by 2: start=2, end=3
|
||||
# 4) start still smaller end -> iterate once more.
|
||||
# 5) start=2 is even -> do nothing.
|
||||
# 6) end=3 is odd -> end-- -> end=2 -> add value to result: result=1
|
||||
# NOTE: This adds the sum of indices 4 and 5 to the result.
|
||||
|
||||
# Iterate as long as start != end.
|
||||
while start < end:
|
||||
|
||||
# If start is odd: Add its value to result and move start to
|
||||
# next even value.
|
||||
if start & 1:
|
||||
result = self.operation(result, self.value[start])
|
||||
start += 1
|
||||
|
||||
# If end is odd: Move end to previous even value, then add its
|
||||
# value to result. NOTE: This takes care of excluding `end` in any
|
||||
# situation.
|
||||
if end & 1:
|
||||
end -= 1
|
||||
result = self.operation(result, self.value[end])
|
||||
|
||||
# Divide both start and end by 2 to make them "jump" into the
|
||||
# next upper level reduce-index space.
|
||||
start //= 2
|
||||
end //= 2
|
||||
|
||||
# Then repeat till start == end.
|
||||
|
||||
return result
|
||||
|
||||
def __setitem__(self, idx, val):
|
||||
"""
|
||||
Inserts/overwrites a value in/into the tree.
|
||||
|
||||
Args:
|
||||
idx (int): The index to insert to. Must be in [0, `self.capacity`[
|
||||
val (float): The value to insert.
|
||||
"""
|
||||
assert 0 <= idx < self.capacity
|
||||
|
||||
# Index of the leaf to insert into (always insert in "second half"
|
||||
# of the tree, the first half is reserved for already calculated
|
||||
# reduction-values).
|
||||
idx += self.capacity
|
||||
self.value[idx] = val
|
||||
|
||||
# Recalculate all affected reduction values (in "first half" of tree).
|
||||
idx = idx >> 1 # Divide by 2 (faster than division).
|
||||
while idx >= 1:
|
||||
update_idx = 2 * idx # calculate only once
|
||||
# Update the reduction value at the correct "first half" idx.
|
||||
self.value[idx] = self.operation(self.value[update_idx],
|
||||
self.value[update_idx + 1])
|
||||
idx = idx >> 1 # Divide by 2 (faster than division).
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert 0 <= idx < self.capacity
|
||||
return self.value[idx + self.capacity]
|
||||
|
||||
|
||||
class SumSegmentTree(SegmentTree):
|
||||
"""A SegmentTree with the reduction `operation`=operator.add."""
|
||||
|
||||
def __init__(self, capacity):
|
||||
super(SumSegmentTree, self).__init__(
|
||||
capacity=capacity, operation=operator.add)
|
||||
|
||||
def sum(self, start=0, end=None):
|
||||
"""Returns the sum over a sub-segment of the tree."""
|
||||
return self.reduce(start, end)
|
||||
|
||||
def find_prefixsum_idx(self, prefixsum):
|
||||
"""Finds highest i, for which: sum(arr[0]+..+arr[i - i]) <= prefixsum.
|
||||
|
||||
Args:
|
||||
prefixsum (float): `prefixsum` upper bound in above constraint.
|
||||
|
||||
Returns:
|
||||
int: Largest possible index (i) satisfying above constraint.
|
||||
"""
|
||||
assert 0 <= prefixsum <= self.sum() + 1e-5
|
||||
# Global sum node.
|
||||
idx = 1
|
||||
|
||||
# While non-leaf (first half of tree).
|
||||
while idx < self.capacity:
|
||||
update_idx = 2 * idx
|
||||
if self.value[update_idx] > prefixsum:
|
||||
idx = update_idx
|
||||
else:
|
||||
prefixsum -= self.value[update_idx]
|
||||
idx = update_idx + 1
|
||||
return idx - self.capacity
|
||||
|
||||
|
||||
class MinSegmentTree(SegmentTree):
|
||||
def __init__(self, capacity):
|
||||
super(MinSegmentTree, self).__init__(capacity=capacity, operation=min)
|
||||
|
||||
def min(self, start=0, end=None):
|
||||
"""Returns min(arr[start], ..., arr[end])"""
|
||||
return self.reduce(start, end)
|
||||
@@ -1,116 +0,0 @@
|
||||
import random
|
||||
|
||||
import ray
|
||||
from ray.rllib.evaluation.metrics import get_learner_stats
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.policy.sample_batch import SampleBatch, DEFAULT_POLICY_ID, \
|
||||
MultiAgentBatch
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
|
||||
class SyncBatchReplayOptimizer(PolicyOptimizer):
|
||||
"""Variant of the sync replay optimizer that replays entire batches.
|
||||
|
||||
This enables RNN support. Does not currently support prioritization."""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
learning_starts=1000,
|
||||
buffer_size=10000,
|
||||
train_batch_size=32):
|
||||
"""Initialize a batch replay optimizer.
|
||||
|
||||
Arguments:
|
||||
workers (WorkerSet): set of all workers
|
||||
learning_starts (int): start learning after this number of
|
||||
timesteps have been collected
|
||||
buffer_size (int): max timesteps to keep in the replay buffer
|
||||
train_batch_size (int): number of timesteps to train on at once
|
||||
"""
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
self.replay_starts = learning_starts
|
||||
self.max_buffer_size = buffer_size
|
||||
self.train_batch_size = train_batch_size
|
||||
assert self.max_buffer_size >= self.replay_starts
|
||||
|
||||
# List of buffered sample batches
|
||||
self.replay_buffer = []
|
||||
self.buffer_size = 0
|
||||
|
||||
# Stats
|
||||
self.update_weights_timer = TimerStat()
|
||||
self.sample_timer = TimerStat()
|
||||
self.grad_timer = TimerStat()
|
||||
self.learner_stats = {}
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
with self.update_weights_timer:
|
||||
if self.workers.remote_workers():
|
||||
weights = ray.put(self.workers.local_worker().get_weights())
|
||||
for e in self.workers.remote_workers():
|
||||
e.set_weights.remote(weights)
|
||||
|
||||
with self.sample_timer:
|
||||
if self.workers.remote_workers():
|
||||
batches = ray.get(
|
||||
[e.sample.remote() for e in self.workers.remote_workers()])
|
||||
else:
|
||||
batches = [self.workers.local_worker().sample()]
|
||||
|
||||
# Handle everything as if multiagent
|
||||
tmp = []
|
||||
for batch in batches:
|
||||
if isinstance(batch, SampleBatch):
|
||||
batch = MultiAgentBatch({
|
||||
DEFAULT_POLICY_ID: batch
|
||||
}, batch.count)
|
||||
tmp.append(batch)
|
||||
batches = tmp
|
||||
|
||||
for batch in batches:
|
||||
if batch.count > self.max_buffer_size:
|
||||
raise ValueError(
|
||||
"The size of a single sample batch exceeds the replay "
|
||||
"buffer size ({} > {})".format(batch.count,
|
||||
self.max_buffer_size))
|
||||
self.replay_buffer.append(batch)
|
||||
self.num_steps_sampled += batch.count
|
||||
self.buffer_size += batch.count
|
||||
while self.buffer_size > self.max_buffer_size:
|
||||
evicted = self.replay_buffer.pop(0)
|
||||
self.buffer_size -= evicted.count
|
||||
|
||||
if self.num_steps_sampled >= self.replay_starts:
|
||||
return self._optimize()
|
||||
else:
|
||||
return {}
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
return dict(
|
||||
PolicyOptimizer.stats(self), **{
|
||||
"sample_time_ms": round(1000 * self.sample_timer.mean, 3),
|
||||
"grad_time_ms": round(1000 * self.grad_timer.mean, 3),
|
||||
"update_time_ms": round(1000 * self.update_weights_timer.mean,
|
||||
3),
|
||||
"opt_peak_throughput": round(self.grad_timer.mean_throughput,
|
||||
3),
|
||||
"opt_samples": round(self.grad_timer.mean_units_processed, 3),
|
||||
"learner": self.learner_stats,
|
||||
})
|
||||
|
||||
def _optimize(self):
|
||||
samples = [random.choice(self.replay_buffer)]
|
||||
while sum(s.count for s in samples) < self.train_batch_size:
|
||||
samples.append(random.choice(self.replay_buffer))
|
||||
samples = SampleBatch.concat_samples(samples)
|
||||
with self.grad_timer:
|
||||
info_dict = self.workers.local_worker().learn_on_batch(samples)
|
||||
for policy_id, info in info_dict.items():
|
||||
self.learner_stats[policy_id] = get_learner_stats(info)
|
||||
self.grad_timer.push_units_processed(samples.count)
|
||||
self.num_steps_trained += samples.count
|
||||
return info_dict
|
||||
@@ -1,231 +0,0 @@
|
||||
import logging
|
||||
import collections
|
||||
import numpy as np
|
||||
|
||||
import ray
|
||||
from ray.rllib.optimizers.replay_buffer import ReplayBuffer, \
|
||||
PrioritizedReplayBuffer
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.evaluation.metrics import get_learner_stats
|
||||
from ray.rllib.policy.sample_batch import SampleBatch, DEFAULT_POLICY_ID, \
|
||||
MultiAgentBatch
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.compression import pack_if_needed
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
from ray.rllib.utils.schedules import PiecewiseSchedule
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncReplayOptimizer(PolicyOptimizer):
|
||||
"""Variant of the local sync optimizer that supports replay (for DQN).
|
||||
|
||||
This optimizer requires that rollout workers return an additional
|
||||
"td_error" array in the info return of compute_gradients(). This error
|
||||
term will be used for sample prioritization."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workers,
|
||||
learning_starts=1000,
|
||||
buffer_size=10000,
|
||||
prioritized_replay=True,
|
||||
prioritized_replay_alpha=0.6,
|
||||
prioritized_replay_beta=0.4,
|
||||
prioritized_replay_eps=1e-6,
|
||||
final_prioritized_replay_beta=0.4,
|
||||
train_batch_size=32,
|
||||
before_learn_on_batch=None,
|
||||
synchronize_sampling=False,
|
||||
prioritized_replay_beta_annealing_timesteps=100000 * 0.2,
|
||||
):
|
||||
"""Initialize an sync replay optimizer.
|
||||
|
||||
Args:
|
||||
workers (WorkerSet): all workers
|
||||
learning_starts (int): wait until this many steps have been sampled
|
||||
before starting optimization.
|
||||
buffer_size (int): max size of the replay buffer
|
||||
prioritized_replay (bool): whether to enable prioritized replay
|
||||
prioritized_replay_alpha (float): replay alpha hyperparameter
|
||||
prioritized_replay_beta (float): replay beta hyperparameter
|
||||
prioritized_replay_eps (float): replay eps hyperparameter
|
||||
final_prioritized_replay_beta (float): Final value of beta.
|
||||
train_batch_size (int): size of batches to learn on
|
||||
before_learn_on_batch (function): callback to run before passing
|
||||
the sampled batch to learn on
|
||||
synchronize_sampling (bool): whether to sample the experiences for
|
||||
all policies with the same indices (used in MADDPG).
|
||||
prioritized_replay_beta_annealing_timesteps (int): The timestep at
|
||||
which PR-beta annealing should end.
|
||||
"""
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
self.replay_starts = learning_starts
|
||||
|
||||
# Linearly annealing beta used in Rainbow paper, stopping at
|
||||
# `final_prioritized_replay_beta`.
|
||||
self.prioritized_replay_beta = PiecewiseSchedule(
|
||||
endpoints=[(0, prioritized_replay_beta),
|
||||
(prioritized_replay_beta_annealing_timesteps,
|
||||
final_prioritized_replay_beta)],
|
||||
outside_value=final_prioritized_replay_beta,
|
||||
framework=None)
|
||||
self.prioritized_replay_eps = prioritized_replay_eps
|
||||
self.train_batch_size = train_batch_size
|
||||
self.before_learn_on_batch = before_learn_on_batch
|
||||
self.synchronize_sampling = synchronize_sampling
|
||||
|
||||
# Stats
|
||||
self.update_weights_timer = TimerStat()
|
||||
self.sample_timer = TimerStat()
|
||||
self.replay_timer = TimerStat()
|
||||
self.grad_timer = TimerStat()
|
||||
self.learner_stats = {}
|
||||
|
||||
# Set up replay buffer
|
||||
if prioritized_replay:
|
||||
|
||||
def new_buffer():
|
||||
return PrioritizedReplayBuffer(
|
||||
buffer_size, alpha=prioritized_replay_alpha)
|
||||
else:
|
||||
|
||||
def new_buffer():
|
||||
return ReplayBuffer(buffer_size)
|
||||
|
||||
self.replay_buffers = collections.defaultdict(new_buffer)
|
||||
|
||||
if buffer_size < self.replay_starts:
|
||||
logger.warning("buffer_size={} < replay_starts={}".format(
|
||||
buffer_size, self.replay_starts))
|
||||
|
||||
# If set, will use this batch for stepping/updating, instead of
|
||||
# sampling from the replay buffer. Actual sampling from the env
|
||||
# (and adding collected experiences to the replay will still happen
|
||||
# normally).
|
||||
# After self.step(), self.fake_batch must be set again.
|
||||
self._fake_batch = None
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
with self.update_weights_timer:
|
||||
if self.workers.remote_workers():
|
||||
weights = ray.put(self.workers.local_worker().get_weights())
|
||||
for e in self.workers.remote_workers():
|
||||
e.set_weights.remote(weights)
|
||||
|
||||
with self.sample_timer:
|
||||
if self.workers.remote_workers():
|
||||
batch = SampleBatch.concat_samples(
|
||||
ray.get([
|
||||
e.sample.remote()
|
||||
for e in self.workers.remote_workers()
|
||||
]))
|
||||
else:
|
||||
batch = self.workers.local_worker().sample()
|
||||
|
||||
# Handle everything as if multiagent
|
||||
if isinstance(batch, SampleBatch):
|
||||
batch = MultiAgentBatch({
|
||||
DEFAULT_POLICY_ID: batch
|
||||
}, batch.count)
|
||||
|
||||
for policy_id, s in batch.policy_batches.items():
|
||||
for row in s.rows():
|
||||
self.replay_buffers[policy_id].add(
|
||||
pack_if_needed(row["obs"]),
|
||||
row["actions"],
|
||||
row["rewards"],
|
||||
pack_if_needed(row["new_obs"]),
|
||||
row["dones"],
|
||||
weight=None)
|
||||
|
||||
if self.num_steps_sampled >= self.replay_starts:
|
||||
self._optimize()
|
||||
|
||||
self.num_steps_sampled += batch.count
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
return dict(
|
||||
PolicyOptimizer.stats(self), **{
|
||||
"sample_time_ms": round(1000 * self.sample_timer.mean, 3),
|
||||
"replay_time_ms": round(1000 * self.replay_timer.mean, 3),
|
||||
"grad_time_ms": round(1000 * self.grad_timer.mean, 3),
|
||||
"update_time_ms": round(1000 * self.update_weights_timer.mean,
|
||||
3),
|
||||
"opt_peak_throughput": round(self.grad_timer.mean_throughput,
|
||||
3),
|
||||
"opt_samples": round(self.grad_timer.mean_units_processed, 3),
|
||||
"learner": self.learner_stats,
|
||||
})
|
||||
|
||||
def _optimize(self):
|
||||
if self._fake_batch:
|
||||
fake_batch = SampleBatch(self._fake_batch)
|
||||
samples = MultiAgentBatch({
|
||||
DEFAULT_POLICY_ID: fake_batch
|
||||
}, fake_batch.count)
|
||||
else:
|
||||
samples = self._replay()
|
||||
|
||||
with self.grad_timer:
|
||||
if self.before_learn_on_batch:
|
||||
samples = self.before_learn_on_batch(
|
||||
samples,
|
||||
self.workers.local_worker().policy_map,
|
||||
self.train_batch_size)
|
||||
info_dict = self.workers.local_worker().learn_on_batch(samples)
|
||||
for policy_id, info in info_dict.items():
|
||||
self.learner_stats[policy_id] = get_learner_stats(info)
|
||||
replay_buffer = self.replay_buffers[policy_id]
|
||||
if isinstance(replay_buffer, PrioritizedReplayBuffer):
|
||||
# TODO(sven): This is currently structured differently for
|
||||
# torch/tf. Clean up these results/info dicts across
|
||||
# policies (note: fixing this in torch_policy.py will
|
||||
# break e.g. DDPPO!).
|
||||
td_error = info.get("td_error",
|
||||
info["learner_stats"].get("td_error"))
|
||||
new_priorities = (
|
||||
np.abs(td_error) + self.prioritized_replay_eps)
|
||||
replay_buffer.update_priorities(
|
||||
samples.policy_batches[policy_id]["batch_indexes"],
|
||||
new_priorities)
|
||||
self.grad_timer.push_units_processed(samples.count)
|
||||
|
||||
self.num_steps_trained += samples.count
|
||||
|
||||
def _replay(self):
|
||||
samples = {}
|
||||
idxes = None
|
||||
with self.replay_timer:
|
||||
for policy_id, replay_buffer in self.replay_buffers.items():
|
||||
if self.synchronize_sampling:
|
||||
if idxes is None:
|
||||
idxes = replay_buffer.sample_idxes(
|
||||
self.train_batch_size)
|
||||
else:
|
||||
idxes = replay_buffer.sample_idxes(self.train_batch_size)
|
||||
|
||||
if isinstance(replay_buffer, PrioritizedReplayBuffer):
|
||||
(obses_t, actions, rewards, obses_tp1, dones, weights,
|
||||
batch_indexes) = replay_buffer.sample_with_idxes(
|
||||
idxes,
|
||||
beta=self.prioritized_replay_beta.value(
|
||||
self.num_steps_trained))
|
||||
else:
|
||||
(obses_t, actions, rewards, obses_tp1,
|
||||
dones) = replay_buffer.sample_with_idxes(idxes)
|
||||
weights = np.ones_like(rewards)
|
||||
batch_indexes = -np.ones_like(rewards)
|
||||
samples[policy_id] = SampleBatch({
|
||||
"obs": obses_t,
|
||||
"actions": actions,
|
||||
"rewards": rewards,
|
||||
"new_obs": obses_tp1,
|
||||
"dones": dones,
|
||||
"weights": weights,
|
||||
"batch_indexes": batch_indexes
|
||||
})
|
||||
return MultiAgentBatch(samples, self.train_batch_size)
|
||||
@@ -1,95 +0,0 @@
|
||||
import logging
|
||||
|
||||
import ray
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.policy.sample_batch import SampleBatch, DEFAULT_POLICY_ID
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.filter import RunningStat
|
||||
from ray.rllib.utils.sgd import do_minibatch_sgd
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncSamplesOptimizer(PolicyOptimizer):
|
||||
"""A simple synchronous RL optimizer.
|
||||
|
||||
In each step, this optimizer pulls samples from a number of remote
|
||||
workers, concatenates them, and then updates a local model. The updated
|
||||
model weights are then broadcast to all remote workers.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
num_sgd_iter=1,
|
||||
train_batch_size=1,
|
||||
sgd_minibatch_size=0,
|
||||
standardize_fields=frozenset([])):
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
|
||||
self.update_weights_timer = TimerStat()
|
||||
self.standardize_fields = standardize_fields
|
||||
self.sample_timer = TimerStat()
|
||||
self.grad_timer = TimerStat()
|
||||
self.throughput = RunningStat()
|
||||
self.num_sgd_iter = num_sgd_iter
|
||||
self.sgd_minibatch_size = sgd_minibatch_size
|
||||
self.train_batch_size = train_batch_size
|
||||
self.learner_stats = {}
|
||||
self.policies = dict(self.workers.local_worker()
|
||||
.foreach_trainable_policy(lambda p, i: (i, p)))
|
||||
logger.debug("Policies to train: {}".format(self.policies))
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
with self.update_weights_timer:
|
||||
if self.workers.remote_workers():
|
||||
weights = ray.put(self.workers.local_worker().get_weights())
|
||||
for e in self.workers.remote_workers():
|
||||
e.set_weights.remote(weights)
|
||||
|
||||
with self.sample_timer:
|
||||
samples = []
|
||||
while sum(s.count for s in samples) < self.train_batch_size:
|
||||
if self.workers.remote_workers():
|
||||
samples.extend(
|
||||
ray.get([
|
||||
e.sample.remote()
|
||||
for e in self.workers.remote_workers()
|
||||
]))
|
||||
else:
|
||||
samples.append(self.workers.local_worker().sample())
|
||||
samples = SampleBatch.concat_samples(samples)
|
||||
self.sample_timer.push_units_processed(samples.count)
|
||||
|
||||
with self.grad_timer:
|
||||
fetches = do_minibatch_sgd(samples, self.policies,
|
||||
self.workers.local_worker(),
|
||||
self.num_sgd_iter,
|
||||
self.sgd_minibatch_size,
|
||||
self.standardize_fields)
|
||||
self.grad_timer.push_units_processed(samples.count)
|
||||
|
||||
if len(fetches) == 1 and DEFAULT_POLICY_ID in fetches:
|
||||
self.learner_stats = fetches[DEFAULT_POLICY_ID]
|
||||
else:
|
||||
self.learner_stats = fetches
|
||||
self.num_steps_sampled += samples.count
|
||||
self.num_steps_trained += samples.count
|
||||
return self.learner_stats
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
return dict(
|
||||
PolicyOptimizer.stats(self), **{
|
||||
"sample_time_ms": round(1000 * self.sample_timer.mean, 3),
|
||||
"grad_time_ms": round(1000 * self.grad_timer.mean, 3),
|
||||
"update_time_ms": round(1000 * self.update_weights_timer.mean,
|
||||
3),
|
||||
"opt_peak_throughput": round(self.grad_timer.mean_throughput,
|
||||
3),
|
||||
"sample_peak_throughput": round(
|
||||
self.sample_timer.mean_throughput, 3),
|
||||
"opt_samples": round(self.grad_timer.mean_units_processed, 3),
|
||||
"learner": self.learner_stats,
|
||||
})
|
||||
@@ -1,143 +0,0 @@
|
||||
import operator
|
||||
|
||||
|
||||
class OldSegmentTree(object):
|
||||
def __init__(self, capacity, operation, neutral_element):
|
||||
"""Build a Segment Tree data structure.
|
||||
|
||||
https://en.wikipedia.org/wiki/Segment_tree
|
||||
|
||||
Can be used as regular array, but with two
|
||||
important differences:
|
||||
|
||||
a) setting item's value is slightly slower.
|
||||
It is O(lg capacity) instead of O(1).
|
||||
b) user has access to an efficient `reduce`
|
||||
operation which reduces `operation` over
|
||||
a contiguous subsequence of items in the
|
||||
array.
|
||||
|
||||
Paramters
|
||||
---------
|
||||
capacity: int
|
||||
Total size of the array - must be a power of two.
|
||||
operation: lambda obj, obj -> obj
|
||||
and operation for combining elements (eg. sum, max)
|
||||
must for a mathematical group together with the set of
|
||||
possible values for array elements.
|
||||
neutral_element: obj
|
||||
neutral element for the operation above. eg. float('-inf')
|
||||
for max and 0 for sum.
|
||||
"""
|
||||
|
||||
assert capacity > 0 and capacity & (capacity - 1) == 0, \
|
||||
"capacity must be positive and a power of 2."
|
||||
self._capacity = capacity
|
||||
self._value = [neutral_element for _ in range(2 * capacity)]
|
||||
self._operation = operation
|
||||
|
||||
def _reduce_helper(self, start, end, node, node_start, node_end):
|
||||
if start == node_start and end == node_end:
|
||||
return self._value[node]
|
||||
mid = (node_start + node_end) // 2
|
||||
if end <= mid:
|
||||
return self._reduce_helper(start, end, 2 * node, node_start, mid)
|
||||
else:
|
||||
if mid + 1 <= start:
|
||||
return self._reduce_helper(start, end, 2 * node + 1, mid + 1,
|
||||
node_end)
|
||||
else:
|
||||
return self._operation(
|
||||
self._reduce_helper(start, mid, 2 * node, node_start, mid),
|
||||
self._reduce_helper(mid + 1, end, 2 * node + 1, mid + 1,
|
||||
node_end))
|
||||
|
||||
def reduce(self, start=0, end=None):
|
||||
"""Returns result of applying `self.operation`
|
||||
to a contiguous subsequence of the array.
|
||||
|
||||
self.operation(
|
||||
arr[start], operation(arr[start+1], operation(... arr[end])))
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start: int
|
||||
beginning of the subsequence
|
||||
end: int
|
||||
end of the subsequences
|
||||
|
||||
Returns
|
||||
-------
|
||||
reduced: obj
|
||||
result of reducing self.operation over the specified range of array
|
||||
elements.
|
||||
"""
|
||||
if end is None:
|
||||
end = self._capacity
|
||||
if end < 0:
|
||||
end += self._capacity
|
||||
end -= 1
|
||||
return self._reduce_helper(start, end, 1, 0, self._capacity - 1)
|
||||
|
||||
def __setitem__(self, idx, val):
|
||||
# index of the leaf
|
||||
idx += self._capacity
|
||||
self._value[idx] = val
|
||||
idx //= 2
|
||||
while idx >= 1:
|
||||
self._value[idx] = self._operation(self._value[2 * idx],
|
||||
self._value[2 * idx + 1])
|
||||
idx //= 2
|
||||
|
||||
def __getitem__(self, idx):
|
||||
assert 0 <= idx < self._capacity
|
||||
return self._value[self._capacity + idx]
|
||||
|
||||
|
||||
class OldSumSegmentTree(OldSegmentTree):
|
||||
def __init__(self, capacity):
|
||||
super(OldSumSegmentTree, self).__init__(
|
||||
capacity=capacity, operation=operator.add, neutral_element=0.0)
|
||||
|
||||
def sum(self, start=0, end=None):
|
||||
"""Returns arr[start] + ... + arr[end]"""
|
||||
return super(OldSumSegmentTree, self).reduce(start, end)
|
||||
|
||||
def find_prefixsum_idx(self, prefixsum):
|
||||
"""Find the highest index `i` in the array such that
|
||||
sum(arr[0] + arr[1] + ... + arr[i - i]) <= prefixsum
|
||||
|
||||
if array values are probabilities, this function
|
||||
allows to sample indexes according to the discrete
|
||||
probability efficiently.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
perfixsum: float
|
||||
upperbound on the sum of array prefix
|
||||
|
||||
Returns
|
||||
-------
|
||||
idx: int
|
||||
highest index satisfying the prefixsum constraint
|
||||
"""
|
||||
assert 0 <= prefixsum <= self.sum() + 1e-5
|
||||
idx = 1
|
||||
while idx < self._capacity: # while non-leaf
|
||||
if self._value[2 * idx] > prefixsum:
|
||||
idx = 2 * idx
|
||||
else:
|
||||
prefixsum -= self._value[2 * idx]
|
||||
idx = 2 * idx + 1
|
||||
return idx - self._capacity
|
||||
|
||||
|
||||
class OldMinSegmentTree(OldSegmentTree):
|
||||
def __init__(self, capacity):
|
||||
super(OldMinSegmentTree, self).__init__(
|
||||
capacity=capacity, operation=min, neutral_element=float("inf"))
|
||||
|
||||
def min(self, start=0, end=None):
|
||||
"""Returns min(arr[start], ..., arr[end])"""
|
||||
|
||||
return super(OldMinSegmentTree, self).reduce(start, end)
|
||||
@@ -1,281 +0,0 @@
|
||||
import gym
|
||||
import numpy as np
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import ray
|
||||
from ray.rllib.agents.ppo import PPOTrainer
|
||||
from ray.rllib.agents.ppo.ppo_tf_policy import PPOTFPolicy
|
||||
from ray.rllib.evaluation.rollout_worker import RolloutWorker
|
||||
from ray.rllib.evaluation.worker_set import WorkerSet
|
||||
from ray.rllib.optimizers import AsyncGradientsOptimizer, AsyncSamplesOptimizer
|
||||
from ray.rllib.optimizers.aso_tree_aggregator import TreeAggregator
|
||||
from ray.rllib.policy.sample_batch import SampleBatch
|
||||
from ray.rllib.tests.mock_worker import _MockWorker
|
||||
from ray.rllib.utils.framework import try_import_tf
|
||||
|
||||
tf1, tf, tfv = try_import_tf()
|
||||
|
||||
|
||||
class LRScheduleTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
ray.init(num_cpus=2)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
ray.shutdown()
|
||||
|
||||
def test_basic(self):
|
||||
ppo = PPOTrainer(
|
||||
env="CartPole-v0",
|
||||
config={"lr_schedule": [[0, 1e-5], [1000, 0.0]]})
|
||||
for _ in range(10):
|
||||
result = ppo.train()
|
||||
assert result["episode_reward_mean"] < 100, "should not have learned"
|
||||
|
||||
|
||||
class AsyncOptimizerTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
ray.init(num_cpus=4, object_store_memory=1000 * 1024 * 1024)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
ray.shutdown()
|
||||
|
||||
def test_basic(self):
|
||||
local = _MockWorker()
|
||||
remotes = ray.remote(_MockWorker)
|
||||
remote_workers = [remotes.remote() for i in range(5)]
|
||||
workers = WorkerSet._from_existing(local, remote_workers)
|
||||
test_optimizer = AsyncGradientsOptimizer(workers, grads_per_step=10)
|
||||
test_optimizer.step()
|
||||
self.assertTrue(all(local.get_weights() == 0))
|
||||
|
||||
|
||||
class PPOCollectTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
ray.init(num_cpus=4, object_store_memory=1000 * 1024 * 1024)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
ray.shutdown()
|
||||
|
||||
def test_ppo_sample_waste(self):
|
||||
# Check we at least collect the initial wave of samples
|
||||
ppo = PPOTrainer(
|
||||
env="CartPole-v0",
|
||||
config={
|
||||
"rollout_fragment_length": 200,
|
||||
"train_batch_size": 128,
|
||||
"num_workers": 3,
|
||||
})
|
||||
result = ppo.train()
|
||||
self.assertEqual(result["info"]["num_steps_sampled"], 600)
|
||||
ppo.stop()
|
||||
|
||||
# Check we collect at least the specified amount of samples
|
||||
ppo = PPOTrainer(
|
||||
env="CartPole-v0",
|
||||
config={
|
||||
"rollout_fragment_length": 200,
|
||||
"train_batch_size": 900,
|
||||
"num_workers": 3,
|
||||
})
|
||||
result = ppo.train()
|
||||
self.assertEqual(result["info"]["num_steps_sampled"], 1200)
|
||||
ppo.stop()
|
||||
|
||||
# Check in vectorized mode
|
||||
ppo = PPOTrainer(
|
||||
env="CartPole-v0",
|
||||
config={
|
||||
"rollout_fragment_length": 200,
|
||||
"num_envs_per_worker": 2,
|
||||
"train_batch_size": 900,
|
||||
"num_workers": 3,
|
||||
})
|
||||
result = ppo.train()
|
||||
self.assertEqual(result["info"]["num_steps_sampled"], 1200)
|
||||
ppo.stop()
|
||||
|
||||
|
||||
class SampleBatchTest(unittest.TestCase):
|
||||
def test_concat(self):
|
||||
b1 = SampleBatch({"a": np.array([1, 2, 3]), "b": np.array([4, 5, 6])})
|
||||
b2 = SampleBatch({"a": np.array([1]), "b": np.array([4])})
|
||||
b3 = SampleBatch({"a": np.array([1]), "b": np.array([5])})
|
||||
b12 = b1.concat(b2)
|
||||
self.assertEqual(b12["a"].tolist(), [1, 2, 3, 1])
|
||||
self.assertEqual(b12["b"].tolist(), [4, 5, 6, 4])
|
||||
b = SampleBatch.concat_samples([b1, b2, b3])
|
||||
self.assertEqual(b["a"].tolist(), [1, 2, 3, 1, 1])
|
||||
self.assertEqual(b["b"].tolist(), [4, 5, 6, 4, 5])
|
||||
|
||||
|
||||
class AsyncSamplesOptimizerTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
ray.init(num_cpus=8, object_store_memory=1000 * 1024 * 1024)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
ray.shutdown()
|
||||
|
||||
def test_simple(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
optimizer = AsyncSamplesOptimizer(workers)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
|
||||
def test_multi_gpu(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
optimizer = AsyncSamplesOptimizer(workers, num_gpus=1, _fake_gpus=True)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
|
||||
def test_multi_gpu_parallel_load(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers, num_gpus=1, num_data_loader_buffers=1, _fake_gpus=True)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
|
||||
def test_multiple_passes(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers,
|
||||
minibatch_buffer_size=10,
|
||||
num_sgd_iter=10,
|
||||
rollout_fragment_length=10,
|
||||
train_batch_size=50)
|
||||
self._wait_for(optimizer, 1000, 10000)
|
||||
self.assertLess(optimizer.stats()["num_steps_sampled"], 5000)
|
||||
self.assertGreater(optimizer.stats()["num_steps_trained"], 8000)
|
||||
|
||||
def test_replay(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers,
|
||||
replay_buffer_num_slots=100,
|
||||
replay_proportion=10,
|
||||
rollout_fragment_length=10,
|
||||
train_batch_size=10,
|
||||
)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
stats = optimizer.stats()
|
||||
self.assertLess(stats["num_steps_sampled"], 5000)
|
||||
replay_ratio = stats["num_steps_replayed"] / stats["num_steps_sampled"]
|
||||
self.assertGreater(replay_ratio, 0.7)
|
||||
self.assertLess(stats["num_steps_trained"], stats["num_steps_sampled"])
|
||||
|
||||
def test_replay_and_multiple_passes(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers,
|
||||
minibatch_buffer_size=10,
|
||||
num_sgd_iter=10,
|
||||
replay_buffer_num_slots=100,
|
||||
replay_proportion=10,
|
||||
rollout_fragment_length=10,
|
||||
train_batch_size=10)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
|
||||
stats = optimizer.stats()
|
||||
print(stats)
|
||||
self.assertLess(stats["num_steps_sampled"], 5000)
|
||||
replay_ratio = stats["num_steps_replayed"] / stats["num_steps_sampled"]
|
||||
self.assertGreater(replay_ratio, 0.7)
|
||||
|
||||
def test_multi_tier_aggregation_bad_conf(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
aggregators = TreeAggregator.precreate_aggregators(4)
|
||||
optimizer = AsyncSamplesOptimizer(workers, num_aggregation_workers=4)
|
||||
self.assertRaises(ValueError,
|
||||
lambda: optimizer.aggregator.init(aggregators))
|
||||
|
||||
def test_multi_tier_aggregation(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
aggregators = TreeAggregator.precreate_aggregators(1)
|
||||
optimizer = AsyncSamplesOptimizer(workers, num_aggregation_workers=1)
|
||||
optimizer.aggregator.init(aggregators)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
|
||||
def test_reject_bad_configs(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
self.assertRaises(
|
||||
ValueError, lambda: AsyncSamplesOptimizer(
|
||||
local, remotes,
|
||||
num_data_loader_buffers=2, minibatch_buffer_size=4))
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers,
|
||||
num_gpus=1,
|
||||
train_batch_size=100,
|
||||
rollout_fragment_length=50,
|
||||
_fake_gpus=True)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers,
|
||||
num_gpus=1,
|
||||
train_batch_size=100,
|
||||
rollout_fragment_length=25,
|
||||
_fake_gpus=True)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers,
|
||||
num_gpus=1,
|
||||
train_batch_size=100,
|
||||
rollout_fragment_length=74,
|
||||
_fake_gpus=True)
|
||||
self._wait_for(optimizer, 1000, 1000)
|
||||
|
||||
def test_learner_queue_timeout(self):
|
||||
local, remotes = self._make_envs()
|
||||
workers = WorkerSet._from_existing(local, remotes)
|
||||
optimizer = AsyncSamplesOptimizer(
|
||||
workers,
|
||||
rollout_fragment_length=1000,
|
||||
train_batch_size=1000,
|
||||
learner_queue_timeout=1)
|
||||
self.assertRaises(AssertionError,
|
||||
lambda: self._wait_for(optimizer, 1000, 1000))
|
||||
|
||||
def _make_envs(self):
|
||||
def make_sess():
|
||||
return tf1.Session(config=tf1.ConfigProto(device_count={"CPU": 2}))
|
||||
|
||||
local = RolloutWorker(
|
||||
env_creator=lambda _: gym.make("CartPole-v0"),
|
||||
policy=PPOTFPolicy,
|
||||
tf_session_creator=make_sess)
|
||||
remotes = [
|
||||
RolloutWorker.as_remote().remote(
|
||||
env_creator=lambda _: gym.make("CartPole-v0"),
|
||||
policy=PPOTFPolicy,
|
||||
tf_session_creator=make_sess)
|
||||
]
|
||||
return local, remotes
|
||||
|
||||
def _wait_for(self, optimizer, num_steps_sampled, num_steps_trained):
|
||||
start = time.time()
|
||||
while time.time() - start < 30:
|
||||
optimizer.step()
|
||||
if optimizer.num_steps_sampled > num_steps_sampled and \
|
||||
optimizer.num_steps_trained > num_steps_trained:
|
||||
print("OK", optimizer.stats())
|
||||
return
|
||||
raise AssertionError("TIMED OUT", optimizer.stats())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pytest
|
||||
import sys
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -1,180 +0,0 @@
|
||||
from collections import Counter
|
||||
import numpy as np
|
||||
import unittest
|
||||
|
||||
from ray.rllib.optimizers.replay_buffer import PrioritizedReplayBuffer
|
||||
from ray.rllib.utils.test_utils import check
|
||||
|
||||
|
||||
class TestPrioritizedReplayBuffer(unittest.TestCase):
|
||||
"""
|
||||
Tests insertion and (weighted) sampling of the PrioritizedReplayBuffer.
|
||||
"""
|
||||
|
||||
capacity = 10
|
||||
alpha = 1.0
|
||||
beta = 1.0
|
||||
max_priority = 1.0
|
||||
|
||||
def _generate_data(self):
|
||||
return (
|
||||
np.random.random((4, )), # obs_t
|
||||
np.random.choice([0, 1]), # action
|
||||
np.random.rand(), # reward
|
||||
np.random.random((4, )), # obs_tp1
|
||||
np.random.choice([False, True]), # done
|
||||
)
|
||||
|
||||
def test_add(self):
|
||||
memory = PrioritizedReplayBuffer(
|
||||
size=2,
|
||||
alpha=self.alpha,
|
||||
)
|
||||
|
||||
# Assert indices 0 before insert.
|
||||
self.assertEqual(len(memory), 0)
|
||||
self.assertEqual(memory._next_idx, 0)
|
||||
|
||||
# Insert single record.
|
||||
data = self._generate_data()
|
||||
memory.add(*data, weight=0.5)
|
||||
self.assertTrue(len(memory) == 1)
|
||||
self.assertTrue(memory._next_idx == 1)
|
||||
|
||||
# Insert single record.
|
||||
data = self._generate_data()
|
||||
memory.add(*data, weight=0.1)
|
||||
self.assertTrue(len(memory) == 2)
|
||||
self.assertTrue(memory._next_idx == 0)
|
||||
|
||||
# Insert over capacity.
|
||||
data = self._generate_data()
|
||||
memory.add(*data, weight=1.0)
|
||||
self.assertTrue(len(memory) == 2)
|
||||
self.assertTrue(memory._next_idx == 1)
|
||||
|
||||
def test_update_priorities(self):
|
||||
memory = PrioritizedReplayBuffer(size=self.capacity, alpha=self.alpha)
|
||||
|
||||
# Insert n samples.
|
||||
num_records = 5
|
||||
for i in range(num_records):
|
||||
data = self._generate_data()
|
||||
memory.add(*data, weight=1.0)
|
||||
self.assertTrue(len(memory) == i + 1)
|
||||
self.assertTrue(memory._next_idx == i + 1)
|
||||
|
||||
# Fetch records, their indices and weights.
|
||||
_, _, _, _, _, weights, indices = \
|
||||
memory.sample(3, beta=self.beta)
|
||||
check(weights, np.ones(shape=(3, )))
|
||||
self.assertEqual(3, len(indices))
|
||||
self.assertTrue(len(memory) == num_records)
|
||||
self.assertTrue(memory._next_idx == num_records)
|
||||
|
||||
# Update weight of indices 0, 2, 3, 4 to very small.
|
||||
memory.update_priorities(
|
||||
np.array([0, 2, 3, 4]), np.array([0.01, 0.01, 0.01, 0.01]))
|
||||
# Expect to sample almost only index 1
|
||||
# (which still has a weight of 1.0).
|
||||
for _ in range(10):
|
||||
_, _, _, _, _, weights, indices = memory.sample(
|
||||
1000, beta=self.beta)
|
||||
self.assertTrue(970 < np.sum(indices) < 1100)
|
||||
|
||||
# Update weight of indices 0 and 1 to >> 0.01.
|
||||
# Expect to sample 0 and 1 equally (and some 2s, 3s, and 4s).
|
||||
for _ in range(10):
|
||||
rand = np.random.random() + 0.2
|
||||
memory.update_priorities(np.array([0, 1]), np.array([rand, rand]))
|
||||
_, _, _, _, _, _, indices = memory.sample(1000, beta=self.beta)
|
||||
# Expect biased to higher values due to some 2s, 3s, and 4s.
|
||||
# print(np.sum(indices))
|
||||
self.assertTrue(400 < np.sum(indices) < 800)
|
||||
|
||||
# Update weights to be 1:2.
|
||||
# Expect to sample double as often index 1 over index 0
|
||||
# plus very few times indices 2, 3, or 4.
|
||||
for _ in range(10):
|
||||
rand = np.random.random() + 0.2
|
||||
memory.update_priorities(
|
||||
np.array([0, 1]), np.array([rand, rand * 2]))
|
||||
_, _, _, _, _, _, indices = memory.sample(1000, beta=self.beta)
|
||||
# print(np.sum(indices))
|
||||
self.assertTrue(600 < np.sum(indices) < 850)
|
||||
|
||||
# Update weights to be 1:4.
|
||||
# Expect to sample quadruple as often index 1 over index 0
|
||||
# plus very few times indices 2, 3, or 4.
|
||||
for _ in range(10):
|
||||
rand = np.random.random() + 0.2
|
||||
memory.update_priorities(
|
||||
np.array([0, 1]), np.array([rand, rand * 4]))
|
||||
_, _, _, _, _, _, indices = memory.sample(1000, beta=self.beta)
|
||||
# print(np.sum(indices))
|
||||
self.assertTrue(750 < np.sum(indices) < 950)
|
||||
|
||||
# Update weights to be 1:9.
|
||||
# Expect to sample 9 times as often index 1 over index 0.
|
||||
# plus very few times indices 2, 3, or 4.
|
||||
for _ in range(10):
|
||||
rand = np.random.random() + 0.2
|
||||
memory.update_priorities(
|
||||
np.array([0, 1]), np.array([rand, rand * 9]))
|
||||
_, _, _, _, _, _, indices = memory.sample(1000, beta=self.beta)
|
||||
# print(np.sum(indices))
|
||||
self.assertTrue(850 < np.sum(indices) < 1100)
|
||||
|
||||
# Insert n more samples.
|
||||
num_records = 5
|
||||
for i in range(num_records):
|
||||
data = self._generate_data()
|
||||
memory.add(*data, weight=1.0)
|
||||
self.assertTrue(len(memory) == i + 6)
|
||||
self.assertTrue(memory._next_idx == (i + 6) % self.capacity)
|
||||
|
||||
# Update all weights to be 1.0 to 10.0 and sample a >100 batch.
|
||||
memory.update_priorities(
|
||||
np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
|
||||
np.array([0.001, 0.1, 2., 8., 16., 32., 64., 128., 256., 512.]))
|
||||
counts = Counter()
|
||||
for _ in range(10):
|
||||
_, _, _, _, _, _, indices = memory.sample(
|
||||
np.random.randint(100, 600), beta=self.beta)
|
||||
for i in indices:
|
||||
counts[i] += 1
|
||||
print(counts)
|
||||
# Expect an approximately correct distribution of indices.
|
||||
self.assertTrue(
|
||||
counts[9] >= counts[8] >= counts[7] >= counts[6] >= counts[5] >=
|
||||
counts[4] >= counts[3] >= counts[2] >= counts[1] >= counts[0])
|
||||
|
||||
def test_alpha_parameter(self):
|
||||
# Test sampling from a PR with a very small alpha (should behave just
|
||||
# like a regular ReplayBuffer).
|
||||
memory = PrioritizedReplayBuffer(size=self.capacity, alpha=0.01)
|
||||
|
||||
# Insert n samples.
|
||||
num_records = 5
|
||||
for i in range(num_records):
|
||||
data = self._generate_data()
|
||||
memory.add(*data, weight=np.random.rand())
|
||||
self.assertTrue(len(memory) == i + 1)
|
||||
self.assertTrue(memory._next_idx == i + 1)
|
||||
|
||||
# Fetch records, their indices and weights.
|
||||
_, _, _, _, _, weights, indices = \
|
||||
memory.sample(1000, beta=self.beta)
|
||||
counts = Counter()
|
||||
for i in indices:
|
||||
counts[i] += 1
|
||||
print(counts)
|
||||
# Expect an approximately uniform distribution of indices.
|
||||
for i in counts.values():
|
||||
self.assertTrue(100 < i < 300)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pytest
|
||||
import sys
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -1,133 +0,0 @@
|
||||
import numpy as np
|
||||
import timeit
|
||||
import unittest
|
||||
|
||||
from ray.rllib.optimizers.segment_tree import SumSegmentTree, MinSegmentTree
|
||||
|
||||
|
||||
class TestSegmentTree(unittest.TestCase):
|
||||
def test_tree_set(self):
|
||||
tree = SumSegmentTree(4)
|
||||
|
||||
tree[2] = 1.0
|
||||
tree[3] = 3.0
|
||||
|
||||
assert np.isclose(tree.sum(), 4.0)
|
||||
assert np.isclose(tree.sum(0, 2), 0.0)
|
||||
assert np.isclose(tree.sum(0, 3), 1.0)
|
||||
assert np.isclose(tree.sum(2, 3), 1.0)
|
||||
assert np.isclose(tree.sum(2, -1), 1.0)
|
||||
assert np.isclose(tree.sum(2, 4), 4.0)
|
||||
assert np.isclose(tree.sum(2), 4.0)
|
||||
|
||||
def test_tree_set_overlap(self):
|
||||
tree = SumSegmentTree(4)
|
||||
|
||||
tree[2] = 1.0
|
||||
tree[2] = 3.0
|
||||
|
||||
assert np.isclose(tree.sum(), 3.0)
|
||||
assert np.isclose(tree.sum(2, 3), 3.0)
|
||||
assert np.isclose(tree.sum(2, -1), 3.0)
|
||||
assert np.isclose(tree.sum(2, 4), 3.0)
|
||||
assert np.isclose(tree.sum(2), 3.0)
|
||||
assert np.isclose(tree.sum(1, 2), 0.0)
|
||||
|
||||
def test_prefixsum_idx(self):
|
||||
tree = SumSegmentTree(4)
|
||||
|
||||
tree[2] = 1.0
|
||||
tree[3] = 3.0
|
||||
|
||||
assert tree.find_prefixsum_idx(0.0) == 2
|
||||
assert tree.find_prefixsum_idx(0.5) == 2
|
||||
assert tree.find_prefixsum_idx(0.99) == 2
|
||||
assert tree.find_prefixsum_idx(1.01) == 3
|
||||
assert tree.find_prefixsum_idx(3.00) == 3
|
||||
assert tree.find_prefixsum_idx(4.00) == 3
|
||||
|
||||
def test_prefixsum_idx2(self):
|
||||
tree = SumSegmentTree(4)
|
||||
|
||||
tree[0] = 0.5
|
||||
tree[1] = 1.0
|
||||
tree[2] = 1.0
|
||||
tree[3] = 3.0
|
||||
|
||||
assert tree.find_prefixsum_idx(0.00) == 0
|
||||
assert tree.find_prefixsum_idx(0.55) == 1
|
||||
assert tree.find_prefixsum_idx(0.99) == 1
|
||||
assert tree.find_prefixsum_idx(1.51) == 2
|
||||
assert tree.find_prefixsum_idx(3.00) == 3
|
||||
assert tree.find_prefixsum_idx(5.50) == 3
|
||||
|
||||
def test_max_interval_tree(self):
|
||||
tree = MinSegmentTree(4)
|
||||
|
||||
tree[0] = 1.0
|
||||
tree[2] = 0.5
|
||||
tree[3] = 3.0
|
||||
|
||||
assert np.isclose(tree.min(), 0.5)
|
||||
assert np.isclose(tree.min(0, 2), 1.0)
|
||||
assert np.isclose(tree.min(0, 3), 0.5)
|
||||
assert np.isclose(tree.min(0, -1), 0.5)
|
||||
assert np.isclose(tree.min(2, 4), 0.5)
|
||||
assert np.isclose(tree.min(3, 4), 3.0)
|
||||
|
||||
tree[2] = 0.7
|
||||
|
||||
assert np.isclose(tree.min(), 0.7)
|
||||
assert np.isclose(tree.min(0, 2), 1.0)
|
||||
assert np.isclose(tree.min(0, 3), 0.7)
|
||||
assert np.isclose(tree.min(0, -1), 0.7)
|
||||
assert np.isclose(tree.min(2, 4), 0.7)
|
||||
assert np.isclose(tree.min(3, 4), 3.0)
|
||||
|
||||
tree[2] = 4.0
|
||||
|
||||
assert np.isclose(tree.min(), 1.0)
|
||||
assert np.isclose(tree.min(0, 2), 1.0)
|
||||
assert np.isclose(tree.min(0, 3), 1.0)
|
||||
assert np.isclose(tree.min(0, -1), 1.0)
|
||||
assert np.isclose(tree.min(2, 4), 3.0)
|
||||
assert np.isclose(tree.min(2, 3), 4.0)
|
||||
assert np.isclose(tree.min(2, -1), 4.0)
|
||||
assert np.isclose(tree.min(3, 4), 3.0)
|
||||
|
||||
def test_microbenchmark_vs_old_version(self):
|
||||
"""
|
||||
Results from March 2020 (capacity=1048576):
|
||||
|
||||
New tree:
|
||||
0.049599366000000256s
|
||||
results = timeit.timeit("tree.sum(5, 60000)",
|
||||
setup="from ray.rllib.optimizers.segment_tree import
|
||||
SumSegmentTree; tree = SumSegmentTree({})".format(capacity),
|
||||
number=10000)
|
||||
|
||||
Old tree:
|
||||
0.13390400999999974s
|
||||
results = timeit.timeit("tree.sum(5, 60000)",
|
||||
setup="from ray.rllib.optimizers.tests.old_segment_tree import
|
||||
OldSumSegmentTree; tree = OldSumSegmentTree({})".format(capacity),
|
||||
number=10000)
|
||||
"""
|
||||
capacity = 2**20
|
||||
new = timeit.timeit(
|
||||
"tree.sum(5, 60000)",
|
||||
setup="from ray.rllib.optimizers.segment_tree import "
|
||||
"SumSegmentTree; tree = SumSegmentTree({})".format(capacity),
|
||||
number=10000)
|
||||
old = timeit.timeit(
|
||||
"tree.sum(5, 60000)",
|
||||
setup="from ray.rllib.optimizers.tests.old_segment_tree import "
|
||||
"OldSumSegmentTree; tree = OldSumSegmentTree({})".format(capacity),
|
||||
number=10000)
|
||||
self.assertGreater(old, new)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pytest
|
||||
import sys
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -1,108 +0,0 @@
|
||||
import logging
|
||||
|
||||
import ray
|
||||
from ray.rllib.optimizers.policy_optimizer import PolicyOptimizer
|
||||
from ray.rllib.utils.annotations import override
|
||||
from ray.rllib.utils.timer import TimerStat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TorchDistributedDataParallelOptimizer(PolicyOptimizer):
|
||||
"""EXPERIMENTAL: torch distributed multi-node SGD."""
|
||||
|
||||
def __init__(self,
|
||||
workers,
|
||||
expected_batch_size,
|
||||
num_sgd_iter=1,
|
||||
sgd_minibatch_size=0,
|
||||
standardize_fields=frozenset([]),
|
||||
keep_local_weights_in_sync=True,
|
||||
backend="gloo"):
|
||||
PolicyOptimizer.__init__(self, workers)
|
||||
self.learner_stats = {}
|
||||
self.num_sgd_iter = num_sgd_iter
|
||||
self.expected_batch_size = expected_batch_size
|
||||
self.sgd_minibatch_size = sgd_minibatch_size
|
||||
self.standardize_fields = standardize_fields
|
||||
self.keep_local_weights_in_sync = keep_local_weights_in_sync
|
||||
self.sync_down_timer = TimerStat()
|
||||
self.sync_up_timer = TimerStat()
|
||||
self.learn_timer = TimerStat()
|
||||
|
||||
# Setup the distributed processes.
|
||||
if not self.workers.remote_workers():
|
||||
raise ValueError("This optimizer requires >0 remote workers.")
|
||||
ip = ray.get(workers.remote_workers()[0].get_node_ip.remote())
|
||||
port = ray.get(workers.remote_workers()[0].find_free_port.remote())
|
||||
address = "tcp://{ip}:{port}".format(ip=ip, port=port)
|
||||
logger.info(
|
||||
"Creating torch process group with leader {}".format(address))
|
||||
|
||||
# Get setup tasks in order to throw errors on failure.
|
||||
ray.get([
|
||||
worker.setup_torch_data_parallel.remote(
|
||||
address, i, len(workers.remote_workers()), backend)
|
||||
for i, worker in enumerate(workers.remote_workers())
|
||||
])
|
||||
logger.info("Torch process group init completed")
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def step(self):
|
||||
# Sync up the weights. In principle we don't need this, but it doesn't
|
||||
# add too much overhead and handles the case where the user manually
|
||||
# updates the local weights.
|
||||
if self.keep_local_weights_in_sync:
|
||||
with self.sync_up_timer:
|
||||
weights = ray.put(self.workers.local_worker().get_weights())
|
||||
for e in self.workers.remote_workers():
|
||||
e.set_weights.remote(weights)
|
||||
|
||||
with self.learn_timer:
|
||||
results = ray.get([
|
||||
w.sample_and_learn.remote(
|
||||
self.expected_batch_size, self.num_sgd_iter,
|
||||
self.sgd_minibatch_size, self.standardize_fields)
|
||||
for w in self.workers.remote_workers()
|
||||
])
|
||||
for info, count in results:
|
||||
self.num_steps_sampled += count
|
||||
self.num_steps_trained += count
|
||||
self.learner_stats = results[0][0]
|
||||
|
||||
# In debug mode, check the allreduce successfully synced the weights.
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
weights = ray.get([
|
||||
w.get_weights.remote() for w in self.workers.remote_workers()
|
||||
])
|
||||
sums = []
|
||||
for w in weights:
|
||||
acc = 0
|
||||
for p in w.values():
|
||||
for k, v in p.items():
|
||||
acc += v.sum()
|
||||
sums.append(float(acc))
|
||||
logger.debug("The worker weight sums are {}".format(sums))
|
||||
assert len(set(sums)) == 1, sums
|
||||
|
||||
# Sync down the weights. As with the sync up, this is not really
|
||||
# needed unless the user is reading the local weights.
|
||||
if self.keep_local_weights_in_sync:
|
||||
with self.sync_down_timer:
|
||||
self.workers.local_worker().set_weights(
|
||||
ray.get(
|
||||
self.workers.remote_workers()[0].get_weights.remote()))
|
||||
|
||||
return self.learner_stats
|
||||
|
||||
@override(PolicyOptimizer)
|
||||
def stats(self):
|
||||
return dict(
|
||||
PolicyOptimizer.stats(self), **{
|
||||
"sync_weights_up_time": round(1000 * self.sync_up_timer.mean,
|
||||
3),
|
||||
"sync_weights_down_time": round(
|
||||
1000 * self.sync_down_timer.mean, 3),
|
||||
"learn_time_ms": round(1000 * self.learn_timer.mean, 3),
|
||||
"learner": self.learner_stats,
|
||||
})
|
||||
@@ -64,7 +64,10 @@ def try_import_tf(error=False):
|
||||
except AttributeError:
|
||||
tf1_module = tf_module
|
||||
|
||||
version = 2 if "2." in tf_module.__version__[:2] else 1
|
||||
if not hasattr(tf_module, "__version__"):
|
||||
version = 1 # sphinx doc gen
|
||||
else:
|
||||
version = 2 if "2." in tf_module.__version__[:2] else 1
|
||||
|
||||
return tf1_module, tf_module, version
|
||||
|
||||
|
||||
Reference in New Issue
Block a user