Files
2026-07-13 13:17:40 +08:00

520 lines
18 KiB
Python

import unittest
import numpy as np
import ray
from ray.rllib.algorithms.ppo import PPO, PPOConfig
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.connectors.connector import ActionConnector, ConnectorContext
from ray.rllib.core.rl_module.multi_rl_module import MultiRLModuleSpec
# The new RLModule / Learner API
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
from ray.rllib.env.tests.test_multi_agent_env import BasicMultiAgent
from ray.rllib.evaluation.metrics import RolloutMetrics
from ray.rllib.examples._old_api_stack.policy.random_policy import RandomPolicy
from ray.rllib.examples.envs.classes.debug_counter_env import DebugCounterEnv
from ray.rllib.examples.envs.classes.multi_agent import GuessTheNumberGame
from ray.rllib.examples.rl_modules.classes.random_rlm import RandomRLModule
from ray.rllib.policy.policy import PolicySpec
from ray.rllib.policy.sample_batch import convert_ma_batch_to_sample_batch
from ray.rllib.utils.test_utils import check
from ray.tune import register_env
register_env("basic_multiagent", lambda _: BasicMultiAgent(2))
def _get_mapper():
# Note(Artur): This was originally part of the unittest.TestCase.setUpClass
# method but caused trouble when serializing the config because we ended up
# serializing `self`, which is an instance of unittest.TestCase.
# When dealing with two policies in these tests, simply alternate between the 2
# policies to make sure we have data for inference for both policies for each
# step.
class AlternatePolicyMapper:
def __init__(self):
self.policies = ["one", "two"]
self.next = 0
def map(self):
p = self.policies[self.next]
self.next = 1 - self.next
return p
return AlternatePolicyMapper()
class TestEnvRunnerV2(unittest.TestCase):
@classmethod
def setUpClass(cls):
ray.init()
@classmethod
def tearDownClass(cls):
ray.shutdown()
def test_sample_batch_rollout_single_agent_env(self):
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment(DebugCounterEnv)
.framework("torch")
.training(
# Specifically ask for a batch of 200 samples.
train_batch_size=200,
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
)
algo = PPO(config)
rollout_worker = algo.env_runner
sample_batch = rollout_worker.sample()
sample_batch = convert_ma_batch_to_sample_batch(sample_batch)
self.assertEqual(sample_batch["t"][0], 0)
self.assertEqual(sample_batch.env_steps(), 200)
self.assertEqual(sample_batch.agent_steps(), 200)
def test_sample_batch_rollout_multi_agent_env(self):
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("basic_multiagent")
.framework("torch")
.training(
# Specifically ask for a batch of 200 samples.
train_batch_size=200,
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
)
algo = PPO(config)
rollout_worker = algo.env_runner
sample_batch = rollout_worker.sample()
# 2 agents. So the multi-agent SampleBatch should have
# 200 env steps, and 400 agent steps.
self.assertEqual(sample_batch.env_steps(), 200)
self.assertEqual(sample_batch.agent_steps(), 400)
def test_guess_the_number_multi_agent(self):
"""This test will test env runner in the game of GuessTheNumberGame.
The policies are chosen to be deterministic, so that we can test for an
expected reward. Agent 1 will always pick 1, and agent 2 will always guess that
the picked number is higher than 1. The game will end when the picked number is
1, and agent 1 will win. The reward will be 100 for winning, and 1 for each
step that the game is dragged on for. So the expected reward for agent 1 is 100
+ 19 = 119. 19 is the number of steps that the game will last for agent 1
before it wins or loses.
"""
register_env("env_under_test", lambda config: GuessTheNumberGame(config))
def mapping_fn(agent_id, *args, **kwargs):
return "pol1" if agent_id == 0 else "pol2"
class PickOne(RandomPolicy):
"""This policy will always pick 1."""
def compute_actions(
self,
obs_batch,
state_batches=None,
prev_action_batch=None,
prev_reward_batch=None,
**kwargs
):
return [np.array([2, 1])] * len(obs_batch), [], {}
class GuessHigherThanOne(RandomPolicy):
"""This policy will guess that the picked number is higher than 1."""
def compute_actions(
self,
obs_batch,
state_batches=None,
prev_action_batch=None,
prev_reward_batch=None,
**kwargs
):
return [np.array([1, 1])] * len(obs_batch), [], {}
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.framework("torch")
.environment("env_under_test")
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
rollout_fragment_length=100,
)
.multi_agent(
# this makes it independent of neural networks
policies={
"pol1": PolicySpec(policy_class=PickOne),
"pol2": PolicySpec(policy_class=GuessHigherThanOne),
},
policy_mapping_fn=mapping_fn,
)
# TODO (Kourosh): We need to later create the PickOne and
# GuessHigherThanOne RLModules but for now, the policy only needs a
# placeholder RLModule, since the compute_actions() method is
# directly overridden in the policy class.
.rl_module(
rl_module_spec=MultiRLModuleSpec(
rl_module_specs={
"pol1": RLModuleSpec(module_class=RandomRLModule),
"pol2": RLModuleSpec(module_class=RandomRLModule),
}
),
)
.debugging(seed=42)
)
algo = PPO(config)
rollout_worker = algo.env_runner
sample_batch = rollout_worker.sample()
pol1_batch = sample_batch.policy_batches["pol1"]
# reward should be 100 (for winning) + 19 (for dragging the game for 19 steps)
check(pol1_batch["rewards"], 119 * np.ones_like(pol1_batch["rewards"]))
# check if pol1 only has one timestep of transition informatio per each episode
check(len(set(pol1_batch["eps_id"])), len(pol1_batch["eps_id"]))
# check if pol2 has 19 timesteps of transition information per each episode
pol2_batch = sample_batch.policy_batches["pol2"]
check(len(set(pol2_batch["eps_id"])) * 19, len(pol2_batch["eps_id"]))
def test_inference_batches_are_grouped_by_policy(self):
# Create 2 policies that have different inference batch shapes.
class RandomPolicyOne(RandomPolicy):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.view_requirements["rewards"].used_for_compute_actions = True
self.view_requirements["terminateds"].used_for_compute_actions = True
# Create 2 policies that have different inference batch shapes.
class RandomPolicyTwo(RandomPolicy):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.view_requirements["rewards"].used_for_compute_actions = False
self.view_requirements["terminateds"].used_for_compute_actions = False
mapper = _get_mapper()
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("basic_multiagent")
.framework("torch")
.training(
# Specifically ask for a batch of 200 samples.
train_batch_size=200,
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
.multi_agent(
policies={
"one": PolicySpec(
policy_class=RandomPolicyOne,
),
"two": PolicySpec(
policy_class=RandomPolicyTwo,
),
},
policy_mapping_fn=lambda *args, **kwargs: mapper.map(),
policies_to_train=["one"],
count_steps_by="agent_steps",
)
.rl_module(
rl_module_spec=MultiRLModuleSpec(
rl_module_specs={
"one": RLModuleSpec(module_class=RandomRLModule),
"two": RLModuleSpec(module_class=RandomRLModule),
}
),
)
)
algo = PPO(config)
local_worker = algo.env_runner
env = local_worker.env
obs, rewards, terminateds, truncateds, infos = local_worker.env.step(
{0: env.action_space.sample(), 1: env.action_space.sample()}
)
env_id = 0
env_runner = local_worker.sampler._env_runner_obj
env_runner.create_episode(env_id)
_, to_eval, _ = env_runner._process_observations(
{0: obs}, {0: rewards}, {0: terminateds}, {0: truncateds}, {0: infos}
)
# We should have 2 separate batches for both policies.
# Each batch has 1 samples.
self.assertTrue("one" in to_eval)
self.assertEqual(len(to_eval["one"]), 1)
self.assertTrue("two" in to_eval)
self.assertEqual(len(to_eval["two"]), 1)
def test_action_connector_gets_raw_input_dict(self):
class CheckInputDictActionConnector(ActionConnector):
def __call__(self, ac_data):
assert ac_data.input_dict, "raw input dict should be available"
return ac_data
class AddActionConnectorCallbacks(RLlibCallback):
def on_create_policy(self, *, policy_id, policy) -> None:
policy.action_connectors.append(
CheckInputDictActionConnector(ConnectorContext.from_policy(policy))
)
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("basic_multiagent")
.framework("torch")
.training(
# Specifically ask for a batch of 200 samples.
train_batch_size=200,
)
.callbacks(
callbacks_class=AddActionConnectorCallbacks,
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
)
algo = PPO(config)
rollout_worker = algo.env_runner
# As long as we can successfully sample(), things should be good.
_ = rollout_worker.sample()
def test_start_episode(self):
mapper = _get_mapper()
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("basic_multiagent")
.framework("torch")
.training(
# Specifically ask for a batch of 200 samples.
train_batch_size=200,
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
.multi_agent(
policies={
"one": PolicySpec(
policy_class=RandomPolicy,
),
"two": PolicySpec(
policy_class=RandomPolicy,
),
},
policy_mapping_fn=lambda *args, **kwargs: mapper.map(),
policies_to_train=["one"],
count_steps_by="agent_steps",
)
.rl_module(
rl_module_spec=MultiRLModuleSpec(
rl_module_specs={
"one": RLModuleSpec(module_class=RandomRLModule),
"two": RLModuleSpec(module_class=RandomRLModule),
}
),
)
)
algo = PPO(config)
local_worker = algo.env_runner
env_runner = local_worker.sampler._env_runner_obj
# No episodes present
self.assertEqual(env_runner._active_episodes.get(0), None)
env_runner.step()
# Only initial observation collected, add_init_obs called on episode
self.assertEqual(env_runner._active_episodes[0].total_env_steps, 0)
self.assertEqual(env_runner._active_episodes[0].total_agent_steps, 0)
env_runner.step()
# First recorded step, add_action_reward_done_next_obs called
self.assertEqual(env_runner._active_episodes[0].total_env_steps, 1)
self.assertEqual(env_runner._active_episodes[0].total_agent_steps, 2)
def test_env_runner_output(self):
mapper = _get_mapper()
# Test if we can produce RolloutMetrics just by stepping
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("basic_multiagent")
.framework("torch")
.training(
# Specifically ask for a batch of 200 samples.
train_batch_size=200,
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
.multi_agent(
policies={
"one": PolicySpec(
policy_class=RandomPolicy,
),
"two": PolicySpec(
policy_class=RandomPolicy,
),
},
policy_mapping_fn=lambda *args, **kwargs: mapper.map(),
policies_to_train=["one"],
count_steps_by="agent_steps",
)
.rl_module(
rl_module_spec=MultiRLModuleSpec(
rl_module_specs={
"one": RLModuleSpec(module_class=RandomRLModule),
"two": RLModuleSpec(module_class=RandomRLModule),
}
),
)
)
algo = PPO(config)
local_worker = algo.env_runner
env_runner = local_worker.sampler._env_runner_obj
outputs = []
while not outputs:
outputs = env_runner.step()
self.assertEqual(len(outputs), 1)
self.assertTrue(len(list(outputs[0].agent_rewards.keys())) == 2)
def test_env_error(self):
class CheckErrorCallbacks(RLlibCallback):
def on_episode_end(
self, *, worker, base_env, policies, episode, env_index=None, **kwargs
) -> None:
# We should see an error episode.
assert isinstance(episode, Exception)
mapper = _get_mapper()
# Test if we can produce RolloutMetrics just by stepping
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("basic_multiagent")
.framework("torch")
.training(
# Specifically ask for a batch of 200 samples.
train_batch_size=200,
)
.env_runners(
num_envs_per_env_runner=1,
num_env_runners=0,
)
.multi_agent(
policies={
"one": PolicySpec(
policy_class=RandomPolicy,
),
"two": PolicySpec(
policy_class=RandomPolicy,
),
},
policy_mapping_fn=lambda *args, **kwargs: mapper.map(),
policies_to_train=["one"],
count_steps_by="agent_steps",
)
.rl_module(
rl_module_spec=MultiRLModuleSpec(
rl_module_specs={
"one": RLModuleSpec(module_class=RandomRLModule),
"two": RLModuleSpec(module_class=RandomRLModule),
}
),
)
.callbacks(
callbacks_class=CheckErrorCallbacks,
)
)
algo = PPO(config)
local_worker = algo.env_runner
env_runner = local_worker.sampler._env_runner_obj
# Run a couple of steps.
env_runner.step()
env_runner.step()
active_envs, to_eval, outputs = env_runner._process_observations(
unfiltered_obs={0: AttributeError("mock error")},
rewards={0: {}},
terminateds={0: {"__all__": True}},
truncateds={0: {"__all__": False}},
infos={0: {}},
)
self.assertEqual(active_envs, {0})
self.assertTrue(to_eval) # to_eval contains data for the resetted new episode.
self.assertEqual(len(outputs), 1)
self.assertTrue(isinstance(outputs[0], RolloutMetrics))
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))