chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# @OldAPIStack
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
import ray
|
||||
from ray import air
|
||||
from ray._common.deprecation import deprecation_warning
|
||||
from ray.air.integrations.wandb import WandbLoggerCallback
|
||||
from ray.rllib import _register_all
|
||||
from ray.rllib.utils.metrics import (
|
||||
ENV_RUNNER_RESULTS,
|
||||
EPISODE_RETURN_MEAN,
|
||||
EVALUATION_RESULTS,
|
||||
)
|
||||
from ray.tune import run_experiments
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--framework",
|
||||
type=str,
|
||||
choices=["torch", "tf2", "tf"],
|
||||
default=None,
|
||||
help="The deep learning framework to use. If not provided, try using the one "
|
||||
"specified in the file, otherwise, use RLlib's default: `torch`.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dir",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The directory or file in which to find all tests.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env",
|
||||
type=str,
|
||||
default=None,
|
||||
help="An optional env override setting. If not provided, try using the one "
|
||||
"specified in the file.",
|
||||
)
|
||||
parser.add_argument("--num-cpus", type=int, default=None)
|
||||
parser.add_argument(
|
||||
"--local-mode",
|
||||
action="store_true",
|
||||
help=argparse.SUPPRESS, # Deprecated.
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-samples",
|
||||
type=int,
|
||||
default=1,
|
||||
help="The number of seeds/samples to run with the given experiment config.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--override-mean-reward",
|
||||
type=float,
|
||||
default=0.0,
|
||||
help=(
|
||||
"Override the mean reward specified by the yaml file in the stopping criteria. "
|
||||
"This is particularly useful for timed tests."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
type=int,
|
||||
default=2,
|
||||
help="The verbosity level for the main `tune.run_experiments()` call.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wandb-key",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The WandB API key to use for uploading results.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wandb-project",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The WandB project name to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wandb-run-name",
|
||||
type=str,
|
||||
default=None,
|
||||
help="The WandB run name to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--checkpoint-freq",
|
||||
type=int,
|
||||
default=0,
|
||||
help=(
|
||||
"The frequency (in training iterations) with which to create checkpoints. "
|
||||
"Note that if --wandb-key is provided, these checkpoints will automatically "
|
||||
"be uploaded to WandB."
|
||||
),
|
||||
)
|
||||
|
||||
# Obsoleted arg, use --dir instead.
|
||||
parser.add_argument("--yaml-dir", type=str, default="")
|
||||
|
||||
|
||||
def _load_experiments_from_file(
|
||||
config_file: str,
|
||||
file_type: str,
|
||||
stop=None,
|
||||
checkpoint_config=None,
|
||||
) -> dict:
|
||||
# Yaml file.
|
||||
if file_type == "yaml":
|
||||
with open(config_file) as f:
|
||||
experiments = yaml.safe_load(f)
|
||||
if stop is not None and stop != "{}":
|
||||
raise ValueError("`stop` criteria only supported for python files.")
|
||||
# Make sure yaml experiments are always old API stack.
|
||||
for experiment in experiments.values():
|
||||
experiment["config"]["enable_rl_module_and_learner"] = False
|
||||
experiment["config"]["enable_env_runner_and_connector_v2"] = False
|
||||
# Python file case (ensured by file type enum)
|
||||
else:
|
||||
module_name = os.path.basename(config_file).replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, config_file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
if not hasattr(module, "config"):
|
||||
raise ValueError(
|
||||
"Your Python file must contain a 'config' variable "
|
||||
"that is an AlgorithmConfig object."
|
||||
)
|
||||
algo_config = module.config
|
||||
if stop is None:
|
||||
stop = getattr(module, "stop", {})
|
||||
else:
|
||||
stop = json.loads(stop)
|
||||
|
||||
# Note: we do this gymnastics to support the old format that
|
||||
# "_run_rllib_experiments" expects. Ideally, we'd just build the config and
|
||||
# run the algo.
|
||||
config = algo_config.to_dict()
|
||||
experiments = {
|
||||
f"default_{uuid.uuid4().hex}": {
|
||||
"run": algo_config.algo_class,
|
||||
"env": config.get("env"),
|
||||
"config": config,
|
||||
"stop": stop,
|
||||
}
|
||||
}
|
||||
|
||||
for key, val in experiments.items():
|
||||
experiments[key]["checkpoint_config"] = checkpoint_config or {}
|
||||
|
||||
return experiments
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.yaml_dir != "":
|
||||
deprecation_warning(old="--yaml-dir", new="--dir", error=True)
|
||||
|
||||
# Bazel regression test mode: Get path to look for yaml files.
|
||||
# Get the path or single file to use.
|
||||
rllib_dir = Path(__file__).parent.parent
|
||||
print(f"rllib dir={rllib_dir}")
|
||||
|
||||
abs_path = os.path.join(rllib_dir, args.dir)
|
||||
# Single file given.
|
||||
if os.path.isfile(abs_path):
|
||||
files = [abs_path]
|
||||
# Path given -> Get all yaml files in there via rglob.
|
||||
elif os.path.isdir(abs_path):
|
||||
files = []
|
||||
for type_ in ["yaml", "yml", "py"]:
|
||||
files += list(rllib_dir.rglob(args.dir + f"/*.{type_}"))
|
||||
files = sorted(map(lambda path: str(path.absolute()), files), reverse=True)
|
||||
# Given path/file does not exist.
|
||||
else:
|
||||
raise ValueError(f"--dir ({args.dir}) not found!")
|
||||
|
||||
print("Will run the following regression tests:")
|
||||
for file in files:
|
||||
print("->", file)
|
||||
|
||||
# Loop through all collected files.
|
||||
for file in files:
|
||||
config_is_python = False
|
||||
# For python files, need to make sure, we only deliver the module name into the
|
||||
# `_load_experiments_from_file` function (everything from "/ray/rllib" on).
|
||||
if file.endswith(".py"):
|
||||
if file.endswith("__init__.py"): # weird CI learning test (BAZEL) case
|
||||
continue
|
||||
experiments = _load_experiments_from_file(file, "py")
|
||||
config_is_python = True
|
||||
else:
|
||||
experiments = _load_experiments_from_file(file, "yaml")
|
||||
|
||||
assert (
|
||||
len(experiments) == 1
|
||||
), "Error, can only run a single experiment per file!"
|
||||
|
||||
exp = list(experiments.values())[0]
|
||||
exp_name = list(experiments.keys())[0]
|
||||
|
||||
# Set the number of samples to run.
|
||||
exp["num_samples"] = args.num_samples
|
||||
|
||||
# Make sure there is a config and a stopping criterium.
|
||||
exp["config"] = exp.get("config", {})
|
||||
exp["stop"] = exp.get("stop", {})
|
||||
|
||||
# Override framework setting with the command line one, if provided.
|
||||
# Otherwise, will use framework setting in file (or default: torch).
|
||||
if args.framework is not None:
|
||||
exp["config"]["framework"] = args.framework
|
||||
# Override env setting if given on command line.
|
||||
if args.env is not None:
|
||||
exp["config"]["env"] = args.env
|
||||
else:
|
||||
exp["config"]["env"] = exp["env"]
|
||||
|
||||
# Override the mean reward if specified. This is used by the ray ci
|
||||
# for overriding the episode reward mean for tf2 tests for off policy
|
||||
# long learning tests such as sac and ddpg on the pendulum environment.
|
||||
if args.override_mean_reward != 0.0:
|
||||
exp["stop"][
|
||||
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}"
|
||||
] = args.override_mean_reward
|
||||
|
||||
# Checkpoint settings.
|
||||
exp["checkpoint_config"] = air.CheckpointConfig(
|
||||
checkpoint_frequency=args.checkpoint_freq,
|
||||
checkpoint_at_end=args.checkpoint_freq > 0,
|
||||
)
|
||||
|
||||
# Always run with eager-tracing when framework=tf2, if not in local-mode
|
||||
# and unless the yaml explicitly tells us to disable eager tracing.
|
||||
if (
|
||||
(args.framework == "tf2" or exp["config"].get("framework") == "tf2")
|
||||
# Note: This check will always fail for python configs, b/c normally,
|
||||
# algorithm configs have `self.eager_tracing=False` by default.
|
||||
# Thus, you'd have to set `eager_tracing` to True explicitly in your python
|
||||
# config to make sure we are indeed using eager tracing.
|
||||
and exp["config"].get("eager_tracing") is not False
|
||||
):
|
||||
exp["config"]["eager_tracing"] = True
|
||||
|
||||
# Print out the actual config (not for py files as yaml.dump weirdly fails).
|
||||
if not config_is_python:
|
||||
print("== Test config ==")
|
||||
print(yaml.dump(experiments))
|
||||
|
||||
callbacks = None
|
||||
if args.wandb_key is not None:
|
||||
project = args.wandb_project or (
|
||||
exp["run"].lower()
|
||||
+ "-"
|
||||
+ re.sub("\\W+", "-", exp["config"]["env"].lower())
|
||||
if config_is_python
|
||||
else list(experiments.keys())[0]
|
||||
)
|
||||
callbacks = [
|
||||
WandbLoggerCallback(
|
||||
api_key=args.wandb_key,
|
||||
project=project,
|
||||
upload_checkpoints=True,
|
||||
**({"name": args.wandb_run_name} if args.wandb_run_name else {}),
|
||||
)
|
||||
]
|
||||
|
||||
if args.local_mode:
|
||||
raise ValueError("`--local-mode` is no longer supported.")
|
||||
|
||||
# Try running each test 3 times and make sure it reaches the given
|
||||
# reward.
|
||||
passed = False
|
||||
for i in range(3):
|
||||
# Try starting a new ray cluster.
|
||||
try:
|
||||
ray.init(num_cpus=args.num_cpus)
|
||||
# Allow running this script on existing cluster as well.
|
||||
except ConnectionError:
|
||||
ray.init()
|
||||
else:
|
||||
try:
|
||||
trials = run_experiments(
|
||||
experiments,
|
||||
resume=False,
|
||||
verbose=args.verbose,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
finally:
|
||||
ray.shutdown()
|
||||
_register_all()
|
||||
|
||||
for t in trials:
|
||||
# If we have evaluation workers, use their rewards.
|
||||
# This is useful for offline learning tests, where
|
||||
# we evaluate against an actual environment.
|
||||
check_eval = bool(exp["config"].get("evaluation_interval"))
|
||||
reward_mean = (
|
||||
t.last_result[EVALUATION_RESULTS][ENV_RUNNER_RESULTS][
|
||||
EPISODE_RETURN_MEAN
|
||||
]
|
||||
if check_eval
|
||||
else (
|
||||
# Some algos don't store sampler results under `env_runners`
|
||||
# e.g. ARS. Need to keep this logic around for now.
|
||||
t.last_result[ENV_RUNNER_RESULTS][EPISODE_RETURN_MEAN]
|
||||
if ENV_RUNNER_RESULTS in t.last_result
|
||||
else t.last_result[EPISODE_RETURN_MEAN]
|
||||
)
|
||||
)
|
||||
|
||||
# If we are using evaluation workers, we may have
|
||||
# a stopping criterion under the "evaluation/" scope. If
|
||||
# not, use `episode_return_mean`.
|
||||
if check_eval:
|
||||
min_reward = t.stopping_criterion.get(
|
||||
f"{EVALUATION_RESULTS}/{ENV_RUNNER_RESULTS}/"
|
||||
f"{EPISODE_RETURN_MEAN}",
|
||||
t.stopping_criterion.get(
|
||||
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}"
|
||||
),
|
||||
)
|
||||
# Otherwise, expect `env_runners/episode_return_mean` to be set.
|
||||
else:
|
||||
min_reward = t.stopping_criterion.get(
|
||||
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}"
|
||||
)
|
||||
|
||||
# If min reward not defined, always pass.
|
||||
if min_reward is None or reward_mean >= min_reward:
|
||||
passed = True
|
||||
break
|
||||
|
||||
if passed:
|
||||
print("Regression test PASSED")
|
||||
break
|
||||
else:
|
||||
print("Regression test FAILED on attempt {}".format(i + 1))
|
||||
|
||||
if not passed:
|
||||
print("Overall regression FAILED: Exiting with Error.")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,685 @@
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from random import choice
|
||||
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
|
||||
import ray
|
||||
import ray.rllib.algorithms.dqn as dqn
|
||||
import ray.rllib.algorithms.ppo as ppo
|
||||
from ray.rllib.algorithms.algorithm import Algorithm
|
||||
from ray.rllib.algorithms.bc import BCConfig
|
||||
from ray.rllib.core.columns import Columns
|
||||
from ray.rllib.core.rl_module.default_model_config import DefaultModelConfig
|
||||
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
|
||||
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
|
||||
from ray.rllib.examples.evaluation.evaluation_parallel_to_training import (
|
||||
AssertEvalCallback,
|
||||
)
|
||||
from ray.rllib.utils.annotations import OldAPIStack
|
||||
from ray.rllib.utils.framework import convert_to_tensor
|
||||
from ray.rllib.utils.metrics import (
|
||||
ENV_RUNNER_RESULTS,
|
||||
EPISODE_RETURN_MEAN,
|
||||
EVALUATION_RESULTS,
|
||||
LEARNER_RESULTS,
|
||||
)
|
||||
from ray.rllib.utils.metrics.learner_info import LEARNER_INFO
|
||||
from ray.tune import register_env
|
||||
|
||||
|
||||
class TestAlgorithm(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
ray.init()
|
||||
register_env("multi_cart", lambda cfg: MultiAgentCartPole(cfg))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
ray.shutdown()
|
||||
|
||||
def test_add_module_and_remove_module(self):
|
||||
config = (
|
||||
ppo.PPOConfig()
|
||||
.environment(
|
||||
env="multi_cart",
|
||||
env_config={"num_agents": 4},
|
||||
)
|
||||
.env_runners(num_cpus_per_env_runner=0.1)
|
||||
.training(
|
||||
train_batch_size=100,
|
||||
minibatch_size=50,
|
||||
num_epochs=1,
|
||||
)
|
||||
.rl_module(
|
||||
model_config=DefaultModelConfig(
|
||||
fcnet_hiddens=[5], fcnet_activation="linear"
|
||||
),
|
||||
)
|
||||
.multi_agent(
|
||||
# Start with a single policy.
|
||||
policies={"p0"},
|
||||
policy_mapping_fn=lambda *a, **kw: "p0",
|
||||
# TODO (sven): Support object store caching on new API stack.
|
||||
# # And only two policies that can be stored in memory at a
|
||||
# # time.
|
||||
# policy_map_capacity=2,
|
||||
)
|
||||
.evaluation(
|
||||
evaluation_num_env_runners=1,
|
||||
evaluation_config=ppo.PPOConfig.overrides(num_cpus_per_env_runner=0.1),
|
||||
)
|
||||
)
|
||||
|
||||
# Construct the Algorithm with a single policy in it.
|
||||
algo = config.build()
|
||||
mod0 = algo.get_module("p0")
|
||||
r = algo.train()
|
||||
self.assertTrue("p0" in r[LEARNER_RESULTS])
|
||||
for i in range(1, 3):
|
||||
|
||||
def new_mapping_fn(agent_id, episode, i=i, **kwargs):
|
||||
return f"p{choice([i, i - 1])}"
|
||||
|
||||
# Add a new RLModule by class (and options).
|
||||
mid = f"p{i}"
|
||||
print(f"Adding new RLModule {mid} ...")
|
||||
new_marl_spec = algo.add_module(
|
||||
module_id=mid,
|
||||
module_spec=RLModuleSpec.from_module(mod0),
|
||||
# Test changing the mapping fn.
|
||||
new_agent_to_module_mapping_fn=new_mapping_fn,
|
||||
# Change the list of modules to train.
|
||||
new_should_module_be_updated=[f"p{i}", f"p{i-1}"],
|
||||
)
|
||||
new_module = algo.get_module(mid)
|
||||
self._assert_modules_added(
|
||||
algo=algo,
|
||||
marl_spec=new_marl_spec,
|
||||
mids=[0, i],
|
||||
trainable=[i, i - 1],
|
||||
mapped=[i, i - 1],
|
||||
not_mapped=[i - 2],
|
||||
)
|
||||
|
||||
# Assert new policy is part of local worker (eval worker set does NOT
|
||||
# have a local worker, only the main EnvRunnerGroup does).
|
||||
multi_rl_module = algo.env_runner.module
|
||||
self.assertTrue(new_module is not mod0)
|
||||
for j in range(i + 1):
|
||||
self.assertTrue(f"p{j}" in multi_rl_module)
|
||||
self.assertTrue(len(multi_rl_module) == i + 1)
|
||||
algo.train()
|
||||
checkpoint = algo.save_to_path()
|
||||
|
||||
# Test restoring from the checkpoint (which has more policies
|
||||
# than what's defined in the config dict).
|
||||
test = Algorithm.from_checkpoint(checkpoint)
|
||||
self._assert_modules_added(
|
||||
algo=test,
|
||||
marl_spec=None,
|
||||
mids=[0, i - 1, i],
|
||||
trainable=[i - 1, i],
|
||||
mapped=[i - 1, i],
|
||||
not_mapped=[i - 2],
|
||||
)
|
||||
# Make sure algorithm can continue training the restored policy.
|
||||
test.train()
|
||||
# Test creating an inference action with the added (and restored) RLModule.
|
||||
mod0 = test.get_module("p0")
|
||||
out = mod0.forward_inference(
|
||||
{
|
||||
Columns.OBS: convert_to_tensor(
|
||||
np.expand_dims(mod0.config.observation_space.sample(), 0),
|
||||
framework=mod0.framework,
|
||||
),
|
||||
},
|
||||
)
|
||||
action_dist_inputs = out[Columns.ACTION_DIST_INPUTS]
|
||||
self.assertTrue(action_dist_inputs.shape == (1, 2))
|
||||
test.stop()
|
||||
|
||||
# After having added 2 Modules, try to restore the Algorithm,
|
||||
# but only with 1 of the originally added Modules (plus the initial
|
||||
# p0).
|
||||
if i == 2:
|
||||
|
||||
def new_mapping_fn(agent_id, episode, **kwargs):
|
||||
return f"p{choice([0, 2])}"
|
||||
|
||||
test2 = Algorithm.from_checkpoint(path=checkpoint)
|
||||
test2.remove_module(
|
||||
module_id="p1",
|
||||
new_agent_to_module_mapping_fn=new_mapping_fn,
|
||||
new_should_module_be_updated=["p0"],
|
||||
)
|
||||
self._assert_modules_added(
|
||||
algo=test2,
|
||||
marl_spec=None,
|
||||
mids=[0, 2],
|
||||
trainable=[0],
|
||||
mapped=[0, 2],
|
||||
not_mapped=[1, 4, 5, 6],
|
||||
)
|
||||
# Make sure algorithm can continue training the restored policy.
|
||||
mod2 = test2.get_module("p2")
|
||||
test2.train()
|
||||
# Test creating an inference action with the added (and restored)
|
||||
# RLModule.
|
||||
out = mod2.forward_exploration(
|
||||
{
|
||||
Columns.OBS: convert_to_tensor(
|
||||
np.expand_dims(mod0.config.observation_space.sample(), 0),
|
||||
framework=mod0.framework,
|
||||
),
|
||||
},
|
||||
)
|
||||
action_dist_inputs = out[Columns.ACTION_DIST_INPUTS]
|
||||
self.assertTrue(action_dist_inputs.shape == (1, 2))
|
||||
test2.stop()
|
||||
|
||||
# Delete all added modules again from Algorithm.
|
||||
for i in range(2, 0, -1):
|
||||
mid = f"p{i}"
|
||||
marl_spec = algo.remove_module(
|
||||
mid,
|
||||
# Note that the complete signature of a policy_mapping_fn
|
||||
# is: `agent_id, episode, worker, **kwargs`.
|
||||
new_agent_to_module_mapping_fn=(
|
||||
lambda agent_id, episode, i=i, **kwargs: f"p{i - 1}"
|
||||
),
|
||||
# Update list of policies to train.
|
||||
new_should_module_be_updated=[f"p{i - 1}"],
|
||||
)
|
||||
self._assert_modules_added(
|
||||
algo=algo,
|
||||
marl_spec=marl_spec,
|
||||
mids=[0, i - 1],
|
||||
trainable=[i - 1],
|
||||
mapped=[i - 1],
|
||||
not_mapped=[i, i + 1],
|
||||
)
|
||||
|
||||
algo.stop()
|
||||
|
||||
@OldAPIStack
|
||||
def test_add_policy_and_remove_policy(self):
|
||||
config = (
|
||||
ppo.PPOConfig()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
enable_rl_module_and_learner=False,
|
||||
)
|
||||
.environment(
|
||||
env=MultiAgentCartPole,
|
||||
env_config={
|
||||
"config": {
|
||||
"num_agents": 4,
|
||||
},
|
||||
},
|
||||
)
|
||||
.env_runners(num_cpus_per_env_runner=0.1)
|
||||
.training(
|
||||
train_batch_size=100,
|
||||
minibatch_size=50,
|
||||
num_epochs=1,
|
||||
model={
|
||||
"fcnet_hiddens": [5],
|
||||
"fcnet_activation": "linear",
|
||||
},
|
||||
)
|
||||
.multi_agent(
|
||||
# Start with a single policy.
|
||||
policies={"p0"},
|
||||
policy_mapping_fn=lambda agent_id, episode, worker, **kwargs: "p0",
|
||||
# And only two policies that can be stored in memory at a
|
||||
# time.
|
||||
policy_map_capacity=2,
|
||||
)
|
||||
.evaluation(
|
||||
evaluation_num_env_runners=1,
|
||||
evaluation_config=ppo.PPOConfig.overrides(num_cpus_per_env_runner=0.1),
|
||||
)
|
||||
)
|
||||
|
||||
obs_space = gym.spaces.Box(-2.0, 2.0, (4,))
|
||||
act_space = gym.spaces.Discrete(2)
|
||||
|
||||
# Pre-generate a policy instance to test adding these directly to an
|
||||
# existing algorithm.
|
||||
policy_obj = ppo.PPOTorchPolicy(obs_space, act_space, config.to_dict())
|
||||
|
||||
# Construct the Algorithm with a single policy in it.
|
||||
algo = config.build()
|
||||
pol0 = algo.get_policy("p0")
|
||||
r = algo.train()
|
||||
self.assertTrue("p0" in r["info"][LEARNER_INFO])
|
||||
for i in range(1, 3):
|
||||
|
||||
def new_mapping_fn(agent_id, episode, worker, i=i, **kwargs):
|
||||
return f"p{choice([i, i - 1])}"
|
||||
|
||||
# Add a new policy either by class (and options) or by instance.
|
||||
pid = f"p{i}"
|
||||
print(f"Adding policy {pid} ...")
|
||||
# By (already instantiated) instance.
|
||||
if i == 2:
|
||||
new_pol = algo.add_policy(
|
||||
pid,
|
||||
# Pass in an already existing policy instance.
|
||||
policy=policy_obj,
|
||||
# Test changing the mapping fn.
|
||||
policy_mapping_fn=new_mapping_fn,
|
||||
# Change the list of policies to train.
|
||||
policies_to_train=[f"p{i}", f"p{i - 1}"],
|
||||
)
|
||||
# By class (and options).
|
||||
else:
|
||||
new_pol = algo.add_policy(
|
||||
pid,
|
||||
algo.get_default_policy_class(config),
|
||||
observation_space=obs_space,
|
||||
action_space=act_space,
|
||||
# Test changing the mapping fn.
|
||||
policy_mapping_fn=new_mapping_fn,
|
||||
# Change the list of policies to train.
|
||||
policies_to_train=[f"p{i}", f"p{i-1}"],
|
||||
)
|
||||
|
||||
# Make sure new policy is part of remote workers in the
|
||||
# worker set and the eval worker set.
|
||||
self.assertTrue(
|
||||
all(
|
||||
algo.env_runner_group.foreach_env_runner(
|
||||
func=lambda w, pid=pid: pid in w.policy_map
|
||||
)
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
algo.eval_env_runner_group.foreach_env_runner(
|
||||
func=lambda w, pid=pid: pid in w.policy_map
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Assert new policy is part of local worker (eval worker set does NOT
|
||||
# have a local worker, only the main EnvRunnerGroup does).
|
||||
pol_map = algo.env_runner.policy_map
|
||||
self.assertTrue(new_pol is not pol0)
|
||||
for j in range(i + 1):
|
||||
self.assertTrue(f"p{j}" in pol_map)
|
||||
self.assertTrue(len(pol_map) == i + 1)
|
||||
algo.train()
|
||||
checkpoint = algo.save().checkpoint
|
||||
|
||||
# Test restoring from the checkpoint (which has more policies
|
||||
# than what's defined in the config dict).
|
||||
test = ppo.PPO.from_checkpoint(checkpoint)
|
||||
|
||||
# Make sure evaluation worker also got the restored, added policy.
|
||||
def _has_policies(w, pid=pid):
|
||||
return w.get_policy("p0") is not None and w.get_policy(pid) is not None
|
||||
|
||||
self.assertTrue(
|
||||
all(test.eval_env_runner_group.foreach_env_runner(_has_policies))
|
||||
)
|
||||
|
||||
# Make sure algorithm can continue training the restored policy.
|
||||
pol0 = test.get_policy("p0")
|
||||
test.train()
|
||||
# Test creating an action with the added (and restored) policy.
|
||||
a = test.compute_single_action(
|
||||
np.zeros_like(pol0.observation_space.sample()), policy_id=pid
|
||||
)
|
||||
self.assertTrue(pol0.action_space.contains(a))
|
||||
test.stop()
|
||||
|
||||
# After having added 2 policies, try to restore the Algorithm,
|
||||
# but only with 1 of the originally added policies (plus the initial
|
||||
# p0).
|
||||
if i == 2:
|
||||
|
||||
def new_mapping_fn(agent_id, episode, worker, **kwargs):
|
||||
return f"p{choice([0, 2])}"
|
||||
|
||||
test2 = ppo.PPO.from_checkpoint(
|
||||
path=checkpoint,
|
||||
policy_ids=["p0", "p2"],
|
||||
policy_mapping_fn=new_mapping_fn,
|
||||
policies_to_train=["p0"],
|
||||
)
|
||||
|
||||
# Make sure evaluation workers have the same policies.
|
||||
def _has_policies(w):
|
||||
return (
|
||||
w.get_policy("p0") is not None
|
||||
and w.get_policy("p2") is not None
|
||||
and w.get_policy("p1") is None
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
all(test2.eval_env_runner_group.foreach_env_runner(_has_policies))
|
||||
)
|
||||
|
||||
# Make sure algorithm can continue training the restored policy.
|
||||
pol2 = test2.get_policy("p2")
|
||||
test2.train()
|
||||
# Test creating an action with the added (and restored) policy.
|
||||
a = test2.compute_single_action(
|
||||
np.zeros_like(pol2.observation_space.sample()), policy_id=pid
|
||||
)
|
||||
self.assertTrue(pol2.action_space.contains(a))
|
||||
test2.stop()
|
||||
|
||||
# Delete all added policies again from Algorithm.
|
||||
for i in range(2, 0, -1):
|
||||
pid = f"p{i}"
|
||||
algo.remove_policy(
|
||||
pid,
|
||||
# Note that the complete signature of a policy_mapping_fn
|
||||
# is: `agent_id, episode, worker, **kwargs`.
|
||||
policy_mapping_fn=(
|
||||
lambda agent_id, episode, worker, i=i, **kwargs: f"p{i - 1}"
|
||||
),
|
||||
# Update list of policies to train.
|
||||
policies_to_train=[f"p{i - 1}"],
|
||||
)
|
||||
# Make sure removed policy is no longer part of remote workers in the
|
||||
# worker set and the eval worker set.
|
||||
self.assertTrue(
|
||||
algo.env_runner_group.foreach_env_runner(
|
||||
func=lambda w, pid=pid: pid not in w.policy_map
|
||||
)[0]
|
||||
)
|
||||
self.assertTrue(
|
||||
algo.eval_env_runner_group.foreach_env_runner(
|
||||
func=lambda w, pid=pid: pid not in w.policy_map
|
||||
)[0]
|
||||
)
|
||||
# Assert removed policy is no longer part of local worker
|
||||
# (eval worker set does NOT have a local worker, only the main
|
||||
# EnvRunnerGroup does).
|
||||
pol_map = algo.env_runner.policy_map
|
||||
self.assertTrue(pid not in pol_map)
|
||||
self.assertTrue(len(pol_map) == i)
|
||||
|
||||
algo.stop()
|
||||
|
||||
def test_evaluation_option(self):
|
||||
# Use a custom callback that asserts that we are running the
|
||||
# configured exact number of episodes per evaluation.
|
||||
config = (
|
||||
dqn.DQNConfig()
|
||||
.environment(env="CartPole-v1")
|
||||
.evaluation(
|
||||
evaluation_interval=2,
|
||||
evaluation_duration=2,
|
||||
evaluation_duration_unit="episodes",
|
||||
evaluation_config=dqn.DQNConfig.overrides(gamma=0.98),
|
||||
)
|
||||
.callbacks(callbacks_class=AssertEvalCallback)
|
||||
)
|
||||
|
||||
algo = config.build()
|
||||
# Given evaluation_interval=2, r0, r2 should not contain
|
||||
# evaluation metrics, while r1, r3 should.
|
||||
r0 = algo.train()
|
||||
print(r0)
|
||||
r1 = algo.train()
|
||||
print(r1)
|
||||
r2 = algo.train()
|
||||
print(r2)
|
||||
r3 = algo.train()
|
||||
print(r3)
|
||||
algo.stop()
|
||||
|
||||
# No eval results yet in first iteration (eval has not run yet).
|
||||
self.assertFalse(EVALUATION_RESULTS in r0)
|
||||
self.assertTrue(EVALUATION_RESULTS in r1)
|
||||
self.assertTrue(EVALUATION_RESULTS in r2)
|
||||
self.assertTrue(EVALUATION_RESULTS in r3)
|
||||
self.assertTrue(ENV_RUNNER_RESULTS in r1[EVALUATION_RESULTS])
|
||||
self.assertTrue(
|
||||
EPISODE_RETURN_MEAN in r1[EVALUATION_RESULTS][ENV_RUNNER_RESULTS]
|
||||
)
|
||||
self.assertNotEqual(r1[EVALUATION_RESULTS], r3[EVALUATION_RESULTS])
|
||||
|
||||
def test_evaluation_option_always_attach_eval_metrics(self):
|
||||
# Use a custom callback that asserts that we are running the
|
||||
# configured exact number of episodes per evaluation.
|
||||
config = (
|
||||
dqn.DQNConfig()
|
||||
.environment("CartPole-v1")
|
||||
.evaluation(
|
||||
evaluation_interval=2,
|
||||
evaluation_duration=2,
|
||||
evaluation_duration_unit="episodes",
|
||||
evaluation_config=dqn.DQNConfig.overrides(gamma=0.98),
|
||||
)
|
||||
.reporting(min_sample_timesteps_per_iteration=100)
|
||||
.callbacks(callbacks_class=AssertEvalCallback)
|
||||
)
|
||||
algo = config.build()
|
||||
# Should only see eval results, when eval actually ran.
|
||||
r0 = algo.train()
|
||||
r1 = algo.train()
|
||||
r2 = algo.train()
|
||||
r3 = algo.train()
|
||||
algo.stop()
|
||||
|
||||
# Eval results are not available at step 0.
|
||||
self.assertTrue(EVALUATION_RESULTS not in r0)
|
||||
# But step 3 should still have it, even though no eval was
|
||||
# run during that step (b/c the new API stack always attaches eval
|
||||
# results, after the very first evaluation).
|
||||
self.assertTrue(EVALUATION_RESULTS in r1)
|
||||
self.assertTrue(EVALUATION_RESULTS in r2)
|
||||
self.assertTrue(EVALUATION_RESULTS in r3)
|
||||
|
||||
def test_evaluation_wo_eval_env_runner_group(self):
|
||||
# Use a custom callback that asserts that we are running the
|
||||
# configured exact number of episodes per evaluation.
|
||||
config = (
|
||||
ppo.PPOConfig()
|
||||
.environment(env="CartPole-v1")
|
||||
.callbacks(callbacks_class=AssertEvalCallback)
|
||||
)
|
||||
|
||||
# Setup algorithm w/o evaluation worker set and still call
|
||||
# evaluate() -> Expect error.
|
||||
algo_wo_env_on_local_worker = config.build()
|
||||
self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"doesn't have an env!",
|
||||
algo_wo_env_on_local_worker.evaluate,
|
||||
)
|
||||
algo_wo_env_on_local_worker.stop()
|
||||
|
||||
# Try again using `create_local_env_runner=True`.
|
||||
# This force-adds the env on the local-worker, so this Algorithm
|
||||
# can `evaluate` even though it doesn't have an evaluation-worker
|
||||
# set.
|
||||
config.create_env_on_local_worker = True
|
||||
algo_w_env_on_local_worker = config.build()
|
||||
results = algo_w_env_on_local_worker.evaluate()
|
||||
assert (
|
||||
ENV_RUNNER_RESULTS in results
|
||||
and EPISODE_RETURN_MEAN in results[ENV_RUNNER_RESULTS]
|
||||
)
|
||||
algo_w_env_on_local_worker.stop()
|
||||
|
||||
def test_no_env_but_eval_workers_do_have_env(self):
|
||||
"""Tests whether no env on workers, but env on eval workers works ok."""
|
||||
script_path = Path(__file__)
|
||||
input_file = os.path.join(
|
||||
script_path.parent.parent.parent, "offline/tests/data/cartpole/small.json"
|
||||
)
|
||||
|
||||
env = gym.make("CartPole-v1")
|
||||
|
||||
offline_rl_config = (
|
||||
BCConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=False,
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
)
|
||||
.environment(
|
||||
observation_space=env.observation_space,
|
||||
action_space=env.action_space,
|
||||
)
|
||||
.evaluation(
|
||||
evaluation_interval=1,
|
||||
evaluation_num_env_runners=1,
|
||||
evaluation_config=BCConfig.overrides(
|
||||
env="CartPole-v1",
|
||||
input_="sampler",
|
||||
observation_space=None, # Test, whether this is inferred.
|
||||
action_space=None, # Test, whether this is inferred.
|
||||
),
|
||||
)
|
||||
.offline_data(input_=[input_file])
|
||||
)
|
||||
|
||||
bc = offline_rl_config.build()
|
||||
bc.train()
|
||||
bc.stop()
|
||||
|
||||
def test_counters_after_checkpoint(self):
|
||||
# We expect algorithm to no start counters from zero after loading a
|
||||
# checkpoint on a fresh Algorithm instance
|
||||
config = (
|
||||
ppo.PPOConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=False,
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
)
|
||||
.environment(env="CartPole-v1")
|
||||
)
|
||||
algo = config.build()
|
||||
|
||||
self.assertTrue(all(c == 0 for c in algo._counters.values()))
|
||||
algo.step()
|
||||
self.assertTrue((all(c != 0 for c in algo._counters.values())))
|
||||
counter_values = list(algo._counters.values())
|
||||
state = algo.__getstate__()
|
||||
algo.stop()
|
||||
|
||||
algo2 = config.build()
|
||||
self.assertTrue(all(c == 0 for c in algo2._counters.values()))
|
||||
algo2.__setstate__(state)
|
||||
counter_values2 = list(algo2._counters.values())
|
||||
self.assertEqual(counter_values, counter_values2)
|
||||
|
||||
def _assert_modules_added(
|
||||
self,
|
||||
*,
|
||||
algo,
|
||||
marl_spec,
|
||||
mids,
|
||||
trainable,
|
||||
mapped,
|
||||
not_mapped,
|
||||
):
|
||||
# Make sure Learner has the correct `should_module_be_updated` list.
|
||||
self.assertEqual(
|
||||
set(algo.learner_group._learner.config.policies_to_train),
|
||||
{f"p{i}" for i in trainable},
|
||||
)
|
||||
# Make sure mids are all in marl_spec.
|
||||
if marl_spec is not None:
|
||||
self.assertTrue(all(f"p{m}" in marl_spec for m in mids))
|
||||
# Make sure module is part of remote EnvRunners in the
|
||||
# EnvRunnerGroup and the eval EnvRunnerGroup.
|
||||
self.assertTrue(
|
||||
all(
|
||||
algo.env_runner_group.foreach_env_runner(
|
||||
lambda w, mids=mids: all(f"p{i}" in w.module for i in mids)
|
||||
)
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
algo.eval_env_runner_group.foreach_env_runner(
|
||||
lambda w, mids=mids: all(f"p{i}" in w.module for i in mids)
|
||||
)
|
||||
)
|
||||
)
|
||||
# Make sure that EnvRunners have received the correct mapping fn.
|
||||
mapped_pols = [
|
||||
algo.env_runner.config.policy_mapping_fn(0, None) for _ in range(100)
|
||||
]
|
||||
self.assertTrue(all(f"p{i}" in mapped_pols for i in mapped))
|
||||
self.assertTrue(not any(f"p{i}" in mapped_pols for i in not_mapped))
|
||||
|
||||
def test_evaluation_in_parallel_to_training(self):
|
||||
SECONDS_TO_SLEEP = 2
|
||||
|
||||
class SluggishEnv(gym.Env):
|
||||
def __init__(self, config):
|
||||
self.action_space = gym.spaces.Discrete(2)
|
||||
self.observation_space = gym.spaces.Box(-1, 1, dtype=np.float32)
|
||||
|
||||
def step(self, action):
|
||||
time.sleep(SECONDS_TO_SLEEP)
|
||||
return self.observation_space.sample(), 1, True, False, {}
|
||||
|
||||
def reset(self, *, seed=None, options=None):
|
||||
super().reset(seed=seed)
|
||||
return self.observation_space.sample(), {}
|
||||
|
||||
config = (
|
||||
ppo.PPOConfig()
|
||||
.environment(env=SluggishEnv)
|
||||
.evaluation(
|
||||
evaluation_parallel_to_training=True,
|
||||
evaluation_interval=1,
|
||||
evaluation_num_env_runners=1,
|
||||
evaluation_duration=1,
|
||||
evaluation_duration_unit="timesteps",
|
||||
)
|
||||
.training(train_batch_size=1, minibatch_size=1) # Speed things up
|
||||
)
|
||||
algo = config.build()
|
||||
metrics = algo.train()
|
||||
# This can only be true if we do not execute training and evaluation in sequence
|
||||
assert metrics["time_this_iter_s"] < SECONDS_TO_SLEEP * 2
|
||||
assert metrics["time_this_iter_s"] > SECONDS_TO_SLEEP
|
||||
algo.stop()
|
||||
|
||||
config.evaluation(evaluation_parallel_to_training=False)
|
||||
algo_2 = config.build()
|
||||
metrics_2 = algo_2.train()
|
||||
# This must be true if we execute training and evaluation in sequence
|
||||
assert metrics_2["time_this_iter_s"] > SECONDS_TO_SLEEP * 2
|
||||
algo_2.stop()
|
||||
|
||||
def test_custom_eval_function_falsy_results(self):
|
||||
"""Test that custom eval function can return ({}, 0, 0)."""
|
||||
config = (
|
||||
ppo.PPOConfig()
|
||||
.environment("CartPole-v1")
|
||||
.evaluation(
|
||||
custom_evaluation_function=lambda algo, eval_workers: ({}, 0, 0),
|
||||
evaluation_interval=1,
|
||||
evaluation_num_env_runners=1,
|
||||
evaluation_duration=1,
|
||||
evaluation_duration_unit="episodes",
|
||||
)
|
||||
.training(train_batch_size=50, minibatch_size=25, num_epochs=1)
|
||||
)
|
||||
algo = config.build()
|
||||
metrics = algo.train()
|
||||
self.assertIn(EVALUATION_RESULTS, metrics)
|
||||
algo.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,525 @@
|
||||
import unittest
|
||||
from typing import Type
|
||||
|
||||
import gymnasium as gym
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
|
||||
from ray.rllib.algorithms.ppo import PPO, PPOConfig
|
||||
from ray.rllib.algorithms.ppo.torch.ppo_torch_learner import PPOTorchLearner
|
||||
from ray.rllib.algorithms.ppo.torch.ppo_torch_rl_module import PPOTorchRLModule
|
||||
from ray.rllib.core.rl_module.multi_rl_module import (
|
||||
MultiRLModule,
|
||||
MultiRLModuleSpec,
|
||||
)
|
||||
from ray.rllib.core.rl_module.rl_module import RLModule, RLModuleSpec
|
||||
from ray.rllib.utils.test_utils import check
|
||||
|
||||
|
||||
class TestAlgorithmConfig(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
ray.init()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
ray.shutdown()
|
||||
|
||||
def test_running_specific_algo_with_generic_config(self):
|
||||
"""Tests, whether some algo can be run with the generic AlgorithmConfig."""
|
||||
config = (
|
||||
AlgorithmConfig(algo_class=PPO)
|
||||
.environment("CartPole-v0")
|
||||
.training(lr=0.12345, train_batch_size=3000, minibatch_size=300)
|
||||
)
|
||||
algo = config.build()
|
||||
self.assertTrue(algo.config.lr == 0.12345)
|
||||
self.assertTrue(algo.config.train_batch_size == 3000)
|
||||
algo.train()
|
||||
algo.stop()
|
||||
|
||||
def test_freezing_of_algo_config(self):
|
||||
"""Tests, whether freezing an AlgorithmConfig actually works as expected."""
|
||||
config = (
|
||||
AlgorithmConfig()
|
||||
.environment("CartPole-v0")
|
||||
.training(lr=0.12345, train_batch_size=3000)
|
||||
.multi_agent(
|
||||
policies={
|
||||
"pol1": (None, None, None, AlgorithmConfig.overrides(lr=0.001))
|
||||
},
|
||||
policy_mapping_fn=lambda agent_id, episode, worker, **kw: "pol1",
|
||||
)
|
||||
)
|
||||
config.freeze()
|
||||
|
||||
def set_lr(config):
|
||||
config.lr = 0.01
|
||||
|
||||
self.assertRaisesRegex(
|
||||
AttributeError,
|
||||
"Cannot set attribute.+of an already frozen AlgorithmConfig",
|
||||
lambda: set_lr(config),
|
||||
)
|
||||
|
||||
# TODO: Figure out, whether we should convert all nested structures into
|
||||
# frozen ones (set -> frozenset; dict -> frozendict; list -> tuple).
|
||||
|
||||
def set_one_policy(config):
|
||||
config.policies["pol1"] = (None, None, None, {"lr": 0.123})
|
||||
|
||||
# self.assertRaisesRegex(
|
||||
# AttributeError,
|
||||
# "Cannot set attribute.+of an already frozen AlgorithmConfig",
|
||||
# lambda: set_one_policy(config),
|
||||
# )
|
||||
|
||||
def test_rollout_fragment_length(self):
|
||||
"""Tests the proper auto-computation of the `rollout_fragment_length`."""
|
||||
config = (
|
||||
AlgorithmConfig()
|
||||
.env_runners(
|
||||
num_env_runners=4,
|
||||
num_envs_per_env_runner=3,
|
||||
rollout_fragment_length="auto",
|
||||
)
|
||||
.training(train_batch_size=2456)
|
||||
)
|
||||
# 2456 / (3 * 4) -> 204.666 -> 204 or 205 (depending on worker index).
|
||||
# Actual train batch size: 2457 (off by only 1).
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=0) == 205)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=1) == 205)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=2) == 205)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=3) == 205)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=4) == 204)
|
||||
|
||||
config = (
|
||||
AlgorithmConfig()
|
||||
.env_runners(
|
||||
num_env_runners=3,
|
||||
num_envs_per_env_runner=2,
|
||||
rollout_fragment_length="auto",
|
||||
)
|
||||
.training(train_batch_size=4000)
|
||||
)
|
||||
# 4000 / 6 -> 666.66 -> 666 or 667 (depending on worker index)
|
||||
# Actual train batch size: 4000 (perfect match)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=0) == 667)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=1) == 667)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=2) == 667)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=3) == 666)
|
||||
|
||||
config = (
|
||||
AlgorithmConfig()
|
||||
.env_runners(
|
||||
num_env_runners=12,
|
||||
rollout_fragment_length="auto",
|
||||
)
|
||||
.training(train_batch_size=1342)
|
||||
)
|
||||
# 1342 / 12 -> 111.83 -> 111 or 112 (depending on worker index)
|
||||
# Actual train batch size: 1342 (perfect match)
|
||||
for i in range(11):
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=i) == 112)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=11) == 111)
|
||||
self.assertTrue(config.get_rollout_fragment_length(worker_index=12) == 111)
|
||||
|
||||
def test_detect_atari_env(self):
|
||||
"""Tests that we can properly detect Atari envs."""
|
||||
config = AlgorithmConfig().environment(
|
||||
env="ale_py:ALE/Breakout-v5", env_config={"frameskip": 1}
|
||||
)
|
||||
self.assertTrue(config.is_atari)
|
||||
|
||||
config = AlgorithmConfig().environment(env="ale_py:ALE/Pong-v5")
|
||||
self.assertTrue(config.is_atari)
|
||||
|
||||
config = AlgorithmConfig().environment(env="CartPole-v1")
|
||||
# We do not auto-detect callable env makers for Atari envs.
|
||||
self.assertFalse(config.is_atari)
|
||||
|
||||
config = AlgorithmConfig().environment(
|
||||
env=lambda ctx: gym.make(
|
||||
"ale_py:ALE/Breakout-v5",
|
||||
frameskip=1,
|
||||
)
|
||||
)
|
||||
# We do not auto-detect callable env makers for Atari envs.
|
||||
self.assertFalse(config.is_atari)
|
||||
|
||||
config = AlgorithmConfig().environment(env="NotAtari")
|
||||
self.assertFalse(config.is_atari)
|
||||
|
||||
def test_rl_module_api(self):
|
||||
config = PPOConfig().environment("CartPole-v1").framework("torch")
|
||||
|
||||
self.assertEqual(config.rl_module_spec.module_class, PPOTorchRLModule)
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
config = config.rl_module(rl_module_spec=RLModuleSpec(A))
|
||||
self.assertEqual(config.rl_module_spec.module_class, A)
|
||||
|
||||
def test_config_per_module(self):
|
||||
"""Tests, whether per-module config overrides (multi-agent) work as expected."""
|
||||
|
||||
# Compile individual agents' PPO configs from a config object.
|
||||
config = (
|
||||
PPOConfig()
|
||||
.training(kl_coeff=0.5)
|
||||
.multi_agent(
|
||||
policies={"module_1", "module_2", "module_3"},
|
||||
# Override config settings fro `module_1` and `module_2`.
|
||||
algorithm_config_overrides_per_module={
|
||||
"module_1": PPOConfig.overrides(lr=0.01, kl_coeff=0.1),
|
||||
"module_2": PPOConfig.overrides(grad_clip=100.0),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Check default config.
|
||||
check(config.lr, 0.00005)
|
||||
check(config.grad_clip, None)
|
||||
check(config.grad_clip_by, "global_norm")
|
||||
check(config.kl_coeff, 0.5)
|
||||
|
||||
# `module_1` overrides.
|
||||
config_1 = config.get_config_for_module("module_1")
|
||||
check(config_1.lr, 0.01)
|
||||
check(config_1.grad_clip, None)
|
||||
check(config_1.grad_clip_by, "global_norm")
|
||||
check(config_1.kl_coeff, 0.1)
|
||||
|
||||
# `module_2` overrides.
|
||||
config_2 = config.get_config_for_module("module_2")
|
||||
check(config_2.lr, 0.00005)
|
||||
check(config_2.grad_clip, 100.0)
|
||||
check(config_2.grad_clip_by, "global_norm")
|
||||
check(config_2.kl_coeff, 0.5)
|
||||
|
||||
# No `module_3` overrides (b/c module_3 uses the top-level config
|
||||
# object directly).
|
||||
self.assertTrue("module_3" not in config._per_module_overrides)
|
||||
config_3 = config.get_config_for_module("module_3")
|
||||
self.assertTrue(config_3 is config)
|
||||
|
||||
def test_learner_api(self):
|
||||
config = PPOConfig().environment("CartPole-v1")
|
||||
|
||||
self.assertEqual(config.learner_class, PPOTorchLearner)
|
||||
|
||||
def _assertEqualMARLSpecs(self, spec1, spec2):
|
||||
self.assertEqual(spec1.multi_rl_module_class, spec2.multi_rl_module_class)
|
||||
|
||||
self.assertEqual(set(spec1.module_specs.keys()), set(spec2.module_specs.keys()))
|
||||
for k, module_spec1 in spec1.module_specs.items():
|
||||
module_spec2 = spec2.module_specs[k]
|
||||
|
||||
self.assertEqual(module_spec1.module_class, module_spec2.module_class)
|
||||
self.assertEqual(
|
||||
module_spec1.observation_space, module_spec2.observation_space
|
||||
)
|
||||
self.assertEqual(module_spec1.action_space, module_spec2.action_space)
|
||||
self.assertEqual(
|
||||
module_spec1.model_config_dict, module_spec2.model_config_dict
|
||||
)
|
||||
|
||||
def _get_expected_marl_spec(
|
||||
self,
|
||||
config: AlgorithmConfig,
|
||||
expected_module_class: Type[RLModule],
|
||||
passed_module_class: Type[RLModule] = None,
|
||||
expected_multi_rl_module_class: Type[MultiRLModule] = None,
|
||||
):
|
||||
"""This is a utility function that retrieves the expected marl specs.
|
||||
|
||||
Args:
|
||||
config: The algorithm config.
|
||||
expected_module_class: This is the expected RLModule class that is going to
|
||||
be reference in the RLModuleSpec parts of the MultiLModuleSpec.
|
||||
passed_module_class: This is the RLModule class that is passed into the
|
||||
module_spec argument of get_multi_rl_module_spec. The function is
|
||||
designed so that it will use the passed in module_spec for the
|
||||
RLModuleSpec parts of the MultiRLModuleSpec.
|
||||
expected_multi_rl_module_class: This is the expected MultiRLModule class
|
||||
that is going to be reference in the MultiRLModuleSpec.
|
||||
|
||||
Returns:
|
||||
Tuple of the returned MultiRLModuleSpec from config.
|
||||
get_multi_rl_module_spec() and the expected MultiRLModuleSpec.
|
||||
"""
|
||||
from ray.rllib.policy.policy import PolicySpec
|
||||
|
||||
if expected_multi_rl_module_class is None:
|
||||
expected_multi_rl_module_class = MultiRLModule
|
||||
|
||||
env = gym.make("CartPole-v1")
|
||||
policy_spec_ph = PolicySpec(
|
||||
observation_space=env.observation_space,
|
||||
action_space=env.action_space,
|
||||
config=AlgorithmConfig(),
|
||||
)
|
||||
|
||||
marl_spec = config.get_multi_rl_module_spec(
|
||||
policy_dict={"p1": policy_spec_ph, "p2": policy_spec_ph},
|
||||
single_agent_rl_module_spec=RLModuleSpec(module_class=passed_module_class)
|
||||
if passed_module_class
|
||||
else None,
|
||||
)
|
||||
|
||||
expected_marl_spec = MultiRLModuleSpec(
|
||||
multi_rl_module_class=expected_multi_rl_module_class,
|
||||
rl_module_specs={
|
||||
"p1": RLModuleSpec(
|
||||
module_class=expected_module_class,
|
||||
observation_space=env.observation_space,
|
||||
action_space=env.action_space,
|
||||
),
|
||||
"p2": RLModuleSpec(
|
||||
module_class=expected_module_class,
|
||||
observation_space=env.observation_space,
|
||||
action_space=env.action_space,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return marl_spec, expected_marl_spec
|
||||
|
||||
def test_get_multi_rl_module_spec(self):
|
||||
"""Tests whether the get_multi_rl_module_spec() method works properly."""
|
||||
from ray.rllib.examples.rl_modules.classes.vpg_torch_rlm import VPGTorchRLModule
|
||||
|
||||
class CustomRLModule1(VPGTorchRLModule):
|
||||
pass
|
||||
|
||||
class CustomRLModule2(VPGTorchRLModule):
|
||||
pass
|
||||
|
||||
class CustomRLModule3(VPGTorchRLModule):
|
||||
pass
|
||||
|
||||
class CustomMultiRLModule1(MultiRLModule):
|
||||
pass
|
||||
|
||||
########################################
|
||||
# single agent
|
||||
class SingleAgentAlgoConfig(AlgorithmConfig):
|
||||
def get_default_rl_module_spec(self):
|
||||
return RLModuleSpec(module_class=VPGTorchRLModule)
|
||||
|
||||
# multi-agent
|
||||
class MultiAgentAlgoConfigWithNoSingleAgentSpec(AlgorithmConfig):
|
||||
def get_default_rl_module_spec(self):
|
||||
return MultiRLModuleSpec(multi_rl_module_class=CustomMultiRLModule1)
|
||||
|
||||
########################################
|
||||
# This is the simplest case where we have to construct the MultiRLModule based
|
||||
# on the default specs only.
|
||||
config = SingleAgentAlgoConfig().api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
|
||||
spec, expected = self._get_expected_marl_spec(config, VPGTorchRLModule)
|
||||
self._assertEqualMARLSpecs(spec, expected)
|
||||
|
||||
# expected module should become the passed module if we pass it in.
|
||||
spec, expected = self._get_expected_marl_spec(
|
||||
config, CustomRLModule2, passed_module_class=CustomRLModule2
|
||||
)
|
||||
self._assertEqualMARLSpecs(spec, expected)
|
||||
|
||||
########################################
|
||||
# This is the case where we pass in a `MultiRLModuleSpec` that asks the
|
||||
# algorithm to assign a specific type of RLModule class to certain module_ids.
|
||||
config = (
|
||||
SingleAgentAlgoConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.rl_module(
|
||||
rl_module_spec=MultiRLModuleSpec(
|
||||
rl_module_specs={
|
||||
"p1": RLModuleSpec(module_class=CustomRLModule1),
|
||||
"p2": RLModuleSpec(module_class=CustomRLModule1),
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
spec, expected = self._get_expected_marl_spec(config, CustomRLModule1)
|
||||
self._assertEqualMARLSpecs(spec, expected)
|
||||
|
||||
########################################
|
||||
# This is the case where we ask the algorithm to assign a specific type of
|
||||
# RLModule class to ALL module_ids.
|
||||
config = (
|
||||
SingleAgentAlgoConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.rl_module(
|
||||
rl_module_spec=RLModuleSpec(module_class=CustomRLModule1),
|
||||
)
|
||||
)
|
||||
|
||||
spec, expected = self._get_expected_marl_spec(config, CustomRLModule1)
|
||||
self._assertEqualMARLSpecs(spec, expected)
|
||||
|
||||
# expected module should become the passed module if we pass it in.
|
||||
spec, expected = self._get_expected_marl_spec(
|
||||
config, CustomRLModule2, passed_module_class=CustomRLModule2
|
||||
)
|
||||
self._assertEqualMARLSpecs(spec, expected)
|
||||
|
||||
########################################
|
||||
# This is not only assigning a specific type of RLModule class to EACH
|
||||
# module_id, but also defining a new custom MultiRLModule class to be used
|
||||
# in the multi-agent scenario.
|
||||
config = (
|
||||
SingleAgentAlgoConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.rl_module(
|
||||
rl_module_spec=MultiRLModuleSpec(
|
||||
multi_rl_module_class=CustomMultiRLModule1,
|
||||
rl_module_specs={
|
||||
"p1": RLModuleSpec(module_class=CustomRLModule1),
|
||||
"p2": RLModuleSpec(module_class=CustomRLModule1),
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
spec, expected = self._get_expected_marl_spec(
|
||||
config, CustomRLModule1, expected_multi_rl_module_class=CustomMultiRLModule1
|
||||
)
|
||||
self._assertEqualMARLSpecs(spec, expected)
|
||||
|
||||
# This is expected to return CustomRLModule1 instead of CustomRLModule3 which
|
||||
# is passed in. Because the default for p1, p2 is to use CustomRLModule1. The
|
||||
# passed module_spec only sets a default to fall back onto in case the
|
||||
# module_id is not specified in the original MultiRLModuleSpec. Since P1
|
||||
# and P2 are both assigned to CustomeRLModule1, the passed module_spec will not
|
||||
# be used. This is the expected behavior for adding a new modules to a
|
||||
# `MultiRLModule` that is not defined in the original MultiRLModuleSpec.
|
||||
spec, expected = self._get_expected_marl_spec(
|
||||
config,
|
||||
CustomRLModule1,
|
||||
passed_module_class=CustomRLModule3,
|
||||
expected_multi_rl_module_class=CustomMultiRLModule1,
|
||||
)
|
||||
self._assertEqualMARLSpecs(spec, expected)
|
||||
|
||||
########################################
|
||||
# This is the case where we ask the algorithm to use its default
|
||||
# MultiRLModuleSpec, but the MultiRLModuleSpec has not defined its
|
||||
# RLModuleSpecs.
|
||||
config = MultiAgentAlgoConfigWithNoSingleAgentSpec().api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
|
||||
self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"Module_specs cannot be None",
|
||||
lambda: config.rl_module_spec,
|
||||
)
|
||||
|
||||
def test_rollout_fragment_length_with_small_batch_and_multiple_learners(self):
|
||||
"""Test that get_rollout_fragment_length doesn't return 0 when train_batch_size=1 and num_learners > 1."""
|
||||
for num_env_runners in [1, 2, 3, 4]:
|
||||
config = (
|
||||
AlgorithmConfig()
|
||||
.env_runners(
|
||||
rollout_fragment_length="auto",
|
||||
num_env_runners=num_env_runners,
|
||||
)
|
||||
.learners(
|
||||
num_learners=2
|
||||
) # Multiple learners with train_batch_size=1 causes the issue
|
||||
.training(
|
||||
train_batch_size=1
|
||||
) # Small batch size with multiple learners causes integer division to 0
|
||||
)
|
||||
|
||||
# This should not return 0
|
||||
rollout_fragment_length = config.get_rollout_fragment_length(0)
|
||||
self.assertEqual(
|
||||
rollout_fragment_length,
|
||||
1,
|
||||
)
|
||||
|
||||
def test_to_dict_roundtrip_new_api_stack(self):
|
||||
"""Tests that to_dict() round-trips New API stack batch sizes.
|
||||
|
||||
`to_dict()` does NOT eagerly resolve the effective batch size (that stays
|
||||
lazy via the `total_train_batch_size` property). It only serializes the raw
|
||||
fields, which is what makes it safe to call on an as-yet-unresolved config
|
||||
(e.g. one carrying Tune search spaces).
|
||||
"""
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
|
||||
# 1. Create a config on the New API Stack
|
||||
config = (
|
||||
PPOConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.training(train_batch_size_per_learner=123)
|
||||
)
|
||||
|
||||
# 2. Export to dictionary
|
||||
config_dict = config.to_dict()
|
||||
|
||||
# to_dict() does not inject computed properties (would break round-trip).
|
||||
self.assertNotIn("total_train_batch_size", config_dict)
|
||||
self.assertNotIn("train_batch_size_per_learner", config_dict)
|
||||
|
||||
# 3. Roundtrip: Create a new config and update from the dictionary, and
|
||||
# verify the per-learner batch size (and the total derived from it) survives.
|
||||
new_config = PPOConfig().update_from_dict(config_dict)
|
||||
self.assertEqual(new_config.train_batch_size_per_learner, 123)
|
||||
self.assertEqual(new_config.total_train_batch_size, 123)
|
||||
|
||||
def test_to_dict_with_tune_search_space(self):
|
||||
"""to_dict() must not eagerly resolve batch size when it's a Tune search space.
|
||||
|
||||
Regression test: passing an AlgorithmConfig with a search-space
|
||||
`train_batch_size_per_learner` as Tune's `param_space` calls `to_dict()` on
|
||||
an unresolved config. Computing `total_train_batch_size` (`Domain * int`)
|
||||
would raise TypeError, so `to_dict()` must not attempt it.
|
||||
"""
|
||||
from ray import tune
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
|
||||
config = (
|
||||
PPOConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.training(train_batch_size_per_learner=tune.qrandint(256, 2048, 64))
|
||||
)
|
||||
|
||||
# Must not raise (this is the bug: TypeError from `Domain * int`).
|
||||
config_dict = config.to_dict()
|
||||
|
||||
# The unresolved search space survives serialization so Tune can sample it.
|
||||
self.assertIsInstance(
|
||||
config_dict["_train_batch_size_per_learner"], tune.search.sample.Domain
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ray
|
||||
import ray._common
|
||||
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
|
||||
from ray.rllib.policy.sample_batch import DEFAULT_POLICY_ID
|
||||
from ray.rllib.utils.framework import try_import_torch
|
||||
from ray.tune.registry import get_trainable_cls
|
||||
|
||||
torch, _ = try_import_torch()
|
||||
|
||||
# Keep a set of all RLlib algos that support the RLModule API.
|
||||
# For these algos we need to disable the RLModule API in the config for the purpose of
|
||||
# this test. This test is made for the ModelV2 API which is not the same as RLModule.
|
||||
RLMODULE_SUPPORTED_ALGOS = {"PPO"}
|
||||
|
||||
|
||||
def save_test(alg_name, framework="tf", multi_agent=False):
|
||||
config = (
|
||||
get_trainable_cls(alg_name)
|
||||
.get_default_config()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=False, enable_rl_module_and_learner=False
|
||||
)
|
||||
.framework(framework)
|
||||
# Switch on saving native DL-framework (tf, torch) model files.
|
||||
.checkpointing(export_native_model_files=True)
|
||||
)
|
||||
|
||||
if alg_name in RLMODULE_SUPPORTED_ALGOS:
|
||||
config.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
|
||||
if multi_agent:
|
||||
config.multi_agent(
|
||||
policies={"pol1", "pol2"},
|
||||
policy_mapping_fn=(
|
||||
lambda agent_id, episode, worker, **kwargs: "pol1"
|
||||
if agent_id == "agent1"
|
||||
else "pol2"
|
||||
),
|
||||
)
|
||||
config.environment(MultiAgentCartPole, env_config={"num_agents": 2})
|
||||
else:
|
||||
config.environment("CartPole-v1")
|
||||
algo = config.build()
|
||||
test_obs = np.array([[0.1, 0.2, 0.3, 0.4]])
|
||||
|
||||
export_dir = os.path.join(
|
||||
ray._common.utils.get_default_ray_temp_dir(),
|
||||
"export_dir_%s" % alg_name,
|
||||
)
|
||||
|
||||
algo.train()
|
||||
print("Exporting algo checkpoint", alg_name, export_dir)
|
||||
export_dir = algo.save(export_dir).checkpoint.path
|
||||
model_dir = os.path.join(
|
||||
export_dir,
|
||||
"policies",
|
||||
"pol1" if multi_agent else DEFAULT_POLICY_ID,
|
||||
"model",
|
||||
)
|
||||
|
||||
# Test loading exported model and perform forward pass.
|
||||
filename = os.path.join(model_dir, "model.pt")
|
||||
model = torch.load(filename, weights_only=False)
|
||||
assert model
|
||||
results = model(
|
||||
input_dict={"obs": torch.from_numpy(test_obs)},
|
||||
# TODO (sven): Make non-RNN models NOT expect these args at all.
|
||||
state=[torch.tensor(0)], # dummy value
|
||||
seq_lens=torch.tensor(0), # dummy value
|
||||
)
|
||||
assert len(results) == 2
|
||||
assert results[0].shape == (1, 2)
|
||||
assert results[1] == [torch.tensor(0)] # dummy
|
||||
|
||||
shutil.rmtree(export_dir)
|
||||
|
||||
|
||||
class TestAlgorithmSave(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
ray.init(num_cpus=4)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
ray.shutdown()
|
||||
|
||||
def test_save_appo_multi_agent(self):
|
||||
save_test("APPO", "torch", multi_agent=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,24 @@
|
||||
import unittest
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.registry import ALGORITHMS
|
||||
|
||||
|
||||
class TestAlgorithmImport(unittest.TestCase):
|
||||
def setUp(self):
|
||||
ray.init()
|
||||
|
||||
def tearDown(self):
|
||||
ray.shutdown()
|
||||
|
||||
def test_algo_import(self):
|
||||
for name, func in ALGORITHMS.items():
|
||||
func()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,336 @@
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import gymnasium as gym
|
||||
import numpy as np
|
||||
import tree
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
from ray.rllib.algorithms.ppo.ppo_catalog import PPOCatalog
|
||||
from ray.rllib.algorithms.ppo.torch.ppo_torch_rl_module import PPOTorchRLModule
|
||||
from ray.rllib.core import DEFAULT_MODULE_ID
|
||||
from ray.rllib.core.rl_module.default_model_config import DefaultModelConfig
|
||||
from ray.rllib.core.rl_module.multi_rl_module import (
|
||||
MultiRLModule,
|
||||
MultiRLModuleSpec,
|
||||
)
|
||||
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
|
||||
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
|
||||
from ray.rllib.utils.numpy import convert_to_numpy
|
||||
from ray.rllib.utils.test_utils import check
|
||||
|
||||
NUM_AGENTS = 2
|
||||
|
||||
|
||||
class TestAlgorithmRLModuleRestore(unittest.TestCase):
|
||||
"""Test RLModule loading from rl module spec across a local node."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
ray.init()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
ray.shutdown()
|
||||
|
||||
@staticmethod
|
||||
def get_ppo_config(num_agents=NUM_AGENTS):
|
||||
def policy_mapping_fn(agent_id, episode, **kwargs):
|
||||
# policy_id is policy_i where i is the agent id
|
||||
pol_id = f"policy_{agent_id}"
|
||||
return pol_id
|
||||
|
||||
scaling_config = {
|
||||
"num_learners": 0,
|
||||
"num_gpus_per_learner": 0,
|
||||
}
|
||||
|
||||
policies = {f"policy_{i}" for i in range(num_agents)}
|
||||
|
||||
config = (
|
||||
PPOConfig()
|
||||
.env_runners(rollout_fragment_length=4)
|
||||
.learners(**scaling_config)
|
||||
.environment(MultiAgentCartPole, env_config={"num_agents": num_agents})
|
||||
.training(num_epochs=1, train_batch_size=8, minibatch_size=8)
|
||||
.multi_agent(policies=policies, policy_mapping_fn=policy_mapping_fn)
|
||||
)
|
||||
return config
|
||||
|
||||
def test_e2e_load_simple_multi_rl_module(self):
|
||||
"""Test if we can train a PPO algo with a checkpointed MultiRLModule e2e."""
|
||||
config = self.get_ppo_config()
|
||||
env = MultiAgentCartPole({"num_agents": NUM_AGENTS})
|
||||
# create a multi_rl_module to load and save it to a checkpoint directory
|
||||
module_specs = {}
|
||||
for i in range(NUM_AGENTS):
|
||||
module_specs[f"policy_{i}"] = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.get_observation_space(0),
|
||||
action_space=env.get_action_space(0),
|
||||
# If we want to use this externally created module in the algorithm,
|
||||
# we need to provide the same config as the algorithm.
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[32 * (i + 1)]),
|
||||
catalog_class=PPOCatalog,
|
||||
)
|
||||
multi_rl_module_spec = MultiRLModuleSpec(rl_module_specs=module_specs)
|
||||
multi_rl_module = multi_rl_module_spec.build()
|
||||
multi_rl_module_weights = convert_to_numpy(multi_rl_module.get_state())
|
||||
marl_checkpoint_path = tempfile.mkdtemp()
|
||||
multi_rl_module.save_to_path(marl_checkpoint_path)
|
||||
|
||||
# create a new MARL_spec with the checkpoint from the previous one
|
||||
multi_rl_module_spec_from_checkpoint = MultiRLModuleSpec(
|
||||
rl_module_specs=module_specs,
|
||||
load_state_path=marl_checkpoint_path,
|
||||
)
|
||||
config.rl_module(rl_module_spec=multi_rl_module_spec_from_checkpoint)
|
||||
|
||||
# create the algorithm with multiple nodes and check if the weights
|
||||
# are the same as the original MultiRLModule
|
||||
algo = config.build()
|
||||
algo_module_weights = algo.learner_group.get_weights()
|
||||
check(algo_module_weights, multi_rl_module_weights)
|
||||
algo.train()
|
||||
algo.stop()
|
||||
del algo
|
||||
shutil.rmtree(marl_checkpoint_path)
|
||||
|
||||
def test_e2e_load_complex_multi_rl_module(self):
|
||||
"""Test if we can train a PPO algorithm with a cpkt MARL and RL module e2e."""
|
||||
config = self.get_ppo_config()
|
||||
env = MultiAgentCartPole({"num_agents": NUM_AGENTS})
|
||||
# create a multi_rl_module to load and save it to a checkpoint directory
|
||||
module_specs = {}
|
||||
for i in range(NUM_AGENTS):
|
||||
module_specs[f"policy_{i}"] = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.get_observation_space(0),
|
||||
action_space=env.get_action_space(0),
|
||||
# If we want to use this externally created module in the algorithm,
|
||||
# we need to provide the same config as the algorithm.
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[32 * (i + 1)]),
|
||||
catalog_class=PPOCatalog,
|
||||
)
|
||||
multi_rl_module_spec = MultiRLModuleSpec(rl_module_specs=module_specs)
|
||||
multi_rl_module = multi_rl_module_spec.build()
|
||||
marl_checkpoint_path = tempfile.mkdtemp()
|
||||
multi_rl_module.save_to_path(marl_checkpoint_path)
|
||||
|
||||
# create a RLModule to load and override the "policy_1" module with
|
||||
module_to_swap_in = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.get_observation_space(0),
|
||||
action_space=env.get_action_space(0),
|
||||
# Note, we need to pass in the default model config for the algorithm
|
||||
# to be able to use this module later.
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[64]),
|
||||
catalog_class=PPOCatalog,
|
||||
).build()
|
||||
|
||||
module_to_swap_in_path = tempfile.mkdtemp()
|
||||
module_to_swap_in.save_to_path(module_to_swap_in_path)
|
||||
|
||||
# create a new MARL_spec with the checkpoint from the marl_checkpoint
|
||||
# and the module_to_swap_in_checkpoint
|
||||
module_specs["policy_1"] = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.get_observation_space(0),
|
||||
action_space=env.get_action_space(0),
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[64]),
|
||||
catalog_class=PPOCatalog,
|
||||
load_state_path=module_to_swap_in_path,
|
||||
)
|
||||
multi_rl_module_spec_from_checkpoint = MultiRLModuleSpec(
|
||||
rl_module_specs=module_specs,
|
||||
load_state_path=marl_checkpoint_path,
|
||||
)
|
||||
config = config.rl_module(rl_module_spec=multi_rl_module_spec_from_checkpoint)
|
||||
|
||||
# create the algorithm with multiple nodes and check if the weights
|
||||
# are the same as the original MultiRLModule
|
||||
algo = config.build()
|
||||
algo_module_weights = algo.learner_group.get_weights()
|
||||
|
||||
multi_rl_module_with_swapped_in_module = MultiRLModule()
|
||||
multi_rl_module_with_swapped_in_module.add_module(
|
||||
"policy_0", multi_rl_module["policy_0"]
|
||||
)
|
||||
multi_rl_module_with_swapped_in_module.add_module("policy_1", module_to_swap_in)
|
||||
|
||||
check(
|
||||
algo_module_weights,
|
||||
convert_to_numpy(multi_rl_module_with_swapped_in_module.get_state()),
|
||||
)
|
||||
algo.train()
|
||||
algo.stop()
|
||||
del algo
|
||||
shutil.rmtree(marl_checkpoint_path)
|
||||
|
||||
def test_e2e_load_rl_module(self):
|
||||
"""Test if we can train a PPO algorithm with a cpkt RL module e2e."""
|
||||
scaling_config = {
|
||||
"num_learners": 0,
|
||||
"num_gpus_per_learner": 0,
|
||||
}
|
||||
|
||||
config = (
|
||||
PPOConfig()
|
||||
.env_runners(rollout_fragment_length=4)
|
||||
.learners(**scaling_config)
|
||||
.environment("CartPole-v1")
|
||||
.training(num_epochs=1, train_batch_size=8, minibatch_size=8)
|
||||
)
|
||||
env = gym.make("CartPole-v1")
|
||||
# create a multi_rl_module to load and save it to a checkpoint directory
|
||||
module_spec = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.observation_space,
|
||||
action_space=env.action_space,
|
||||
# If we want to use this externally created module in the algorithm,
|
||||
# we need to provide the same config as the algorithm.
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[32]),
|
||||
catalog_class=PPOCatalog,
|
||||
)
|
||||
module = module_spec.build()
|
||||
|
||||
module_ckpt_path = tempfile.mkdtemp()
|
||||
module.save_to_path(module_ckpt_path)
|
||||
|
||||
module_to_load_spec = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.observation_space,
|
||||
action_space=env.action_space,
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[32]),
|
||||
catalog_class=PPOCatalog,
|
||||
load_state_path=module_ckpt_path,
|
||||
)
|
||||
|
||||
config.rl_module(rl_module_spec=module_to_load_spec)
|
||||
|
||||
# create the algorithm with multiple nodes and check if the weights
|
||||
# are the same as the original MultiRLModule
|
||||
algo = config.build()
|
||||
algo_module_weights = algo.learner_group.get_weights()
|
||||
|
||||
check(
|
||||
algo_module_weights[DEFAULT_MODULE_ID],
|
||||
convert_to_numpy(module.get_state()),
|
||||
)
|
||||
algo.train()
|
||||
algo.stop()
|
||||
del algo
|
||||
shutil.rmtree(module_ckpt_path)
|
||||
|
||||
def test_e2e_load_complex_multi_rl_module_with_modules_to_load(self):
|
||||
"""Test if we can train a PPO algorithm with a cpkt MARL and RL module e2e.
|
||||
|
||||
Additionally, check if we can set modules to load so that we can exclude
|
||||
a module from our ckpted MultiRLModule from being loaded.
|
||||
|
||||
"""
|
||||
num_agents = 3
|
||||
config = self.get_ppo_config(num_agents=num_agents)
|
||||
env = MultiAgentCartPole({"num_agents": num_agents})
|
||||
# create a multi_rl_module to load and save it to a checkpoint directory
|
||||
module_specs = {}
|
||||
for i in range(num_agents):
|
||||
module_specs[f"policy_{i}"] = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.get_observation_space(0),
|
||||
action_space=env.get_action_space(0),
|
||||
# Note, we need to pass in the default model config for the
|
||||
# algorithm to be able to use this module later.
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[32 * (i + 1)]),
|
||||
catalog_class=PPOCatalog,
|
||||
)
|
||||
multi_rl_module_spec = MultiRLModuleSpec(rl_module_specs=module_specs)
|
||||
multi_rl_module = multi_rl_module_spec.build()
|
||||
marl_checkpoint_path = tempfile.mkdtemp()
|
||||
multi_rl_module.save_to_path(marl_checkpoint_path)
|
||||
|
||||
# create a RLModule to load and override the "policy_1" module with
|
||||
module_to_swap_in = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.get_observation_space(0),
|
||||
action_space=env.get_action_space(0),
|
||||
# Note, we need to pass in the default model config for the algorithm
|
||||
# to be able to use this module later.
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[64]),
|
||||
catalog_class=PPOCatalog,
|
||||
).build()
|
||||
|
||||
module_to_swap_in_path = tempfile.mkdtemp()
|
||||
module_to_swap_in.save_to_path(module_to_swap_in_path)
|
||||
|
||||
# create a new MARL_spec with the checkpoint from the marl_checkpoint
|
||||
# and the module_to_swap_in_checkpoint
|
||||
module_specs["policy_1"] = RLModuleSpec(
|
||||
module_class=PPOTorchRLModule,
|
||||
observation_space=env.get_observation_space(0),
|
||||
action_space=env.get_action_space(0),
|
||||
model_config=DefaultModelConfig(fcnet_hiddens=[64]),
|
||||
catalog_class=PPOCatalog,
|
||||
load_state_path=module_to_swap_in_path,
|
||||
)
|
||||
multi_rl_module_spec_from_checkpoint = MultiRLModuleSpec(
|
||||
rl_module_specs=module_specs,
|
||||
load_state_path=marl_checkpoint_path,
|
||||
modules_to_load={
|
||||
"policy_0",
|
||||
},
|
||||
)
|
||||
config.rl_module(rl_module_spec=multi_rl_module_spec_from_checkpoint)
|
||||
|
||||
# create the algorithm with multiple nodes and check if the weights
|
||||
# are the same as the original MultiRLModule
|
||||
algo = config.build()
|
||||
algo_module_weights = algo.learner_group.get_weights()
|
||||
|
||||
# weights of "policy_0" should be the same as in the loaded MultiRLModule
|
||||
# since we specified it as being apart of the modules_to_load
|
||||
check(
|
||||
algo_module_weights["policy_0"],
|
||||
convert_to_numpy(multi_rl_module["policy_0"].get_state()),
|
||||
)
|
||||
# weights of "policy_1" should be the same as in the module_to_swap_in since
|
||||
# we specified its load path separately in an rl_module_spec inside of the
|
||||
# multi_rl_module_spec_from_checkpoint
|
||||
check(
|
||||
algo_module_weights["policy_1"],
|
||||
convert_to_numpy(module_to_swap_in.get_state()),
|
||||
)
|
||||
# weights of "policy_2" should be different from the loaded MultiRLModule
|
||||
# since we didn't specify it as being apart of the modules_to_load
|
||||
policy_2_algo_module_weight_sum = np.sum(
|
||||
[
|
||||
np.sum(s)
|
||||
for s in tree.flatten(convert_to_numpy(algo_module_weights["policy_2"]))
|
||||
]
|
||||
)
|
||||
policy_2_multi_rl_module_weight_sum = np.sum(
|
||||
[
|
||||
np.sum(s)
|
||||
for s in tree.flatten(
|
||||
convert_to_numpy(multi_rl_module["policy_2"].get_state())
|
||||
)
|
||||
]
|
||||
)
|
||||
check(
|
||||
policy_2_algo_module_weight_sum,
|
||||
policy_2_multi_rl_module_weight_sum,
|
||||
false=True,
|
||||
)
|
||||
|
||||
algo.train()
|
||||
algo.stop()
|
||||
del algo
|
||||
shutil.rmtree(marl_checkpoint_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,232 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
from ray.rllib.connectors.env_to_module.mean_std_filter import MeanStdFilter
|
||||
from ray.rllib.core import COMPONENT_ENV_TO_MODULE_CONNECTOR
|
||||
from ray.rllib.utils.filter import RunningStat
|
||||
from ray.rllib.utils.test_utils import check
|
||||
|
||||
algorithms_and_configs = {
|
||||
"PPO": (PPOConfig().training(train_batch_size=2, minibatch_size=2))
|
||||
}
|
||||
|
||||
|
||||
@ray.remote
|
||||
def save_train_and_get_states(
|
||||
algo_cfg: AlgorithmConfig, num_env_runners: int, env: str, tmpdir
|
||||
):
|
||||
"""Create an algo, train for 10 iterations, then checkpoint it.
|
||||
|
||||
Note: This function uses a seeded algorithm that can modify the global random state.
|
||||
Running it multiple times in the same process can affect other algorithms.
|
||||
Making it a Ray task runs it in a separate process and prevents it from
|
||||
affecting other algorithms' random state.
|
||||
|
||||
Args:
|
||||
algo_cfg: The algorithm config to build the algo from.
|
||||
num_env_runners: Number of environment runners to use.
|
||||
env: The gym genvironment to train on.
|
||||
tmpdir: The temporary directory to save the checkpoint to.
|
||||
|
||||
Returns:
|
||||
The env-runner states after 10 iterations of training.
|
||||
"""
|
||||
algo_cfg = (
|
||||
algo_cfg.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.environment(env)
|
||||
.env_runners(
|
||||
num_env_runners=num_env_runners,
|
||||
env_to_module_connector=lambda env, spaces, device: MeanStdFilter(),
|
||||
)
|
||||
# setting min_time_s_per_iteration=0 and min_sample_timesteps_per_iteration=1
|
||||
# to make sure that we get results as soon as sampling/training is done at
|
||||
# least once
|
||||
.reporting(min_time_s_per_iteration=0, min_sample_timesteps_per_iteration=1)
|
||||
.debugging(seed=10)
|
||||
)
|
||||
algo = algo_cfg.build()
|
||||
for _ in range(10):
|
||||
algo.train()
|
||||
algo.save_to_path(tmpdir)
|
||||
states = algo.env_runner_group.foreach_env_runner(
|
||||
"get_state",
|
||||
local_env_runner=False,
|
||||
)
|
||||
return states
|
||||
|
||||
|
||||
@ray.remote
|
||||
def load_and_get_states(
|
||||
algo_cfg: AlgorithmConfig, num_env_runners: int, env: str, tmpdir
|
||||
):
|
||||
"""Loads the checkpoint saved by save_train_and_get_states and returns connector states.
|
||||
|
||||
Note: This function uses a seeded algorithm that can modify the global random state.
|
||||
Running it multiple times in the same process can affect other algorithms.
|
||||
Making it a Ray task runs it in a separate process and prevents it from
|
||||
affecting other algorithms' random state.
|
||||
|
||||
Args:
|
||||
algo_cfg: The algorithm config to build the algo from.
|
||||
num_env_runners: Number of env-runners to use.
|
||||
env: The gym genvironment to train on.
|
||||
tmpdir: The temporary directory to save the checkpoint to.
|
||||
|
||||
Returns:
|
||||
The connector states of remote env-runners after 10 iterations of training.
|
||||
|
||||
"""
|
||||
algo_cfg = (
|
||||
algo_cfg.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.environment(env)
|
||||
.env_runners(
|
||||
num_env_runners=num_env_runners,
|
||||
env_to_module_connector=lambda env, spaces, device: MeanStdFilter(),
|
||||
)
|
||||
# setting min_time_s_per_iteration=0 and min_sample_timesteps_per_iteration=1
|
||||
# to make sure that we get results as soon as sampling/training is done at
|
||||
# least once
|
||||
.reporting(min_time_s_per_iteration=0, min_sample_timesteps_per_iteration=1)
|
||||
.debugging(seed=10)
|
||||
)
|
||||
algo = algo_cfg.build()
|
||||
algo.restore_from_path(tmpdir)
|
||||
states = algo.env_runner_group.foreach_env_runner(
|
||||
"get_state",
|
||||
local_env_runner=False,
|
||||
)
|
||||
|
||||
return states
|
||||
|
||||
|
||||
class TestAlgorithmWithConnectorsSaveAndRestore(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
ray.init()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
ray.shutdown()
|
||||
|
||||
def test_save_and_restore_w_remote_env_runners(self):
|
||||
num_env_runners = 2
|
||||
for algo_name in algorithms_and_configs:
|
||||
config = algorithms_and_configs[algo_name]
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# create an algorithm, checkpoint it, then train for 2 iterations
|
||||
connector_states_algo_1 = ray.get(
|
||||
save_train_and_get_states.remote(
|
||||
config, num_env_runners, "CartPole-v1", tmpdir
|
||||
)
|
||||
)
|
||||
# load that checkpoint into a new algorithm and check the states.
|
||||
connector_states_algo_2 = ray.get( # noqa
|
||||
load_and_get_states.remote(
|
||||
config, num_env_runners, "CartPole-v1", tmpdir
|
||||
)
|
||||
)
|
||||
|
||||
# Assert that all running stats are the same.
|
||||
self._assert_running_stats_consistency(
|
||||
connector_states_algo_1, connector_states_algo_2
|
||||
)
|
||||
|
||||
def test_save_and_restore_w_remote_env_runners_and_wo_local_env_runner(self):
|
||||
num_env_runners = 2
|
||||
for algo_name in algorithms_and_configs:
|
||||
config = algorithms_and_configs[algo_name].env_runners(
|
||||
create_local_env_runner=False
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# create an algorithm, checkpoint it, then train for 2 iterations
|
||||
connector_states_algo_1 = ray.get(
|
||||
save_train_and_get_states.remote(
|
||||
config, num_env_runners, "CartPole-v1", tmpdir
|
||||
)
|
||||
)
|
||||
# load that checkpoint into a new algorithm and check the states.
|
||||
connector_states_algo_2 = ray.get( # noqa
|
||||
load_and_get_states.remote(
|
||||
config, num_env_runners, "CartPole-v1", tmpdir
|
||||
)
|
||||
)
|
||||
# Assert that all running stats are the same.
|
||||
self._assert_running_stats_consistency(
|
||||
connector_states_algo_1, connector_states_algo_2
|
||||
)
|
||||
|
||||
def _assert_running_stats_consistency(
|
||||
self, connector_states_algo_1: list, connector_states_algo_2: list
|
||||
):
|
||||
"""
|
||||
Asserts consistency of running stats within and between algorithms.
|
||||
"""
|
||||
|
||||
running_stats_states_algo_1 = [
|
||||
state[COMPONENT_ENV_TO_MODULE_CONNECTOR]["MeanStdFilter"][None][
|
||||
"running_stats"
|
||||
]
|
||||
for state in connector_states_algo_1
|
||||
]
|
||||
running_stats_states_algo_2 = [
|
||||
state[COMPONENT_ENV_TO_MODULE_CONNECTOR]["MeanStdFilter"][None][
|
||||
"running_stats"
|
||||
]
|
||||
for state in connector_states_algo_2
|
||||
]
|
||||
|
||||
running_stats_states_algo_1 = [
|
||||
[RunningStat.from_state(s) for s in running_stats_state]
|
||||
for running_stats_state in running_stats_states_algo_1
|
||||
]
|
||||
running_stats_states_algo_2 = [
|
||||
[RunningStat.from_state(s) for s in running_stats_state]
|
||||
for running_stats_state in running_stats_states_algo_2
|
||||
]
|
||||
|
||||
running_stats_states_algo_1 = [
|
||||
(
|
||||
running_stat[0].n,
|
||||
running_stat[0].mean_array,
|
||||
running_stat[0].sum_sq_diff_array,
|
||||
)
|
||||
for running_stat in running_stats_states_algo_1
|
||||
]
|
||||
running_stats_states_algo_2 = [
|
||||
(
|
||||
running_stat[0].n,
|
||||
running_stat[0].mean_array,
|
||||
running_stat[0].sum_sq_diff_array,
|
||||
)
|
||||
for running_stat in running_stats_states_algo_2
|
||||
]
|
||||
|
||||
# The number of env-runners must be two for the following checks to make sense.
|
||||
self.assertEqual(len(running_stats_states_algo_1), 2)
|
||||
self.assertEqual(len(running_stats_states_algo_2), 2)
|
||||
|
||||
# Assert that all running stats in algo-1 are the same (for consistency).
|
||||
check(running_stats_states_algo_1[0][0], running_stats_states_algo_1[1][0])
|
||||
|
||||
# Now ensure that the connector states on remote `EnvRunner`s were restored.
|
||||
check(running_stats_states_algo_1[0][0], running_stats_states_algo_2[0][0])
|
||||
|
||||
# Ensure also that all states are the same in algo-2 (for consistency).
|
||||
check(running_stats_states_algo_2[0][0], running_stats_states_algo_2[1][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,131 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
from ray.rllib.core import DEFAULT_MODULE_ID
|
||||
from ray.rllib.utils.metrics import LEARNER_RESULTS
|
||||
|
||||
algorithms_and_configs = {
|
||||
"PPO": (PPOConfig().training(train_batch_size=2, minibatch_size=2))
|
||||
}
|
||||
|
||||
|
||||
@ray.remote
|
||||
def save_and_train(algo_cfg: AlgorithmConfig, env: str, tmpdir):
|
||||
"""Create an algo, checkpoint it, then train for 2 iterations.
|
||||
|
||||
Note: This function uses a seeded algorithm that can modify the global random state.
|
||||
Running it multiple times in the same process can affect other algorithms.
|
||||
Making it a Ray task runs it in a separate process and prevents it from
|
||||
affecting other algorithms' random state.
|
||||
|
||||
Args:
|
||||
algo_cfg: The algorithm config to build the algo from.
|
||||
env: The gym genvironment to train on.
|
||||
tmpdir: The temporary directory to save the checkpoint to.
|
||||
|
||||
Returns:
|
||||
The learner stats after 2 iterations of training.
|
||||
"""
|
||||
algo_cfg = (
|
||||
algo_cfg.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.environment(env)
|
||||
.env_runners(num_env_runners=0)
|
||||
# setting min_time_s_per_iteration=0 and min_sample_timesteps_per_iteration=1
|
||||
# to make sure that we get results as soon as sampling/training is done at
|
||||
# least once
|
||||
.reporting(min_time_s_per_iteration=0, min_sample_timesteps_per_iteration=1)
|
||||
.debugging(seed=10)
|
||||
)
|
||||
algo = algo_cfg.build()
|
||||
|
||||
algo.save_to_path(tmpdir)
|
||||
for _ in range(2):
|
||||
results = algo.train()
|
||||
return results[LEARNER_RESULTS][DEFAULT_MODULE_ID]
|
||||
|
||||
|
||||
@ray.remote
|
||||
def load_and_train(algo_cfg: AlgorithmConfig, env: str, tmpdir):
|
||||
"""Loads the checkpoint saved by save_and_train and trains for 2 iterations.
|
||||
|
||||
Note: This function uses a seeded algorithm that can modify the global random state.
|
||||
Running it multiple times in the same process can affect other algorithms.
|
||||
Making it a Ray task runs it in a separate process and prevents it from
|
||||
affecting other algorithms' random state.
|
||||
|
||||
Args:
|
||||
algo_cfg: The algorithm config to build the algo from.
|
||||
env: The gym genvironment to train on.
|
||||
tmpdir: The temporary directory to save the checkpoint to.
|
||||
|
||||
Returns:
|
||||
The learner stats after 2 iterations of training.
|
||||
|
||||
"""
|
||||
algo_cfg = (
|
||||
algo_cfg.api_stack(
|
||||
enable_rl_module_and_learner=True,
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
)
|
||||
.environment(env)
|
||||
.env_runners(num_env_runners=0)
|
||||
# setting min_time_s_per_iteration=0 and min_sample_timesteps_per_iteration=1
|
||||
# to make sure that we get results as soon as sampling/training is done at
|
||||
# least once
|
||||
.reporting(min_time_s_per_iteration=0, min_sample_timesteps_per_iteration=1)
|
||||
.debugging(seed=10)
|
||||
)
|
||||
algo = algo_cfg.build()
|
||||
algo.restore_from_path(tmpdir)
|
||||
for _ in range(2):
|
||||
results = algo.train()
|
||||
return results[LEARNER_RESULTS][DEFAULT_MODULE_ID]
|
||||
|
||||
|
||||
class TestAlgorithmWithLearnerSaveAndRestore(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
ray.init()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
ray.shutdown()
|
||||
|
||||
def test_save_and_restore(self):
|
||||
for algo_name in algorithms_and_configs:
|
||||
config = algorithms_and_configs[algo_name]
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# create an algorithm, checkpoint it, then train for 2 iterations
|
||||
ray.get(save_and_train.remote(config, "CartPole-v1", tmpdir))
|
||||
# load that checkpoint into a new algorithm and train for 2
|
||||
# iterations
|
||||
results_algo_2 = ray.get( # noqa
|
||||
load_and_train.remote(config, "CartPole-v1", tmpdir)
|
||||
)
|
||||
|
||||
# load that checkpoint into another new algorithm and train for 2
|
||||
# iterations
|
||||
results_algo_3 = ray.get( # noqa
|
||||
load_and_train.remote(config, "CartPole-v1", tmpdir)
|
||||
)
|
||||
|
||||
# check that the results are the same across loaded algorithms
|
||||
# they won't be the same as the first algorithm since the random
|
||||
# state that is used for each algorithm is not preserved across
|
||||
# checkpoints.
|
||||
# TODO (sven): Uncomment once seeding works on EnvRunners.
|
||||
# check(results_algo_3, results_algo_2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray import tune
|
||||
from ray.tune.registry import get_trainable_cls
|
||||
from ray.tune.result import TRAINING_ITERATION
|
||||
|
||||
|
||||
@pytest.mark.parametrize("algorithm", ["PPO", "IMPALA"])
|
||||
def test_custom_resource(algorithm):
|
||||
if ray.is_initialized:
|
||||
ray.shutdown()
|
||||
|
||||
ray.init(
|
||||
resources={"custom_resource": 1},
|
||||
include_dashboard=False,
|
||||
)
|
||||
|
||||
config = (
|
||||
get_trainable_cls(algorithm)
|
||||
.get_default_config()
|
||||
.environment("CartPole-v1")
|
||||
.framework("torch")
|
||||
.env_runners(
|
||||
num_env_runners=1,
|
||||
custom_resources_per_env_runner={"custom_resource": 0.01},
|
||||
)
|
||||
.resources(num_gpus=0)
|
||||
)
|
||||
stop = {TRAINING_ITERATION: 1}
|
||||
|
||||
tune.Tuner(
|
||||
algorithm,
|
||||
param_space=config,
|
||||
run_config=tune.RunConfig(stop=stop, verbose=0),
|
||||
tune_config=tune.TuneConfig(num_samples=1),
|
||||
).fit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,117 @@
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
import ray._common
|
||||
from ray.cluster_utils import Cluster
|
||||
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
from ray.rllib.algorithms.utils import _get_learner_bundles
|
||||
|
||||
EXPECTED_PER_NODE_OBJECT_STORE_MEMORY = 10**8
|
||||
HEAD_REDIS_PORT = 6379
|
||||
HEAD_CPUS = 4
|
||||
WORKER_CPUS = 4
|
||||
PINNED_RESOURCE = "learner_pool"
|
||||
DECOY_RESOURCE = "decoy_pool"
|
||||
|
||||
|
||||
def test_round_trip():
|
||||
"""Test that custom_resources_per_learner is set correctly."""
|
||||
cfg = AlgorithmConfig().learners(
|
||||
custom_resources_per_learner={"my_label": 0.001, "other": 1}
|
||||
)
|
||||
assert cfg.custom_resources_per_learner == {"my_label": 0.001, "other": 1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reserved", ["CPU", "GPU"])
|
||||
def test_reserved_keys_rejected(reserved):
|
||||
"""`CPU`/`GPU` belong to `num_*_per_learner`, not custom resources."""
|
||||
with pytest.raises(ValueError, match="CPU.*GPU"):
|
||||
AlgorithmConfig().learners(custom_resources_per_learner={reserved: 1})
|
||||
|
||||
|
||||
def test_placement_group_bundles_include_custom_resources():
|
||||
"""The PG bundles built for Tune must reserve the custom learner resources;
|
||||
otherwise learners scheduled within the PG can never satisfy their request.
|
||||
"""
|
||||
cfg = AlgorithmConfig().learners(
|
||||
num_learners=2,
|
||||
num_cpus_per_learner=1,
|
||||
custom_resources_per_learner={"learner_pool": 0.5},
|
||||
)
|
||||
bundles = _get_learner_bundles(cfg)
|
||||
assert len(bundles) == 2
|
||||
assert all(b.get("learner_pool") == 0.5 for b in bundles)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cluster():
|
||||
"""3-node fake cluster:
|
||||
|
||||
- head: HEAD_CPUS CPUs, no custom resources
|
||||
- pinned: WORKER_CPUS CPUs, {PINNED_RESOURCE: 4}
|
||||
- decoy: WORKER_CPUS CPUs, {DECOY_RESOURCE: 4} (must not host learners
|
||||
when requesting PINNED_RESOURCE)
|
||||
"""
|
||||
assert (
|
||||
3 * EXPECTED_PER_NODE_OBJECT_STORE_MEMORY
|
||||
< ray._common.utils.get_system_memory() / 2
|
||||
), "Not enough memory on this machine to run this workload."
|
||||
|
||||
cluster = Cluster()
|
||||
head = cluster.add_node(
|
||||
redis_port=HEAD_REDIS_PORT,
|
||||
num_cpus=HEAD_CPUS,
|
||||
object_store_memory=EXPECTED_PER_NODE_OBJECT_STORE_MEMORY,
|
||||
include_dashboard=True,
|
||||
)
|
||||
pinned = cluster.add_node(
|
||||
num_cpus=WORKER_CPUS,
|
||||
resources={PINNED_RESOURCE: 4},
|
||||
object_store_memory=EXPECTED_PER_NODE_OBJECT_STORE_MEMORY,
|
||||
include_dashboard=False,
|
||||
)
|
||||
decoy = cluster.add_node(
|
||||
num_cpus=WORKER_CPUS,
|
||||
resources={DECOY_RESOURCE: 4},
|
||||
object_store_memory=EXPECTED_PER_NODE_OBJECT_STORE_MEMORY,
|
||||
include_dashboard=False,
|
||||
)
|
||||
cluster.wait_for_nodes()
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
yield {"cluster": cluster, "head": head, "pinned": pinned, "decoy": decoy}
|
||||
|
||||
ray.shutdown()
|
||||
cluster.shutdown()
|
||||
|
||||
|
||||
def test_algorithm_pins_learners_to_node_with_custom_resource(cluster):
|
||||
"""End-to-end: a built Algorithm places its learners on the node exposing
|
||||
PINNED_RESOURCE, even though head+decoy together have 8 free CPUs that
|
||||
would otherwise absorb them.
|
||||
"""
|
||||
config = (
|
||||
PPOConfig()
|
||||
.environment("CartPole-v1")
|
||||
.env_runners(num_env_runners=0)
|
||||
.learners(
|
||||
num_learners=2,
|
||||
num_cpus_per_learner=1,
|
||||
custom_resources_per_learner={PINNED_RESOURCE: 1},
|
||||
)
|
||||
)
|
||||
algo = config.build()
|
||||
|
||||
refs = algo.learner_group.foreach_learner(
|
||||
lambda _: ray.get_runtime_context().get_node_id()
|
||||
)
|
||||
node_ids = [r.get() for r in refs]
|
||||
assert len(node_ids) == 2
|
||||
assert all(nid == cluster["pinned"].node_id for nid in node_ids)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Do not import tf for testing purposes.
|
||||
os.environ["RLLIB_TEST_NO_TF_IMPORT"] = "1"
|
||||
|
||||
# Test registering (includes importing) all Algorithms.
|
||||
from ray.rllib import _register_all
|
||||
|
||||
# This should surface any dependency on tf, e.g. inside function
|
||||
# signatures/typehints.
|
||||
_register_all()
|
||||
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
|
||||
assert (
|
||||
"tensorflow" not in sys.modules
|
||||
), "`tensorflow` initially present, when it shouldn't!"
|
||||
|
||||
config = (
|
||||
PPOConfig()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=True,
|
||||
enable_rl_module_and_learner=True,
|
||||
)
|
||||
.environment("CartPole-v1")
|
||||
.framework("torch")
|
||||
.env_runners(num_env_runners=0)
|
||||
)
|
||||
# Note: No ray.init(), to test it works without Ray
|
||||
algo = config.build()
|
||||
algo.train()
|
||||
|
||||
assert (
|
||||
"tensorflow" not in sys.modules
|
||||
), "`tensorflow` should not be imported after creating and training A3C!"
|
||||
|
||||
# Clean up.
|
||||
del os.environ["RLLIB_TEST_NO_TF_IMPORT"]
|
||||
|
||||
algo.stop()
|
||||
|
||||
print("ok")
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Do not import torch for testing purposes.
|
||||
os.environ["RLLIB_TEST_NO_TORCH_IMPORT"] = "1"
|
||||
|
||||
# Test registering (includes importing) all Algorithms.
|
||||
from ray.rllib import _register_all
|
||||
|
||||
# This should surface any dependency on torch, e.g. inside function
|
||||
# signatures/typehints.
|
||||
_register_all()
|
||||
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
|
||||
assert "torch" not in sys.modules, "`torch` initially present, when it shouldn't!"
|
||||
|
||||
# Note: No ray.init(), to test it works without Ray
|
||||
config = (
|
||||
PPOConfig()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
enable_rl_module_and_learner=False,
|
||||
)
|
||||
.environment("CartPole-v1")
|
||||
.framework("tf")
|
||||
.env_runners(num_env_runners=0)
|
||||
)
|
||||
|
||||
# Disable auto-added TBX logger callback to avoid importing torch
|
||||
# via the tensorboardX.SummaryWriter class.
|
||||
os.environ["TUNE_DISABLE_AUTO_CALLBACK_LOGGERS"] = "1"
|
||||
algo = config.build()
|
||||
algo.train()
|
||||
|
||||
assert (
|
||||
"torch" not in sys.modules
|
||||
), "`torch` should not be imported after creating and training A3C!"
|
||||
|
||||
# Clean up.
|
||||
del os.environ["RLLIB_TEST_NO_TORCH_IMPORT"]
|
||||
del os.environ["TUNE_DISABLE_AUTO_CALLBACK_LOGGERS"]
|
||||
|
||||
algo.stop()
|
||||
|
||||
print("ok")
|
||||
@@ -0,0 +1,63 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ray.rllib.env.env_runner_state_server import EnvRunnerStateServer
|
||||
from ray.rllib.utils.metrics import WEIGHTS_SEQ_NO
|
||||
|
||||
|
||||
def _state(seq_no, weights="WEIGHTS_OBJ_REF"):
|
||||
# The server stores whatever StateDict it is handed. In production the `rl_module`
|
||||
# value is a `ray.ObjectRef`, but the server never dereferences it, so a plain
|
||||
# sentinel is sufficient to exercise the store/serve logic here.
|
||||
return {WEIGHTS_SEQ_NO: seq_no, "rl_module": weights}
|
||||
|
||||
|
||||
def test_push_pull_roundtrip_preserves_state_verbatim():
|
||||
server = EnvRunnerStateServer()
|
||||
state = _state(3)
|
||||
server.push(state)
|
||||
# Returned verbatim (including the - here sentinel - weights "ObjectRef"); the
|
||||
# server must NOT dereference or copy it.
|
||||
assert server.pull() is state
|
||||
assert server.pull()["rl_module"] == "WEIGHTS_OBJ_REF"
|
||||
assert server.get_version() == 3
|
||||
|
||||
|
||||
def test_push_replaces_and_advances_version():
|
||||
server = EnvRunnerStateServer()
|
||||
server.push(_state(1))
|
||||
server.push(_state(2))
|
||||
assert server.get_version() == 2
|
||||
assert server.pull()[WEIGHTS_SEQ_NO] == 2
|
||||
|
||||
|
||||
def test_push_rejects_state_without_seq_no():
|
||||
# A state without WEIGHTS_SEQ_NO could never be pulled (EnvRunners version-gate via
|
||||
# `pull_if_newer`), so the server rejects it loudly instead of holding state that no
|
||||
# EnvRunner would ever apply.
|
||||
server = EnvRunnerStateServer()
|
||||
with pytest.raises(ValueError, match="WEIGHTS_SEQ_NO"):
|
||||
server.push({"rl_module": "WEIGHTS_OBJ_REF"})
|
||||
# Nothing was stored.
|
||||
assert server.pull() is None
|
||||
assert server.get_version() == -1
|
||||
|
||||
|
||||
def test_pull_if_newer_only_returns_strictly_newer_state():
|
||||
server = EnvRunnerStateServer()
|
||||
# Empty server -> nothing to return, regardless of the caller's version.
|
||||
assert server.pull_if_newer(-1) is None
|
||||
|
||||
state = _state(5)
|
||||
server.push(state)
|
||||
# Equal-or-newer caller version -> None, so the (heavy) state is NOT transferred.
|
||||
assert server.pull_if_newer(5) is None
|
||||
assert server.pull_if_newer(6) is None
|
||||
# Older caller version -> the full state, returned verbatim (same object).
|
||||
assert server.pull_if_newer(4) is state
|
||||
assert server.pull_if_newer(-1) is state
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Tests for evaluating when all configured remote eval EnvRunners are
|
||||
unhealthy.
|
||||
|
||||
The behavior is controlled by two orthogonal config knobs:
|
||||
|
||||
- ``evaluation_unhealthy_workers_timeout_s``: how long to wait for at
|
||||
least one eval EnvRunner to recover before deciding what to do
|
||||
(default 0: don't wait).
|
||||
- ``evaluation_error_after_n_consecutive_skips``: tolerate this many
|
||||
consecutive evaluation iterations in which all configured remote eval
|
||||
EnvRunners are unhealthy. On the next such iteration, ``evaluate()``
|
||||
raises ``RuntimeError``. ``None`` (default) tolerates an unbounded
|
||||
number of consecutive skips.
|
||||
|
||||
Both apply identically regardless of ``evaluation_parallel_to_training``.
|
||||
"""
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
|
||||
|
||||
@pytest.fixture(params=[True, False], ids=["parallel", "sequential"])
|
||||
def parallel_to_training(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def _algo_with_unhealthy_eval_workers(
|
||||
*,
|
||||
timeout_s=0,
|
||||
error_after_n_consecutive_skips=None,
|
||||
parallel_to_training=True,
|
||||
spawn_replacement=False,
|
||||
enable_v2_stack=True,
|
||||
):
|
||||
"""Build a PPO algo and put every remote eval worker into the failed
|
||||
state.
|
||||
|
||||
Always ``ray.kill(no_restart=True)``-s the original actors and sets
|
||||
``restart_failed_env_runners=False`` so Ray Core does not auto-resurrect
|
||||
them. This keeps each test deterministic: the only way a worker reappears
|
||||
is for the test to explicitly add one back (see ``spawn_replacement``).
|
||||
|
||||
Args:
|
||||
spawn_replacement: If True, additionally spawn a fresh remote eval
|
||||
EnvRunner via ``add_workers(1)`` after the kill, then flip its
|
||||
health flag to False as well. The replacement is what a
|
||||
subsequent ``probe_unhealthy_env_runners`` will be able to
|
||||
ping successfully -- simulating "a new worker has come back"
|
||||
without relying on Ray Core's restart timing.
|
||||
enable_v2_stack: If True (default), use the new API stack (v2
|
||||
connectors + Learner). If False, use the old API stack so
|
||||
the recovery path's ``_sync_filters_if_needed`` branch is
|
||||
exercised instead of ``sync_env_runner_states``.
|
||||
|
||||
Config is the smallest that exercises the failure path: no remote
|
||||
training EnvRunners, 1 remote eval EnvRunner, fixed-duration eval so
|
||||
we can call ``evaluate()`` directly without a parallel-training
|
||||
future.
|
||||
"""
|
||||
config = PPOConfig()
|
||||
if not enable_v2_stack:
|
||||
config = config.api_stack(
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
enable_rl_module_and_learner=False,
|
||||
)
|
||||
config = (
|
||||
config.environment("CartPole-v1")
|
||||
# Local-only training: skips remote train EnvRunner setup.
|
||||
.env_runners(num_env_runners=0).evaluation(
|
||||
# 1 eval worker is enough; killing it leaves 0 healthy, which
|
||||
# is the condition we're testing.
|
||||
evaluation_num_env_runners=1,
|
||||
evaluation_interval=1,
|
||||
evaluation_parallel_to_training=parallel_to_training,
|
||||
# Fixed duration so `evaluate()` doesn't need a parallel-train
|
||||
# future (avoids the `auto` branch's `assert future is not None`).
|
||||
evaluation_duration=1,
|
||||
evaluation_duration_unit="episodes",
|
||||
evaluation_unhealthy_workers_timeout_s=timeout_s,
|
||||
evaluation_error_after_n_consecutive_skips=(
|
||||
error_after_n_consecutive_skips
|
||||
),
|
||||
)
|
||||
# Disable auto-restart so the tests are fully in control of when
|
||||
# (and whether) a worker comes back.
|
||||
.fault_tolerance(restart_failed_env_runners=False)
|
||||
)
|
||||
algo = config.build()
|
||||
|
||||
eval_grp = algo.eval_env_runner_group
|
||||
# Hard-kill the originals; no Ray Core restart.
|
||||
for a in list(eval_grp._worker_manager._actors.values()):
|
||||
ray.kill(a, no_restart=True)
|
||||
|
||||
if spawn_replacement:
|
||||
# The dead originals are evicted from the worker manager *before*
|
||||
# spawning the replacement so that `add_workers` assigns the
|
||||
# fresh worker the lowest free `worker_index` (1). Eval's
|
||||
# `_evaluate_with_fixed_duration` builds a per-worker num-units
|
||||
# list of length `evaluation_num_env_runners + 1` indexed by
|
||||
# `worker.worker_index`; leaving the dead originals in the
|
||||
# manager would push the new index past the end of that list.
|
||||
for actor_id in list(eval_grp._worker_manager.actor_ids()):
|
||||
eval_grp._worker_manager.remove_actor(actor_id)
|
||||
before = set(eval_grp._worker_manager.actor_ids())
|
||||
eval_grp.add_workers(1, validate=False)
|
||||
new_ids = set(eval_grp._worker_manager.actor_ids()) - before
|
||||
# `add_workers` registers the replacement with `is_healthy=True`;
|
||||
# flip it so the wait loop is forced to probe. The probe's
|
||||
# `ping()` then succeeds against this alive actor and flips it
|
||||
# back to healthy -- mirroring the production "Ray Core restarted
|
||||
# the actor" recovery sequence.
|
||||
for actor_id in new_ids:
|
||||
eval_grp._worker_manager.set_actor_state(actor_id, healthy=False)
|
||||
else:
|
||||
# No replacement: just mark the (dead) originals unhealthy.
|
||||
for actor_id in list(eval_grp._worker_manager.actor_ids()):
|
||||
eval_grp._worker_manager.set_actor_state(actor_id, healthy=False)
|
||||
|
||||
assert eval_grp.num_healthy_remote_workers() == 0
|
||||
return algo
|
||||
|
||||
|
||||
def test_default_skips_eval_silently(parallel_to_training):
|
||||
"""Default (threshold=None): evaluate() must return cleanly even if
|
||||
every eval iteration finds 0 healthy workers, indefinitely."""
|
||||
algo = _algo_with_unhealthy_eval_workers(parallel_to_training=parallel_to_training)
|
||||
for _ in range(3):
|
||||
algo.evaluate() # must not raise on any of these
|
||||
|
||||
|
||||
def test_threshold_one_raises_on_first_skip(parallel_to_training):
|
||||
"""threshold=1: first failed iteration raises (strictest setting)."""
|
||||
algo = _algo_with_unhealthy_eval_workers(
|
||||
error_after_n_consecutive_skips=1,
|
||||
parallel_to_training=parallel_to_training,
|
||||
)
|
||||
with pytest.raises(
|
||||
RuntimeError, match="evaluation_error_after_n_consecutive_skips"
|
||||
):
|
||||
algo.evaluate()
|
||||
|
||||
|
||||
def test_threshold_n_tolerates_n_minus_1_skips(parallel_to_training):
|
||||
"""threshold=N: tolerate N-1 consecutive skips, raise on the N-th."""
|
||||
threshold = 3
|
||||
algo = _algo_with_unhealthy_eval_workers(
|
||||
error_after_n_consecutive_skips=threshold,
|
||||
parallel_to_training=parallel_to_training,
|
||||
)
|
||||
# First (threshold - 1) iterations skip silently.
|
||||
for _ in range(threshold - 1):
|
||||
algo.evaluate()
|
||||
# The threshold-th iteration trips the check and raises.
|
||||
with pytest.raises(
|
||||
RuntimeError, match="evaluation_error_after_n_consecutive_skips"
|
||||
):
|
||||
algo.evaluate()
|
||||
|
||||
|
||||
def test_timeout_waits_then_skips_when_no_recovery(parallel_to_training):
|
||||
"""timeout_s>0 with workers that never come back: evaluate() should
|
||||
take at least roughly that long (waiting for recovery), then skip
|
||||
silently because the threshold defaults to None."""
|
||||
timeout_s = 2
|
||||
algo = _algo_with_unhealthy_eval_workers(
|
||||
timeout_s=timeout_s, parallel_to_training=parallel_to_training
|
||||
)
|
||||
start = time.monotonic()
|
||||
algo.evaluate()
|
||||
elapsed = time.monotonic() - start
|
||||
# Check against lower bound but also a loose upper bound to sanity check
|
||||
assert 5 > elapsed >= timeout_s
|
||||
|
||||
|
||||
def test_timeout_recovers_resyncs_and_evaluates(parallel_to_training):
|
||||
"""When a fresh replacement eval worker appears during the wait
|
||||
window, ``evaluate()`` must re-sync weights *and* connector state to
|
||||
it and then run eval normally (no skip, no raise, counter stays at 0).
|
||||
|
||||
The corresponding syncs at the top of ``evaluate()`` run before the
|
||||
wait and skip workers that are unhealthy at that moment. Without the
|
||||
post-recovery re-syncs inside ``_maybe_wait_for_eval_env_runner_recovery``,
|
||||
the recovered worker would run eval with stale/initial weights and
|
||||
empty/stale connector state (e.g. no obs-normalization filter), each
|
||||
silently producing wrong metrics for one iteration.
|
||||
"""
|
||||
timeout_s = 30 # generous; the first probe will revive quickly.
|
||||
algo = _algo_with_unhealthy_eval_workers(
|
||||
timeout_s=timeout_s,
|
||||
parallel_to_training=parallel_to_training,
|
||||
spawn_replacement=True,
|
||||
)
|
||||
eval_grp = algo.eval_env_runner_group
|
||||
|
||||
# Spy on `sync_weights` AND `sync_env_runner_states` (v2 stack) so we
|
||||
# can verify both re-syncs after recovery. Each is expected to be
|
||||
# called >=2 times in one `evaluate()`:
|
||||
# 1) the unconditional sync at the start of `evaluate()` (effectively
|
||||
# a no-op here because no eval worker is healthy at that moment),
|
||||
# 2) the post-recovery re-sync inside the wait method we target here.
|
||||
with patch.object(
|
||||
eval_grp, "sync_weights", wraps=eval_grp.sync_weights
|
||||
) as weights_spy, patch.object(
|
||||
eval_grp,
|
||||
"sync_env_runner_states",
|
||||
wraps=eval_grp.sync_env_runner_states,
|
||||
) as states_spy:
|
||||
start = time.monotonic()
|
||||
algo.evaluate()
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
# Recovery should be near-instant
|
||||
assert elapsed < timeout_s
|
||||
assert algo._counters["num_consecutive_eval_no_workers_iterations"] == 0
|
||||
assert weights_spy.call_count == 2
|
||||
assert states_spy.call_count == 2
|
||||
|
||||
|
||||
def test_timeout_recovers_resyncs_and_evaluates_old_stack(parallel_to_training):
|
||||
"""Same as ``test_timeout_recovers_resyncs_and_evaluates`` but for the *old* API stack"""
|
||||
timeout_s = 30
|
||||
algo = _algo_with_unhealthy_eval_workers(
|
||||
timeout_s=timeout_s,
|
||||
parallel_to_training=parallel_to_training,
|
||||
spawn_replacement=True,
|
||||
enable_v2_stack=False,
|
||||
)
|
||||
eval_grp = algo.eval_env_runner_group
|
||||
|
||||
# Spy on `sync_weights` (on the eval group) AND
|
||||
# `_sync_filters_if_needed` (on the algo itself; that's where the
|
||||
# method lives in the old API stack).
|
||||
with patch.object(
|
||||
eval_grp, "sync_weights", wraps=eval_grp.sync_weights
|
||||
) as weights_spy, patch.object(
|
||||
algo,
|
||||
"_sync_filters_if_needed",
|
||||
wraps=algo._sync_filters_if_needed,
|
||||
) as filters_spy:
|
||||
start = time.monotonic()
|
||||
algo.evaluate()
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
assert elapsed < timeout_s
|
||||
assert algo._counters["num_consecutive_eval_no_workers_iterations"] == 0
|
||||
assert weights_spy.call_count == 2
|
||||
assert filters_spy.call_count == 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
|
||||
from ray.rllib.algorithms.algorithm import Algorithm
|
||||
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
|
||||
from ray.rllib.algorithms.utils import _get_main_process_bundle
|
||||
|
||||
|
||||
def test_get_main_process_bundle():
|
||||
# num_learners=0, so main process gets num_cpus_per_learner.
|
||||
config = AlgorithmConfig().learners(
|
||||
num_learners=0, num_cpus_per_learner=4, num_gpus_per_learner=1
|
||||
)
|
||||
bundle = _get_main_process_bundle(config)
|
||||
assert bundle["CPU"] == 4
|
||||
assert bundle["GPU"] == 1
|
||||
|
||||
# custom_resources_for_main_process included in bundle (num_learners>0).
|
||||
config = (
|
||||
AlgorithmConfig()
|
||||
.resources(custom_resources_for_main_process={"my_resource": 1})
|
||||
.learners(num_learners=1)
|
||||
)
|
||||
bundle = _get_main_process_bundle(config)
|
||||
assert bundle == {"CPU": 1, "GPU": 0, "my_resource": 1}
|
||||
|
||||
|
||||
def test_default_resource_request_old_stack_custom_resources():
|
||||
"""custom_resources_for_main_process must appear in the placement group
|
||||
even when using the old API stack (enable_rl_module_and_learner=False)."""
|
||||
config = (
|
||||
AlgorithmConfig()
|
||||
.api_stack(
|
||||
enable_rl_module_and_learner=False,
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
)
|
||||
.resources(custom_resources_for_main_process={"my_resource": 2})
|
||||
.env_runners(num_env_runners=0)
|
||||
)
|
||||
pg_factory = Algorithm.default_resource_request(config)
|
||||
main_bundle = pg_factory.bundles[0]
|
||||
assert main_bundle["my_resource"] == 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import ray.rllib.algorithms.ppo as ppo
|
||||
|
||||
|
||||
def test_dont_import_torch_error():
|
||||
"""Check error being thrown, if torch not installed but configured."""
|
||||
# Do not import tf for testing purposes.
|
||||
os.environ["RLLIB_TEST_NO_TORCH_IMPORT"] = "1"
|
||||
config = (
|
||||
ppo.PPOConfig()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
enable_rl_module_and_learner=False,
|
||||
)
|
||||
.environment("CartPole-v1")
|
||||
.framework("torch")
|
||||
)
|
||||
with pytest.raises(ImportError, match="However, no installation was found"):
|
||||
config.build()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_dont_import_torch_error()
|
||||
@@ -0,0 +1,230 @@
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
import ray._common
|
||||
from ray._private.test_utils import get_other_nodes
|
||||
from ray.cluster_utils import Cluster
|
||||
from ray.rllib.algorithms.appo import APPOConfig
|
||||
from ray.rllib.algorithms.ppo import PPOConfig
|
||||
|
||||
EXPECTED_PER_NODE_OBJECT_STORE_MEMORY = 10**8
|
||||
HEAD_REDIS_PORT = 6379
|
||||
HEAD_CPUS = 2
|
||||
WORKER_CPUS = 4
|
||||
NUM_ENV_RUNNERS = 4
|
||||
|
||||
|
||||
def _add_node(cluster, worker=True):
|
||||
cluster.add_node(
|
||||
redis_port=None if worker else HEAD_REDIS_PORT,
|
||||
num_cpus=WORKER_CPUS if worker else HEAD_CPUS,
|
||||
object_store_memory=EXPECTED_PER_NODE_OBJECT_STORE_MEMORY,
|
||||
include_dashboard=not worker,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cluster():
|
||||
"""Create a 2-node fake cluster: head (2 CPUs) + worker (4 CPUs).
|
||||
|
||||
Head holds the algo process + local env runner.
|
||||
Worker holds all 4 remote env runners (deterministic placement).
|
||||
"""
|
||||
assert (
|
||||
2 * EXPECTED_PER_NODE_OBJECT_STORE_MEMORY
|
||||
< ray._common.utils.get_system_memory() / 2
|
||||
), "Not enough memory on this machine to run this workload."
|
||||
|
||||
cluster = Cluster()
|
||||
_add_node(cluster, worker=False)
|
||||
_add_node(cluster)
|
||||
cluster.wait_for_nodes()
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
yield cluster
|
||||
|
||||
# Detach head_node before cluster.shutdown() and kill its processes
|
||||
# directly afterwards. Otherwise cluster.remove_node(head) refuses if a
|
||||
# daemon thread (e.g. APPO/IMPALA's learner thread) called an
|
||||
# auto-init-wrapped Ray API and re-established global_worker.node after
|
||||
# our ray.shutdown(), leaving port 8265 bound for the next test.
|
||||
ray.shutdown()
|
||||
head = cluster.head_node
|
||||
cluster.head_node = None
|
||||
cluster.shutdown()
|
||||
head.kill_all_processes(check_alive=False, allow_graceful=False, wait=True)
|
||||
|
||||
|
||||
def _kill_worker_node(cluster):
|
||||
others = get_other_nodes(cluster, exclude_head=True)
|
||||
if others:
|
||||
cluster.remove_node(others[0])
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _train(cluster, algo, config, iters, preempt_freq):
|
||||
"""Train loop with periodic node kill/restore and health tracking."""
|
||||
num_runners = config.num_env_runners
|
||||
saw_healthy_drop = False
|
||||
saw_recovery = False
|
||||
|
||||
for i in range(iters):
|
||||
algo.train()
|
||||
|
||||
assert algo.env_runner_group.num_remote_env_runners() == num_runners
|
||||
healthy = algo.env_runner_group.num_healthy_remote_workers()
|
||||
assert 0 <= healthy <= num_runners
|
||||
|
||||
if healthy < num_runners:
|
||||
saw_healthy_drop = True
|
||||
if saw_healthy_drop and healthy == num_runners:
|
||||
saw_recovery = True
|
||||
|
||||
print(
|
||||
f"ITER={i}, healthy={healthy}/{num_runners}, "
|
||||
f"saw_drop={saw_healthy_drop}, saw_recovery={saw_recovery}"
|
||||
)
|
||||
|
||||
# Shut down one node every preempt_freq iterations.
|
||||
if i % preempt_freq == 0:
|
||||
_kill_worker_node(cluster)
|
||||
|
||||
# Bring back a previously failed node.
|
||||
elif (i - 1) % preempt_freq == 0:
|
||||
_add_node(cluster)
|
||||
|
||||
# Workers must have gone down at some point.
|
||||
assert saw_healthy_drop, (
|
||||
"Expected healthy worker count to drop after node kill, " "but it never did."
|
||||
)
|
||||
# If restart is enabled, workers must have come back.
|
||||
if config.restart_failed_env_runners:
|
||||
assert saw_recovery, (
|
||||
"Expected workers to recover after node restore "
|
||||
"(restart_failed_env_runners=True), but they never did."
|
||||
)
|
||||
# If restart is disabled, workers must NOT have come back.
|
||||
if not config.restart_failed_env_runners:
|
||||
assert (
|
||||
not saw_recovery
|
||||
), "Workers recovered despite restart_failed_env_runners=False."
|
||||
|
||||
|
||||
def test_node_failure_ignore(cluster):
|
||||
"""restart=False, ignore=True: workers die and stay dead, training
|
||||
continues with fewer workers."""
|
||||
config = (
|
||||
PPOConfig()
|
||||
.environment("CartPole-v1")
|
||||
.env_runners(
|
||||
num_env_runners=NUM_ENV_RUNNERS,
|
||||
sample_timeout_s=5.0,
|
||||
)
|
||||
.training(
|
||||
train_batch_size=500,
|
||||
num_epochs=1,
|
||||
minibatch_size=500,
|
||||
)
|
||||
.reporting(min_train_timesteps_per_iteration=1)
|
||||
.fault_tolerance(
|
||||
ignore_env_runner_failures=True,
|
||||
restart_failed_env_runners=False,
|
||||
env_runner_health_probe_timeout_s=20.0,
|
||||
)
|
||||
)
|
||||
|
||||
algo = config.build()
|
||||
_train(cluster, algo, config, iters=10, preempt_freq=3)
|
||||
|
||||
|
||||
def test_node_failure_recreate_appo(cluster):
|
||||
"""restart=True with APPO (async): workers die, get auto-restarted by Ray,
|
||||
and restore_env_runners() syncs their state."""
|
||||
config = (
|
||||
APPOConfig()
|
||||
.environment("CartPole-v1")
|
||||
.learners(num_learners=0)
|
||||
.experimental(_validate_config=False)
|
||||
.env_runners(
|
||||
num_env_runners=NUM_ENV_RUNNERS,
|
||||
)
|
||||
.reporting(
|
||||
# Must be >= 2s so APPO's async mechanism has time to detect
|
||||
# worker death within a single iteration.
|
||||
min_time_s_per_iteration=2,
|
||||
min_train_timesteps_per_iteration=1,
|
||||
)
|
||||
.fault_tolerance(
|
||||
restart_failed_env_runners=True,
|
||||
env_runner_health_probe_timeout_s=20.0,
|
||||
)
|
||||
)
|
||||
|
||||
algo = config.build()
|
||||
_train(cluster, algo, config, iters=10, preempt_freq=7)
|
||||
|
||||
|
||||
def test_node_failure_recreate_ppo(cluster):
|
||||
"""restart=True with PPO (sync): workers die, get auto-restarted by Ray,
|
||||
and restore_env_runners() syncs their state."""
|
||||
config = (
|
||||
PPOConfig()
|
||||
.environment("CartPole-v1")
|
||||
.learners(num_learners=0)
|
||||
.env_runners(
|
||||
num_env_runners=NUM_ENV_RUNNERS,
|
||||
sample_timeout_s=5.0,
|
||||
)
|
||||
.training(
|
||||
train_batch_size=500,
|
||||
num_epochs=1,
|
||||
minibatch_size=500,
|
||||
)
|
||||
.reporting(
|
||||
min_time_s_per_iteration=2,
|
||||
min_train_timesteps_per_iteration=1,
|
||||
)
|
||||
.fault_tolerance(
|
||||
restart_failed_env_runners=True,
|
||||
env_runner_health_probe_timeout_s=20.0,
|
||||
)
|
||||
)
|
||||
|
||||
algo = config.build()
|
||||
_train(cluster, algo, config, iters=10, preempt_freq=7)
|
||||
|
||||
|
||||
def test_node_failure_no_recovery(cluster):
|
||||
"""restart=False, ignore=False: dead worker RayErrors propagate and
|
||||
crash training. Verify the crash happens."""
|
||||
config = (
|
||||
PPOConfig()
|
||||
.environment("CartPole-v1")
|
||||
.env_runners(
|
||||
num_env_runners=NUM_ENV_RUNNERS,
|
||||
sample_timeout_s=5.0,
|
||||
)
|
||||
.training(
|
||||
train_batch_size=500,
|
||||
num_epochs=1,
|
||||
minibatch_size=500,
|
||||
)
|
||||
.reporting(min_train_timesteps_per_iteration=1)
|
||||
.fault_tolerance(
|
||||
restart_failed_env_runners=False,
|
||||
env_runner_health_probe_timeout_s=20.0,
|
||||
)
|
||||
)
|
||||
|
||||
algo = config.build()
|
||||
# _train will crash with an ActorDiedError when dead workers are detected
|
||||
# (ignore=False, restart=False → errors propagate).
|
||||
with pytest.raises(ray.exceptions.ActorDiedError, match="actor died unexpectedly"):
|
||||
_train(cluster, algo, config, iters=10, preempt_freq=3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,133 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import ray
|
||||
from ray import tune
|
||||
from ray.rllib.algorithms.ppo import PPO, PPOConfig
|
||||
from ray.tune import Callback
|
||||
from ray.tune.execution.placement_groups import PlacementGroupFactory
|
||||
from ray.tune.experiment import Trial
|
||||
from ray.tune.result import TRAINING_ITERATION
|
||||
|
||||
trial_executor = None
|
||||
|
||||
|
||||
class _TestCallback(Callback):
|
||||
def on_step_end(self, iteration, trials, **info):
|
||||
num_running = len([t for t in trials if t.status == Trial.RUNNING])
|
||||
|
||||
# All 3 trials (3 different learning rates) should be scheduled.
|
||||
assert 3 == min(3, len(trials))
|
||||
# Cannot run more than 2 at a time
|
||||
# (due to different resource restrictions in the test cases).
|
||||
assert num_running <= 2
|
||||
|
||||
|
||||
class TestPlacementGroups(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
os.environ["TUNE_PLACEMENT_GROUP_RECON_INTERVAL"] = "0"
|
||||
ray.init(num_cpus=6)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
ray.shutdown()
|
||||
|
||||
def test_overriding_default_resource_request(self):
|
||||
# 3 Trials: Can only run 2 at a time (num_cpus=6; needed: 3).
|
||||
config = (
|
||||
PPOConfig()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
enable_rl_module_and_learner=False,
|
||||
)
|
||||
.training(
|
||||
model={"fcnet_hiddens": [10]}, lr=tune.grid_search([0.1, 0.01, 0.001])
|
||||
)
|
||||
.environment("CartPole-v1")
|
||||
.env_runners(num_env_runners=2)
|
||||
.framework("tf")
|
||||
)
|
||||
|
||||
# Create an Algorithm with an overridden default_resource_request
|
||||
# method that returns a PlacementGroupFactory.
|
||||
|
||||
class MyAlgo(PPO):
|
||||
@classmethod
|
||||
def default_resource_request(cls, config):
|
||||
head_bundle = {"CPU": 1, "GPU": 0}
|
||||
child_bundle = {"CPU": 1}
|
||||
return PlacementGroupFactory(
|
||||
[head_bundle, child_bundle, child_bundle],
|
||||
strategy=config["placement_strategy"],
|
||||
)
|
||||
|
||||
tune.register_trainable("my_trainable", MyAlgo)
|
||||
|
||||
tune.Tuner(
|
||||
"my_trainable",
|
||||
param_space=config,
|
||||
run_config=tune.RunConfig(
|
||||
stop={TRAINING_ITERATION: 2},
|
||||
verbose=2,
|
||||
callbacks=[_TestCallback()],
|
||||
),
|
||||
).fit()
|
||||
|
||||
def test_default_resource_request(self):
|
||||
config = (
|
||||
PPOConfig()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
enable_rl_module_and_learner=False,
|
||||
)
|
||||
.resources(placement_strategy="SPREAD")
|
||||
.env_runners(
|
||||
num_env_runners=2,
|
||||
num_cpus_per_env_runner=2,
|
||||
)
|
||||
.training(
|
||||
model={"fcnet_hiddens": [10]}, lr=tune.grid_search([0.1, 0.01, 0.001])
|
||||
)
|
||||
.environment("CartPole-v1")
|
||||
.framework("torch")
|
||||
)
|
||||
# 3 Trials: Can only run 1 at a time (num_cpus=6; needed: 5).
|
||||
|
||||
tune.Tuner(
|
||||
PPO,
|
||||
param_space=config,
|
||||
run_config=tune.RunConfig(
|
||||
stop={TRAINING_ITERATION: 2},
|
||||
verbose=2,
|
||||
callbacks=[_TestCallback()],
|
||||
),
|
||||
tune_config=tune.TuneConfig(reuse_actors=False),
|
||||
).fit()
|
||||
|
||||
def test_default_resource_request_plus_manual_leads_to_error(self):
|
||||
config = (
|
||||
PPOConfig()
|
||||
.api_stack(
|
||||
enable_env_runner_and_connector_v2=False,
|
||||
enable_rl_module_and_learner=False,
|
||||
)
|
||||
.training(model={"fcnet_hiddens": [10]})
|
||||
.environment("CartPole-v1")
|
||||
.env_runners(num_env_runners=0)
|
||||
)
|
||||
|
||||
try:
|
||||
tune.Tuner(
|
||||
tune.with_resources(PPO, PlacementGroupFactory([{"CPU": 1}])),
|
||||
param_space=config,
|
||||
run_config=tune.RunConfig(stop={TRAINING_ITERATION: 2}, verbose=2),
|
||||
).fit()
|
||||
except ValueError as e:
|
||||
assert "have been automatically set to" in e.args[0]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,27 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ray._private.client_mode_hook import client_mode_should_convert, enable_client_mode
|
||||
from ray.rllib.algorithms import dqn
|
||||
from ray.util.client.ray_client_helpers import ray_start_client_server
|
||||
|
||||
|
||||
def test_basic_dqn():
|
||||
with ray_start_client_server():
|
||||
# Need to enable this for client APIs to be used.
|
||||
with enable_client_mode():
|
||||
# Confirming mode hook is enabled.
|
||||
assert client_mode_should_convert()
|
||||
config = (
|
||||
dqn.DQNConfig()
|
||||
.environment("CartPole-v1")
|
||||
.env_runners(num_env_runners=0, compress_observations=True)
|
||||
)
|
||||
trainer = config.build()
|
||||
for i in range(2):
|
||||
trainer.train()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", "-s", __file__]))
|
||||
@@ -0,0 +1,38 @@
|
||||
import unittest
|
||||
|
||||
from ray.rllib.algorithms.registry import (
|
||||
ALGORITHMS,
|
||||
ALGORITHMS_CLASS_TO_NAME,
|
||||
POLICIES,
|
||||
get_policy_class,
|
||||
get_policy_class_name,
|
||||
)
|
||||
|
||||
|
||||
class TestPolicies(unittest.TestCase):
|
||||
def test_load_policies(self):
|
||||
for name in POLICIES.keys():
|
||||
self.assertIsNotNone(get_policy_class(name))
|
||||
|
||||
def test_get_eager_traced_class_name(self):
|
||||
from ray.rllib.algorithms.ppo.ppo_tf_policy import PPOTF2Policy
|
||||
|
||||
traced = PPOTF2Policy.with_tracing()
|
||||
self.assertEqual(get_policy_class_name(traced), "PPOTF2Policy")
|
||||
|
||||
def test_registered_algorithm_names(self):
|
||||
"""All RLlib registered algorithms should have their name listed in the
|
||||
registry dictionary."""
|
||||
|
||||
for class_name in ALGORITHMS_CLASS_TO_NAME.keys():
|
||||
registered_name = ALGORITHMS_CLASS_TO_NAME[class_name]
|
||||
algo_class, _ = ALGORITHMS[registered_name]()
|
||||
self.assertEqual(class_name.upper(), algo_class.__name__.upper())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,46 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
import ray._common.usage.usage_lib as ray_usage_lib
|
||||
from ray._common.test_utils import TelemetryCallsite, check_library_usage_telemetry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_usage_lib():
|
||||
yield
|
||||
ray.shutdown()
|
||||
ray_usage_lib.reset_global_state()
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Usage currently marked on import.")
|
||||
@pytest.mark.parametrize("callsite", list(TelemetryCallsite))
|
||||
def test_not_used_on_import(reset_usage_lib, callsite: TelemetryCallsite):
|
||||
def _import_rllib():
|
||||
from ray import rllib # noqa: F401
|
||||
|
||||
check_library_usage_telemetry(
|
||||
_import_rllib, callsite=callsite, expected_library_usages=[set(), {"core"}]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("callsite", list(TelemetryCallsite))
|
||||
def test_used_on_import(reset_usage_lib, callsite: TelemetryCallsite):
|
||||
def _use_rllib():
|
||||
# TODO(edoakes): this test currently fails if we don't call `ray.init()`
|
||||
# prior to the import. This is a bug.
|
||||
if callsite == TelemetryCallsite.DRIVER:
|
||||
ray.init()
|
||||
|
||||
from ray import rllib # noqa: F401
|
||||
|
||||
check_library_usage_telemetry(
|
||||
_use_rllib,
|
||||
callsite=callsite,
|
||||
expected_library_usages=[{"rllib"}, {"core", "rllib"}],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", "-s", __file__]))
|
||||
Reference in New Issue
Block a user