chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+340
View File
@@ -0,0 +1,340 @@
import tempfile
import unittest
import gymnasium as gym
import numpy as np
import ray
from ray.rllib.core import DEFAULT_MODULE_ID
from ray.rllib.core.learner.learner import Learner
from ray.rllib.core.testing.testing_learner import BaseTestingAlgorithmConfig
from ray.rllib.policy.sample_batch import MultiAgentBatch
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.metrics import (
ALL_MODULES,
MODULE_TRAIN_BATCH_SIZE_MEAN,
NUM_ENV_STEPS_TRAINED,
NUM_ENV_STEPS_TRAINED_LIFETIME,
NUM_MODULE_STEPS_TRAINED,
NUM_MODULE_STEPS_TRAINED_LIFETIME,
WEIGHTS_SEQ_NO,
)
from ray.rllib.utils.numpy import convert_to_numpy
from ray.rllib.utils.test_utils import check, get_cartpole_dataset_reader
torch, _ = try_import_torch()
class TestLearner(unittest.TestCase):
ENV = gym.make("CartPole-v1")
@classmethod
def setUp(cls) -> None:
ray.init()
@classmethod
def tearDown(cls) -> None:
ray.shutdown()
def test_end_to_end_update(self):
"""Tests the end-to-end update process for a single-agent scenario.
We check that the loss is decreasing and that the metrics are where we expect them and that values are as expected.
"""
config = BaseTestingAlgorithmConfig()
learner = config.build_learner(env=self.ENV)
reader = get_cartpole_dataset_reader(batch_size=512)
for seq_num in range(1, 1000):
batch = reader.next().as_multi_agent()
batch = learner._convert_batch_type(batch)
results = learner.update(batch=batch)
self.assertEqual(
batch.count, results[DEFAULT_MODULE_ID][MODULE_TRAIN_BATCH_SIZE_MEAN]
)
self.assertEqual(
batch.count, results[DEFAULT_MODULE_ID][NUM_MODULE_STEPS_TRAINED]
)
self.assertEqual(
batch.count,
results[DEFAULT_MODULE_ID][NUM_MODULE_STEPS_TRAINED_LIFETIME],
)
self.assertEqual(seq_num, results[DEFAULT_MODULE_ID][WEIGHTS_SEQ_NO])
self.assertEqual(
batch.count, results[DEFAULT_MODULE_ID][MODULE_TRAIN_BATCH_SIZE_MEAN]
)
self.assertTrue(learner.TOTAL_LOSS_KEY in results[DEFAULT_MODULE_ID])
self.assertEqual(
batch.count, results[ALL_MODULES][NUM_MODULE_STEPS_TRAINED]
)
self.assertEqual(
batch.count, results[ALL_MODULES][NUM_MODULE_STEPS_TRAINED_LIFETIME]
)
self.assertEqual(batch.count, results[ALL_MODULES][NUM_ENV_STEPS_TRAINED])
self.assertEqual(
batch.count, results[ALL_MODULES][NUM_ENV_STEPS_TRAINED_LIFETIME]
)
self.assertLess(results[DEFAULT_MODULE_ID][Learner.TOTAL_LOSS_KEY], 0.58)
def test_compute_gradients(self):
"""Tests the compute_gradients correctness.
Tests that if we sum all the trainable variables the gradient of output w.r.t.
the weights is all ones.
"""
config = BaseTestingAlgorithmConfig()
learner = config.build_learner(env=self.ENV)
params = learner.get_parameters(learner.module[DEFAULT_MODULE_ID])
tape = None
loss_per_module = {ALL_MODULES: sum(param.sum() for param in params)}
gradients = learner.compute_gradients(loss_per_module, gradient_tape=tape)
# Type should be a mapping from ParamRefs to gradients.
self.assertIsInstance(gradients, dict)
for grad in gradients.values():
check(grad, np.ones(grad.shape))
def test_postprocess_gradients(self):
"""Tests the base grad clipping logic in `postprocess_gradients()`."""
# Clip by value only.
config = BaseTestingAlgorithmConfig().training(
lr=0.0003, grad_clip=0.75, grad_clip_by="value"
)
learner = config.build_learner(env=self.ENV)
# Pretend our computed gradients are our weights + 1.0.
grads = {
learner.get_param_ref(v): v + 1.0
for v in learner.get_parameters(learner.module[DEFAULT_MODULE_ID])
}
# Call the learner's postprocessing method.
processed_grads = list(learner.postprocess_gradients(grads).values())
# Check clipped gradients.
# No single gradient must be larger than 0.1 or smaller than -0.1:
self.assertTrue(
all(
np.max(grad) <= config.grad_clip and np.min(grad) >= -config.grad_clip
for grad in convert_to_numpy(processed_grads)
)
)
# Clip by norm.
config.grad_clip = 1.0
config.grad_clip_by = "norm"
learner = config.build_learner(env=self.ENV)
# Pretend our computed gradients are our weights + 1.0.
grads = {
learner.get_param_ref(v): v + 1.0
for v in learner.get_parameters(learner.module[DEFAULT_MODULE_ID])
}
# Call the learner's postprocessing method.
processed_grads = list(learner.postprocess_gradients(grads).values())
# Check clipped gradients.
for proc_grad, grad in zip(
convert_to_numpy(processed_grads),
convert_to_numpy(list(grads.values())),
):
l2_norm = np.sqrt(np.sum(grad**2.0))
if l2_norm > config.grad_clip:
check(proc_grad, grad * (config.grad_clip / l2_norm))
# Clip by global norm.
config.grad_clip = 5.0
config.grad_clip_by = "global_norm"
learner = config.build_learner(env=self.ENV)
# Pretend our computed gradients are our weights + 1.0.
grads = {
learner.get_param_ref(v): v + 1.0
for v in learner.get_parameters(learner.module[DEFAULT_MODULE_ID])
}
# Call the learner's postprocessing method.
processed_grads = list(learner.postprocess_gradients(grads).values())
# Check clipped gradients.
global_norm = np.sqrt(
np.sum(
[np.sum(grad**2.0) for grad in convert_to_numpy(list(grads.values()))]
)
)
if global_norm > config.grad_clip:
for proc_grad, grad in zip(
convert_to_numpy(processed_grads),
grads.values(),
):
check(proc_grad, grad * (config.grad_clip / global_norm))
def test_apply_gradients(self):
"""Tests the apply_gradients correctness.
Tests that if we apply gradients of all ones, the new params are equal to the
standard SGD/Adam update rule.
"""
config = BaseTestingAlgorithmConfig().training(lr=0.0003)
learner = config.build_learner(env=self.ENV)
# calculated the expected new params based on gradients of all ones.
params = learner.get_parameters(learner.module[DEFAULT_MODULE_ID])
n_steps = 100
expected = [
(
convert_to_numpy(param)
- n_steps * learner.config.lr * np.ones(param.shape)
)
for param in params
]
for _ in range(n_steps):
gradients = {learner.get_param_ref(p): torch.ones_like(p) for p in params}
learner.apply_gradients(gradients)
check(params, expected)
def test_add_remove_module(self):
"""Tests the compute/apply_gradients with add/remove modules.
Tests that if we add a module with SGD optimizer with a known lr (different
from default), and remove the default module, with a loss that is the sum of
all variables the updated parameters follow the SGD update rule.
"""
config = BaseTestingAlgorithmConfig().training(lr=0.0003)
learner = config.build_learner(env=self.ENV)
rl_module_spec = config.get_default_rl_module_spec()
rl_module_spec.observation_space = self.ENV.observation_space
rl_module_spec.action_space = self.ENV.action_space
learner.add_module(
module_id="test",
module_spec=rl_module_spec,
)
learner.remove_module(DEFAULT_MODULE_ID)
# only test module should be left
self.assertEqual(set(learner.module.keys()), {"test"})
# calculated the expected new params based on gradients of all ones.
params = learner.get_parameters(learner.module["test"])
n_steps = 100
expected = [
convert_to_numpy(param) - n_steps * learner.config.lr * np.ones(param.shape)
for param in params
]
for _ in range(n_steps):
tape = None
loss_per_module = {ALL_MODULES: sum(param.sum() for param in params)}
gradients = learner.compute_gradients(loss_per_module, gradient_tape=tape)
learner.apply_gradients(gradients)
check(params, expected)
def test_save_to_path_and_restore_from_path(self):
"""Tests, whether a Learner's state is properly saved and restored."""
config = BaseTestingAlgorithmConfig()
# Get a Learner instance for the framework and env.
learner1 = config.build_learner(env=self.ENV)
with tempfile.TemporaryDirectory() as tmpdir:
learner1.save_to_path(tmpdir)
learner2 = config.build_learner(env=self.ENV)
learner2.restore_from_path(tmpdir)
self._check_learner_states("torch", learner1, learner2)
# Add a module then save/load and check states.
with tempfile.TemporaryDirectory() as tmpdir:
rl_module_spec = config.get_default_rl_module_spec()
rl_module_spec.observation_space = self.ENV.observation_space
rl_module_spec.action_space = self.ENV.action_space
learner1.add_module(
module_id="test",
module_spec=rl_module_spec,
)
learner1.save_to_path(tmpdir)
learner2 = Learner.from_checkpoint(tmpdir)
self._check_learner_states("torch", learner1, learner2)
# Remove a module then save/load and check states.
with tempfile.TemporaryDirectory() as tmpdir:
learner1.remove_module(module_id=DEFAULT_MODULE_ID)
learner1.save_to_path(tmpdir)
learner2 = Learner.from_checkpoint(tmpdir)
self._check_learner_states("torch", learner1, learner2)
def _check_learner_states(self, framework, learner1, learner2):
check(learner1.module.get_state(), learner2.module.get_state())
check(learner1._get_optimizer_state(), learner2._get_optimizer_state())
check(learner1._module_optimizers, learner2._module_optimizers)
def test_multi_agent_learner_results(self):
"""Tests the learner results for a multi-agent scenario.
We check that all metrics are where we expect them and that values are as expected.
"""
config = BaseTestingAlgorithmConfig()
learner = config.build_learner(env=self.ENV)
learner.remove_module(module_id=DEFAULT_MODULE_ID)
learner.add_module(
module_id="mod1", module_spec=config.get_rl_module_spec(env=self.ENV)
)
learner.add_module(
module_id="mod2", module_spec=config.get_rl_module_spec(env=self.ENV)
)
reader = get_cartpole_dataset_reader(batch_size=512)
results = {}
for seq_num in range(1, 5):
batch1 = reader.next()
batch2 = reader.next()
multi_agent_batch = MultiAgentBatch(
{"mod1": batch1, "mod2": batch2}, batch1.count + batch2.count
)
batch = learner._convert_batch_type(multi_agent_batch)
results = learner.update(batch)
# Lifetime steps are aggregated at the root, so the return value in the results will contain only the last step.
for module_id, sa_batch_count in zip(
["mod1", "mod2"], [batch1.count, batch2.count]
):
self.assertEqual(
sa_batch_count,
results[module_id][NUM_MODULE_STEPS_TRAINED_LIFETIME],
)
self.assertEqual(seq_num, results[module_id][WEIGHTS_SEQ_NO])
self.assertEqual(
sa_batch_count, results[module_id][MODULE_TRAIN_BATCH_SIZE_MEAN]
)
# We don't know what the value should be, just check for existence.
self.assertTrue(learner.TOTAL_LOSS_KEY in results[module_id])
self.assertEqual(
batch1.count + batch2.count,
results[ALL_MODULES][NUM_MODULE_STEPS_TRAINED_LIFETIME],
)
self.assertEqual(
batch1.count + batch2.count,
results[ALL_MODULES][NUM_MODULE_STEPS_TRAINED],
)
self.assertEqual(
batch1.count + batch2.count,
results[ALL_MODULES][NUM_ENV_STEPS_TRAINED_LIFETIME],
)
self.assertEqual(
batch1.count + batch2.count, results[ALL_MODULES][NUM_ENV_STEPS_TRAINED]
)
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))
@@ -0,0 +1,573 @@
import tempfile
import unittest
import gymnasium as gym
import numpy as np
import pytest
import ray
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
from ray.rllib.algorithms.bc import BCConfig
from ray.rllib.core import (
COMPONENT_LEARNER,
COMPONENT_RL_MODULE,
DEFAULT_MODULE_ID,
Columns,
)
from ray.rllib.core.learner.learner import Learner
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.core.testing.testing_learner import BaseTestingAlgorithmConfig
from ray.rllib.core.testing.torch.bc_learner import BCTorchLearner
from ray.rllib.core.testing.torch.bc_module import DiscreteBCTorchModule
from ray.rllib.env.multi_agent_episode import MultiAgentEpisode
from ray.rllib.env.single_agent_episode import SingleAgentEpisode
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
from ray.rllib.utils.metrics import ALL_MODULES, LEARNER_CONNECTOR
from ray.rllib.utils.metrics.metrics_logger import MetricsLogger
from ray.rllib.utils.test_utils import check
from ray.util.timer import _Timer
REMOTE_CONFIGS = {
"remote-cpu": AlgorithmConfig.overrides(num_learners=1),
"remote-gpu": AlgorithmConfig.overrides(num_learners=1, num_gpus_per_learner=1),
"multi-gpu-ddp": AlgorithmConfig.overrides(num_learners=2, num_gpus_per_learner=1),
"multi-cpu-ddp": AlgorithmConfig.overrides(num_learners=2, num_cpus_per_learner=2),
# "multi-gpu-ddp-pipeline": AlgorithmConfig.overrides(
# num_learners=2, num_gpus_per_learner=2
# ),
}
LOCAL_CONFIGS = {
"local-cpu": AlgorithmConfig.overrides(num_learners=0, num_gpus_per_learner=0),
"local-gpu": AlgorithmConfig.overrides(num_learners=0, num_gpus_per_learner=1),
}
FAKE_EPISODES = [
SingleAgentEpisode(
observation_space=gym.spaces.Box(-1.0, 1.0, (4,), np.float32),
observations=[
np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32),
np.array([0.5, 0.6, 0.7, 0.8], dtype=np.float32),
np.array([0.9, 1.0, 1.1, 1.2], dtype=np.float32),
np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32),
np.array([-0.1, -0.2, -0.3, -0.4], dtype=np.float32),
],
action_space=gym.spaces.Discrete(2),
actions=[0, 1, 1, 0],
rewards=[1.0, -1.0, 0.5, 0.3],
terminated=True,
len_lookback_buffer=0, # all data part of actual episode
),
]
FAKE_MA_EPISODES = [
MultiAgentEpisode(
agent_module_ids={
0: "p0",
1: "p1",
},
observation_space=gym.spaces.Dict(
{
0: FAKE_EPISODES[0].observation_space,
1: FAKE_EPISODES[0].observation_space,
}
),
observations=[
{
0: FAKE_EPISODES[0].get_observations(i),
1: FAKE_EPISODES[0].get_observations(i),
}
for i in range(5)
],
action_space=gym.spaces.Dict(
{
0: FAKE_EPISODES[0].action_space,
1: FAKE_EPISODES[0].action_space,
}
),
actions=[
{
0: FAKE_EPISODES[0].get_actions(i),
1: FAKE_EPISODES[0].get_actions(i),
}
for i in range(4)
],
rewards=[
{
0: FAKE_EPISODES[0].get_rewards(i),
1: FAKE_EPISODES[0].get_rewards(i),
}
for i in range(4)
],
len_lookback_buffer=0, # all data part of actual episode
),
]
FAKE_MA_EPISODES[0].to_numpy()
FAKE_MA_EPISODES_WO_P1 = [
MultiAgentEpisode(
agent_module_ids={0: "p0"},
observation_space=gym.spaces.Dict({0: FAKE_EPISODES[0].observation_space}),
observations=[{0: FAKE_EPISODES[0].get_observations(i)} for i in range(5)],
action_space=gym.spaces.Dict({0: FAKE_EPISODES[0].action_space}),
actions=[{0: FAKE_EPISODES[0].get_actions(i)} for i in range(4)],
rewards=[{0: FAKE_EPISODES[0].get_rewards(i)} for i in range(4)],
len_lookback_buffer=0, # all data part of actual episode
),
]
FAKE_MA_EPISODES_WO_P1[0].to_numpy()
class TestLearnerGroupSyncUpdate(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
ray.init()
@classmethod
def tearDownClass(cls) -> None:
ray.shutdown()
def test_learner_group_build_from_algorithm_config(self):
"""Tests whether we can build a learner_groupobject from algorithm_config."""
env = gym.make("CartPole-v1")
# Config that has its own learner class and RLModule spec.
config = BaseTestingAlgorithmConfig()
learner_group = config.build_learner_group(env=env)
print(learner_group)
learner_group.shutdown()
# Config for which user defines custom learner class and RLModule spec.
config = (
BaseTestingAlgorithmConfig()
.training(learner_class=BCTorchLearner)
.rl_module(
rl_module_spec=RLModuleSpec(
module_class=DiscreteBCTorchModule,
model_config={"fcnet_hiddens": [32]},
)
)
)
learner_group = config.build_learner_group(env=env)
print(learner_group)
learner_group.shutdown()
def test_update_multi_gpu(self):
return
scaling_modes = ["multi-gpu-ddp", "remote-gpu"]
for scaling_mode in scaling_modes:
print(f"Testing scaling mode: {scaling_mode}.")
env = gym.make("CartPole-v1")
config_overrides = REMOTE_CONFIGS[scaling_mode]
config = BaseTestingAlgorithmConfig().update_from_dict(config_overrides)
learner_group = config.build_learner_group(env=env)
min_loss = float("inf")
for iter_i in range(1000):
results = learner_group.update(episodes=FAKE_EPISODES)
loss = np.mean(
[res[ALL_MODULES][Learner.TOTAL_LOSS_KEY] for res in results]
)
min_loss = min(loss, min_loss)
print(f"[iter = {iter_i}] Loss: {loss:.3f}, Min Loss: {min_loss:.3f}")
# The loss is initially around 0.69 (ln2). When it gets to around
# 0.57 the return of the policy gets to around 100.
if min_loss < 0.57:
break
for res1, res2 in zip(results, results[1:]):
self.assertEqual(
res1[DEFAULT_MODULE_ID]["mean_weight"],
res2[DEFAULT_MODULE_ID]["mean_weight"],
)
self.assertLess(min_loss, 0.57)
# Make sure the learner_group resources are freed up so that we don't
# autoscale.
learner_group.shutdown()
del learner_group
def test_add_module_and_remove_module(self):
scaling_modes = ["local-cpu", "multi-cpu-ddp"]
for scaling_mode in scaling_modes:
print(f"Testing scaling mode: {scaling_mode}.")
ma_env = MultiAgentCartPole({"num_agents": 2})
config_overrides = REMOTE_CONFIGS.get(scaling_mode) or LOCAL_CONFIGS.get(
scaling_mode
)
config = (
BCConfig()
.update_from_dict(config_overrides)
.multi_agent(
policies={"p0"},
policy_mapping_fn=lambda aid, *ar, **kw: f"p{aid}",
)
.rl_module(
rl_module_spec=MultiRLModuleSpec(
rl_module_specs={"p0": RLModuleSpec()},
)
)
)
learner_group = config.build_learner_group(env=ma_env)
# Update once with the default policy.
learner_group.update(episodes=FAKE_MA_EPISODES_WO_P1)
# Add a test_module.
learner_group.add_module(
module_id="p1",
module_spec=config.get_multi_rl_module_spec(env=ma_env).module_specs[
"p0"
],
)
# Do training that includes the test_module.
results = learner_group.update(episodes=FAKE_MA_EPISODES)
# check that module ids are updated to include the new module
module_ids_after_add = {"p0", "p1"}
# Compare module IDs in results with expected ones.
self.assertEqual(
set(results[0].keys()) - {ALL_MODULES}, module_ids_after_add
)
# Remove the test_module.
learner_group.remove_module(module_id="p1")
# Run training without the test_module.
results = learner_group.update(episodes=FAKE_MA_EPISODES_WO_P1)
# check that module ids are updated after remove operation to not
# include the new module
# remove the total_loss key since its not a module key
self.assertEqual(set(results[0].keys()) - {ALL_MODULES}, {"p0"})
# make sure the learner_group resources are freed up so that we don't
# autoscale
learner_group.shutdown()
del learner_group
class TestLearnerGroupCheckpointRestore(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
ray.init()
@classmethod
def tearDownClass(cls) -> None:
ray.shutdown()
def test_restore_from_path_multi_rl_module_and_individual_modules(self):
"""Tests whether MultiRLModule- and single RLModule states can be restored."""
# this is expanded to more scaling modes on the release ci.
scaling_modes = ["local-cpu", "multi-gpu-ddp"]
for scaling_mode in scaling_modes:
print(f"Testing scaling mode: {scaling_mode}.")
# env will have agent ids 0 and 1
env = MultiAgentCartPole({"num_agents": 2})
config_overrides = REMOTE_CONFIGS.get(scaling_mode) or LOCAL_CONFIGS.get(
scaling_mode
)
config = BaseTestingAlgorithmConfig().update_from_dict(config_overrides)
learner_group = config.build_learner_group(env=env)
spec = config.get_multi_rl_module_spec(env=env).module_specs[
DEFAULT_MODULE_ID
]
learner_group.add_module(module_id="0", module_spec=spec)
learner_group.add_module(module_id="1", module_spec=spec)
learner_group.remove_module(DEFAULT_MODULE_ID)
module_0 = spec.build()
module_1 = spec.build()
multi_rl_module = MultiRLModule()
multi_rl_module.add_module(module_id="0", module=module_0)
multi_rl_module.add_module(module_id="1", module=module_1)
# Check if we can load just the MultiRLModule.
with tempfile.TemporaryDirectory() as tmpdir:
multi_rl_module.save_to_path(tmpdir)
old_learner_weights = learner_group.get_weights()
learner_group.restore_from_path(
tmpdir,
component=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE,
)
# Check the weights of the module in the learner group are the
# same as the weights of the newly created MultiRLModule
check(learner_group.get_weights(), multi_rl_module.get_state())
learner_group.set_state(
{
COMPONENT_LEARNER: {COMPONENT_RL_MODULE: old_learner_weights},
}
)
check(learner_group.get_weights(), old_learner_weights)
# Check if we can load just single agent RL Modules.
with tempfile.TemporaryDirectory() as tmpdir:
module_0.save_to_path(tmpdir)
with tempfile.TemporaryDirectory() as tmpdir2:
temp_module = spec.build()
temp_module.save_to_path(tmpdir2)
old_learner_weights = learner_group.get_weights()
learner_group.restore_from_path(
tmpdir,
component=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE + "/0",
)
learner_group.restore_from_path(
tmpdir2,
component=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE + "/1",
)
# check the weights of the module in the learner group are the
# same as the weights of the newly created MultiRLModule
new_multi_rl_module = MultiRLModule()
new_multi_rl_module.add_module(module_id="0", module=module_0)
new_multi_rl_module.add_module(module_id="1", module=temp_module)
check(learner_group.get_weights(), new_multi_rl_module.get_state())
learner_group.set_weights(old_learner_weights)
# Check if we can first load a MultiRLModule, then a single agent RLModule
# (within that MultiRLModule). Check that the single agent RL Module is
# loaded over the matching submodule in the MultiRLModule.
with tempfile.TemporaryDirectory() as tmpdir:
module_0 = spec.build()
multi_rl_module = MultiRLModule()
multi_rl_module.add_module(module_id="0", module=module_0)
multi_rl_module.add_module(module_id="1", module=spec.build())
multi_rl_module.save_to_path(tmpdir)
with tempfile.TemporaryDirectory() as tmpdir2:
module_1 = spec.build()
module_1.save_to_path(tmpdir2)
learner_group.restore_from_path(
tmpdir,
component=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE,
)
learner_group.restore_from_path(
tmpdir2,
component=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE + "/1",
)
new_multi_rl_module = MultiRLModule()
new_multi_rl_module.add_module(module_id="0", module=module_0)
new_multi_rl_module.add_module(module_id="1", module=module_1)
check(learner_group.get_weights(), new_multi_rl_module.get_state())
del learner_group
class TestLearnerGroupSaveAndRestoreState(unittest.TestCase):
FAKE_BATCH = {
Columns.OBS: np.array(
[
[0.1, 0.2, 0.3, 0.4],
[0.5, 0.6, 0.7, 0.8],
[0.9, 1.0, 1.1, 1.2],
[1.3, 1.4, 1.5, 1.6],
],
dtype=np.float32,
),
Columns.NEXT_OBS: np.array(
[
[0.1, 0.2, 0.3, 0.4],
[0.5, 0.6, 0.7, 0.8],
[0.9, 1.0, 1.1, 1.2],
[1.3, 1.4, 1.5, 1.6],
],
dtype=np.float32,
),
Columns.ACTIONS: np.array([0, 1, 1, 0]),
Columns.REWARDS: np.array([1.0, -1.0, 0.5, 0.6], dtype=np.float32),
Columns.TERMINATEDS: np.array([False, False, True, False]),
Columns.TRUNCATEDS: np.array([False, False, False, False]),
Columns.VF_PREDS: np.array([0.5, 0.6, 0.7, 0.8], dtype=np.float32),
Columns.ACTION_DIST_INPUTS: np.array(
[[-2.0, 0.5], [-3.0, -0.3], [-0.1, 2.5], [-0.2, 3.5]], dtype=np.float32
),
Columns.ACTION_LOGP: np.array([-0.5, -0.1, -0.2, -0.3], dtype=np.float32),
Columns.EPS_ID: np.array([0, 0, 0, 0]),
}
@classmethod
def setUpClass(cls) -> None:
ray.init()
@classmethod
def tearDownClass(cls) -> None:
ray.shutdown()
def test_save_to_path_and_restore_from_path(self):
"""Check that saving and loading learner group state works."""
# this is expanded to more scaling modes on the release ci.
scaling_modes = ["local-cpu"] # , "multi-gpu-ddp"]
for scaling_mode in scaling_modes:
print(f"Testing scaling mode: {scaling_mode}.")
env = gym.make("CartPole-v1")
config_overrides = REMOTE_CONFIGS.get(scaling_mode) or LOCAL_CONFIGS.get(
scaling_mode
)
config = BaseTestingAlgorithmConfig().update_from_dict(config_overrides)
learner_group = config.build_learner_group(env=env)
# Checkpoint the initial learner state for later comparison.
initial_learner_checkpoint_dir = tempfile.TemporaryDirectory().name
learner_group.save_to_path(initial_learner_checkpoint_dir)
# Test the convenience method `.get_weights()`.
initial_weights = learner_group.get_weights()
# Do a single update.
learner_group.update(episodes=FAKE_EPISODES)
weights_after_update = learner_group.get_state(
components=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE
)[COMPONENT_LEARNER][COMPONENT_RL_MODULE]
# Weights after the update must be different from original ones.
check(initial_weights, weights_after_update, false=True)
# Checkpoint the learner state after 1 update for later comparison.
learner_after_1_update_checkpoint_dir = tempfile.TemporaryDirectory().name
learner_group.save_to_path(learner_after_1_update_checkpoint_dir)
# Remove that learner, construct a new one, and load the state of the old
# learner into the new one.
learner_group.shutdown()
del learner_group
learner_group = config.build_learner_group(env=env)
learner_group.restore_from_path(learner_after_1_update_checkpoint_dir)
# Do another update.
results_2nd_update_with_break = learner_group.update(episodes=FAKE_EPISODES)
weights_after_2_updates_with_break = learner_group.get_state(
components=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE
)[COMPONENT_LEARNER][COMPONENT_RL_MODULE]
learner_group.shutdown()
del learner_group
# Construct a new learner group and load the initial state of the learner.
learner_group = config.build_learner_group(env=env)
learner_group.restore_from_path(initial_learner_checkpoint_dir)
weights_after_restore = learner_group.get_state(
components=COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE
)[COMPONENT_LEARNER][COMPONENT_RL_MODULE]
check(initial_weights, weights_after_restore)
# Perform 2 updates to get to the same state as the previous learners.
learner_group.update(episodes=FAKE_EPISODES)
results_2nd_update_without_break = learner_group.update(
episodes=FAKE_EPISODES
)
weights_after_2_updates_without_break = learner_group.get_weights()
learner_group.shutdown()
del learner_group
# Compare the results of the two updates.
for r1, r2 in zip(
results_2nd_update_with_break,
results_2nd_update_without_break,
):
r1[ALL_MODULES].pop(LEARNER_CONNECTOR)
r2[ALL_MODULES].pop(LEARNER_CONNECTOR)
check(
MetricsLogger.peek_results(results_2nd_update_with_break),
MetricsLogger.peek_results(results_2nd_update_without_break),
rtol=0.05,
)
check(
weights_after_2_updates_with_break,
weights_after_2_updates_without_break,
rtol=0.05,
)
class TestLearnerGroupAsyncUpdate(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
ray.init()
@classmethod
def tearDown(cls) -> None:
ray.shutdown()
def test_async_update(self):
"""Test that async style updates converge to the same result as sync."""
scaling_modes = ["multi-gpu-ddp", "multi-cpu-ddp", "remote-gpu"]
for scaling_mode in scaling_modes:
print(f"Testing scaling mode: {scaling_mode}.")
env = gym.make("CartPole-v1")
config_overrides = REMOTE_CONFIGS[scaling_mode]
config = BaseTestingAlgorithmConfig().update_from_dict(config_overrides)
learner_group = config.build_learner_group(env=env)
timer_sync = _Timer()
timer_async = _Timer()
with timer_sync:
learner_group.update(episodes=FAKE_EPISODES, async_update=False)
with timer_async:
result_async = learner_group.update(
episodes=FAKE_EPISODES, async_update=True
)
# Ideally the first async update will return nothing, and an easy
# way to check that is if the time for an async update call is faster
# than the time for a sync update call.
self.assertLess(timer_async.mean, timer_sync.mean)
self.assertIsInstance(result_async, list)
loss = float("inf")
iter_i = 0
while True:
result_async = learner_group.update(
episodes=FAKE_EPISODES, async_update=True
)
if not result_async:
continue
self.assertIsInstance(result_async, list)
self.assertIsInstance(result_async[0], dict)
# Check one async Learner result.
loss = result_async[0][DEFAULT_MODULE_ID][Learner.TOTAL_LOSS_KEY]
# The loss is initially around 0.69 (ln2). When it gets to around
# 0.57 the return of the policy gets to around 100.
if loss < 0.57:
break
# Compare reported "mean_weight" with actual ones.
_check_multi_worker_weights(learner_group, result_async)
iter_i += 1
learner_group.shutdown()
self.assertLess(loss, 0.57)
def _check_multi_worker_weights(learner_group, results):
# Check that module weights are updated across workers and synchronized.
# for i in range(1, len(results)):
learner_1_results = results[0]
for module_id, mod_result in learner_1_results.items():
if module_id == ALL_MODULES:
continue
results = MetricsLogger.peek_results(results)
reported_mean_weights = np.mean([r[module_id]["mean_weight"] for r in results])
# Compare the reported mean weights (merged across all Learner workers,
# which all should have the same weights after updating) with the actual
# current mean weights.
parameters = learner_group.get_state(
components=(
COMPONENT_LEARNER + "/" + COMPONENT_RL_MODULE + "/" + module_id
),
)[COMPONENT_LEARNER][COMPONENT_RL_MODULE][module_id]
actual_mean_weights = np.mean([w.mean() for w in parameters.values()])
check(reported_mean_weights, actual_mean_weights, rtol=0.02)
if __name__ == "__main__":
import sys
class_ = sys.argv[1] if len(sys.argv) > 1 else None
sys.exit(pytest.main(["-v", __file__ + ("" if class_ is None else "::" + class_)]))