Files
ray-project--ray/rllib/env/tests/test_env_runner_failures.py
2026-07-13 13:17:40 +08:00

868 lines
32 KiB
Python

import time
import unittest
from collections import defaultdict
import gymnasium as gym
import numpy as np
import ray
from ray.rllib.algorithms.algorithm_config import AlgorithmConfig
from ray.rllib.algorithms.impala import IMPALAConfig
from ray.rllib.algorithms.ppo import PPOConfig
from ray.rllib.algorithms.sac.sac import SACConfig
from ray.rllib.connectors.env_to_module.flatten_observations import FlattenObservations
from ray.rllib.core.rl_module.default_model_config import DefaultModelConfig
from ray.rllib.env.multi_agent_env import make_multi_agent
from ray.rllib.env.multi_agent_env_runner import MultiAgentEnvRunner
from ray.rllib.env.single_agent_env_runner import SingleAgentEnvRunner
from ray.rllib.examples.envs.classes.cartpole_crashing import CartPoleCrashing
from ray.rllib.examples.envs.classes.random_env import RandomEnv
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
EVALUATION_RESULTS,
)
from ray.tune.registry import register_env
@ray.remote
class Counter:
"""Remote counter service that survives restarts."""
def __init__(self):
self.reset()
def _key(self, eval, worker_index, vector_index):
return f"{eval}:{worker_index}:{vector_index}"
def increment(self, eval, worker_index, vector_index):
self.counter[self._key(eval, worker_index, vector_index)] += 1
def get(self, eval, worker_index, vector_index):
return self.counter[self._key(eval, worker_index, vector_index)]
def reset(self):
self.counter = defaultdict(int)
class FaultInjectEnv(gym.Env):
"""Env that fails upon calling `step()`, but only for some remote EnvRunner indices.
The EnvRunner indices that should produce the failure (a ValueError) can be
provided by a list (of ints) under the "bad_indices" key in the env's
config.
.. testcode::
:skipif: True
from ray.rllib.env.env_context import EnvContext
# This env will fail for EnvRunners 1 and 2 (not for the local EnvRunner
# or any others with an index != [1|2]).
bad_env = FaultInjectEnv(
EnvContext(
{"bad_indices": [1, 2]},
worker_index=1,
num_workers=3,
)
)
from ray.rllib.env.env_context import EnvContext
# This env will fail only on the first evaluation EnvRunner, not on the first
# regular EnvRunner.
bad_env = FaultInjectEnv(
EnvContext(
{"bad_indices": [1], "eval_only": True},
worker_index=2,
num_workers=5,
)
)
"""
def __init__(self, config):
# Use RandomEnv to control episode length if needed.
self.env = RandomEnv(config)
self.action_space = self.env.action_space
self.observation_space = self.env.observation_space
self.config = config
# External counter service.
if "counter" in config:
self.counter = ray.get_actor(config["counter"])
else:
self.counter = None
if (
config.get("init_delay", 0) > 0.0
and (
not config.get("init_delay_indices", [])
or self.config.worker_index in config.get("init_delay_indices", [])
)
and
# constructor delay can only happen for recreated actors.
self._get_count() > 0
):
# Simulate an initialization delay.
time.sleep(config.get("init_delay"))
def _increment_count(self):
if self.counter:
eval = self.config.get("evaluation", False)
worker_index = self.config.worker_index
vector_index = self.config.vector_index
ray.wait([self.counter.increment.remote(eval, worker_index, vector_index)])
def _get_count(self):
if self.counter:
eval = self.config.get("evaluation", False)
worker_index = self.config.worker_index
vector_index = self.config.vector_index
return ray.get(self.counter.get.remote(eval, worker_index, vector_index))
return -1
def _maybe_raise_error(self):
# Do not raise simulated error if this EnvRunner is not bad.
if self.config.worker_index not in self.config.get("bad_indices", []):
return
if self.counter:
count = self._get_count()
if self.config.get(
"failure_start_count", -1
) >= 0 and count < self.config.get("failure_start_count"):
return
if self.config.get(
"failure_stop_count", -1
) >= 0 and count >= self.config.get("failure_stop_count"):
return
raise ValueError(
"This is a simulated error from "
f"{'eval-' if self.config.get('evaluation', False) else ''}"
f"env-runner-idx={self.config.worker_index}!"
)
def reset(self, *, seed=None, options=None):
self._increment_count()
self._maybe_raise_error()
return self.env.reset()
def step(self, action):
self._increment_count()
self._maybe_raise_error()
if self.config.get("step_delay", 0) > 0.0 and (
not self.config.get("init_delay_indices", [])
or self.config.worker_index in self.config.get("step_delay_indices", [])
):
# Simulate a step delay.
time.sleep(self.config.get("step_delay"))
return self.env.step(action)
class ForwardHealthCheckToEnvWorker(SingleAgentEnvRunner):
"""Configuring EnvRunner to error in specific condition is hard.
So we take a short-cut, and simply forward ping() to env.sample().
"""
def ping(self) -> str:
# See if Env wants to throw error.
self.env.reset()
actions = self.env.action_space.sample()
_ = self.env.step(actions)
# If there is no error raised from sample(), we simply reply pong.
return super().ping()
class ForwardHealthCheckToEnvWorkerMultiAgent(MultiAgentEnvRunner):
"""Configure EnvRunner to error in specific condition is hard.
So we take a short-cut, and simply forward ping() to env.sample().
"""
def ping(self) -> str:
# See if Env wants to throw error.
self.sample(num_timesteps=1, random_actions=True)
# If there is no error raised from sample(), we simply reply pong.
return super().ping()
def on_algorithm_init(algorithm, **kwargs):
# Add a custom module to algorithm.
spec = algorithm.config.get_default_rl_module_spec()
spec.observation_space = gym.spaces.Box(low=0, high=1, shape=(8,))
spec.action_space = gym.spaces.Discrete(2)
spec.inference_only = True
algorithm.add_module(
module_id="test_module",
module_spec=spec,
add_to_eval_env_runners=True,
)
class TestEnvRunnerFailures(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
ray.init()
obs_space = gym.spaces.Box(0, 1, (2,), np.float32)
def _sa(ctx):
ctx.update({"observation_space": obs_space})
return FaultInjectEnv(ctx)
register_env("fault_env", _sa)
def _ma(ctx):
ctx.update({"observation_space": obs_space})
return make_multi_agent(FaultInjectEnv)(ctx)
register_env("multi_agent_fault_env", _ma)
@classmethod
def tearDownClass(cls) -> None:
ray.shutdown()
def _do_test_failing_fatal(self, config, fail_eval=False):
"""Test raises real error when out of EnvRunners."""
config.num_env_runners = 2
config.env = "multi_agent_fault_env" if config.is_multi_agent else "fault_env"
# Make both EnvRunners idx=1 and 2 fail.
config.env_config = {"bad_indices": [1, 2]}
config.restart_failed_env_runners = False
if fail_eval:
config.evaluation_num_env_runners = 2
config.evaluation_interval = 1
config.evaluation_config = {
# Make eval EnvRunners (index 1) fail.
"env_config": {
"bad_indices": [1],
"evaluation": True,
},
"restart_failed_env_runners": False,
}
# TODO(Artur): Unify where fatal env-runner errors surface. MultiAgentEnvRunner
# checks env during init and resets it during init.
# SingleAgentEnvRunner resets the env during sampling.
# This behaviour should be unified and this test should be updated accordingly.
if config.is_multi_agent:
self.assertRaises(ValueError, lambda: config.build())
else:
algo = config.build()
try:
self.assertRaises(ray.exceptions.RayError, lambda: algo.train())
finally:
algo.stop()
def _do_test_failing_ignore(self, config: AlgorithmConfig, fail_eval: bool = False):
# Test fault handling
config.num_env_runners = 2
config.ignore_env_runner_failures = True
config.validate_env_runners_after_construction = False
config.restart_failed_env_runners = False
config.env = "fault_env"
# Make EnvRunner idx=1 fail. Other EnvRunners will be ok.
config.environment(
env_config={
"bad_indices": [1],
}
)
if fail_eval:
config.evaluation_num_env_runners = 2
config.evaluation_interval = 1
config.evaluation_config = {
"ignore_env_runner_failures": True,
"restart_failed_env_runners": False,
"env_config": {
# Make EnvRunner idx=1 fail. Other EnvRunners will be ok.
"bad_indices": [1],
"evaluation": True,
},
}
algo = config.build()
algo.train()
# One of the EnvRunners failed.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 1)
if fail_eval:
# One of the eval EnvRunners failed.
self.assertEqual(algo.eval_env_runner_group.num_healthy_remote_workers(), 1)
algo.stop()
def _do_test_failing_recover(self, config, multi_agent=False):
# Counter that will survive restarts.
COUNTER_NAME = f"_do_test_failing_recover{'_ma' if multi_agent else ''}"
counter = Counter.options(name=COUNTER_NAME).remote()
# Test raises real error when out of EnvRunners.
config.num_env_runners = 1
config.evaluation_num_env_runners = 1
config.evaluation_interval = 1
config.env = "fault_env" if not multi_agent else "multi_agent_fault_env"
config.evaluation_config = AlgorithmConfig.overrides(
restart_failed_env_runners=True,
# 0 delay for testing purposes.
delay_between_env_runner_restarts_s=0,
# Make eval EnvRunner (index 1) fail.
env_config={
"bad_indices": [1],
"failure_start_count": 3,
"failure_stop_count": 4,
"counter": COUNTER_NAME,
},
**(
dict(
policy_mapping_fn=(
lambda aid, episode, **kwargs: (
# Allows this test to query this
# different-from-training-workers policy mapping fn.
"This is the eval mapping fn"
if episode is None
else "main"
if hash(episode.id_) % 2 == aid
else "p{}".format(np.random.choice([0, 1]))
)
)
)
if multi_agent
else {}
),
)
# Reset interaction counter.
ray.wait([counter.reset.remote()])
algo = config.build()
# This should also work several times.
for _ in range(2):
algo.train()
time.sleep(15.0)
algo.restore_env_runners(algo.env_runner_group)
algo.restore_env_runners(algo.eval_env_runner_group)
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 1)
self.assertEqual(algo.eval_env_runner_group.num_healthy_remote_workers(), 1)
if multi_agent:
# Make a dummy call to the eval EnvRunner's policy_mapping_fn and
# make sure the restored eval EnvRunner received the correct one from
# the eval config (not the main EnvRunners' one).
test = algo.eval_env_runner_group.foreach_env_runner(
lambda w: w.config.policy_mapping_fn(0, None)
)
self.assertEqual(test[0], "This is the eval mapping fn")
algo.stop()
def test_fatal_single_agent(self):
# Test the case where all EnvRunners fail (w/o recovery).
self._do_test_failing_fatal(
PPOConfig().env_runners(
env_to_module_connector=(
lambda env, spaces, device: FlattenObservations()
),
)
)
def test_fatal_multi_agent(self):
# Test the case where all EnvRunners fail (w/o recovery).
self._do_test_failing_fatal(
PPOConfig().multi_agent(
policies={"p0"}, policy_mapping_fn=lambda *a, **k: "p0"
),
)
def test_async_samples(self):
self._do_test_failing_ignore(
IMPALAConfig().env_runners(env_runner_cls=ForwardHealthCheckToEnvWorker)
)
def test_sync_replay(self):
self._do_test_failing_ignore(
SACConfig()
.environment(
env_config={"action_space": gym.spaces.Box(0, 1, (2,), np.float32)}
)
.env_runners(env_runner_cls=ForwardHealthCheckToEnvWorker)
.reporting(min_sample_timesteps_per_iteration=1)
)
def test_multi_gpu(self):
self._do_test_failing_ignore(
PPOConfig()
.env_runners(env_runner_cls=ForwardHealthCheckToEnvWorker)
.training(
train_batch_size=10,
minibatch_size=1,
num_epochs=1,
)
)
def test_sync_samples(self):
self._do_test_failing_ignore(
PPOConfig()
.env_runners(env_runner_cls=ForwardHealthCheckToEnvWorker)
.training(optimizer={})
)
def test_env_crash_during_sampling_but_restart_crashed_sub_envs(self):
"""Expect sub-envs to fail (and not recover), but re-start them individually."""
register_env(
"ma_cartpole_crashing",
lambda cfg: (
cfg.update({"num_agents": 2}),
make_multi_agent(CartPoleCrashing)(cfg),
)[1],
)
config = (
PPOConfig()
.env_runners(num_env_runners=4)
.fault_tolerance(
# Re-start failed individual sub-envs (then continue).
# This means no EnvRunners will ever fail due to individual env errors
# (only maybe for reasons other than the env).
restart_failed_sub_environments=True,
# If the EnvRunner was affected by an error (other than the env error),
# allow it to be removed, but training will continue.
ignore_env_runner_failures=True,
)
.environment(
env_config={
# Crash prob=0.1%. Keep this as low as necessary to be able to
# get at least a train batch sampled w/o too many interruptions.
"p_crash": 0.0005,
}
)
.training(num_epochs=1)
)
for multi_agent in [False, True]:
if multi_agent:
config.environment("ma_cartpole_crashing")
config.env_runners(num_envs_per_env_runner=1)
config.multi_agent(
policies={"p0", "p1"},
policy_mapping_fn=lambda aid, eps, **kw: f"p{aid}",
)
else:
config.environment(CartPoleCrashing)
config.env_runners(num_envs_per_env_runner=2)
# Pre-checking disables, so building the Algorithm is save.
algo = config.build()
# Try to re-create the sub-env for infinite amount of times.
for _ in range(5):
# Expect some errors being logged here, but in general, should continue
# as we recover from all sub-env failures.
algo.train()
# No EnvRunner has been removed. Still 2 left.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 4)
algo.stop()
def test_eval_env_runners_failing_ignore(self):
# Test the case where one eval EnvRunner fails, but we chose to ignore.
self._do_test_failing_ignore(
PPOConfig()
.env_runners(env_runner_cls=ForwardHealthCheckToEnvWorker)
.training(model={"fcnet_hiddens": [4]}),
fail_eval=True,
)
def test_eval_env_runners_parallel_to_training_failing_recover(self):
# Test the case where all eval EnvRunners fail, but we chose to recover.
config = (
PPOConfig()
.env_runners(env_runner_cls=ForwardHealthCheckToEnvWorker)
.evaluation(
evaluation_num_env_runners=1,
evaluation_parallel_to_training=True,
evaluation_duration="auto",
)
.training(model={"fcnet_hiddens": [4]})
)
self._do_test_failing_recover(config)
def test_eval_env_runners_parallel_to_training_multi_agent_failing_recover(
self,
):
# Test the case where all eval EnvRunners fail on a multi-agent env with
# different `policy_mapping_fn` in eval- vs train EnvRunners, but we chose
# to recover.
config = (
PPOConfig()
.env_runners(env_runner_cls=ForwardHealthCheckToEnvWorkerMultiAgent)
.multi_agent(
policies={"main", "p0", "p1"},
policy_mapping_fn=(
lambda aid, episode, **kwargs: (
"main"
if hash(episode.id_) % 2 == aid
else "p{}".format(np.random.choice([0, 1]))
)
),
)
.evaluation(
evaluation_num_env_runners=1,
# evaluation_parallel_to_training=True,
# evaluation_duration="auto",
)
.training(model={"fcnet_hiddens": [4]})
)
self._do_test_failing_recover(config, multi_agent=True)
def test_eval_env_runners_failing_fatal(self):
# Test the case where all eval EnvRunners fail (w/o recovery).
self._do_test_failing_fatal(
(
PPOConfig()
.api_stack(
enable_rl_module_and_learner=True,
enable_env_runner_and_connector_v2=True,
)
.training(model={"fcnet_hiddens": [4]})
),
fail_eval=True,
)
def test_env_runners_failing_recover(self):
# Counter that will survive restarts.
COUNTER_NAME = "test_env_runners_fatal_but_recover"
counter = Counter.options(name=COUNTER_NAME).remote()
config = (
PPOConfig()
.env_runners(
env_runner_cls=ForwardHealthCheckToEnvWorker,
num_env_runners=2,
rollout_fragment_length=16,
)
.rl_module(
model_config=DefaultModelConfig(fcnet_hiddens=[4]),
)
.training(
train_batch_size_per_learner=32,
minibatch_size=32,
)
.environment(
env="fault_env",
env_config={
# Make both EnvRunners idx=1 and 2 fail.
"bad_indices": [1, 2],
"failure_start_count": 3,
"failure_stop_count": 4,
"counter": COUNTER_NAME,
},
)
.fault_tolerance(
restart_failed_env_runners=True, # But recover.
# 0 delay for testing purposes.
delay_between_env_runner_restarts_s=0,
)
)
# Try with both local EnvRunner and without.
for local_env_runner in [True, False]:
config.env_runners(create_local_env_runner=local_env_runner)
# Reset interaciton counter.
ray.wait([counter.reset.remote()])
algo = config.build()
# Before training, 2 healthy EnvRunners.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 2)
# Nothing is restarted.
self.assertEqual(algo.env_runner_group.num_remote_worker_restarts(), 0)
algo.train()
time.sleep(15.0)
algo.restore_env_runners(algo.env_runner_group)
# After training, still 2 healthy EnvRunners.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 2)
# Both EnvRunners are restarted.
self.assertEqual(algo.env_runner_group.num_remote_worker_restarts(), 2)
algo.stop()
def test_modules_are_restored_on_recovered_env_runner(self):
# Counter that will survive restarts.
COUNTER_NAME = "test_modules_are_restored_on_recovered_env_runner"
counter = Counter.options(name=COUNTER_NAME).remote()
config = (
PPOConfig()
.env_runners(
env_runner_cls=ForwardHealthCheckToEnvWorkerMultiAgent,
num_env_runners=2,
rollout_fragment_length=16,
)
.rl_module(
model_config=DefaultModelConfig(fcnet_hiddens=[4]),
)
.training(
train_batch_size_per_learner=32,
minibatch_size=32,
)
.environment(
env="multi_agent_fault_env",
env_config={
# Make both EnvRunners idx=1 and 2 fail.
"bad_indices": [1, 2],
"failure_start_count": 3,
"failure_stop_count": 4,
"counter": COUNTER_NAME,
},
)
.evaluation(
evaluation_num_env_runners=1,
evaluation_interval=1,
evaluation_config=PPOConfig.overrides(
restart_failed_env_runners=True,
# Restart the entire eval EnvRunner.
restart_failed_sub_environments=False,
env_config={
"evaluation": True,
# Make eval EnvRunner (index 1) fail.
"bad_indices": [1],
"failure_start_count": 3,
"failure_stop_count": 4,
"counter": COUNTER_NAME,
},
),
)
.callbacks(on_algorithm_init=on_algorithm_init)
.fault_tolerance(
restart_failed_env_runners=True, # But recover.
# Throwing error in constructor is a bad idea.
# 0 delay for testing purposes.
delay_between_env_runner_restarts_s=0,
)
.multi_agent(
policies={"p0"},
policy_mapping_fn=lambda *a, **kw: "p0",
)
)
# Reset interaction counter.
ray.wait([counter.reset.remote()])
algo = config.build()
# Should have the custom module.
self.assertIsNotNone(algo.get_module("test_module"))
# Before train loop, EnvRunners are fresh and not recreated.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 2)
self.assertEqual(algo.env_runner_group.num_remote_worker_restarts(), 0)
self.assertEqual(algo.eval_env_runner_group.num_healthy_remote_workers(), 1)
self.assertEqual(algo.eval_env_runner_group.num_remote_worker_restarts(), 0)
algo.train()
time.sleep(15.0)
algo.restore_env_runners(algo.env_runner_group)
algo.restore_env_runners(algo.eval_env_runner_group)
# Everything healthy again. And all EnvRunners have been restarted.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 2)
self.assertEqual(algo.env_runner_group.num_remote_worker_restarts(), 2)
self.assertEqual(algo.eval_env_runner_group.num_healthy_remote_workers(), 1)
self.assertEqual(algo.eval_env_runner_group.num_remote_worker_restarts(), 1)
# Let's verify that our custom module exists on all recovered EnvRunners.
def has_test_module(w):
return "test_module" in w.module
# EnvRunner has test module.
self.assertTrue(
all(
algo.env_runner_group.foreach_env_runner(
has_test_module, local_env_runner=False
)
)
)
# Eval EnvRunner has test module.
self.assertTrue(
all(
algo.eval_env_runner_group.foreach_env_runner(
has_test_module, local_env_runner=False
)
)
)
def test_eval_env_runners_failing_recover(self):
# Counter that will survive restarts.
COUNTER_NAME = "test_eval_env_runners_fault_but_recover"
counter = Counter.options(name=COUNTER_NAME).remote()
config = (
PPOConfig()
.env_runners(
env_runner_cls=ForwardHealthCheckToEnvWorker,
num_env_runners=2,
rollout_fragment_length=16,
)
.rl_module(
model_config=DefaultModelConfig(fcnet_hiddens=[4]),
)
.training(
train_batch_size_per_learner=32,
minibatch_size=32,
)
.environment(env="fault_env")
.evaluation(
evaluation_num_env_runners=2,
evaluation_interval=1,
evaluation_config=PPOConfig.overrides(
env_config={
"evaluation": True,
"p_terminated": 0.0,
"max_episode_len": 20,
# Make both eval EnvRunners fail.
"bad_indices": [1, 2],
# Env throws error between steps 10 and 12.
"failure_start_count": 3,
"failure_stop_count": 4,
"counter": COUNTER_NAME,
},
),
)
.fault_tolerance(
restart_failed_env_runners=True, # And recover
# 0 delay for testing purposes.
delay_between_env_runner_restarts_s=0,
)
)
# Reset interaciton counter.
ray.wait([counter.reset.remote()])
algo = config.build()
# Before train loop, EnvRunners are fresh and not recreated.
self.assertEqual(algo.eval_env_runner_group.num_healthy_remote_workers(), 2)
self.assertEqual(algo.eval_env_runner_group.num_remote_worker_restarts(), 0)
algo.train()
time.sleep(15.0)
algo.restore_env_runners(algo.eval_env_runner_group)
# Everything still healthy. And all EnvRunners are restarted.
self.assertEqual(algo.eval_env_runner_group.num_healthy_remote_workers(), 2)
self.assertEqual(algo.eval_env_runner_group.num_remote_worker_restarts(), 2)
def test_env_runner_failing_recover_with_hanging_env_runners(self):
# Counter that will survive restarts.
COUNTER_NAME = "test_eval_env_runners_fault_but_recover"
counter = Counter.options(name=COUNTER_NAME).remote()
config = (
# First thought: We are using an off-policy algorithm here, b/c we have
# hanging EnvRunners (samples may be delayed, thus off-policy?).
# However, this actually does NOT matter. All synchronously sampling algos
# (whether off- or on-policy) now have a sampling timeout to NOT block
# the execution of the algorithm b/c of a single heavily stalling EnvRunner.
# Timeout data (batches or episodes) are discarded.
SACConfig()
.env_runners(
env_runner_cls=ForwardHealthCheckToEnvWorker,
num_env_runners=3,
rollout_fragment_length=16,
sample_timeout_s=5.0,
)
.reporting(
# Make sure each iteration doesn't take too long.
min_time_s_per_iteration=0.5,
# Make sure metrics reporting doesn't hang for too long
# since we will have a hanging EnvRunner.
metrics_episode_collection_timeout_s=1,
)
.environment(
env="fault_env",
env_config={
"action_space": gym.spaces.Box(0, 1, (2,), np.float32),
"evaluation": True,
"p_terminated": 0.0,
"max_episode_len": 20,
# EnvRunners 1 and 2 will fail in step().
"bad_indices": [1, 2],
# Env throws error between steps 3 and 4.
"failure_start_count": 3,
"failure_stop_count": 4,
"counter": COUNTER_NAME,
# EnvRunner 2 will hang for long time during init after restart.
"init_delay": 3600,
"init_delay_indices": [2],
# EnvRunner 3 will hang in env.step().
"step_delay": 3600,
"step_delay_indices": [3],
},
)
.fault_tolerance(
restart_failed_env_runners=True, # And recover
env_runner_health_probe_timeout_s=0.01,
env_runner_restore_timeout_s=5,
delay_between_env_runner_restarts_s=0, # For testing, no delay.
)
)
# Reset interaciton counter.
ray.wait([counter.reset.remote()])
algo = config.build()
# Before train loop, EnvRunners are fresh and not recreated.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 3)
self.assertEqual(algo.env_runner_group.num_remote_worker_restarts(), 0)
algo.train()
time.sleep(15.0)
# Most importantly, training progresses fine b/c the stalling EnvRunner is
# ignored via a timeout.
algo.train()
# 2 healthy remote EnvRunners left, although EnvRunner 3 is stuck in rollout.
self.assertEqual(algo.env_runner_group.num_healthy_remote_workers(), 2)
# Only 1 successful restore, since EnvRunner 2 is stuck in indefinite init
# and can not be properly restored.
self.assertEqual(algo.env_runner_group.num_remote_worker_restarts(), 1)
def test_eval_env_runners_on_infinite_episodes(self):
"""Tests whether eval EnvRunners warn appropriately after episode timeout."""
# Create infinitely running episodes, but with horizon setting (RLlib will
# auto-terminate the episode). However, in the eval EnvRunners, don't set a
# horizon -> Expect warning and no proper evaluation results.
config = (
PPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment(RandomEnv, env_config={"p_terminated": 0.0})
.training(train_batch_size_per_learner=200)
.evaluation(
evaluation_num_env_runners=1,
evaluation_interval=1,
evaluation_sample_timeout_s=2.0,
)
)
algo = config.build()
results = algo.train()
self.assertTrue(
np.isnan(
results[EVALUATION_RESULTS][ENV_RUNNER_RESULTS][EPISODE_RETURN_MEAN]
)
)
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))