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
@@ -0,0 +1,35 @@
# @OldAPIStack
atari-dist-dqn:
env:
grid_search:
- ale_py:ALE/Breakout-v5
- ale_py:ALE/BeamRider-v5
- ale_py:ALE/Qbert-v5
- ale_py:ALE/SpaceInvaders-v5
run: DQN
config:
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
double_q: false
dueling: false
num_atoms: 51
noisy: false
replay_buffer_config:
type: MultiAgentReplayBuffer
capacity: 1000000
num_steps_sampled_before_learning_starts: 20000
n_step: 1
target_network_update_freq: 8000
lr: .0000625
adam_epsilon: .00015
hiddens: [512]
rollout_fragment_length: 4
train_batch_size: 32
exploration_config:
epsilon_timesteps: 200000
final_epsilon: 0.01
num_gpus: 0.2
min_sample_timesteps_per_iteration: 10000
@@ -0,0 +1,39 @@
# @OldAPIStack
# Runs on a single g3.4xl node
# See https://github.com/ray-project/rl-experiments for results
atari-basic-dqn:
env:
grid_search:
- ale_py:ALE/Breakout-v5
- ale_py:ALE/BeamRider-v5
- ale_py:ALE/Qbert-v5
- ale_py:ALE/SpaceInvaders-v5
run: DQN
config:
# Works for both torch and tf.
framework: torch
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
double_q: false
dueling: false
num_atoms: 1
noisy: false
replay_buffer_config:
type: MultiAgentReplayBuffer
capacity: 1000000
num_steps_sampled_before_learning_starts: 20000
n_step: 1
target_network_update_freq: 8000
lr: .0000625
adam_epsilon: .00015
hiddens: [512]
rollout_fragment_length: 4
train_batch_size: 32
exploration_config:
epsilon_timesteps: 200000
final_epsilon: 0.01
num_gpus: 0.2
min_sample_timesteps_per_iteration: 10000
@@ -0,0 +1,39 @@
# @OldAPIStack
# Runs on a single g3.4xl node
# See https://github.com/ray-project/rl-experiments for results
dueling-ddqn:
env:
grid_search:
- ale_py:ALE/Breakout-v5
- ale_py:ALE/BeamRider-v5
- ale_py:ALE/Qbert-v5
- ale_py:ALE/SpaceInvaders-v5
run: DQN
config:
# Works for both torch and tf.
framework: torch
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
double_q: true
dueling: true
num_atoms: 1
noisy: false
replay_buffer_config:
type: MultiAgentReplayBuffer
capacity: 1000000
num_steps_sampled_before_learning_starts: 20000
n_step: 1
target_network_update_freq: 8000
lr: .0000625
adam_epsilon: .00015
hiddens: [512]
rollout_fragment_length: 4
train_batch_size: 32
exploration_config:
epsilon_timesteps: 200000
final_epsilon: 0.01
num_gpus: 0.2
min_sample_timesteps_per_iteration: 10000
@@ -0,0 +1,28 @@
# @OldAPIStack
# Runs on a g3.16xl node with 5 m5.24xl workers
# Takes roughly 10 minutes.
atari-impala:
env:
grid_search:
- ale_py:ALE/Breakout-v5
- ale_py:ALE/BeamRider-v5
- ale_py:ALE/Qbert-v5
- ale_py:ALE/SpaceInvaders-v5
run: IMPALA
stop:
timesteps_total: 3000000
config:
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
rollout_fragment_length: 50
train_batch_size: 500
num_env_runners: 128
num_envs_per_env_runner: 5
clip_rewards: True
lr_schedule: [
[0, 0.0005],
[20000000, 0.000000000001],
]
@@ -0,0 +1,25 @@
# @OldAPIStack
# Runs on a p2.8xlarge single head node machine.
# Should reach ~400 reward in about 1h and after 15-20M ts.
atari-impala:
env: ale_py:ALE/Breakout-v5
run: IMPALA
config:
# Works for both torch and tf.
framework: torch
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
rollout_fragment_length: 50
train_batch_size: 4000
num_gpus: 4
num_env_runners: 31
num_gpus_per_env_runner: 0 # works also for partial GPUs (<1.0) per worker
num_envs_per_env_runner: 5
clip_rewards: True
lr_schedule: [
[0, 0.0005],
[20000000, 0.000000000001],
]
@@ -0,0 +1,26 @@
# @OldAPIStack
# Runs on a g3.16xl node with 3 m4.16xl workers
# See https://github.com/ray-project/rl-experiments for results
atari-impala:
env:
grid_search:
- ale_py:ALE/Breakout-v5
- ale_py:ALE/BeamRider-v5
- ale_py:ALE/Qbert-v5
- ale_py:ALE/SpaceInvaders-v5
run: IMPALA
config:
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
rollout_fragment_length: 50
train_batch_size: 500
num_env_runners: 32
num_envs_per_env_runner: 5
clip_rewards: True
lr_schedule: [
[0, 0.0005],
[20000000, 0.000000000001],
]
@@ -0,0 +1,55 @@
# @OldAPIStack
# Run e.g. on a g3.16xlarge (4 GPUs) with `num_gpus=1` (1 for each trial;
# MsPacman torch + tf; Pong torch + tf).
# Uses the hyperparameters published in [2] (see rllib/algorithms/sac/README.md).
atari-sac-tf-and-torch:
env:
grid_search:
- ale_py:ALE/MsPacman-v5
- ale_py:ALE/Pong-v5
run: SAC
stop:
timesteps_total: 20000000
config:
# Works for both torch and tf.
framework:
grid_search: [tf, torch]
env_config:
frameskip: 1 # no frameskip
gamma: 0.99
q_model_config:
hidden_activation: relu
hidden_layer_sizes: [512]
policy_model_config:
hidden_activation: relu
hidden_layer_sizes: [512]
# Do hard syncs.
# Soft-syncs seem to work less reliably for discrete action spaces.
tau: 1.0
target_network_update_freq: 8000
# auto = 0.98 * -log(1/|A|)
target_entropy: auto
clip_rewards: 1.0
n_step: 1
rollout_fragment_length: 1
replay_buffer_config:
type: MultiAgentPrioritizedReplayBuffer
capacity: 1000000
# How many steps of the model to sample before learning starts.
# If True prioritized replay buffer will be used.
prioritized_replay_alpha: 0.6
prioritized_replay_beta: 0.4
prioritized_replay_eps: 1e-6
num_steps_sampled_before_learning_starts: 100000
train_batch_size: 64
min_sample_timesteps_per_iteration: 4
# Paper uses 20k random timesteps, which is not exactly the same, but
# seems to work nevertheless. We use 100k here for the longer Atari
# runs (DQN style: filling up the buffer a bit before learning).
optimization:
actor_learning_rate: 0.0003
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0003
num_env_runners: 0
num_gpus: 1
metrics_num_episodes_for_smoothing: 5
@@ -0,0 +1,44 @@
# @OldAPIStack
from ray import tune
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
stop = {
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 400,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 200000,
}
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("CartPole-v1")
# Switch on >1 loss/optimizer API for TFPolicy and EagerTFPolicy.
.experimental(_tf_policy_handles_more_than_one_loss=True)
.training(
# APPO will produce two separate loss terms: policy loss + value function loss.
_separate_vf_optimizer=True,
# Separate learning rate (and schedule) for the value function branch.
_lr_vf=tune.grid_search([0.00075, [[0, 0.00075], [100000, 0.0003]]]),
num_epochs=6,
# `vf_loss_coeff` will be ignored anyways as we use separate loss terms.
vf_loss_coeff=0.01,
vtrace=True,
model={
# Make sure we really have completely separate branches.
"vf_share_layers": False,
},
)
.env_runners(
num_envs_per_env_runner=5,
num_env_runners=1,
observation_filter="MeanStdFilter",
)
.resources(num_gpus=0)
)
@@ -0,0 +1,26 @@
# @OldAPIStack
# To generate training data, first run:
# $ ./train.py --run=PPO --env=CartPole-v1 \
# --stop='{"timesteps_total": 50000}' \
# --config='{"output": "dataset", "output_config": {"format": "json", "path": "/tmp/out", "max_num_samples_per_file": 1}, "batch_mode": "complete_episodes"}'
cartpole-bc:
env: CartPole-v1
run: BC
stop:
timesteps_total: 500000
config:
# Works for both torch and tf.
framework: torch
enable_rl_module_and_learner: false
enable_env_runner_and_connector_v2: false
# In order to evaluate on an actual environment, use these following
# settings:
evaluation_num_env_runners: 1
evaluation_interval: 1
evaluation_config:
input: sampler
# The historic (offline) data file from the PPO run (at the top).
input: dataset
input_config:
format: json
paths: /tmp/out
@@ -0,0 +1,82 @@
# @OldAPIStack
"""
Tests, whether APPO can learn in a fault-tolerant fashion.
Workers will be configured to automatically get recreated upon failures (here: within
the environment).
The environment we use here is configured to crash with a certain probability on each
`step()` and/or `reset()` call. Additionally, the environment is configured to stall
with a configured probability on each `step()` call for a certain amount of time.
"""
from gymnasium.wrappers import TimeLimit
from ray import tune
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.examples.envs.classes.cartpole_crashing import CartPoleCrashing
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
EVALUATION_RESULTS,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
tune.register_env(
"env",
lambda cfg: TimeLimit(CartPoleCrashing(cfg), max_episode_steps=500),
)
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment(
"env",
env_config={
"p_crash": 0.0001, # prob to crash during step()
"p_crash_reset": 0.001, # prob to crash during reset()
"crash_on_worker_indices": [1, 2],
"init_time_s": 2.0,
"p_stall": 0.0005, # prob to stall during step()
"p_stall_reset": 0.001, # prob to stall during reset()
"stall_time_sec": (2, 5), # stall between 2 and 10sec.
"stall_on_worker_indices": [2, 3],
},
)
.env_runners(
num_env_runners=1,
num_envs_per_env_runner=1,
)
# Switch on resiliency (recreate any failed worker).
.fault_tolerance(
restart_failed_env_runners=True,
)
.evaluation(
evaluation_num_env_runners=4,
evaluation_interval=1,
evaluation_duration=25,
evaluation_duration_unit="episodes",
evaluation_parallel_to_training=True,
evaluation_config=APPOConfig.overrides(
explore=False,
env_config={
# Make eval workers solid.
# This test is to prove that we can learn with crashing envs,
# not evaluate with crashing envs.
"p_crash": 0.0,
"p_crash_reset": 0.0,
"init_time_s": 0.0,
"p_stall": 0.01,
"stall_time_sec": 300, # stall for 5min.
"p_stall_reset": 0.0,
"stall_on_worker_indices": [1, 2],
},
),
)
)
stop = {
f"{EVALUATION_RESULTS}/{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 500.0,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 2000000,
}
@@ -0,0 +1,69 @@
# @OldAPIStack
"""
Tests, whether APPO can learn in a fault-tolerant fashion.
Workers will be configured to automatically get recreated upon failures (here: within
the environment).
The environment we use here is configured to crash with a certain probability on each
`step()` and/or `reset()` call.
"""
from ray import tune
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.examples.envs.classes.cartpole_crashing import CartPoleCrashing
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
EVALUATION_RESULTS,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
tune.register_env("env", lambda cfg: CartPoleCrashing(cfg))
stop = {
f"{EVALUATION_RESULTS}/{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 400.0,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 250000,
}
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment(
"env",
env_config={
# Crash roughly every 500 ts.
"p_crash": 0.0005, # prob to crash during step()
"p_crash_reset": 0.005, # prob to crash during reset()
"crash_on_worker_indices": [1, 2],
},
)
.env_runners(
num_env_runners=3,
num_envs_per_env_runner=1,
)
# Switch on resiliency (recreate any failed worker).
.fault_tolerance(
restart_failed_env_runners=True,
)
.evaluation(
evaluation_num_env_runners=1,
evaluation_interval=1,
evaluation_duration=25,
evaluation_duration_unit="episodes",
evaluation_parallel_to_training=True,
evaluation_config=APPOConfig.overrides(
explore=False,
env_config={
# Make eval workers solid.
# This test is to prove that we can learn with crashing envs,
# not evaluate with crashing envs.
"p_crash": 0.0,
"p_crash_reset": 0.0,
"init_time_s": 0.0,
},
),
)
)
@@ -0,0 +1,20 @@
# @OldAPIStack
cartpole-dqn-fake-gpus:
env: CartPole-v1
run: DQN
stop:
env_runners/episode_return_mean: 150
training_iteration: 400
config:
# Works for both torch and tf.
framework: torch
model:
fcnet_hiddens: [64]
fcnet_activation: linear
n_step: 3
# Double batch size (2 GPUs).
train_batch_size: 64
# Fake 2 GPUs.
num_gpus: 2
_fake_gpus: true
@@ -0,0 +1,20 @@
# @OldAPIStack
cartpole-dqn-w-param-noise:
env: CartPole-v1
run: DQN
stop:
env_runners/episode_return_mean: 150
timesteps_total: 300000
config:
# Works for both torch and tf.
framework: torch
exploration_config:
type: ParameterNoise
random_timesteps: 10000
initial_stddev: 1.0
batch_mode: complete_episodes
lr: 0.0008
num_env_runners: 0
model:
fcnet_hiddens: [32, 32]
fcnet_activation: tanh
@@ -0,0 +1,17 @@
# @OldAPIStack
cartpole-dqn:
env: CartPole-v1
run: DQN
stop:
env_runners/episode_return_mean: 150
timesteps_total: 100000
config:
# Works for both torch and tf.
framework: torch
model:
fcnet_hiddens: [64]
fcnet_activation: linear
n_step: 3
exploration_config:
type: SoftQ
temperature: 0.5
@@ -0,0 +1,14 @@
# @OldAPIStack
cartpole-dqn:
env: CartPole-v1
run: DQN
stop:
env_runners/episode_return_mean: 100
timesteps_total: 100000
config:
# Works for both torch and tf.
framework: torch
model:
fcnet_hiddens: [64]
fcnet_activation: linear
n_step: 3
@@ -0,0 +1,44 @@
# @OldAPIStack
from ray.rllib.algorithms.impala import IMPALAConfig
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
stop = {
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 150,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 200000,
}
config = (
IMPALAConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("CartPole-v1")
# Switch on >1 loss/optimizer API for TFPolicy and EagerTFPolicy.
.experimental(_tf_policy_handles_more_than_one_loss=True)
.training(
# IMPALA will produce two separate loss terms: policy loss + value function
# loss.
_separate_vf_optimizer=True,
# Separate learning rate for the value function branch.
_lr_vf=0.00075,
num_epochs=6,
# `vf_loss_coeff` will be ignored anyways as we use separate loss terms.
vf_loss_coeff=0.01,
vtrace=True,
model={
# Make sure we really have completely separate branches.
"vf_share_layers": False,
},
)
.env_runners(
num_envs_per_env_runner=5,
num_env_runners=1,
observation_filter="MeanStdFilter",
)
.resources(num_gpus=0)
)
@@ -0,0 +1,22 @@
# @OldAPIStack
# To generate training data, first run:
# $ ./train.py --run=PPO --env=CartPole-v1 \
# --stop='{"timesteps_total": 50000}' \
# --config='{"output": "/tmp/out", "batch_mode": "complete_episodes"}'
cartpole-marwil:
env: CartPole-v1
run: MARWIL
stop:
timesteps_total: 500000
config:
# Works for both torch and tf.
framework: torch
# In order to evaluate on an actual environment, use these following
# settings:
evaluation_num_env_runners: 1
evaluation_interval: 1
evaluation_config:
input: sampler
beta: 1.0 # Compare to behavior cloning (beta=0.0).
# The historic (offline) data file from the PPO run (at the top).
input: /tmp/out
@@ -0,0 +1,22 @@
# @OldAPIStack
cartpole-sac:
env: CartPole-v1
run: SAC
stop:
env_runners/episode_return_mean: 150.0
timesteps_total: 100000
config:
# Works for both torch and tf.
framework: torch
gamma: 0.95
target_network_update_freq: 32
tau: 1.0
# initial_alpha: 0.5
train_batch_size: 32
optimization:
actor_learning_rate: 0.005
critic_learning_rate: 0.005
entropy_learning_rate: 0.0001
# grad_norm_clipping: 40.0
# evaluation_config:
# explore: true
@@ -0,0 +1,33 @@
# @OldAPIStack
frozenlake-appo-vtrace:
env: FrozenLake-v1
run: APPO
stop:
env_runners/episode_return_mean: 0.99
timesteps_total: 1000000
config:
# Works for both torch and tf.
framework: torch
# Sparse reward environment (short horizon).
env_config:
desc:
- SFFFFFFF
- FFFFFFFF
- FFFFFFFF
- FFFFFFFF
- FFFFFFFF
- FFFFFFFF
- FFFFFFFF
- FFFFFFFG
is_slippery: false
horizon: 20
rollout_fragment_length: 10
batch_mode: complete_episodes
vtrace: true
num_envs_per_env_runner: 5
num_env_runners: 4
num_gpus: 0
num_epochs: 1
vf_loss_coeff: 0.01
@@ -0,0 +1,50 @@
# @OldAPIStack
halfcheetah_bc:
env:
grid_search:
#- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_random
#- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_medium
- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_expert
#- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_medium_replay
run: CQL
config:
# SAC Configs
#input: d4rl.halfcheetah-random-v0
#input: d4rl.halfcheetah-medium-v0
input: d4rl.halfcheetah-expert-v0
#input: d4rl.halfcheetah-medium-replay-v0
framework: torch
q_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
policy_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
tau: 0.005
target_entropy: auto
n_step: 1
rollout_fragment_length: 1
replay_buffer_config:
type: MultiAgentReplayBuffer
num_steps_sampled_before_learning_starts: 10
train_batch_size: 256
target_network_update_freq: 0
min_train_timesteps_per_iteration: 1000
optimization:
actor_learning_rate: 0.0001
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0001
num_env_runners: 0
num_gpus: 1
clip_actions: false
normalize_actions: true
evaluation_interval: 1
metrics_num_episodes_for_smoothing: 5
# CQL Configs
min_q_weight: 5.0
bc_iters: 200000000
temperature: 1.0
num_actions: 10
lagrangian: False
evaluation_config:
input: sampler
@@ -0,0 +1,51 @@
# @OldAPIStack
halfcheetah_cql:
env:
grid_search:
#- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_random
#- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_medium
- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_expert
#- ray.rllib.examples.envs.classes.d4rl_env.halfcheetah_medium_replay
run: CQL
config:
# SAC Configs
#input: d4rl.halfcheetah-random-v0
#input: d4rl.halfcheetah-medium-v0
input: d4rl.halfcheetah-expert-v0
#input: d4rl.halfcheetah-medium-replay-v0
# Works for both torch and tf.
framework: torch
q_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
policy_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
tau: 0.005
target_entropy: auto
n_step: 3
rollout_fragment_length: 1
replay_buffer_config:
type: MultiAgentReplayBuffer
num_steps_sampled_before_learning_starts: 256
train_batch_size: 256
target_network_update_freq: 0
min_train_timesteps_per_iteration: 1000
optimization:
actor_learning_rate: 0.0001
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0001
num_env_runners: 0
num_gpus: 1
metrics_num_episodes_for_smoothing: 5
# CQL Configs
min_q_weight: 5.0
bc_iters: 20000
temperature: 1.0
num_actions: 10
lagrangian: False
evaluation_interval: 3
evaluation_config:
input: sampler
@@ -0,0 +1,26 @@
# @OldAPIStack
halfcheetah-ppo:
env: HalfCheetah-v2
run: PPO
stop:
env_runners/episode_return_mean: 9800
time_total_s: 10800
config:
# Works for both torch and tf.
framework: torch
gamma: 0.99
lambda: 0.95
kl_coeff: 1.0
num_epochs: 32
lr: .0003
vf_loss_coeff: 0.5
clip_param: 0.2
minibatch_size: 4096
train_batch_size: 65536
num_env_runners: 16
num_gpus: 1
grad_clip: 0.5
num_envs_per_env_runner:
grid_search: [16, 32]
batch_mode: truncate_episodes
observation_filter: MeanStdFilter
@@ -0,0 +1,50 @@
# @OldAPIStack
hopper_bc:
env:
grid_search:
- ray.rllib.examples.envs.classes.d4rl_env.hopper_random
#- ray.rllib.examples.envs.classes..d4rl_env.hopper_medium
#- ray.rllib.examples.envs.classes..d4rl_env.hopper_expert
#- ray.rllib.examples.envs.classes..d4rl_env.hopper_medium_replay
run: CQL
config:
# SAC Configs
input: d4rl.hopper-random-v0
#input: d4rl.hopper-medium-v0
#input: d4rl.hopper-expert-v0
#input: d4rl.hopper-medium-replay-v0
framework: torch
q_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
policy_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
tau: 0.005
target_entropy: auto
n_step: 1
rollout_fragment_length: 1
replay_buffer_config:
type: MultiAgentReplayBuffer
num_steps_sampled_before_learning_starts: 10
train_batch_size: 256
target_network_update_freq: 0
min_train_timesteps_per_iteration: 1000
optimization:
actor_learning_rate: 0.0001
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0001
num_env_runners: 0
num_gpus: 1
clip_actions: false
normalize_actions: true
evaluation_interval: 1
metrics_num_episodes_for_smoothing: 5
# CQL Configs
min_q_weight: 5.0
bc_iters: 200000000
temperature: 1.0
num_actions: 10
lagrangian: False
evaluation_config:
input: sampler
@@ -0,0 +1,50 @@
# @OldAPIStack
hopper_cql:
env:
grid_search:
#- ray.rllib.examples.envs.classes.d4rl_env.hopper_random
- ray.rllib.examples.envs.classes.d4rl_env.hopper_medium
#- ray.rllib.examples.envs.classes.d4rl_env.hopper_expert
#- ray.rllib.examples.envs.classes.d4rl_env.hopper_medium_replay
run: CQL
config:
# SAC Configs
#input: d4rl.hopper-random-v0
input: d4rl.hopper-medium-v0
#input: d4rl.hopper-expert-v0
#input: d4rl.hopper-medium-replay-v0
framework: torch
q_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
policy_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256, 256]
tau: 0.005
target_entropy: auto
n_step: 1
rollout_fragment_length: 1
replay_buffer_config:
type: MultiAgentReplayBuffer
num_steps_sampled_before_learning_starts: 10
train_batch_size: 256
target_network_update_freq: 0
min_train_timesteps_per_iteration: 1000
optimization:
actor_learning_rate: 0.0001
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0001
num_env_runners: 0
num_gpus: 1
clip_actions: false
normalize_actions: true
evaluation_interval: 1
metrics_num_episodes_for_smoothing: 5
# CQL Configs
min_q_weight: 5.0
bc_iters: 20000
temperature: 1.0
num_actions: 10
lagrangian: False
evaluation_config:
input: sampler
@@ -0,0 +1,17 @@
# @OldAPIStack
hopper-ppo:
env: Hopper-v1
run: PPO
config:
# Works for both torch and tf.
framework: torch
gamma: 0.995
kl_coeff: 1.0
num_epochs: 20
lr: .0001
minibatch_size: 32768
train_batch_size: 160000
num_env_runners: 64
num_gpus: 4
batch_mode: complete_episodes
observation_filter: MeanStdFilter
@@ -0,0 +1,24 @@
# @OldAPIStack
humanoid-ppo-gae:
env: Humanoid-v1
run: PPO
stop:
env_runners/episode_return_mean: 6000
config:
# Works for both torch and tf.
framework: torch
gamma: 0.995
lambda: 0.95
clip_param: 0.2
kl_coeff: 1.0
num_epochs: 20
lr: .0001
minibatch_size: 32768
horizon: 5000
train_batch_size: 320000
model:
free_log_std: true
num_env_runners: 64
num_gpus: 4
batch_mode: complete_episodes
observation_filter: MeanStdFilter
@@ -0,0 +1,22 @@
# @OldAPIStack
humanoid-ppo:
env: Humanoid-v1
run: PPO
stop:
env_runners/episode_return_mean: 6000
config:
# Works for both torch and tf.
framework: torch
gamma: 0.995
kl_coeff: 1.0
num_epochs: 20
lr: .0001
minibatch_size: 32768
train_batch_size: 320000
model:
free_log_std: true
use_gae: false
num_env_runners: 64
num_gpus: 4
batch_mode: complete_episodes
observation_filter: MeanStdFilter
@@ -0,0 +1,15 @@
# @OldAPIStack
memory-leak-test-appo:
env:
ray.rllib.examples.envs.classes.random_env.RandomLargeObsSpaceEnv
run: APPO
config:
# Works for both torch and tf.
framework: torch
# Switch off np.random, which is known to have memory leaks.
env_config:
config:
static_samples: true
num_env_runners: 4
num_envs_per_env_runner: 5
rollout_fragment_length: 20
@@ -0,0 +1,14 @@
# @OldAPIStack
memory-leak-test-dqn:
env:
ray.rllib.examples.envs.classes.random_env.RandomLargeObsSpaceEnv
run: DQN
config:
# Works for both torch and tf.
framework: torch
# Switch off np.random, which is known to have memory leaks.
env_config:
config:
static_samples: true
replay_buffer_config:
capacity: 500 # use small buffer to catch memory leaks
@@ -0,0 +1,17 @@
# @OldAPIStack
memory-leak-test-ppo:
env:
ray.rllib.examples.envs.classes.random_env.RandomLargeObsSpaceEnv
run: PPO
config:
# Works for both torch and tf.
framework: torch
# Switch off np.random, which is known to have memory leaks.
env_config:
config:
static_samples: true
num_env_runners: 4
num_envs_per_env_runner: 5
train_batch_size: 500
minibatch_size: 256
num_epochs: 5
@@ -0,0 +1,14 @@
# @OldAPIStack
memory-leak-test-sac:
env:
ray.rllib.examples.envs.classes.random_env.RandomLargeObsSpaceEnvContActions
run: SAC
config:
# Works for both torch and tf.
framework: torch
# Switch off np.random, which is known to have memory leaks.
env_config:
config:
static_samples: true
replay_buffer_config:
capacity: 500 # use small buffer to catch memory leaks
@@ -0,0 +1,45 @@
# @OldAPIStack
# Our implementation of SAC discrete can reach up
# to ~750 reward in 40k timesteps. Run e.g. on a g3.4xlarge with `num_gpus=1`.
# Uses the hyperparameters published in [2] (see rllib/algorithms/sac/README.md).
mspacman-sac-tf:
env: ale_py:ALE/MsPacman-v5
run: SAC
stop:
env_runners/episode_return_mean: 800
timesteps_total: 100000
config:
# Works for both torch and tf.
framework: torch
env_config:
frameskip: 1 # no frameskip
gamma: 0.99
q_model_config:
fcnet_hiddens: [512]
fcnet_activation: relu
policy_model_config:
fcnet_hiddens: [512]
fcnet_activation: relu
# Do hard syncs.
# Soft-syncs seem to work less reliably for discrete action spaces.
tau: 1.0
target_network_update_freq: 8000
# paper uses: 0.98 * -log(1/|A|)
target_entropy: 1.755
clip_rewards: 1.0
n_step: 1
rollout_fragment_length: 1
train_batch_size: 64
min_sample_timesteps_per_iteration: 4
# Paper uses 20k random timesteps, which is not exactly the same, but
# seems to work nevertheless.
replay_buffer_config:
type: MultiAgentPrioritizedReplayBuffer
num_steps_sampled_before_learning_starts: 20000
optimization:
actor_learning_rate: 0.0003
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0003
num_env_runners: 0
num_gpus: 0
metrics_num_episodes_for_smoothing: 5
@@ -0,0 +1,77 @@
# @OldAPIStack
"""
Tests, whether APPO can learn in a fault-tolerant fashion in a
multi-agent setting.
Workers will be configured to automatically get recreated upon failures (here: within
the environment).
The environment we use here is configured to crash with a certain probability on each
`step()` and/or `reset()` call.
"""
from ray import tune
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.examples.envs.classes.cartpole_crashing import MultiAgentCartPoleCrashing
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
EVALUATION_RESULTS,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
tune.register_env("ma_env", lambda cfg: MultiAgentCartPoleCrashing(cfg))
stop = {
f"{EVALUATION_RESULTS}/{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 800.0,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 250000,
}
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment(
"ma_env",
env_config={
"num_agents": 2,
# Crash roughly every 300 ts. This should be ok to measure 180.0
# reward (episodes are 200 ts long).
"p_crash": 0.00005, # prob to crash during step()
"p_crash_reset": 0.0005, # prob to crash during reset()
"init_time_s": 2.0,
"p_stall": 0.001, # prob to stall during step()
"p_stall_reset": 0.001, # prob to stall during reset()
"stall_time_sec": (2, 5), # stall between 2 and 10sec.
"stall_on_worker_indices": [2, 3],
},
)
.env_runners(
num_env_runners=3,
num_envs_per_env_runner=1,
)
# Switch on resiliency (recreate any failed worker).
.fault_tolerance(
restart_failed_env_runners=True,
)
.evaluation(
evaluation_num_env_runners=1,
evaluation_interval=1,
evaluation_duration=25,
evaluation_duration_unit="episodes",
evaluation_parallel_to_training=True,
evaluation_config=APPOConfig.overrides(
explore=False,
env_config={
# Make eval workers solid.
# This test is to prove that we can learn with crashing envs,
# not evaluate with crashing envs.
"p_crash": 0.0,
"p_crash_reset": 0.0,
"init_time_s": 0.0,
"p_stall": 0.0,
"p_stall_reset": 0.0,
},
),
)
)
@@ -0,0 +1,70 @@
# @OldAPIStack
"""
Tests, whether APPO can learn in a fault-tolerant fashion in a
multi-agent setting.
Workers will be configured to automatically get recreated upon failures (here: within
the environment).
The environment we use here is configured to crash with a certain probability on each
`step()` and/or `reset()` call.
"""
from ray import tune
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.examples.envs.classes.cartpole_crashing import MultiAgentCartPoleCrashing
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
EVALUATION_RESULTS,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
tune.register_env("ma_env", lambda cfg: MultiAgentCartPoleCrashing(cfg))
stop = {
f"{EVALUATION_RESULTS}/{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 800.0,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 250000,
}
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment(
"ma_env",
env_config={
"num_agents": 2,
# Crash roughly every 300 ts. This should be ok to measure 180.0
# reward (episodes are 200 ts long).
"p_crash": 0.0005, # prob to crash during step()
"p_crash_reset": 0.005, # prob to crash during reset()
},
)
.env_runners(
num_env_runners=4,
num_envs_per_env_runner=1,
)
# Switch on resiliency (recreate any failed worker).
.fault_tolerance(
restart_failed_env_runners=True,
)
.evaluation(
evaluation_num_env_runners=1,
evaluation_interval=1,
evaluation_duration=25,
evaluation_duration_unit="episodes",
evaluation_parallel_to_training=True,
evaluation_config=APPOConfig.overrides(
explore=False,
env_config={
# Make eval workers solid.
# This test is to prove that we can learn with crashing envs,
# not evaluate with crashing envs.
"p_crash": 0.0,
"p_crash_reset": 0.0,
"init_time_s": 0.0,
},
),
)
)
@@ -0,0 +1,83 @@
# @OldAPIStack
import numpy as np
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EVALUATION_RESULTS,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.tune.registry import register_env
register_env("multi_cartpole", lambda _: MultiAgentCartPole({"num_agents": 2}))
# Number of policies overall in the PolicyMap.
num_policies = 20
# Number of those policies that should be trained. These are a subset of `num_policies`.
num_trainable = 10
num_envs_per_env_runner = 5
# Define the config as an APPOConfig object.
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("multi_cartpole")
.env_runners(
num_env_runners=4,
num_envs_per_env_runner=num_envs_per_env_runner,
observation_filter="MeanStdFilter",
)
.training(
model={
"fcnet_hiddens": [32],
"fcnet_activation": "linear",
"vf_share_layers": True,
},
num_epochs=1,
vf_loss_coeff=0.005,
vtrace=True,
)
.multi_agent(
# 2 agents per sub-env.
# This is to avoid excessive swapping during an episode rollout, since
# Policies are only re-picked at the beginning of each episode.
policy_map_capacity=2 * num_envs_per_env_runner,
policy_states_are_swappable=True,
policies={f"pol{i}" for i in range(num_policies)},
# Train only the first n policies.
policies_to_train=[f"pol{i}" for i in range(num_trainable)],
# Pick one trainable and one non-trainable policy per episode.
policy_mapping_fn=(
lambda aid, eps, worker, **kw: "pol"
+ str(
np.random.randint(0, num_trainable)
if aid == 0
else np.random.randint(num_trainable, num_policies)
)
),
)
# On the eval track, always let policy 0 play so we get its results in each results
# dict.
.evaluation(
evaluation_config=APPOConfig.overrides(
policy_mapping_fn=(
lambda aid, eps, worker, **kw: "pol"
+ str(0 if aid == 0 else np.random.randint(num_trainable, num_policies))
),
),
evaluation_num_env_runners=2,
evaluation_interval=1,
evaluation_parallel_to_training=True,
)
)
# Define some stopping criteria.
stop = {
f"{EVALUATION_RESULTS}/{ENV_RUNNER_RESULTS}/policy_reward_mean/pol0": 50.0,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 500000,
}
@@ -0,0 +1,46 @@
# @OldAPIStack
from ray import tune
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
tune.registry.register_env("env", lambda cfg: MultiAgentCartPole(config=cfg))
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("env", env_config={"num_agents": 4})
.env_runners(
num_envs_per_env_runner=5,
num_env_runners=4,
observation_filter="MeanStdFilter",
)
.resources(num_gpus=1, _fake_gpus=True)
.multi_agent(
policies=["p0", "p1", "p2", "p3"],
policy_mapping_fn=(lambda agent_id, episode, worker, **kwargs: f"p{agent_id}"),
)
.training(
num_epochs=1,
vf_loss_coeff=0.005,
vtrace=True,
model={
"fcnet_hiddens": [32],
"fcnet_activation": "linear",
"vf_share_layers": True,
},
)
)
stop = {
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 600, # 600 / 4 (==num_agents) = 150
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 200000,
}
@@ -0,0 +1,47 @@
# @OldAPIStack
from ray import tune
from ray.rllib.algorithms.impala import IMPALAConfig
from ray.rllib.examples.envs.classes.multi_agent import MultiAgentCartPole
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
tune.registry.register_env("env", lambda cfg: MultiAgentCartPole(config=cfg))
config = (
IMPALAConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("env", env_config={"num_agents": 4})
.env_runners(
num_envs_per_env_runner=5,
num_env_runners=4,
observation_filter="MeanStdFilter",
)
.resources(num_gpus=1, _fake_gpus=True)
.multi_agent(
policies=["p0", "p1", "p2", "p3"],
policy_mapping_fn=(lambda agent_id, episode, worker, **kwargs: f"p{agent_id}"),
)
.training(
num_epochs=1,
vf_loss_coeff=0.005,
vtrace=True,
model={
"fcnet_hiddens": [32],
"fcnet_activation": "linear",
"vf_share_layers": True,
},
replay_proportion=0.0,
)
)
stop = {
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": 600, # 600 / 4 (==num_agents) = 150
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": 200000,
}
@@ -0,0 +1,44 @@
# @OldAPIStack
# Given a SAC-generated offline file generated via:
# rllib train -f examples/algorithms/sac/pendulum-sac.yaml --no-ray-ui
# Pendulum CQL can attain ~ -300 reward in 10k from that file.
pendulum-cql:
env: Pendulum-v1
run: CQL
stop:
evaluation/env_runners/episode_return_mean: -700
timesteps_total: 800000
config:
# Works for both torch and tf.
framework: torch
# Set seed.
seed: 0
# Use one or more offline files or "input: sampler" for online learning.
input: 'dataset'
input_config:
paths: ["offline/tests/data/pendulum/enormous.zip"]
format: 'json'
# Our input file above comes from an SAC run. Actions in there
# are already normalized (produced by SquashedGaussian).
actions_in_input_normalized: true
clip_actions: true
twin_q: true
train_batch_size: 2000
bc_iters: 100
num_env_runners: 2
min_time_s_per_iteration: 10
metrics_num_episodes_for_smoothing: 5
# Evaluate in an actual environment.
evaluation_interval: 1
evaluation_num_env_runners: 2
evaluation_duration: 10
evaluation_parallel_to_training: true
evaluation_config:
input: sampler
explore: False
@@ -0,0 +1,35 @@
# @OldAPIStack
# Pendulum SAC can attain -150+ reward in 6-7k
# Configurations are the similar to original softlearning/sac codebase
pendulum-sac:
env: Pendulum-v1
run: SAC
stop:
env_runners/episode_return_mean: -250
timesteps_total: 10000
config:
# Works for both torch and tf.
framework: torch
q_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256]
policy_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256]
tau: 0.005
target_entropy: auto
n_step: 1
rollout_fragment_length: 1
train_batch_size: 256
target_network_update_freq: 1
min_sample_timesteps_per_iteration: 1000
replay_buffer_config:
type: MultiAgentPrioritizedReplayBuffer
num_steps_sampled_before_learning_starts: 256
optimization:
actor_learning_rate: 0.0003
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0003
num_env_runners: 0
num_gpus: 0
metrics_num_episodes_for_smoothing: 5
@@ -0,0 +1,31 @@
# @OldAPIStack
# Can expect improvement to -140 reward in ~300-500k timesteps.
pendulum-ppo:
env: ray.rllib.examples.envs.classes.transformed_action_space_env.TransformedActionPendulum
run: PPO
stop:
env_runners/episode_return_mean: -500
timesteps_total: 400000
config:
# Works for both torch and tf.
framework: torch
# Test, whether PPO is able to learn in "distorted" action spaces.
env_config:
config:
low: 300.0
high: 500.0
normalize_actions: true
clip_actions: false
vf_clip_param: 10.0
num_envs_per_env_runner: 20
lambda: 0.1
gamma: 0.95
lr: 0.0003
train_batch_size: 512
minibatch_size: 64
num_epochs: 6
observation_filter: MeanStdFilter
model:
fcnet_activation: relu
@@ -0,0 +1,44 @@
# @OldAPIStack
# TransformedActionPendulum SAC can attain -150+ reward in 6-7k
# Configurations are the similar to original softlearning/sac codebase
transformed-actions-pendulum-sac-dummy-torch:
env: ray.rllib.examples.envs.classes.transformed_action_space_env.TransformedActionPendulum
run: SAC
stop:
env_runners/episode_return_mean: -200
timesteps_total: 10000
config:
# Works for both torch and tf.
seed: 42
framework: torch
# Test, whether SAC is able to learn in "distorted" action spaces.
env_config:
config:
low: 300.0
high: 500.0
horizon: 200
q_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256]
policy_model_config:
fcnet_activation: relu
fcnet_hiddens: [256, 256]
tau: 0.005
target_entropy: auto
n_step: 1
rollout_fragment_length: 1
train_batch_size: 256
target_network_update_freq: 1
min_sample_timesteps_per_iteration: 1000
replay_buffer_config:
type: MultiAgentPrioritizedReplayBuffer
num_steps_sampled_before_learning_starts: 256
optimization:
actor_learning_rate: 0.0003
critic_learning_rate: 0.0003
entropy_learning_rate: 0.0003
num_env_runners: 0
num_gpus: 0
metrics_num_episodes_for_smoothing: 5
@@ -0,0 +1,35 @@
# @OldAPIStack
# You can expect ~20 reward within 1.1m timesteps / 2.1 hours on a K80 GPU
pong-deterministic-dqn:
env: ale_py:ALE/Pong-v5
run: DQN
stop:
env_runners/episode_return_mean: 20
time_total_s: 7200
config:
# Works for both torch and tf.
framework: torch
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
num_gpus: 1
gamma: 0.99
lr: .0001
replay_buffer_config:
type: MultiAgentPrioritizedReplayBuffer
capacity: 50000
num_steps_sampled_before_learning_starts: 10000
rollout_fragment_length: 4
train_batch_size: 32
exploration_config:
epsilon_timesteps: 200000
final_epsilon: .01
model:
grayscale: True
zero_mean: False
dim: 42
# we should set compress_observations to True because few machines
# would be able to contain the replay buffers in memory otherwise
compress_observations: True
@@ -0,0 +1,25 @@
# @OldAPIStack
# This can reach 18-19 reward in ~3 minutes on p3.16xl head w/m4.16xl workers
# 128 workers -> 3 minutes (best case)
# 64 workers -> 4 minutes
# 32 workers -> 7 minutes
# See also: pong-impala.yaml, pong-impala-vectorized.yaml
pong-impala-fast:
env: ale_py:ALE/Pong-v5
run: IMPALA
config:
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
rollout_fragment_length: 50
train_batch_size: 1000
num_env_runners: 128
num_envs_per_env_runner: 5
broadcast_interval: 5
max_sample_requests_in_flight_per_worker: 1
num_multi_gpu_tower_stacks: 4
num_gpus: 2
model:
dim: 42
@@ -0,0 +1,17 @@
# @OldAPIStack
# This can reach 18-19 reward within 10 minutes on a Tesla M60 GPU (e.g., G3 EC2 node)
# with 32 workers and 10 envs per worker. This is more efficient than the non-vectorized
# configuration which requires 128 workers to achieve the same performance.
pong-impala-vectorized:
env: ale_py:ALE/Pong-v5
run: IMPALA
config:
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
rollout_fragment_length: 50
train_batch_size: 500
num_env_runners: 32
num_envs_per_env_runner: 10
@@ -0,0 +1,19 @@
# @OldAPIStack
# This can reach 18-19 reward within 10 minutes on a Tesla M60 GPU (e.g., G3 EC2 node):
# 128 workers -> 8 minutes
# 32 workers -> 17 minutes
# 16 workers -> 40 min+
# See also: pong-impala-fast.yaml, pong-impala-vectorized.yaml
pong-impala:
env: ale_py:ALE/Pong-v5
run: IMPALA
config:
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
rollout_fragment_length: 50
train_batch_size: 500
num_env_runners: 128
num_envs_per_env_runner: 1
@@ -0,0 +1,37 @@
# @OldAPIStack
pong-deterministic-rainbow:
env: ale_py:ALE/Pong-v5
run: DQN
stop:
env_runners/episode_return_mean: 20
config:
# Make analogous to old v4 + NoFrameskip.
env_config:
frameskip: 1
full_action_space: false
repeat_action_probability: 0.0
num_atoms: 51
noisy: True
gamma: 0.99
lr: .0001
hiddens: [512]
rollout_fragment_length: 4
train_batch_size: 32
exploration_config:
epsilon_timesteps: 2
final_epsilon: 0.0
target_network_update_freq: 500
replay_buffer_config:
type: MultiAgentPrioritizedReplayBuffer
prioritized_replay_alpha: 0.5
capacity: 50000
num_steps_sampled_before_learning_starts: 10000
n_step: 3
gpu: True
model:
grayscale: True
zero_mean: False
dim: 42
# we should set compress_observations to True because few machines
# would be able to contain the replay buffers in memory otherwise
compress_observations: True
@@ -0,0 +1,16 @@
# @OldAPIStack
walker2d-v1-ppo:
env: Walker2d-v1
run: PPO
config:
# Works for both torch and tf.
framework: torch
kl_coeff: 1.0
num_epochs: 20
lr: .0001
minibatch_size: 32768
train_batch_size: 320000
num_env_runners: 64
num_gpus: 4
batch_mode: complete_episodes
observation_filter: MeanStdFilter
@@ -0,0 +1,78 @@
# @OldAPIStack
import numpy as np
from gymnasium.spaces import Box, Discrete
from rllib.models.tf.attention_net import TrXLNet
from ray.rllib.utils.framework import try_import_tf
tf1, tf, tfv = try_import_tf()
def bit_shift_generator(seq_length, shift, batch_size):
while True:
values = np.array([0.0, 1.0], dtype=np.float32)
seq = np.random.choice(values, (batch_size, seq_length, 1))
targets = np.squeeze(np.roll(seq, shift, axis=1).astype(np.int32))
targets[:, :shift] = 0
yield seq, targets
def train_loss(targets, outputs):
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=targets, logits=outputs
)
return tf.reduce_mean(loss)
def train_bit_shift(seq_length, num_iterations, print_every_n):
optimizer = tf.keras.optimizers.Adam(1e-3)
model = TrXLNet(
observation_space=Box(low=0, high=1, shape=(1,), dtype=np.int32),
action_space=Discrete(2),
num_outputs=2,
model_config={"max_seq_len": seq_length},
name="trxl",
num_transformer_units=1,
attention_dim=10,
num_heads=5,
head_dim=20,
position_wise_mlp_dim=20,
)
shift = 10
train_batch = 10
test_batch = 100
data_gen = bit_shift_generator(seq_length, shift=shift, batch_size=train_batch)
test_gen = bit_shift_generator(seq_length, shift=shift, batch_size=test_batch)
@tf.function
def update_step(inputs, targets):
model_out = model(
{"obs": inputs},
state=[tf.reshape(inputs, [-1, seq_length, 1])],
seq_lens=np.full(shape=(train_batch,), fill_value=seq_length),
)
optimizer.minimize(
lambda: train_loss(targets, model_out), lambda: model.trainable_variables
)
for i, (inputs, targets) in zip(range(num_iterations), data_gen):
inputs_in = np.reshape(inputs, [-1, 1])
targets_in = np.reshape(targets, [-1])
update_step(tf.convert_to_tensor(inputs_in), tf.convert_to_tensor(targets_in))
if i % print_every_n == 0:
test_inputs, test_targets = next(test_gen)
print(i, train_loss(test_targets, model(test_inputs)))
if __name__ == "__main__":
tf.enable_eager_execution()
train_bit_shift(
seq_length=20,
num_iterations=2000,
print_every_n=200,
)
@@ -0,0 +1,78 @@
# @OldAPIStack
"""
This example shows two modifications:
- How to write a custom Encoder (using MobileNet v2)
- How to enhance Catalogs with this custom Encoder
With the pattern shown in this example, we can enhance Catalogs such that they extend
to new observation- or action spaces while retaining their original functionality.
"""
# __sphinx_doc_begin__
import gymnasium as gym
import numpy as np
from ray.rllib.algorithms.ppo.ppo import PPOConfig
from ray.rllib.algorithms.ppo.ppo_catalog import PPOCatalog
from ray.rllib.core.rl_module.rl_module import RLModuleSpec
from ray.rllib.examples._old_api_stack.models.mobilenet_v2_encoder import (
MOBILENET_INPUT_SHAPE,
MobileNetV2EncoderConfig,
)
from ray.rllib.examples.envs.classes.random_env import RandomEnv
# Define a PPO Catalog that we can use to inject our MobileNetV2 Encoder into RLlib's
# decision tree of what model to choose
class MobileNetEnhancedPPOCatalog(PPOCatalog):
@classmethod
def _get_encoder_config(
cls,
observation_space: gym.Space,
**kwargs,
):
if (
isinstance(observation_space, gym.spaces.Box)
and observation_space.shape == MOBILENET_INPUT_SHAPE
):
# Inject our custom encoder here, only if the observation space fits it
return MobileNetV2EncoderConfig()
else:
return super()._get_encoder_config(observation_space, **kwargs)
# Create a generic config with our enhanced Catalog
ppo_config = (
PPOConfig()
.rl_module(rl_module_spec=RLModuleSpec(catalog_class=MobileNetEnhancedPPOCatalog))
.env_runners(num_env_runners=0)
# The following training settings make it so that a training iteration is very
# quick. This is just for the sake of this example. PPO will not learn properly
# with these settings!
.training(train_batch_size_per_learner=32, minibatch_size=16, num_epochs=1)
)
# CartPole's observation space is not compatible with our MobileNetV2 Encoder, so
# this will use the default behaviour of Catalogs
ppo_config.environment("CartPole-v1")
results = ppo_config.build().train()
print(results)
# For this training, we use a RandomEnv with observations of shape
# MOBILENET_INPUT_SHAPE. This will use our custom Encoder.
ppo_config.environment(
RandomEnv,
env_config={
"action_space": gym.spaces.Discrete(2),
# Test a simple Image observation space.
"observation_space": gym.spaces.Box(
0.0,
1.0,
shape=MOBILENET_INPUT_SHAPE,
dtype=np.float32,
),
},
)
results = ppo_config.build().train()
print(results)
# __sphinx_doc_end__
@@ -0,0 +1,131 @@
# @OldAPIStack
import numpy as np
import onnxruntime
import ray
import ray.rllib.algorithms.ppo as ppo
from ray.rllib.examples.utils import add_rllib_example_script_args, check
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.torch_utils import convert_to_torch_tensor
torch, _ = try_import_torch()
parser = add_rllib_example_script_args()
parser.set_defaults(
num_env_runners=1,
# ONNX is not supported by RLModule API yet.
old_api_stack=True,
)
class ONNXCompatibleWrapper(torch.nn.Module):
def __init__(self, original_model):
super(ONNXCompatibleWrapper, self).__init__()
self.original_model = original_model
def forward(self, a, b0, b1, c):
# Convert the separate tensor inputs back into the list format
# expected by the original model's forward method.
b = [b0, b1]
ret = self.original_model({"obs": a}, b, c)
# results, state_out_0, state_out_1
return ret[0], ret[1][0], ret[1][1]
if __name__ == "__main__":
args = parser.parse_args()
ray.init()
# Configure our PPO Algorithm.
config = (
ppo.PPOConfig()
.environment("CartPole-v1")
.env_runners(num_env_runners=args.num_env_runners)
.training(model={"use_lstm": True})
)
B = 3
T = 5
LSTM_CELL = 256
# Input data for a python inference forward call.
test_data_python = {
"obs": np.random.uniform(0, 1.0, size=(B * T, 4)).astype(np.float32),
"state_ins": [
np.random.uniform(0, 1.0, size=(B, LSTM_CELL)).astype(np.float32),
np.random.uniform(0, 1.0, size=(B, LSTM_CELL)).astype(np.float32),
],
"seq_lens": np.array([T] * B, np.float32),
}
# Input data for the ONNX session.
test_data_onnx = {
"obs": test_data_python["obs"],
"state_in_0": test_data_python["state_ins"][0],
"state_in_1": test_data_python["state_ins"][1],
"seq_lens": test_data_python["seq_lens"],
}
# Input data for compiling the ONNX model.
test_data_onnx_input = convert_to_torch_tensor(test_data_onnx)
# Initialize a PPO Algorithm.
algo = config.build()
# You could train the model here
# algo.train()
# Let's run inference on the torch model
policy = algo.get_policy()
result_pytorch, _ = policy.model(
{
"obs": torch.tensor(test_data_python["obs"]),
},
[
torch.tensor(test_data_python["state_ins"][0]),
torch.tensor(test_data_python["state_ins"][1]),
],
torch.tensor(test_data_python["seq_lens"]),
)
# Evaluate tensor to fetch numpy array
result_pytorch = result_pytorch.detach().numpy()
# Wrap the actual ModelV2 with the torch wrapper above to make this all work with
# LSTMs (extra `state` in- and outputs and `seq_lens` inputs).
onnx_compatible = ONNXCompatibleWrapper(policy.model)
exported_model_file = "model.onnx"
input_names = [
"obs",
"state_in_0",
"state_in_1",
"seq_lens",
]
# This line will export the model to ONNX.
torch.onnx.export(
onnx_compatible,
tuple(test_data_onnx_input[n] for n in input_names),
exported_model_file,
export_params=True,
opset_version=11,
do_constant_folding=True,
input_names=input_names,
output_names=[
"output",
"state_out_0",
"state_out_1",
],
dynamic_axes={k: {0: "batch_size"} for k in input_names},
)
# Start an inference session for the ONNX model.
session = onnxruntime.InferenceSession(exported_model_file, None)
result_onnx = session.run(["output"], test_data_onnx)
# These results should be equal!
print("PYTORCH", result_pytorch)
print("ONNX", result_onnx[0])
check(result_pytorch, result_onnx[0])
print("Model outputs are equal. PASSED")
@@ -0,0 +1,318 @@
# @OldAPIStack
# ***********************************************************************************
# IMPORTANT NOTE: This script uses the old API stack and will soon be replaced by
# `ray.rllib.examples.multi_agent.pettingzoo_shared_value_function.py`!
# ***********************************************************************************
"""An example of customizing PPO to leverage a centralized critic.
Here the model and policy are hard-coded to implement a centralized critic
for TwoStepGame, but you can adapt this for your own use cases.
Compared to simply running `rllib/examples/two_step_game.py --run=PPO`,
this centralized critic version reaches vf_explained_variance=1.0 more stably
since it takes into account the opponent actions as well as the policy's.
Note that this is also using two independent policies instead of weight-sharing
with one.
See also: centralized_critic_2.py for a simpler approach that instead
modifies the environment.
"""
import argparse
import os
import numpy as np
from gymnasium.spaces import Discrete
from ray import tune
from ray.rllib.algorithms.ppo.ppo import PPO, PPOConfig
from ray.rllib.algorithms.ppo.ppo_tf_policy import (
PPOTF1Policy,
PPOTF2Policy,
)
from ray.rllib.algorithms.ppo.ppo_torch_policy import PPOTorchPolicy
from ray.rllib.evaluation.postprocessing import Postprocessing, compute_advantages
from ray.rllib.examples._old_api_stack.models.centralized_critic_models import (
CentralizedCriticModel,
TorchCentralizedCriticModel,
)
from ray.rllib.examples.envs.classes.multi_agent.two_step_game import TwoStepGame
from ray.rllib.models import ModelCatalog
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf, try_import_torch
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.rllib.utils.numpy import convert_to_numpy
from ray.rllib.utils.test_utils import check_learning_achieved
from ray.rllib.utils.tf_utils import explained_variance, make_tf_callable
from ray.rllib.utils.torch_utils import convert_to_torch_tensor
from ray.tune.result import TRAINING_ITERATION
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
OPPONENT_OBS = "opponent_obs"
OPPONENT_ACTION = "opponent_action"
parser = argparse.ArgumentParser()
parser.add_argument(
"--framework",
choices=["tf", "tf2", "torch"],
default="torch",
help="The DL framework specifier.",
)
parser.add_argument(
"--as-test",
action="store_true",
help="Whether this script should be run as a test: --stop-reward must "
"be achieved within --stop-timesteps AND --stop-iters.",
)
parser.add_argument(
"--stop-iters", type=int, default=100, help="Number of iterations to train."
)
parser.add_argument(
"--stop-timesteps", type=int, default=100000, help="Number of timesteps to train."
)
parser.add_argument(
"--stop-reward", type=float, default=7.99, help="Reward at which we stop training."
)
class CentralizedValueMixin:
"""Add method to evaluate the central value function from the model."""
def __init__(self):
if self.config["framework"] != "torch":
self.compute_central_vf = make_tf_callable(self.get_session())(
self.model.central_value_function
)
else:
self.compute_central_vf = self.model.central_value_function
# Grabs the opponent obs/act and includes it in the experience train_batch,
# and computes GAE using the central vf predictions.
def centralized_critic_postprocessing(
policy, sample_batch, other_agent_batches=None, episode=None
):
pytorch = policy.config["framework"] == "torch"
if (pytorch and hasattr(policy, "compute_central_vf")) or (
not pytorch and policy.loss_initialized()
):
assert other_agent_batches is not None
[(_, _, opponent_batch)] = list(other_agent_batches.values())
# also record the opponent obs and actions in the trajectory
sample_batch[OPPONENT_OBS] = opponent_batch[SampleBatch.CUR_OBS]
sample_batch[OPPONENT_ACTION] = opponent_batch[SampleBatch.ACTIONS]
# overwrite default VF prediction with the central VF
if args.framework == "torch":
sample_batch[SampleBatch.VF_PREDS] = (
policy.compute_central_vf(
convert_to_torch_tensor(
sample_batch[SampleBatch.CUR_OBS], policy.device
),
convert_to_torch_tensor(sample_batch[OPPONENT_OBS], policy.device),
convert_to_torch_tensor(
sample_batch[OPPONENT_ACTION], policy.device
),
)
.cpu()
.detach()
.numpy()
)
else:
sample_batch[SampleBatch.VF_PREDS] = convert_to_numpy(
policy.compute_central_vf(
sample_batch[SampleBatch.CUR_OBS],
sample_batch[OPPONENT_OBS],
sample_batch[OPPONENT_ACTION],
)
)
else:
# Policy hasn't been initialized yet, use zeros.
sample_batch[OPPONENT_OBS] = np.zeros_like(sample_batch[SampleBatch.CUR_OBS])
sample_batch[OPPONENT_ACTION] = np.zeros_like(sample_batch[SampleBatch.ACTIONS])
sample_batch[SampleBatch.VF_PREDS] = np.zeros_like(
sample_batch[SampleBatch.REWARDS], dtype=np.float32
)
completed = sample_batch[SampleBatch.TERMINATEDS][-1]
if completed:
last_r = 0.0
else:
last_r = sample_batch[SampleBatch.VF_PREDS][-1]
train_batch = compute_advantages(
sample_batch,
last_r,
policy.config["gamma"],
policy.config["lambda"],
use_gae=policy.config["use_gae"],
)
return train_batch
# Copied from PPO but optimizing the central value function.
def loss_with_central_critic(policy, base_policy, model, dist_class, train_batch):
# Save original value function.
vf_saved = model.value_function
# Calculate loss with a custom value function.
model.value_function = lambda: policy.model.central_value_function(
train_batch[SampleBatch.CUR_OBS],
train_batch[OPPONENT_OBS],
train_batch[OPPONENT_ACTION],
)
policy._central_value_out = model.value_function()
loss = base_policy.loss(model, dist_class, train_batch)
# Restore original value function.
model.value_function = vf_saved
return loss
def central_vf_stats(policy, train_batch):
# Report the explained variance of the central value function.
return {
"vf_explained_var": explained_variance(
train_batch[Postprocessing.VALUE_TARGETS], policy._central_value_out
)
}
def get_ccppo_policy(base):
class CCPPOTFPolicy(CentralizedValueMixin, base):
def __init__(self, observation_space, action_space, config):
base.__init__(self, observation_space, action_space, config)
CentralizedValueMixin.__init__(self)
@override(base)
def loss(self, model, dist_class, train_batch):
# Use super() to get to the base PPO policy.
# This special loss function utilizes a shared
# value function defined on self, and the loss function
# defined on PPO policies.
return loss_with_central_critic(
self, super(), model, dist_class, train_batch
)
@override(base)
def postprocess_trajectory(
self, sample_batch, other_agent_batches=None, episode=None
):
return centralized_critic_postprocessing(
self, sample_batch, other_agent_batches, episode
)
@override(base)
def stats_fn(self, train_batch: SampleBatch):
stats = super().stats_fn(train_batch)
stats.update(central_vf_stats(self, train_batch))
return stats
return CCPPOTFPolicy
CCPPOStaticGraphTFPolicy = get_ccppo_policy(PPOTF1Policy)
CCPPOEagerTFPolicy = get_ccppo_policy(PPOTF2Policy)
class CCPPOTorchPolicy(CentralizedValueMixin, PPOTorchPolicy):
def __init__(self, observation_space, action_space, config):
PPOTorchPolicy.__init__(self, observation_space, action_space, config)
CentralizedValueMixin.__init__(self)
@override(PPOTorchPolicy)
def loss(self, model, dist_class, train_batch):
return loss_with_central_critic(self, super(), model, dist_class, train_batch)
@override(PPOTorchPolicy)
def postprocess_trajectory(
self, sample_batch, other_agent_batches=None, episode=None
):
return centralized_critic_postprocessing(
self, sample_batch, other_agent_batches, episode
)
class CentralizedCritic(PPO):
@classmethod
@override(PPO)
def get_default_policy_class(cls, config):
if config["framework"] == "torch":
return CCPPOTorchPolicy
elif config["framework"] == "tf":
return CCPPOStaticGraphTFPolicy
else:
return CCPPOEagerTFPolicy
if __name__ == "__main__":
args = parser.parse_args()
ModelCatalog.register_custom_model(
"cc_model",
TorchCentralizedCriticModel
if args.framework == "torch"
else CentralizedCriticModel,
)
config = (
PPOConfig()
.api_stack(
enable_env_runner_and_connector_v2=False,
enable_rl_module_and_learner=False,
)
.environment(TwoStepGame)
.framework(args.framework)
.env_runners(batch_mode="complete_episodes", num_env_runners=0)
.training(model={"custom_model": "cc_model"})
.multi_agent(
policies={
"pol1": (
None,
Discrete(6),
TwoStepGame.action_space,
# `framework` would also be ok here.
PPOConfig.overrides(framework_str=args.framework),
),
"pol2": (
None,
Discrete(6),
TwoStepGame.action_space,
# `framework` would also be ok here.
PPOConfig.overrides(framework_str=args.framework),
),
},
policy_mapping_fn=lambda agent_id, episode, worker, **kwargs: "pol1"
if agent_id == 0
else "pol2",
)
# Use GPUs iff `RLLIB_NUM_GPUS` env var set to > 0.
.resources(num_gpus=int(os.environ.get("RLLIB_NUM_GPUS", "0")))
)
stop = {
TRAINING_ITERATION: args.stop_iters,
NUM_ENV_STEPS_SAMPLED_LIFETIME: args.stop_timesteps,
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": args.stop_reward,
}
tuner = tune.Tuner(
CentralizedCritic,
param_space=config.to_dict(),
run_config=tune.RunConfig(stop=stop, verbose=1),
)
results = tuner.fit()
if args.as_test:
check_learning_achieved(results, args.stop_reward)
@@ -0,0 +1,83 @@
#!/usr/bin/env python
# @OldAPIStack
import os
import numpy as np
import ray
import ray._common
from ray.rllib.policy.policy import Policy
from ray.rllib.utils.framework import try_import_tf
from ray.tune.registry import get_trainable_cls
tf1, tf, tfv = try_import_tf()
ray.init()
def train_and_export_policy_and_model(algo_name, num_steps, model_dir, ckpt_dir):
cls = get_trainable_cls(algo_name)
config = cls.get_default_config()
config.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
# This Example is only for tf.
config.framework("tf")
# Set exporting native (DL-framework) model files to True.
config.export_native_model_files = True
config.env = "CartPole-v1"
alg = config.build()
for _ in range(num_steps):
alg.train()
# Export Policy checkpoint.
alg.export_policy_checkpoint(ckpt_dir)
# Export tensorflow keras Model for online serving
alg.export_policy_model(model_dir)
def restore_saved_model(export_dir):
signature_key = (
tf1.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY
)
g = tf1.Graph()
with g.as_default():
with tf1.Session(graph=g) as sess:
meta_graph_def = tf1.saved_model.load(
sess, [tf1.saved_model.tag_constants.SERVING], export_dir
)
print("Model restored!")
print("Signature Def Information:")
print(meta_graph_def.signature_def[signature_key])
print("You can inspect the model using TensorFlow SavedModel CLI.")
print("https://www.tensorflow.org/guide/saved_model")
def restore_policy_from_checkpoint(export_dir):
# Load the model from the checkpoint.
policy = Policy.from_checkpoint(export_dir)
# Perform a dummy (CartPole) forward pass.
test_obs = np.array([0.1, 0.2, 0.3, 0.4])
results = policy.compute_single_action(test_obs)
# Check results for correctness.
assert len(results) == 3
assert results[0].shape == () # pure single action (int)
assert results[1] == [] # RNN states
assert results[2]["action_dist_inputs"].shape == (2,) # categorical inputs
if __name__ == "__main__":
algo = "PPO"
model_dir = os.path.join(
ray._common.utils.get_default_ray_temp_dir(), "model_export_dir"
)
ckpt_dir = os.path.join(
ray._common.utils.get_default_ray_temp_dir(), "ckpt_export_dir"
)
num_steps = 1
train_and_export_policy_and_model(algo, num_steps, model_dir, ckpt_dir)
restore_saved_model(model_dir)
restore_policy_from_checkpoint(ckpt_dir)
@@ -0,0 +1,158 @@
# @OldAPIStack
"""
Adapted (time-dependent) GAE for PPO algorithm that you can activate by setting
use_adapted_gae=True in the policy config. Additionally, it's required that
"callbacks" include the custom callback class in the Algorithm's config.
Furthermore, the env must return in its info dictionary a key-value pair of
the form "d_ts": ... where the value is the length (time) of recent agent step.
This adapted, time-dependent computation of advantages may be useful in cases
where agent's actions take various times and thus time steps are not
equidistant (https://docdro.id/400TvlR)
"""
import numpy as np
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.evaluation.postprocessing import Postprocessing
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.utils.annotations import override
class MyCallbacks(RLlibCallback):
@override(RLlibCallback)
def on_postprocess_trajectory(
self,
*,
worker,
episode,
agent_id,
policy_id,
policies,
postprocessed_batch,
original_batches,
**kwargs
):
super().on_postprocess_trajectory(
worker=worker,
episode=episode,
agent_id=agent_id,
policy_id=policy_id,
policies=policies,
postprocessed_batch=postprocessed_batch,
original_batches=original_batches,
**kwargs
)
if policies[policy_id].config.get("use_adapted_gae", False):
policy = policies[policy_id]
assert policy.config[
"use_gae"
], "Can't use adapted gae without use_gae=True!"
info_dicts = postprocessed_batch[SampleBatch.INFOS]
assert np.all(
["d_ts" in info_dict for info_dict in info_dicts]
), "Info dicts in sample batch must contain data 'd_ts' \
(=ts[i+1]-ts[i] length of time steps)!"
d_ts = np.array(
[np.float(info_dict.get("d_ts")) for info_dict in info_dicts]
)
assert np.all(
[e.is_integer() for e in d_ts]
), "Elements of 'd_ts' (length of time steps) must be integer!"
# Trajectory is actually complete -> last r=0.0.
if postprocessed_batch[SampleBatch.TERMINATEDS][-1]:
last_r = 0.0
# Trajectory has been truncated -> last r=VF estimate of last obs.
else:
# Input dict is provided to us automatically via the Model's
# requirements. It's a single-timestep (last one in trajectory)
# input_dict.
# Create an input dict according to the Model's requirements.
input_dict = postprocessed_batch.get_single_step_input_dict(
policy.model.view_requirements, index="last"
)
last_r = policy._value(**input_dict)
gamma = policy.config["gamma"]
lambda_ = policy.config["lambda"]
vpred_t = np.concatenate(
[postprocessed_batch[SampleBatch.VF_PREDS], np.array([last_r])]
)
delta_t = (
postprocessed_batch[SampleBatch.REWARDS]
+ gamma**d_ts * vpred_t[1:]
- vpred_t[:-1]
)
# This formula for the advantage is an adaption of
# "Generalized Advantage Estimation"
# (https://arxiv.org/abs/1506.02438) which accounts for time steps
# of irregular length (see proposal here ).
# NOTE: last time step delta is not required
postprocessed_batch[
Postprocessing.ADVANTAGES
] = generalized_discount_cumsum(delta_t, d_ts[:-1], gamma * lambda_)
postprocessed_batch[Postprocessing.VALUE_TARGETS] = (
postprocessed_batch[Postprocessing.ADVANTAGES]
+ postprocessed_batch[SampleBatch.VF_PREDS]
).astype(np.float32)
postprocessed_batch[Postprocessing.ADVANTAGES] = postprocessed_batch[
Postprocessing.ADVANTAGES
].astype(np.float32)
def generalized_discount_cumsum(
x: np.ndarray, deltas: np.ndarray, gamma: float
) -> np.ndarray:
"""Calculates the 'time-dependent' discounted cumulative sum over a
(reward) sequence `x`.
Recursive equations:
y[t] - gamma**deltas[t+1]*y[t+1] = x[t]
reversed(y)[t] - gamma**reversed(deltas)[t-1]*reversed(y)[t-1] =
reversed(x)[t]
Args:
x (np.ndarray): A sequence of rewards or one-step TD residuals.
deltas (np.ndarray): A sequence of time step deltas (length of time
steps).
gamma: The discount factor gamma.
Returns:
np.ndarray: The sequence containing the 'time-dependent' discounted
cumulative sums for each individual element in `x` till the end of
the trajectory.
.. testcode::
:skipif: True
x = np.array([0.0, 1.0, 2.0, 3.0])
deltas = np.array([1.0, 4.0, 15.0])
gamma = 0.9
generalized_discount_cumsum(x, deltas, gamma)
.. testoutput::
array([0.0 + 0.9^1.0*1.0 + 0.9^4.0*2.0 + 0.9^15.0*3.0,
1.0 + 0.9^4.0*2.0 + 0.9^15.0*3.0,
2.0 + 0.9^15.0*3.0,
3.0])
"""
reversed_x = x[::-1]
reversed_deltas = deltas[::-1]
reversed_y = np.empty_like(x)
reversed_y[0] = reversed_x[0]
for i in range(1, x.size):
reversed_y[i] = (
reversed_x[i] + gamma ** reversed_deltas[i - 1] * reversed_y[i - 1]
)
return reversed_y[::-1]
@@ -0,0 +1,51 @@
# @OldAPIStack
import random
from ray.rllib.algorithms.appo import APPOConfig
from ray.rllib.algorithms.sac import SACConfig
def create_appo_cartpole_checkpoint(output_dir, use_lstm=False):
config = (
APPOConfig()
.api_stack(
enable_rl_module_and_learner=False,
enable_env_runner_and_connector_v2=False,
)
.environment("CartPole-v1")
.training(model={"use_lstm": use_lstm})
)
# Build algorithm object.
algo = config.build()
algo.save(checkpoint_dir=output_dir)
def create_open_spiel_checkpoint(output_dir):
def _policy_mapping_fn(*args, **kwargs):
random.choice(["main", "opponent"])
config = (
SACConfig()
.environment("open_spiel_env")
# Intentionally create a TF2 policy to demonstrate that we can restore
# and use a TF policy in a Torch training stack.
.framework("tf2")
.env_runners(
num_env_runners=1,
num_envs_per_env_runner=5,
# We will be restoring a TF2 policy.
# So tell the RolloutWorkers to enable TF eager exec as well, even if
# framework is set to torch.
enable_tf1_exec_eagerly=True,
)
.training(model={"fcnet_hiddens": [512, 512]})
.multi_agent(
policies={"main", "opponent"},
policy_mapping_fn=_policy_mapping_fn,
# Just train the "main" policy.
policies_to_train=["main"],
)
)
# Build algorithm object.
algo = config.build()
algo.save(checkpoint_dir=output_dir)
@@ -0,0 +1,78 @@
# @OldAPIStack
"""This example script loads a connector enabled policy,
and uses it in a serving or inference setting.
"""
import argparse
import os
import tempfile
import gymnasium as gym
from ray.rllib.examples._old_api_stack.connectors.prepare_checkpoint import (
# For demo purpose only. Would normally not need this.
create_appo_cartpole_checkpoint,
)
from ray.rllib.policy.policy import Policy
from ray.rllib.utils.policy import local_policy_inference
parser = argparse.ArgumentParser()
parser.add_argument("--use-lstm", action="store_true", help="Add LSTM to the setup.")
def run(checkpoint_path, policy_id):
# __sphinx_doc_begin__
# Restore policy.
policy = Policy.from_checkpoint(
checkpoint=checkpoint_path,
policy_ids=[policy_id],
)
# Run CartPole.
env = gym.make("CartPole-v1")
env_id = "env_1"
obs, info = env.reset()
# Run for 2 episodes.
episodes = step = 0
while episodes < 2:
# Use local_policy_inference() to run inference, so we do not have to
# provide policy states or extra fetch dictionaries.
# "env_1" and "agent_1" are dummy env and agent IDs to run connectors with.
policy_outputs = local_policy_inference(
policy, env_id, "agent_1", obs, explore=False
)
assert len(policy_outputs) == 1
action, _, _ = policy_outputs[0]
print(f"episode {episodes} step {step}", obs, action)
# Step environment forward one more step.
obs, _, terminated, truncated, _ = env.step(action)
step += 1
# If the episode is done, reset the env and our connectors and start a new
# episode.
if terminated or truncated:
episodes += 1
step = 0
obs, info = env.reset()
policy.agent_connectors.reset(env_id)
# __sphinx_doc_end__
if __name__ == "__main__":
args = parser.parse_args()
with tempfile.TemporaryDirectory() as tmpdir:
policy_id = "default_policy"
# Note, this is just for demo purpose.
# Normally, you would use a policy checkpoint from a real training run.
create_appo_cartpole_checkpoint(tmpdir, args.use_lstm)
policy_checkpoint_path = os.path.join(
tmpdir,
"policies",
policy_id,
)
run(policy_checkpoint_path, policy_id)
@@ -0,0 +1,152 @@
# @OldAPIStack
"""Example showing to restore a connector enabled TF policy
checkpoint for a new self-play PyTorch training job.
You can train the checkpointed policy with a different algorithm too.
"""
import argparse
import os
import tempfile
from functools import partial
import ray
from ray import tune
from ray.rllib.algorithms.sac import SACConfig
from ray.rllib.callbacks.callbacks import RLlibCallback
from ray.rllib.env.utils import try_import_pyspiel
from ray.rllib.env.wrappers.open_spiel import OpenSpielEnv
from ray.rllib.examples._old_api_stack.connectors.prepare_checkpoint import (
create_open_spiel_checkpoint,
)
from ray.rllib.policy.policy import Policy
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
NUM_EPISODES,
)
from ray.tune import CLIReporter, register_env
from ray.tune.result import TRAINING_ITERATION
pyspiel = try_import_pyspiel(error=True)
register_env(
"open_spiel_env", lambda _: OpenSpielEnv(pyspiel.load_game("connect_four"))
)
parser = argparse.ArgumentParser()
parser.add_argument(
"--train_iteration",
type=int,
default=10,
help="Number of iterations to train.",
)
args = parser.parse_args()
MAIN_POLICY_ID = "main"
OPPONENT_POLICY_ID = "opponent"
class AddPolicyCallback(RLlibCallback):
def __init__(self, checkpoint_dir):
self._checkpoint_dir = checkpoint_dir
super().__init__()
def on_algorithm_init(self, *, algorithm, metrics_logger, **kwargs):
policy = Policy.from_checkpoint(
self._checkpoint_dir, policy_ids=[OPPONENT_POLICY_ID]
)
# Add restored policy to Algorithm.
# Note that this policy doesn't have to be trained with the same algorithm
# of the training stack. You can even mix up TF policies with a Torch stack.
algorithm.add_policy(
policy_id=OPPONENT_POLICY_ID,
policy=policy,
add_to_eval_env_runners=True,
)
def policy_mapping_fn(agent_id, episode, worker, **kwargs):
# main policy plays against opponent policy.
return MAIN_POLICY_ID if episode.episode_id % 2 == agent_id else OPPONENT_POLICY_ID
def main(checkpoint_dir):
config = (
SACConfig()
.environment("open_spiel_env")
.framework("torch")
.callbacks(partial(AddPolicyCallback, checkpoint_dir))
.env_runners(
num_env_runners=1,
num_envs_per_env_runner=5,
# We will be restoring a TF2 policy.
# So tell the RolloutWorkers to enable TF eager exec as well, even if
# framework is set to torch.
enable_tf1_exec_eagerly=True,
)
.training(model={"fcnet_hiddens": [512, 512]})
.multi_agent(
# Initial policy map: Random and PPO. This will be expanded
# to more policy snapshots taken from "main" against which "main"
# will then play (instead of "random"). This is done in the
# custom callback defined above (`SelfPlayCallback`).
# Note: We will add the "opponent" policy with callback.
policies={MAIN_POLICY_ID}, # Our main policy, we'd like to optimize.
# Assign agent 0 and 1 randomly to the "main" policy or
# to the opponent ("random" at first). Make sure (via episode_id)
# that "main" always plays against "random" (and not against
# another "main").
policy_mapping_fn=policy_mapping_fn,
# Always just train the "main" policy.
policies_to_train=[MAIN_POLICY_ID],
)
)
stop = {TRAINING_ITERATION: args.train_iteration}
# Train the "main" policy to play really well using self-play.
tuner = tune.Tuner(
"SAC",
param_space=config.to_dict(),
run_config=tune.RunConfig(
stop=stop,
checkpoint_config=tune.CheckpointConfig(
checkpoint_at_end=True,
checkpoint_frequency=10,
),
verbose=2,
progress_reporter=CLIReporter(
metric_columns={
TRAINING_ITERATION: "iter",
"time_total_s": "time_total_s",
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": "ts",
f"{ENV_RUNNER_RESULTS}/{NUM_EPISODES}": "train_episodes",
(
f"{ENV_RUNNER_RESULTS}/module_episode_returns_mean/main"
): "reward_main",
},
sort_by_metric=True,
),
),
)
tuner.fit()
if __name__ == "__main__":
ray.init()
with tempfile.TemporaryDirectory() as tmpdir:
create_open_spiel_checkpoint(tmpdir)
policy_checkpoint_path = os.path.join(
tmpdir,
"checkpoint_000000",
"policies",
OPPONENT_POLICY_ID,
)
main(policy_checkpoint_path)
ray.shutdown()
@@ -0,0 +1,126 @@
# @OldAPIStack
"""
Example of interfacing with an environment that produces 2D observations.
This example shows how turning 2D observations with shape (A, B) into a 3D
observations with shape (C, D, 1) can enable usage of RLlib's default models.
RLlib's default Catalog class does not provide default models for 2D observation
spaces, but it does so for 3D observations.
Therefore, one can either write a custom model or transform the 2D observations into 3D
observations. This enables RLlib to use one of the default CNN filters, even though the
original observation space of the environment does not fit them.
This simple example should reach rewards of 50 within 150k timesteps.
"""
import argparse
from numpy import float32
from pettingzoo.butterfly import pistonball_v6
from supersuit import (
color_reduction_v0,
dtype_v0,
normalize_obs_v0,
reshape_v0,
resize_v1,
)
from ray import tune
from ray.rllib.algorithms.ppo import PPOConfig
from ray.rllib.env import PettingZooEnv
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.tune.registry import register_env
from ray.tune.result import TRAINING_ITERATION
parser = argparse.ArgumentParser()
parser.add_argument(
"--framework",
choices=["tf2", "torch"],
default="torch",
help="The DL framework specifier.",
)
parser.add_argument(
"--as-test",
action="store_true",
help="Whether this script should be run as a compilation test.",
)
parser.add_argument(
"--stop-iters", type=int, default=150, help="Number of iterations to train."
)
parser.add_argument(
"--stop-timesteps", type=int, default=1000000, help="Number of timesteps to train."
)
parser.add_argument(
"--stop-reward", type=float, default=50, help="Reward at which we stop training."
)
args = parser.parse_args()
# The space we down-sample and transform the greyscale pistonball images to.
# Other spaces supported by RLlib can be chosen here.
TRANSFORMED_OBS_SPACE = (42, 42, 1)
def env_creator(config):
env = pistonball_v6.env(n_pistons=5)
env = dtype_v0(env, dtype=float32)
# This gives us greyscale images for the color red
env = color_reduction_v0(env, mode="R")
env = normalize_obs_v0(env)
# This gives us images that are upsampled to the number of pixels in the
# default CNN filter
env = resize_v1(
env, x_size=TRANSFORMED_OBS_SPACE[0], y_size=TRANSFORMED_OBS_SPACE[1]
)
# This gives us 3D images for which we have default filters
env = reshape_v0(env, shape=TRANSFORMED_OBS_SPACE)
return env
# Register env
register_env("pistonball", lambda config: PettingZooEnv(env_creator(config)))
config = (
PPOConfig()
.environment("pistonball", env_config={"local_ratio": 0.5}, clip_rewards=True)
.env_runners(
num_env_runners=15 if not args.as_test else 2,
num_envs_per_env_runner=1,
observation_filter="NoFilter",
rollout_fragment_length="auto",
)
.framework("torch")
.training(
entropy_coeff=0.01,
vf_loss_coeff=0.1,
clip_param=0.1,
vf_clip_param=10.0,
num_epochs=10,
kl_coeff=0.5,
lr=0.0001,
grad_clip=100,
minibatch_size=500,
train_batch_size=5000 if not args.as_test else 1000,
model={"vf_share_layers": True},
)
.resources(num_gpus=1 if not args.as_test else 0)
.reporting(min_time_s_per_iteration=30)
)
tune.Tuner(
"PPO",
param_space=config.to_dict(),
run_config=tune.RunConfig(
stop={
TRAINING_ITERATION: args.stop_iters,
NUM_ENV_STEPS_SAMPLED_LIFETIME: args.stop_timesteps,
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": args.stop_reward,
},
verbose=2,
),
).fit()
@@ -0,0 +1,264 @@
# @OldAPIStack
"""
Example showing how you can use your trained policy for inference
(computing actions) in an environment.
Includes options for LSTM-based models (--use-lstm), attention-net models
(--use-attention), and plain (non-recurrent) models.
"""
import argparse
import os
import gymnasium as gym
import numpy as np
import ray
from ray import tune
from ray.rllib.algorithms.algorithm import Algorithm
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.tune.registry import get_trainable_cls
from ray.tune.result import TRAINING_ITERATION
parser = argparse.ArgumentParser()
parser.add_argument(
"--run", type=str, default="PPO", help="The RLlib-registered algorithm to use."
)
parser.add_argument("--num-cpus", type=int, default=0)
parser.add_argument(
"--framework",
choices=["tf", "tf2", "torch"],
default="torch",
help="The DL framework specifier.",
)
parser.add_argument(
"--prev-n-actions",
type=int,
default=0,
help="Feed n most recent actions to the attention net as part of its input.",
)
parser.add_argument(
"--prev-n-rewards",
type=int,
default=0,
help="Feed n most recent rewards to the attention net as part of its input.",
)
parser.add_argument(
"--stop-iters",
type=int,
default=200,
help="Number of iterations to train before we do inference.",
)
parser.add_argument(
"--stop-timesteps",
type=int,
default=100000,
help="Number of timesteps to train before we do inference.",
)
parser.add_argument(
"--stop-reward",
type=float,
default=150.0,
help="Reward at which we stop training before we do inference.",
)
parser.add_argument(
"--explore-during-inference",
action="store_true",
help="Whether the trained policy should use exploration during action "
"inference.",
)
parser.add_argument(
"--num-episodes-during-inference",
type=int,
default=10,
help="Number of episodes to do inference over after training.",
)
parser.add_argument(
"--use-onnx-for-inference",
action="store_true",
help="Whether to convert the loaded module to ONNX format and then perform "
"inference through this ONNX model.",
)
if __name__ == "__main__":
args = parser.parse_args()
if args.use_onnx_for_inference:
if args.explore_during_inference:
raise ValueError(
"Can't set `--explore-during-inference` and `--use-onnx-for-inference` together!"
)
import onnxruntime
ray.init(num_cpus=args.num_cpus or None)
config = (
get_trainable_cls(args.run)
.get_default_config()
.api_stack(
enable_env_runner_and_connector_v2=False,
enable_rl_module_and_learner=False,
)
.environment("FrozenLake-v1")
# Run with tracing enabled for tf2?
.framework(args.framework)
.training(
model={
"use_attention": True,
"attention_num_transformer_units": 1,
"attention_use_n_prev_actions": args.prev_n_actions,
"attention_use_n_prev_rewards": args.prev_n_rewards,
"attention_dim": 32,
"attention_memory_inference": 10,
"attention_memory_training": 10,
},
)
# Use GPUs iff `RLLIB_NUM_GPUS` env var set to > 0.
.resources(num_gpus=int(os.environ.get("RLLIB_NUM_GPUS", "0")))
)
stop = {
TRAINING_ITERATION: args.stop_iters,
NUM_ENV_STEPS_SAMPLED_LIFETIME: args.stop_timesteps,
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": args.stop_reward,
}
print("Training policy until desired reward/timesteps/iterations. ...")
tuner = tune.Tuner(
args.run,
param_space=config,
run_config=tune.RunConfig(
stop=stop,
verbose=2,
checkpoint_config=tune.CheckpointConfig(
checkpoint_frequency=1,
checkpoint_at_end=True,
),
),
)
results = tuner.fit()
print("Training completed. Restoring new Algorithm for action inference.")
# Get the last checkpoint from the above training run.
checkpoint = results.get_best_result().checkpoint
# Create new Algorithm and restore its state from the last checkpoint.
algo = Algorithm.from_checkpoint(checkpoint)
# Export ONNX model if relevant
if args.use_onnx_for_inference:
algo.get_policy().export_model(
"frozenlake_attention_model_onnx",
# ONNX opset version 12 required to support einsum operator.
# Requires ONNX >= 1.7 and ONNX runtime >= 1.3
onnx=12,
)
# Create the env to do inference in.
env = gym.make("FrozenLake-v1")
obs, info = env.reset()
# In case the model needs previous-reward/action inputs, keep track of
# these via these variables here (we'll have to pass them into the
# compute_actions methods below).
init_prev_a = prev_a = None
init_prev_r = prev_r = None
# Set attention net's initial internal state.
num_transformers = config["model"]["attention_num_transformer_units"]
memory_inference = config["model"]["attention_memory_inference"]
attention_dim = config["model"]["attention_dim"]
init_state = state = [
np.zeros([memory_inference, attention_dim], np.float32)
for _ in range(num_transformers)
]
# Do we need prev-action/reward as part of the input?
if args.prev_n_actions:
init_prev_a = prev_a = np.array([0] * args.prev_n_actions)
if args.prev_n_rewards:
init_prev_r = prev_r = np.array([0.0] * args.prev_n_rewards)
num_episodes = 0
ort_session = None
while num_episodes < args.num_episodes_during_inference:
# Compute an action (`a`).
if args.use_onnx_for_inference:
# Prepare the ONNX runtime session.
if ort_session is None:
ort_session = onnxruntime.InferenceSession(
"frozenlake_attention_model_onnx/model.onnx"
)
# Prepare the inputs dict.
seq_len = np.array([config["model"]["max_seq_len"]], dtype=np.int32)
# pre-process observation: obs is an integer.
# we need to convert it to a one-hot vector (FrozenLake-v1 space).
n = env.observation_space.n
obs_one_hot = np.zeros(n, dtype=np.float32)
obs_one_hot[obs] = 1.0
obs = obs_one_hot
# Add batch dimension.
obs = np.array(obs, dtype=np.float32)[np.newaxis, :]
state_ins = np.array(state, dtype=np.float32)
ort_inputs = {
"obs": obs,
"state_ins": state_ins,
"seq_lens": seq_len,
}
if init_prev_a is not None:
ort_inputs["prev_actions"] = prev_a.astype(np.int64)[np.newaxis, :]
if init_prev_r is not None:
ort_inputs["prev_rewards"] = prev_r.astype(np.float32)[np.newaxis, :]
# Run the ONNX model.
ort_outs = ort_session.run(
output_names=["output", "state_outs"],
input_feed=ort_inputs,
)
# Extract action and state-out from the ONNX model outputs.
dist_inputs = ort_outs[0][0]
# Exploration could be added here based on `dist_inputs`.
# This would require using the configured exploration strategy.
# Not implemented in this example.
a = np.argmax(dist_inputs)
state_out = [ort_outs[i + 1][0] for i in range(len(state))]
else:
a, state_out, _ = algo.compute_single_action(
observation=obs,
state=state,
prev_action=prev_a,
prev_reward=prev_r,
explore=args.explore_during_inference,
policy_id="default_policy", # <- default value
)
# Send the computed action `a` to the env.
obs, reward, done, truncated, _ = env.step(a)
# Is the episode `done`? -> Reset.
if done:
obs, info = env.reset()
num_episodes += 1
state = init_state
prev_a = init_prev_a
prev_r = init_prev_r
# Episode is still ongoing -> Continue.
else:
# Append the just received state-out (most recent timestep) to the
# cascade (memory) of our state-ins and drop the oldest state-in.
state = [
np.concatenate([state[i], [state_out[i]]], axis=0)[1:]
for i in range(num_transformers)
]
if init_prev_a is not None:
prev_a = np.concatenate([prev_a, [a]])[1:]
if init_prev_r is not None:
prev_r = np.concatenate([prev_r, [reward]])[1:]
algo.stop()
ray.shutdown()
@@ -0,0 +1,186 @@
# @OldAPIStack
"""
Example showing how you can use your trained policy for inference
(computing actions) in an environment.
Includes options for LSTM-based models (--use-lstm), attention-net models
(--use-attention), and plain (non-recurrent) models.
"""
import argparse
import os
import gymnasium as gym
import numpy as np
import ray
from ray import tune
from ray.rllib.algorithms.algorithm import Algorithm
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.tune.registry import get_trainable_cls
from ray.tune.result import TRAINING_ITERATION
parser = argparse.ArgumentParser()
parser.add_argument(
"--run", type=str, default="PPO", help="The RLlib-registered algorithm to use."
)
parser.add_argument("--num-cpus", type=int, default=0)
parser.add_argument(
"--framework",
choices=["tf", "tf2", "torch"],
default="torch",
help="The DL framework specifier.",
)
parser.add_argument(
"--prev-action",
action="store_true",
help="Feed most recent action to the LSTM as part of its input.",
)
parser.add_argument(
"--prev-reward",
action="store_true",
help="Feed most recent reward to the LSTM as part of its input.",
)
parser.add_argument(
"--stop-iters",
type=int,
default=2,
help="Number of iterations to train before we do inference.",
)
parser.add_argument(
"--stop-timesteps",
type=int,
default=100000,
help="Number of timesteps to train before we do inference.",
)
parser.add_argument(
"--stop-reward",
type=float,
default=0.8,
help="Reward at which we stop training before we do inference.",
)
parser.add_argument(
"--explore-during-inference",
action="store_true",
help="Whether the trained policy should use exploration during action "
"inference.",
)
parser.add_argument(
"--num-episodes-during-inference",
type=int,
default=10,
help="Number of episodes to do inference over after training.",
)
if __name__ == "__main__":
args = parser.parse_args()
ray.init(num_cpus=args.num_cpus or None)
config = (
get_trainable_cls(args.run)
.get_default_config()
.api_stack(
enable_env_runner_and_connector_v2=False,
enable_rl_module_and_learner=False,
)
.environment("FrozenLake-v1")
# Run with tracing enabled for tf2?
.framework(args.framework)
.training(
model={
"use_lstm": True,
"lstm_cell_size": 256,
"lstm_use_prev_action": args.prev_action,
"lstm_use_prev_reward": args.prev_reward,
},
)
# Use GPUs iff `RLLIB_NUM_GPUS` env var set to > 0.
.resources(num_gpus=int(os.environ.get("RLLIB_NUM_GPUS", "0")))
)
stop = {
TRAINING_ITERATION: args.stop_iters,
NUM_ENV_STEPS_SAMPLED_LIFETIME: args.stop_timesteps,
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": args.stop_reward,
}
print("Training policy until desired reward/timesteps/iterations. ...")
tuner = tune.Tuner(
args.run,
param_space=config,
run_config=tune.RunConfig(
stop=stop,
verbose=2,
checkpoint_config=tune.CheckpointConfig(
checkpoint_frequency=1,
checkpoint_at_end=True,
),
),
)
results = tuner.fit()
print("Training completed. Restoring new Algorithm for action inference.")
# Get the last checkpoint from the above training run.
checkpoint = results.get_best_result().checkpoint
# Create new Algorithm from the last checkpoint.
algo = Algorithm.from_checkpoint(checkpoint)
# Create the env to do inference in.
env = gym.make("FrozenLake-v1")
obs, info = env.reset()
# In case the model needs previous-reward/action inputs, keep track of
# these via these variables here (we'll have to pass them into the
# compute_actions methods below).
init_prev_a = prev_a = None
init_prev_r = prev_r = None
# Set LSTM's initial internal state.
lstm_cell_size = config["model"]["lstm_cell_size"]
# range(2) b/c h- and c-states of the LSTM.
if algo.config.enable_rl_module_and_learner:
init_state = state = algo.get_policy().model.get_initial_state()
else:
init_state = state = [np.zeros([lstm_cell_size], np.float32) for _ in range(2)]
# Do we need prev-action/reward as part of the input?
if args.prev_action:
init_prev_a = prev_a = 0
if args.prev_reward:
init_prev_r = prev_r = 0.0
num_episodes = 0
while num_episodes < args.num_episodes_during_inference:
# Compute an action (`a`).
a, state_out, _ = algo.compute_single_action(
observation=obs,
state=state,
prev_action=prev_a,
prev_reward=prev_r,
explore=args.explore_during_inference,
policy_id="default_policy", # <- default value
)
# Send the computed action `a` to the env.
obs, reward, done, truncated, info = env.step(a)
# Is the episode `done`? -> Reset.
if done:
obs, info = env.reset()
num_episodes += 1
state = init_state
prev_a = init_prev_a
prev_r = init_prev_r
# Episode is still ongoing -> Continue.
else:
state = state_out
if init_prev_a is not None:
prev_a = a
if init_prev_r is not None:
prev_r = reward
algo.stop()
ray.shutdown()
@@ -0,0 +1,126 @@
# @OldAPIStack
from gymnasium.spaces import Dict
from ray.rllib.models.tf.fcnet import FullyConnectedNetwork
from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.torch.fcnet import FullyConnectedNetwork as TorchFC
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.utils.framework import try_import_tf, try_import_torch
from ray.rllib.utils.torch_utils import FLOAT_MIN
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class ActionMaskModel(TFModelV2):
"""Model that handles simple discrete action masking.
This assumes the outputs are logits for a single Categorical action dist.
Getting this to work with a more complex output (e.g., if the action space
is a tuple of several distributions) is also possible but left as an
exercise to the reader.
"""
def __init__(
self, obs_space, action_space, num_outputs, model_config, name, **kwargs
):
orig_space = getattr(obs_space, "original_space", obs_space)
assert (
isinstance(orig_space, Dict)
and "action_mask" in orig_space.spaces
and "observations" in orig_space.spaces
)
super().__init__(obs_space, action_space, num_outputs, model_config, name)
self.internal_model = FullyConnectedNetwork(
orig_space["observations"],
action_space,
num_outputs,
model_config,
name + "_internal",
)
# disable action masking --> will likely lead to invalid actions
self.no_masking = model_config["custom_model_config"].get("no_masking", False)
def forward(self, input_dict, state, seq_lens):
# Extract the available actions tensor from the observation.
action_mask = input_dict["obs"]["action_mask"]
# Compute the unmasked logits.
logits, _ = self.internal_model({"obs": input_dict["obs"]["observations"]})
# If action masking is disabled, directly return unmasked logits
if self.no_masking:
return logits, state
# Convert action_mask into a [0.0 || -inf]-type mask.
inf_mask = tf.maximum(tf.math.log(action_mask), tf.float32.min)
masked_logits = logits + inf_mask
# Return masked logits.
return masked_logits, state
def value_function(self):
return self.internal_model.value_function()
class TorchActionMaskModel(TorchModelV2, nn.Module):
"""PyTorch version of above ActionMaskingModel."""
def __init__(
self,
obs_space,
action_space,
num_outputs,
model_config,
name,
**kwargs,
):
orig_space = getattr(obs_space, "original_space", obs_space)
assert (
isinstance(orig_space, Dict)
and "action_mask" in orig_space.spaces
and "observations" in orig_space.spaces
)
TorchModelV2.__init__(
self, obs_space, action_space, num_outputs, model_config, name, **kwargs
)
nn.Module.__init__(self)
self.internal_model = TorchFC(
orig_space["observations"],
action_space,
num_outputs,
model_config,
name + "_internal",
)
# disable action masking --> will likely lead to invalid actions
self.no_masking = False
if "no_masking" in model_config["custom_model_config"]:
self.no_masking = model_config["custom_model_config"]["no_masking"]
def forward(self, input_dict, state, seq_lens):
# Extract the available actions tensor from the observation.
action_mask = input_dict["obs"]["action_mask"]
# Compute the unmasked logits.
logits, _ = self.internal_model({"obs": input_dict["obs"]["observations"]})
# If action masking is disabled, directly return unmasked logits
if self.no_masking:
return logits, state
# Convert action_mask into a [0.0 || -inf]-type mask.
inf_mask = torch.clamp(torch.log(action_mask), min=FLOAT_MIN)
masked_logits = logits + inf_mask
# Return masked logits.
return masked_logits, state
def value_function(self):
return self.internal_model.value_function()
@@ -0,0 +1,149 @@
# @OldAPIStack
from ray.rllib.models.tf.tf_action_dist import ActionDistribution, Categorical
from ray.rllib.models.torch.torch_action_dist import (
TorchCategorical,
TorchDistributionWrapper,
)
from ray.rllib.utils.framework import try_import_tf, try_import_torch
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class BinaryAutoregressiveDistribution(ActionDistribution):
"""Action distribution P(a1, a2) = P(a1) * P(a2 | a1)"""
def deterministic_sample(self):
# First, sample a1.
a1_dist = self._a1_distribution()
a1 = a1_dist.deterministic_sample()
# Sample a2 conditioned on a1.
a2_dist = self._a2_distribution(a1)
a2 = a2_dist.deterministic_sample()
self._action_logp = a1_dist.logp(a1) + a2_dist.logp(a2)
# Return the action tuple.
return (a1, a2)
def sample(self):
# First, sample a1.
a1_dist = self._a1_distribution()
a1 = a1_dist.sample()
# Sample a2 conditioned on a1.
a2_dist = self._a2_distribution(a1)
a2 = a2_dist.sample()
self._action_logp = a1_dist.logp(a1) + a2_dist.logp(a2)
# Return the action tuple.
return (a1, a2)
def logp(self, actions):
a1, a2 = actions[:, 0], actions[:, 1]
a1_vec = tf.expand_dims(tf.cast(a1, tf.float32), 1)
a1_logits, a2_logits = self.model.action_model([self.inputs, a1_vec])
return Categorical(a1_logits).logp(a1) + Categorical(a2_logits).logp(a2)
def sampled_action_logp(self):
return self._action_logp
def entropy(self):
a1_dist = self._a1_distribution()
a2_dist = self._a2_distribution(a1_dist.sample())
return a1_dist.entropy() + a2_dist.entropy()
def kl(self, other):
a1_dist = self._a1_distribution()
a1_terms = a1_dist.kl(other._a1_distribution())
a1 = a1_dist.sample()
a2_terms = self._a2_distribution(a1).kl(other._a2_distribution(a1))
return a1_terms + a2_terms
def _a1_distribution(self):
BATCH = tf.shape(self.inputs)[0]
a1_logits, _ = self.model.action_model([self.inputs, tf.zeros((BATCH, 1))])
a1_dist = Categorical(a1_logits)
return a1_dist
def _a2_distribution(self, a1):
a1_vec = tf.expand_dims(tf.cast(a1, tf.float32), 1)
_, a2_logits = self.model.action_model([self.inputs, a1_vec])
a2_dist = Categorical(a2_logits)
return a2_dist
@staticmethod
def required_model_output_shape(action_space, model_config):
return 16 # controls model output feature vector size
class TorchBinaryAutoregressiveDistribution(TorchDistributionWrapper):
"""Action distribution P(a1, a2) = P(a1) * P(a2 | a1)"""
def deterministic_sample(self):
# First, sample a1.
a1_dist = self._a1_distribution()
a1 = a1_dist.deterministic_sample()
# Sample a2 conditioned on a1.
a2_dist = self._a2_distribution(a1)
a2 = a2_dist.deterministic_sample()
self._action_logp = a1_dist.logp(a1) + a2_dist.logp(a2)
# Return the action tuple.
return (a1, a2)
def sample(self):
# First, sample a1.
a1_dist = self._a1_distribution()
a1 = a1_dist.sample()
# Sample a2 conditioned on a1.
a2_dist = self._a2_distribution(a1)
a2 = a2_dist.sample()
self._action_logp = a1_dist.logp(a1) + a2_dist.logp(a2)
# Return the action tuple.
return (a1, a2)
def logp(self, actions):
a1, a2 = actions[:, 0], actions[:, 1]
a1_vec = torch.unsqueeze(a1.float(), 1)
a1_logits, a2_logits = self.model.action_module(self.inputs, a1_vec)
return TorchCategorical(a1_logits).logp(a1) + TorchCategorical(a2_logits).logp(
a2
)
def sampled_action_logp(self):
return self._action_logp
def entropy(self):
a1_dist = self._a1_distribution()
a2_dist = self._a2_distribution(a1_dist.sample())
return a1_dist.entropy() + a2_dist.entropy()
def kl(self, other):
a1_dist = self._a1_distribution()
a1_terms = a1_dist.kl(other._a1_distribution())
a1 = a1_dist.sample()
a2_terms = self._a2_distribution(a1).kl(other._a2_distribution(a1))
return a1_terms + a2_terms
def _a1_distribution(self):
BATCH = self.inputs.shape[0]
zeros = torch.zeros((BATCH, 1)).to(self.inputs.device)
a1_logits, _ = self.model.action_module(self.inputs, zeros)
a1_dist = TorchCategorical(a1_logits)
return a1_dist
def _a2_distribution(self, a1):
a1_vec = torch.unsqueeze(a1.float(), 1)
_, a2_logits = self.model.action_module(self.inputs, a1_vec)
a2_dist = TorchCategorical(a2_logits)
return a2_dist
@staticmethod
def required_model_output_shape(action_space, model_config):
return 16 # controls model output feature vector size
@@ -0,0 +1,161 @@
# @OldAPIStack
from gymnasium.spaces import Discrete, Tuple
from ray.rllib.models.tf.misc import normc_initializer
from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.torch.misc import SlimFC, normc_initializer as normc_init_torch
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.utils.framework import try_import_tf, try_import_torch
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class AutoregressiveActionModel(TFModelV2):
"""Implements the `.action_model` branch required above."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
super(AutoregressiveActionModel, self).__init__(
obs_space, action_space, num_outputs, model_config, name
)
if action_space != Tuple([Discrete(2), Discrete(2)]):
raise ValueError("This model only supports the [2, 2] action space")
# Inputs
obs_input = tf.keras.layers.Input(shape=obs_space.shape, name="obs_input")
a1_input = tf.keras.layers.Input(shape=(1,), name="a1_input")
ctx_input = tf.keras.layers.Input(shape=(num_outputs,), name="ctx_input")
# Output of the model (normally 'logits', but for an autoregressive
# dist this is more like a context/feature layer encoding the obs)
context = tf.keras.layers.Dense(
num_outputs,
name="hidden",
activation=tf.nn.tanh,
kernel_initializer=normc_initializer(1.0),
)(obs_input)
# V(s)
value_out = tf.keras.layers.Dense(
1,
name="value_out",
activation=None,
kernel_initializer=normc_initializer(0.01),
)(context)
# P(a1 | obs)
a1_logits = tf.keras.layers.Dense(
2,
name="a1_logits",
activation=None,
kernel_initializer=normc_initializer(0.01),
)(ctx_input)
# P(a2 | a1)
# --note: typically you'd want to implement P(a2 | a1, obs) as follows:
# a2_context = tf.keras.layers.Concatenate(axis=1)(
# [ctx_input, a1_input])
a2_context = a1_input
a2_hidden = tf.keras.layers.Dense(
16,
name="a2_hidden",
activation=tf.nn.tanh,
kernel_initializer=normc_initializer(1.0),
)(a2_context)
a2_logits = tf.keras.layers.Dense(
2,
name="a2_logits",
activation=None,
kernel_initializer=normc_initializer(0.01),
)(a2_hidden)
# Base layers
self.base_model = tf.keras.Model(obs_input, [context, value_out])
self.base_model.summary()
# Autoregressive action sampler
self.action_model = tf.keras.Model(
[ctx_input, a1_input], [a1_logits, a2_logits]
)
self.action_model.summary()
def forward(self, input_dict, state, seq_lens):
context, self._value_out = self.base_model(input_dict["obs"])
return context, state
def value_function(self):
return tf.reshape(self._value_out, [-1])
class TorchAutoregressiveActionModel(TorchModelV2, nn.Module):
"""PyTorch version of the AutoregressiveActionModel above."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
TorchModelV2.__init__(
self, obs_space, action_space, num_outputs, model_config, name
)
nn.Module.__init__(self)
if action_space != Tuple([Discrete(2), Discrete(2)]):
raise ValueError("This model only supports the [2, 2] action space")
# Output of the model (normally 'logits', but for an autoregressive
# dist this is more like a context/feature layer encoding the obs)
self.context_layer = SlimFC(
in_size=obs_space.shape[0],
out_size=num_outputs,
initializer=normc_init_torch(1.0),
activation_fn=nn.Tanh,
)
# V(s)
self.value_branch = SlimFC(
in_size=num_outputs,
out_size=1,
initializer=normc_init_torch(0.01),
activation_fn=None,
)
# P(a1 | obs)
self.a1_logits = SlimFC(
in_size=num_outputs,
out_size=2,
activation_fn=None,
initializer=normc_init_torch(0.01),
)
class _ActionModel(nn.Module):
def __init__(self):
nn.Module.__init__(self)
self.a2_hidden = SlimFC(
in_size=1,
out_size=16,
activation_fn=nn.Tanh,
initializer=normc_init_torch(1.0),
)
self.a2_logits = SlimFC(
in_size=16,
out_size=2,
activation_fn=None,
initializer=normc_init_torch(0.01),
)
def forward(self_, ctx_input, a1_input):
a1_logits = self.a1_logits(ctx_input)
a2_logits = self_.a2_logits(self_.a2_hidden(a1_input))
return a1_logits, a2_logits
# P(a2 | a1)
# --note: typically you'd want to implement P(a2 | a1, obs) as follows:
# a2_context = tf.keras.layers.Concatenate(axis=1)(
# [ctx_input, a1_input])
self.action_module = _ActionModel()
self._context = None
def forward(self, input_dict, state, seq_lens):
self._context = self.context_layer(input_dict["obs"])
return self._context, state
def value_function(self):
return torch.reshape(self.value_branch(self._context), [-1])
@@ -0,0 +1,182 @@
# @OldAPIStack
from gymnasium.spaces import Box
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.models.tf.fcnet import FullyConnectedNetwork
from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.torch.fcnet import FullyConnectedNetwork as TorchFC
from ray.rllib.models.torch.misc import SlimFC
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf, try_import_torch
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class CentralizedCriticModel(TFModelV2):
"""Multi-agent model that implements a centralized value function."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
super(CentralizedCriticModel, self).__init__(
obs_space, action_space, num_outputs, model_config, name
)
# Base of the model
self.model = FullyConnectedNetwork(
obs_space, action_space, num_outputs, model_config, name
)
# Central VF maps (obs, opp_obs, opp_act) -> vf_pred
obs = tf.keras.layers.Input(shape=(6,), name="obs")
opp_obs = tf.keras.layers.Input(shape=(6,), name="opp_obs")
opp_act = tf.keras.layers.Input(shape=(2,), name="opp_act")
concat_obs = tf.keras.layers.Concatenate(axis=1)([obs, opp_obs, opp_act])
central_vf_dense = tf.keras.layers.Dense(
16, activation=tf.nn.tanh, name="c_vf_dense"
)(concat_obs)
central_vf_out = tf.keras.layers.Dense(1, activation=None, name="c_vf_out")(
central_vf_dense
)
self.central_vf = tf.keras.Model(
inputs=[obs, opp_obs, opp_act], outputs=central_vf_out
)
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
return self.model.forward(input_dict, state, seq_lens)
def central_value_function(self, obs, opponent_obs, opponent_actions):
return tf.reshape(
self.central_vf(
[obs, opponent_obs, tf.one_hot(tf.cast(opponent_actions, tf.int32), 2)]
),
[-1],
)
@override(ModelV2)
def value_function(self):
return self.model.value_function() # not used
class YetAnotherCentralizedCriticModel(TFModelV2):
"""Multi-agent model that implements a centralized value function.
It assumes the observation is a dict with 'own_obs' and 'opponent_obs', the
former of which can be used for computing actions (i.e., decentralized
execution), and the latter for optimization (i.e., centralized learning).
This model has two parts:
- An action model that looks at just 'own_obs' to compute actions
- A value model that also looks at the 'opponent_obs' / 'opponent_action'
to compute the value (it does this by using the 'obs_flat' tensor).
"""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
super(YetAnotherCentralizedCriticModel, self).__init__(
obs_space, action_space, num_outputs, model_config, name
)
self.action_model = FullyConnectedNetwork(
Box(low=0, high=1, shape=(6,)), # one-hot encoded Discrete(6)
action_space,
num_outputs,
model_config,
name + "_action",
)
self.value_model = FullyConnectedNetwork(
obs_space, action_space, 1, model_config, name + "_vf"
)
def forward(self, input_dict, state, seq_lens):
self._value_out, _ = self.value_model(
{"obs": input_dict["obs_flat"]}, state, seq_lens
)
return self.action_model({"obs": input_dict["obs"]["own_obs"]}, state, seq_lens)
def value_function(self):
return tf.reshape(self._value_out, [-1])
class TorchCentralizedCriticModel(TorchModelV2, nn.Module):
"""Multi-agent model that implements a centralized VF."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
TorchModelV2.__init__(
self, obs_space, action_space, num_outputs, model_config, name
)
nn.Module.__init__(self)
# Base of the model
self.model = TorchFC(obs_space, action_space, num_outputs, model_config, name)
# Central VF maps (obs, opp_obs, opp_act) -> vf_pred
input_size = 6 + 6 + 2 # obs + opp_obs + opp_act
self.central_vf = nn.Sequential(
SlimFC(input_size, 16, activation_fn=nn.Tanh),
SlimFC(16, 1),
)
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
model_out, _ = self.model(input_dict, state, seq_lens)
return model_out, []
def central_value_function(self, obs, opponent_obs, opponent_actions):
input_ = torch.cat(
[
obs,
opponent_obs,
torch.nn.functional.one_hot(opponent_actions.long(), 2).float(),
],
1,
)
return torch.reshape(self.central_vf(input_), [-1])
@override(ModelV2)
def value_function(self):
return self.model.value_function() # not used
class YetAnotherTorchCentralizedCriticModel(TorchModelV2, nn.Module):
"""Multi-agent model that implements a centralized value function.
It assumes the observation is a dict with 'own_obs' and 'opponent_obs', the
former of which can be used for computing actions (i.e., decentralized
execution), and the latter for optimization (i.e., centralized learning).
This model has two parts:
- An action model that looks at just 'own_obs' to compute actions
- A value model that also looks at the 'opponent_obs' / 'opponent_action'
to compute the value (it does this by using the 'obs_flat' tensor).
"""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
TorchModelV2.__init__(
self, obs_space, action_space, num_outputs, model_config, name
)
nn.Module.__init__(self)
self.action_model = TorchFC(
Box(low=0, high=1, shape=(6,)), # one-hot encoded Discrete(6)
action_space,
num_outputs,
model_config,
name + "_action",
)
self.value_model = TorchFC(
obs_space, action_space, 1, model_config, name + "_vf"
)
self._model_in = None
def forward(self, input_dict, state, seq_lens):
# Store model-input for possible `value_function()` call.
self._model_in = [input_dict["obs_flat"], state, seq_lens]
return self.action_model({"obs": input_dict["obs"]["own_obs"]}, state, seq_lens)
def value_function(self):
value_out, _ = self.value_model(
{"obs": self._model_in[0]}, self._model_in[1], self._model_in[2]
)
return torch.reshape(value_out, [-1])
@@ -0,0 +1,137 @@
import numpy as np
from ray.rllib.models.modelv2 import ModelV2, restore_original_dimensions
from ray.rllib.models.tf.fcnet import FullyConnectedNetwork
from ray.rllib.models.tf.tf_action_dist import Categorical
from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.torch.fcnet import FullyConnectedNetwork as TorchFC
from ray.rllib.models.torch.torch_action_dist import TorchCategorical
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.offline import JsonReader
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf, try_import_torch
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class CustomLossModel(TFModelV2):
"""Custom model that adds an imitation loss on top of the policy loss."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
super().__init__(obs_space, action_space, num_outputs, model_config, name)
self.fcnet = FullyConnectedNetwork(
self.obs_space, self.action_space, num_outputs, model_config, name="fcnet"
)
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
# Delegate to our FCNet.
return self.fcnet(input_dict, state, seq_lens)
@override(ModelV2)
def value_function(self):
# Delegate to our FCNet.
return self.fcnet.value_function()
@override(ModelV2)
def custom_loss(self, policy_loss, loss_inputs):
# Create a new input reader per worker.
reader = JsonReader(self.model_config["custom_model_config"]["input_files"])
input_ops = reader.tf_input_ops()
# Define a secondary loss by building a graph copy with weight sharing.
obs = restore_original_dimensions(
tf.cast(input_ops["obs"], tf.float32), self.obs_space
)
logits, _ = self.forward({"obs": obs}, [], None)
# Compute the IL loss.
action_dist = Categorical(logits, self.model_config)
self.policy_loss = policy_loss
self.imitation_loss = tf.reduce_mean(-action_dist.logp(input_ops["actions"]))
return policy_loss + 10 * self.imitation_loss
def metrics(self):
return {
"policy_loss": self.policy_loss,
"imitation_loss": self.imitation_loss,
}
class TorchCustomLossModel(TorchModelV2, nn.Module):
"""PyTorch version of the CustomLossModel above."""
def __init__(
self, obs_space, action_space, num_outputs, model_config, name, input_files
):
super().__init__(obs_space, action_space, num_outputs, model_config, name)
nn.Module.__init__(self)
self.input_files = input_files
# Create a new input reader per worker.
self.reader = JsonReader(self.input_files)
self.fcnet = TorchFC(
self.obs_space, self.action_space, num_outputs, model_config, name="fcnet"
)
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
# Delegate to our FCNet.
return self.fcnet(input_dict, state, seq_lens)
@override(ModelV2)
def value_function(self):
# Delegate to our FCNet.
return self.fcnet.value_function()
@override(ModelV2)
def custom_loss(self, policy_loss, loss_inputs):
"""Calculates a custom loss on top of the given policy_loss(es).
Args:
policy_loss (List[TensorType]): The list of already calculated
policy losses (as many as there are optimizers).
loss_inputs: Struct of np.ndarrays holding the
entire train batch.
Returns:
List[TensorType]: The altered list of policy losses. In case the
custom loss should have its own optimizer, make sure the
returned list is one larger than the incoming policy_loss list.
In case you simply want to mix in the custom loss into the
already calculated policy losses, return a list of altered
policy losses (as done in this example below).
"""
# Get the next batch from our input files.
batch = self.reader.next()
# Define a secondary loss by building a graph copy with weight sharing.
obs = restore_original_dimensions(
torch.from_numpy(batch["obs"]).float().to(policy_loss[0].device),
self.obs_space,
tensorlib="torch",
)
logits, _ = self.forward({"obs": obs}, [], None)
# Compute the IL loss.
action_dist = TorchCategorical(logits, self.model_config)
imitation_loss = torch.mean(
-action_dist.logp(
torch.from_numpy(batch["actions"]).to(policy_loss[0].device)
)
)
self.imitation_loss_metric = imitation_loss.item()
self.policy_loss_metric = np.mean([loss.item() for loss in policy_loss])
# Add the imitation loss to each already calculated policy loss term.
# Alternatively (if custom loss has its own optimizer):
# return policy_loss + [10 * self.imitation_loss]
return [loss_ + 10 * imitation_loss for loss_ in policy_loss]
def metrics(self):
return {
"policy_loss": self.policy_loss_metric,
"imitation_loss": self.imitation_loss_metric,
}
@@ -0,0 +1,80 @@
# @OldAPIStack
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.torch.misc import SlimFC
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf, try_import_torch
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class FastModel(TFModelV2):
"""An example for a non-Keras ModelV2 in tf that learns a single weight.
Defines all network architecture in `forward` (not `__init__` as it's
usually done for Keras-style TFModelV2s).
"""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
super().__init__(obs_space, action_space, num_outputs, model_config, name)
# Have we registered our vars yet (see `forward`)?
self._registered = False
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
with tf1.variable_scope("model", reuse=tf1.AUTO_REUSE):
bias = tf1.get_variable(
dtype=tf.float32,
name="bias",
initializer=tf.keras.initializers.Zeros(),
shape=(),
)
output = bias + tf.zeros([tf.shape(input_dict["obs"])[0], self.num_outputs])
self._value_out = tf.reduce_mean(output, -1) # fake value
if not self._registered:
self.register_variables(
tf1.get_collection(
tf1.GraphKeys.TRAINABLE_VARIABLES, scope=".+/model/.+"
)
)
self._registered = True
return output, []
@override(ModelV2)
def value_function(self):
return tf.reshape(self._value_out, [-1])
class TorchFastModel(TorchModelV2, nn.Module):
"""Torch version of FastModel (tf)."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
TorchModelV2.__init__(
self, obs_space, action_space, num_outputs, model_config, name
)
nn.Module.__init__(self)
self.bias = nn.Parameter(
torch.tensor([0.0], dtype=torch.float32, requires_grad=True)
)
# Only needed to give some params to the optimizer (even though,
# they are never used anywhere).
self.dummy_layer = SlimFC(1, 1)
self._output = None
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
self._output = self.bias + torch.zeros(
size=(input_dict["obs"].shape[0], self.num_outputs)
).to(self.bias.device)
return self._output, []
@override(ModelV2)
def value_function(self):
assert self._output is not None, "must call forward first!"
return torch.reshape(torch.mean(self._output, -1), [-1])
@@ -0,0 +1,248 @@
# @OldAPIStack
from collections import OrderedDict
from typing import Dict, List, Tuple, Union
import gymnasium as gym
from ray.rllib.models.torch.misc import SlimFC
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.typing import ModelConfigDict, TensorType
try:
from dnc import DNC
except ModuleNotFoundError:
print("dnc module not found. Did you forget to 'pip install dnc'?")
raise
torch, nn = try_import_torch()
class DNCMemory(TorchModelV2, nn.Module):
"""Differentiable Neural Computer wrapper around ixaxaar's DNC implementation,
see https://github.com/ixaxaar/pytorch-dnc"""
DEFAULT_CONFIG = {
"dnc_model": DNC,
# Number of controller hidden layers
"num_hidden_layers": 1,
# Number of weights per controller hidden layer
"hidden_size": 64,
# Number of LSTM units
"num_layers": 1,
# Number of read heads, i.e. how many addrs are read at once
"read_heads": 4,
# Number of memory cells in the controller
"nr_cells": 32,
# Size of each cell
"cell_size": 16,
# LSTM activation function
"nonlinearity": "tanh",
# Observation goes through this torch.nn.Module before
# feeding to the DNC
"preprocessor": torch.nn.Sequential(torch.nn.Linear(64, 64), torch.nn.Tanh()),
# Input size to the preprocessor
"preprocessor_input_size": 64,
# The output size of the preprocessor
# and the input size of the dnc
"preprocessor_output_size": 64,
}
MEMORY_KEYS = [
"memory",
"link_matrix",
"precedence",
"read_weights",
"write_weights",
"usage_vector",
]
def __init__(
self,
obs_space: gym.spaces.Space,
action_space: gym.spaces.Space,
num_outputs: int,
model_config: ModelConfigDict,
name: str,
**custom_model_kwargs,
):
nn.Module.__init__(self)
super(DNCMemory, self).__init__(
obs_space, action_space, num_outputs, model_config, name
)
self.num_outputs = num_outputs
self.obs_dim = gym.spaces.utils.flatdim(obs_space)
self.act_dim = gym.spaces.utils.flatdim(action_space)
self.cfg = dict(self.DEFAULT_CONFIG, **custom_model_kwargs)
assert (
self.cfg["num_layers"] == 1
), "num_layers != 1 has not been implemented yet"
self.cur_val = None
self.preprocessor = torch.nn.Sequential(
torch.nn.Linear(self.obs_dim, self.cfg["preprocessor_input_size"]),
self.cfg["preprocessor"],
)
self.logit_branch = SlimFC(
in_size=self.cfg["hidden_size"],
out_size=self.num_outputs,
activation_fn=None,
initializer=torch.nn.init.xavier_uniform_,
)
self.value_branch = SlimFC(
in_size=self.cfg["hidden_size"],
out_size=1,
activation_fn=None,
initializer=torch.nn.init.xavier_uniform_,
)
self.dnc: Union[None, DNC] = None
def get_initial_state(self) -> List[TensorType]:
ctrl_hidden = [
torch.zeros(self.cfg["num_hidden_layers"], self.cfg["hidden_size"]),
torch.zeros(self.cfg["num_hidden_layers"], self.cfg["hidden_size"]),
]
m = self.cfg["nr_cells"]
r = self.cfg["read_heads"]
w = self.cfg["cell_size"]
memory = [
torch.zeros(m, w), # memory
torch.zeros(1, m, m), # link_matrix
torch.zeros(1, m), # precedence
torch.zeros(r, m), # read_weights
torch.zeros(1, m), # write_weights
torch.zeros(m), # usage_vector
]
read_vecs = torch.zeros(w * r)
state = [*ctrl_hidden, read_vecs, *memory]
assert len(state) == 9
return state
def value_function(self) -> TensorType:
assert self.cur_val is not None, "must call forward() first"
return self.cur_val
def unpack_state(
self,
state: List[TensorType],
) -> Tuple[List[Tuple[TensorType, TensorType]], Dict[str, TensorType], TensorType]:
"""Given a list of tensors, reformat for self.dnc input"""
assert len(state) == 9, "Failed to verify unpacked state"
ctrl_hidden: List[Tuple[TensorType, TensorType]] = [
(
state[0].permute(1, 0, 2).contiguous(),
state[1].permute(1, 0, 2).contiguous(),
)
]
read_vecs: TensorType = state[2]
memory: List[TensorType] = state[3:]
memory_dict: OrderedDict[str, TensorType] = OrderedDict(
zip(self.MEMORY_KEYS, memory)
)
return ctrl_hidden, memory_dict, read_vecs
def pack_state(
self,
ctrl_hidden: List[Tuple[TensorType, TensorType]],
memory_dict: Dict[str, TensorType],
read_vecs: TensorType,
) -> List[TensorType]:
"""Given the dnc output, pack it into a list of tensors
for rllib state. Order is ctrl_hidden, read_vecs, memory_dict"""
state = []
ctrl_hidden = [
ctrl_hidden[0][0].permute(1, 0, 2),
ctrl_hidden[0][1].permute(1, 0, 2),
]
state += ctrl_hidden
assert len(state) == 2, "Failed to verify packed state"
state.append(read_vecs)
assert len(state) == 3, "Failed to verify packed state"
state += memory_dict.values()
assert len(state) == 9, "Failed to verify packed state"
return state
def validate_unpack(self, dnc_output, unpacked_state):
"""Ensure the unpacked state shapes match the DNC output"""
s_ctrl_hidden, s_memory_dict, s_read_vecs = unpacked_state
ctrl_hidden, memory_dict, read_vecs = dnc_output
for i in range(len(ctrl_hidden)):
for j in range(len(ctrl_hidden[i])):
assert s_ctrl_hidden[i][j].shape == ctrl_hidden[i][j].shape, (
"Controller state mismatch: got "
f"{s_ctrl_hidden[i][j].shape} should be "
f"{ctrl_hidden[i][j].shape}"
)
for k in memory_dict:
assert s_memory_dict[k].shape == memory_dict[k].shape, (
"Memory state mismatch at key "
f"{k}: got {s_memory_dict[k].shape} should be "
f"{memory_dict[k].shape}"
)
assert s_read_vecs.shape == read_vecs.shape, (
"Read state mismatch: got "
f"{s_read_vecs.shape} should be "
f"{read_vecs.shape}"
)
def build_dnc(self, device_idx: Union[int, None]) -> None:
self.dnc = self.cfg["dnc_model"](
input_size=self.cfg["preprocessor_output_size"],
hidden_size=self.cfg["hidden_size"],
num_layers=self.cfg["num_layers"],
num_hidden_layers=self.cfg["num_hidden_layers"],
read_heads=self.cfg["read_heads"],
cell_size=self.cfg["cell_size"],
nr_cells=self.cfg["nr_cells"],
nonlinearity=self.cfg["nonlinearity"],
gpu_id=device_idx,
)
def forward(
self,
input_dict: Dict[str, TensorType],
state: List[TensorType],
seq_lens: TensorType,
) -> Tuple[TensorType, List[TensorType]]:
flat = input_dict["obs_flat"]
# Batch and Time
# Forward expects outputs as [B, T, logits]
B = len(seq_lens)
T = flat.shape[0] // B
# Deconstruct batch into batch and time dimensions: [B, T, feats]
flat = torch.reshape(flat, [-1, T] + list(flat.shape[1:]))
# First run
if self.dnc is None:
gpu_id = flat.device.index if flat.device.index is not None else -1
self.build_dnc(gpu_id)
hidden = (None, None, None)
else:
hidden = self.unpack_state(state) # type: ignore
# Run thru preprocessor before DNC
z = self.preprocessor(flat.reshape(B * T, self.obs_dim))
z = z.reshape(B, T, self.cfg["preprocessor_output_size"])
output, hidden = self.dnc(z, hidden)
packed_state = self.pack_state(*hidden)
# Compute action/value from output
logits = self.logit_branch(output.view(B * T, -1))
values = self.value_branch(output.view(B * T, -1))
self.cur_val = values.squeeze(1)
return logits, packed_state
@@ -0,0 +1,201 @@
# @OldAPIStack
from gymnasium.spaces import Box
from ray.rllib.algorithms.dqn.distributional_q_tf_model import DistributionalQTFModel
from ray.rllib.algorithms.dqn.dqn_torch_model import DQNTorchModel
from ray.rllib.models.tf.fcnet import FullyConnectedNetwork
from ray.rllib.models.torch.fcnet import FullyConnectedNetwork as TorchFC
from ray.rllib.utils.framework import try_import_tf, try_import_torch
from ray.rllib.utils.torch_utils import FLOAT_MAX, FLOAT_MIN
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class ParametricActionsModel(DistributionalQTFModel):
"""Parametric action model that handles the dot product and masking.
This assumes the outputs are logits for a single Categorical action dist.
Getting this to work with a more complex output (e.g., if the action space
is a tuple of several distributions) is also possible but left as an
exercise to the reader.
"""
def __init__(
self,
obs_space,
action_space,
num_outputs,
model_config,
name,
true_obs_shape=(4,),
action_embed_size=2,
**kw
):
super(ParametricActionsModel, self).__init__(
obs_space, action_space, num_outputs, model_config, name, **kw
)
self.action_embed_model = FullyConnectedNetwork(
Box(-1, 1, shape=true_obs_shape),
action_space,
action_embed_size,
model_config,
name + "_action_embed",
)
def forward(self, input_dict, state, seq_lens):
# Extract the available actions tensor from the observation.
avail_actions = input_dict["obs"]["avail_actions"]
action_mask = input_dict["obs"]["action_mask"]
# Compute the predicted action embedding
action_embed, _ = self.action_embed_model({"obs": input_dict["obs"]["cart"]})
# Expand the model output to [BATCH, 1, EMBED_SIZE]. Note that the
# avail actions tensor is of shape [BATCH, MAX_ACTIONS, EMBED_SIZE].
intent_vector = tf.expand_dims(action_embed, 1)
# Batch dot product => shape of logits is [BATCH, MAX_ACTIONS].
action_logits = tf.reduce_sum(avail_actions * intent_vector, axis=2)
# Mask out invalid actions (use tf.float32.min for stability)
inf_mask = tf.maximum(tf.math.log(action_mask), tf.float32.min)
return action_logits + inf_mask, state
def value_function(self):
return self.action_embed_model.value_function()
class TorchParametricActionsModel(DQNTorchModel):
"""PyTorch version of above ParametricActionsModel."""
def __init__(
self,
obs_space,
action_space,
num_outputs,
model_config,
name,
true_obs_shape=(4,),
action_embed_size=2,
**kw
):
DQNTorchModel.__init__(
self, obs_space, action_space, num_outputs, model_config, name, **kw
)
self.action_embed_model = TorchFC(
Box(-1, 1, shape=true_obs_shape),
action_space,
action_embed_size,
model_config,
name + "_action_embed",
)
def forward(self, input_dict, state, seq_lens):
# Extract the available actions tensor from the observation.
avail_actions = input_dict["obs"]["avail_actions"]
action_mask = input_dict["obs"]["action_mask"]
# Compute the predicted action embedding
action_embed, _ = self.action_embed_model({"obs": input_dict["obs"]["cart"]})
# Expand the model output to [BATCH, 1, EMBED_SIZE]. Note that the
# avail actions tensor is of shape [BATCH, MAX_ACTIONS, EMBED_SIZE].
intent_vector = torch.unsqueeze(action_embed, 1)
# Batch dot product => shape of logits is [BATCH, MAX_ACTIONS].
action_logits = torch.sum(avail_actions * intent_vector, dim=2)
# Mask out invalid actions (use -inf to tag invalid).
# These are then recognized by the EpsilonGreedy exploration component
# as invalid actions that are not to be chosen.
inf_mask = torch.clamp(torch.log(action_mask), FLOAT_MIN, FLOAT_MAX)
return action_logits + inf_mask, state
def value_function(self):
return self.action_embed_model.value_function()
class ParametricActionsModelThatLearnsEmbeddings(DistributionalQTFModel):
"""Same as the above ParametricActionsModel.
However, this version also learns the action embeddings.
"""
def __init__(
self,
obs_space,
action_space,
num_outputs,
model_config,
name,
true_obs_shape=(4,),
action_embed_size=2,
**kw
):
super(ParametricActionsModelThatLearnsEmbeddings, self).__init__(
obs_space, action_space, num_outputs, model_config, name, **kw
)
action_ids_shifted = tf.constant(
list(range(1, num_outputs + 1)), dtype=tf.float32
)
obs_cart = tf.keras.layers.Input(shape=true_obs_shape, name="obs_cart")
valid_avail_actions_mask = tf.keras.layers.Input(
shape=(num_outputs,), name="valid_avail_actions_mask"
)
self.pred_action_embed_model = FullyConnectedNetwork(
Box(-1, 1, shape=true_obs_shape),
action_space,
action_embed_size,
model_config,
name + "_pred_action_embed",
)
# Compute the predicted action embedding
pred_action_embed, _ = self.pred_action_embed_model({"obs": obs_cart})
_value_out = self.pred_action_embed_model.value_function()
# Expand the model output to [BATCH, 1, EMBED_SIZE]. Note that the
# avail actions tensor is of shape [BATCH, MAX_ACTIONS, EMBED_SIZE].
intent_vector = tf.expand_dims(pred_action_embed, 1)
valid_avail_actions = action_ids_shifted * valid_avail_actions_mask
# Embedding for valid available actions which will be learned.
# Embedding vector for 0 is an invalid embedding (a "dummy embedding").
valid_avail_actions_embed = tf.keras.layers.Embedding(
input_dim=num_outputs + 1,
output_dim=action_embed_size,
name="action_embed_matrix",
)(valid_avail_actions)
# Batch dot product => shape of logits is [BATCH, MAX_ACTIONS].
action_logits = tf.reduce_sum(valid_avail_actions_embed * intent_vector, axis=2)
# Mask out invalid actions (use tf.float32.min for stability)
inf_mask = tf.maximum(tf.math.log(valid_avail_actions_mask), tf.float32.min)
action_logits = action_logits + inf_mask
self.param_actions_model = tf.keras.Model(
inputs=[obs_cart, valid_avail_actions_mask],
outputs=[action_logits, _value_out],
)
self.param_actions_model.summary()
def forward(self, input_dict, state, seq_lens):
# Extract the available actions mask tensor from the observation.
valid_avail_actions_mask = input_dict["obs"]["valid_avail_actions_mask"]
action_logits, self._value_out = self.param_actions_model(
[input_dict["obs"]["cart"], valid_avail_actions_mask]
)
return action_logits, state
def value_function(self):
return self._value_out
@@ -0,0 +1,206 @@
# @OldAPIStack
import numpy as np
from ray.rllib.models.modelv2 import ModelV2
from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.torch.misc import SlimFC
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.utils.annotations import override
from ray.rllib.utils.framework import try_import_tf, try_import_torch
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
TF2_GLOBAL_SHARED_LAYER = None
class TF2SharedWeightsModel(TFModelV2):
"""Example of weight sharing between two different TFModelV2s.
NOTE: This will only work for tf2.x. When running with config.framework=tf,
use SharedWeightsModel1 and SharedWeightsModel2 below, instead!
The shared (single) layer is simply defined outside of the two Models,
then used by both Models in their forward pass.
"""
def __init__(
self, observation_space, action_space, num_outputs, model_config, name
):
super().__init__(
observation_space, action_space, num_outputs, model_config, name
)
global TF2_GLOBAL_SHARED_LAYER
# The global, shared layer to be used by both models.
if TF2_GLOBAL_SHARED_LAYER is None:
TF2_GLOBAL_SHARED_LAYER = tf.keras.layers.Dense(
units=64, activation=tf.nn.relu, name="fc1"
)
inputs = tf.keras.layers.Input(observation_space.shape)
last_layer = TF2_GLOBAL_SHARED_LAYER(inputs)
output = tf.keras.layers.Dense(
units=num_outputs, activation=None, name="fc_out"
)(last_layer)
vf = tf.keras.layers.Dense(units=1, activation=None, name="value_out")(
last_layer
)
self.base_model = tf.keras.models.Model(inputs, [output, vf])
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
out, self._value_out = self.base_model(input_dict["obs"])
return out, []
@override(ModelV2)
def value_function(self):
return tf.reshape(self._value_out, [-1])
class SharedWeightsModel1(TFModelV2):
"""Example of weight sharing between two different TFModelV2s.
NOTE: This will only work for tf1 (static graph). When running with
config.framework_str=tf2, use TF2SharedWeightsModel, instead!
Here, we share the variables defined in the 'shared' variable scope
by entering it explicitly with tf1.AUTO_REUSE. This creates the
variables for the 'fc1' layer in a global scope called 'shared'
(outside of the Policy's normal variable scope).
"""
def __init__(
self, observation_space, action_space, num_outputs, model_config, name
):
super().__init__(
observation_space, action_space, num_outputs, model_config, name
)
inputs = tf.keras.layers.Input(observation_space.shape)
with tf1.variable_scope(
tf1.VariableScope(tf1.AUTO_REUSE, "shared"),
reuse=tf1.AUTO_REUSE,
auxiliary_name_scope=False,
):
last_layer = tf.keras.layers.Dense(
units=64, activation=tf.nn.relu, name="fc1"
)(inputs)
output = tf.keras.layers.Dense(
units=num_outputs, activation=None, name="fc_out"
)(last_layer)
vf = tf.keras.layers.Dense(units=1, activation=None, name="value_out")(
last_layer
)
self.base_model = tf.keras.models.Model(inputs, [output, vf])
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
out, self._value_out = self.base_model(input_dict["obs"])
return out, []
@override(ModelV2)
def value_function(self):
return tf.reshape(self._value_out, [-1])
class SharedWeightsModel2(TFModelV2):
"""The "other" TFModelV2 using the same shared space as the one above."""
def __init__(
self, observation_space, action_space, num_outputs, model_config, name
):
super().__init__(
observation_space, action_space, num_outputs, model_config, name
)
inputs = tf.keras.layers.Input(observation_space.shape)
# Weights shared with SharedWeightsModel1.
with tf1.variable_scope(
tf1.VariableScope(tf1.AUTO_REUSE, "shared"),
reuse=tf1.AUTO_REUSE,
auxiliary_name_scope=False,
):
last_layer = tf.keras.layers.Dense(
units=64, activation=tf.nn.relu, name="fc1"
)(inputs)
output = tf.keras.layers.Dense(
units=num_outputs, activation=None, name="fc_out"
)(last_layer)
vf = tf.keras.layers.Dense(units=1, activation=None, name="value_out")(
last_layer
)
self.base_model = tf.keras.models.Model(inputs, [output, vf])
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
out, self._value_out = self.base_model(input_dict["obs"])
return out, []
@override(ModelV2)
def value_function(self):
return tf.reshape(self._value_out, [-1])
TORCH_GLOBAL_SHARED_LAYER = None
if torch:
# The global, shared layer to be used by both models.
TORCH_GLOBAL_SHARED_LAYER = SlimFC(
64,
64,
activation_fn=nn.ReLU,
initializer=torch.nn.init.xavier_uniform_,
)
class TorchSharedWeightsModel(TorchModelV2, nn.Module):
"""Example of weight sharing between two different TorchModelV2s.
The shared (single) layer is simply defined outside of the two Models,
then used by both Models in their forward pass.
"""
def __init__(
self, observation_space, action_space, num_outputs, model_config, name
):
TorchModelV2.__init__(
self, observation_space, action_space, num_outputs, model_config, name
)
nn.Module.__init__(self)
# Non-shared initial layer.
self.first_layer = SlimFC(
int(np.prod(observation_space.shape)),
64,
activation_fn=nn.ReLU,
initializer=torch.nn.init.xavier_uniform_,
)
# Non-shared final layer.
self.last_layer = SlimFC(
64,
self.num_outputs,
activation_fn=None,
initializer=torch.nn.init.xavier_uniform_,
)
self.vf = SlimFC(
64,
1,
activation_fn=None,
initializer=torch.nn.init.xavier_uniform_,
)
self._global_shared_layer = TORCH_GLOBAL_SHARED_LAYER
self._output = None
@override(ModelV2)
def forward(self, input_dict, state, seq_lens):
out = self.first_layer(input_dict["obs"])
self._output = self._global_shared_layer(out)
model_out = self.last_layer(self._output)
return model_out, []
@override(ModelV2)
def value_function(self):
assert self._output is not None, "must call forward first!"
return torch.reshape(self.vf(self._output), [-1])
@@ -0,0 +1,65 @@
# @OldAPIStack
from ray.rllib.models.tf.fcnet import FullyConnectedNetwork as TFFCNet
from ray.rllib.models.tf.tf_modelv2 import TFModelV2
from ray.rllib.models.torch.fcnet import FullyConnectedNetwork as TorchFCNet
from ray.rllib.models.torch.torch_modelv2 import TorchModelV2
from ray.rllib.utils.framework import try_import_tf, try_import_torch
tf1, tf, tfv = try_import_tf()
torch, nn = try_import_torch()
class CustomTorchRPGModel(TorchModelV2, nn.Module):
"""Example of interpreting repeated observations."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
super().__init__(obs_space, action_space, num_outputs, model_config, name)
nn.Module.__init__(self)
self.model = TorchFCNet(
obs_space, action_space, num_outputs, model_config, name
)
def forward(self, input_dict, state, seq_lens):
# The unpacked input tensors, where M=MAX_PLAYERS, N=MAX_ITEMS:
# {
# 'items', <torch.Tensor shape=(?, M, N, 5)>,
# 'location', <torch.Tensor shape=(?, M, 2)>,
# 'status', <torch.Tensor shape=(?, M, 10)>,
# }
print("The unpacked input tensors:", input_dict["obs"])
print()
print("Unbatched repeat dim", input_dict["obs"].unbatch_repeat_dim())
print()
print("Fully unbatched", input_dict["obs"].unbatch_all())
print()
return self.model.forward(input_dict, state, seq_lens)
def value_function(self):
return self.model.value_function()
class CustomTFRPGModel(TFModelV2):
"""Example of interpreting repeated observations."""
def __init__(self, obs_space, action_space, num_outputs, model_config, name):
super().__init__(obs_space, action_space, num_outputs, model_config, name)
self.model = TFFCNet(obs_space, action_space, num_outputs, model_config, name)
def forward(self, input_dict, state, seq_lens):
# The unpacked input tensors, where M=MAX_PLAYERS, N=MAX_ITEMS:
# {
# 'items', <tf.Tensor shape=(?, M, N, 5)>,
# 'location', <tf.Tensor shape=(?, M, 2)>,
# 'status', <tf.Tensor shape=(?, M, 10)>,
# }
print("The unpacked input tensors:", input_dict["obs"])
print()
print("Unbatched repeat dim", input_dict["obs"].unbatch_repeat_dim())
print()
if tf.executing_eagerly():
print("Fully unbatched", input_dict["obs"].unbatch_all())
print()
return self.model.forward(input_dict, state, seq_lens)
def value_function(self):
return self.model.value_function()
@@ -0,0 +1,134 @@
# @OldAPIStack
"""Example of creating a custom input API
Custom input apis are useful when your data source is in a custom format or
when it is necessary to use an external data loading mechanism.
In this example, we train an rl agent on user specified input data.
Instead of using the built in JsonReader, we will create our own custom input
api, and show how to pass config arguments to it.
To train CQL on the pendulum environment:
$ python custom_input_api.py --input-files=../offline/tests/data/pendulum/enormous.zip
"""
import argparse
import os
import ray
from ray import tune
from ray.rllib.offline import InputReader, IOContext, JsonReader, ShuffledInput
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
EVALUATION_RESULTS,
)
from ray.tune.registry import get_trainable_cls, register_input
from ray.tune.result import TRAINING_ITERATION
parser = argparse.ArgumentParser()
parser.add_argument(
"--run", type=str, default="CQL", help="The RLlib-registered algorithm to use."
)
parser.add_argument(
"--framework",
choices=["tf", "tf2", "torch"],
default="torch",
help="The DL framework specifier.",
)
parser.add_argument("--stop-iters", type=int, default=100)
parser.add_argument(
"--input-files",
type=str,
default=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"../../offline/tests/data/pendulum/small.json",
),
)
class CustomJsonReader(JsonReader):
"""
Example custom InputReader implementation (extended from JsonReader).
This gets wrapped in ShuffledInput to comply with offline rl algorithms.
"""
def __init__(self, ioctx: IOContext):
"""
The constructor must take an IOContext to be used in the input config.
Args:
ioctx: use this to access the `input_config` arguments.
"""
super().__init__(ioctx.input_config["input_files"], ioctx)
def input_creator(ioctx: IOContext) -> InputReader:
"""
The input creator method can be used in the input registry or set as the
config["input"] parameter.
Args:
ioctx: use this to access the `input_config` arguments.
Returns:
instance of ShuffledInput to work with some offline rl algorithms
"""
return ShuffledInput(CustomJsonReader(ioctx))
if __name__ == "__main__":
ray.init()
args = parser.parse_args()
# make absolute path because relative path looks in result directory
args.input_files = os.path.abspath(args.input_files)
# we register our custom input creator with this convenient function
register_input("custom_input", input_creator)
# Config modified from rllib/examples/algorithms/cql/pendulum-cql.yaml
default_config = get_trainable_cls(args.run).get_default_config()
config = (
default_config.environment("Pendulum-v1", clip_actions=True)
.framework(args.framework)
.offline_data(
# We can either use the tune registry ...
input_="custom_input",
# ... full classpath
# input_: "ray.rllib.examples.offline_rl.custom_input_api.CustomJsonReader"
# ... or a direct function to connect our input api.
# input_: input_creator
input_config={"input_files": args.input_files}, # <- passed to IOContext
actions_in_input_normalized=True,
)
.training(train_batch_size=2000)
.evaluation(
evaluation_interval=1,
evaluation_num_env_runners=2,
evaluation_duration=10,
evaluation_parallel_to_training=True,
evaluation_config=default_config.overrides(
input_="sampler",
explore=False,
),
)
.reporting(metrics_num_episodes_for_smoothing=5)
)
if args.run == "CQL":
config.training(
twin_q=True,
num_steps_sampled_before_learning_starts=0,
bc_iters=100,
)
stop = {
TRAINING_ITERATION: args.stop_iters,
f"{EVALUATION_RESULTS}/{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": -600,
}
tuner = tune.Tuner(
args.run, param_space=config, run_config=tune.RunConfig(stop=stop, verbose=1)
)
tuner.fit()
@@ -0,0 +1,168 @@
# @OldAPIStack
"""Example on how to use CQL to learn from an offline JSON file.
Important node: Make sure that your offline data file contains only
a single timestep per line to mimic the way SAC pulls samples from
the buffer.
Generate the offline json file by running an SAC algo until it reaches expert
level on your command line. For example:
$ cd ray
$ rllib train -f rllib/examples/algorithms/sac/pendulum-sac.yaml --no-ray-ui
Also make sure that in the above SAC yaml file (pendulum-sac.yaml),
you specify an additional "output" key with any path on your local
file system. In that path, the offline json files will be written to.
Use the generated file(s) as "input" in the CQL config below
(`config["input"] = [list of your json files]`), then run this script.
"""
import argparse
import numpy as np
from ray.rllib.algorithms import cql as cql
from ray.rllib.execution.rollout_ops import (
synchronous_parallel_sample,
)
from ray.rllib.policy.sample_batch import convert_ma_batch_to_sample_batch
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
EVALUATION_RESULTS,
)
torch, _ = try_import_torch()
parser = argparse.ArgumentParser()
parser.add_argument(
"--as-test",
action="store_true",
help="Whether this script should be run as a test: --stop-reward must "
"be achieved within --stop-timesteps AND --stop-iters.",
)
parser.add_argument(
"--stop-iters", type=int, default=5, help="Number of iterations to train."
)
parser.add_argument(
"--stop-reward", type=float, default=50.0, help="Reward at which we stop training."
)
if __name__ == "__main__":
args = parser.parse_args()
# See rllib/examples/algorithms/cql/pendulum-cql.yaml for comparison.
config = (
cql.CQLConfig()
.api_stack(
enable_env_runner_and_connector_v2=False,
enable_rl_module_and_learner=False,
)
.framework(framework="torch")
.env_runners(num_env_runners=0)
.training(
n_step=3,
bc_iters=0,
clip_actions=False,
tau=0.005,
target_entropy="auto",
q_model_config={
"fcnet_hiddens": [256, 256],
"fcnet_activation": "relu",
},
policy_model_config={
"fcnet_hiddens": [256, 256],
"fcnet_activation": "relu",
},
optimization_config={
"actor_learning_rate": 3e-4,
"critic_learning_rate": 3e-4,
"entropy_learning_rate": 3e-4,
},
train_batch_size=256,
target_network_update_freq=1,
num_steps_sampled_before_learning_starts=256,
)
.reporting(min_train_timesteps_per_iteration=1000)
.debugging(log_level="INFO")
.environment("Pendulum-v1", normalize_actions=True)
.offline_data(
input_config={
"paths": ["offline/tests/data/pendulum/enormous.zip"],
"format": "json",
}
)
.evaluation(
evaluation_num_env_runners=1,
evaluation_interval=1,
evaluation_duration=10,
evaluation_parallel_to_training=False,
evaluation_config=cql.CQLConfig.overrides(input_="sampler"),
)
)
# evaluation_parallel_to_training should be False b/c iterations are very long
# and this would cause evaluation to lag one iter behind training.
# Check, whether we can learn from the given file in `num_iterations`
# iterations, up to a reward of `min_reward`.
num_iterations = 5
min_reward = -300
cql_algorithm = cql.CQL(config=config)
learnt = False
for i in range(num_iterations):
print(f"Iter {i}")
eval_results = cql_algorithm.train().get(EVALUATION_RESULTS)
if eval_results:
print(
"... R={}".format(eval_results[ENV_RUNNER_RESULTS][EPISODE_RETURN_MEAN])
)
# Learn until some reward is reached on an actual live env.
if eval_results[ENV_RUNNER_RESULTS][EPISODE_RETURN_MEAN] >= min_reward:
# Test passed gracefully.
if args.as_test:
print("Test passed after {} iterations.".format(i))
quit(0)
learnt = True
break
# Get policy and model.
cql_policy = cql_algorithm.get_policy()
cql_model = cql_policy.model
# If you would like to query CQL's learnt Q-function for arbitrary
# (cont.) actions, do the following:
obs_batch = torch.from_numpy(np.random.random(size=(5, 3)))
action_batch = torch.from_numpy(np.random.random(size=(5, 1)))
q_values = cql_model.get_q_values(obs_batch, action_batch)[0]
# If you are using the "twin_q", there'll be 2 Q-networks and
# we usually consider the min of the 2 outputs, like so:
twin_q_values = cql_model.get_twin_q_values(obs_batch, action_batch)[0]
final_q_values = torch.min(q_values, twin_q_values)[0]
print(f"final_q_values={final_q_values.detach().numpy()}")
# Example on how to do evaluation on the trained Algorithm.
# using the data from our buffer.
# Get a sample (MultiAgentBatch).
batch = synchronous_parallel_sample(worker_set=cql_algorithm.env_runner_group)
batch = convert_ma_batch_to_sample_batch(batch)
obs = torch.from_numpy(batch["obs"])
# Pass the observations through our model to get the
# features, which then to pass through the Q-head.
model_out, _ = cql_model({"obs": obs})
# The estimated Q-values from the (historic) actions in the batch.
q_values_old = cql_model.get_q_values(
model_out, torch.from_numpy(batch["actions"])
)[0]
# The estimated Q-values for the new actions computed by our policy.
actions_new = cql_policy.compute_actions_from_input_dict({"obs": obs})[0]
q_values_new = cql_model.get_q_values(model_out, torch.from_numpy(actions_new))[0]
print(f"Q-val batch={q_values_old.detach().numpy()}")
print(f"Q-val policy={q_values_new.detach().numpy()}")
cql_algorithm.stop()
@@ -0,0 +1,60 @@
# @OldAPIStack
"""Simple example of writing experiences to a file using JsonWriter."""
# __sphinx_doc_begin__
import os
import gymnasium as gym
import numpy as np
from ray._common.utils import get_default_ray_temp_dir
from ray.rllib.evaluation.sample_batch_builder import SampleBatchBuilder
from ray.rllib.models.preprocessors import get_preprocessor
from ray.rllib.offline.json_writer import JsonWriter
if __name__ == "__main__":
batch_builder = SampleBatchBuilder() # or MultiAgentSampleBatchBuilder
writer = JsonWriter(os.path.join(get_default_ray_temp_dir(), "demo-out"))
# You normally wouldn't want to manually create sample batches if a
# simulator is available, but let's do it anyways for example purposes:
env = gym.make("CartPole-v1")
# RLlib uses preprocessors to implement transforms such as one-hot encoding
# and flattening of tuple and dict observations. For CartPole a no-op
# preprocessor is used, but this may be relevant for more complex envs.
prep = get_preprocessor(env.observation_space)(env.observation_space)
print("The preprocessor is", prep)
for eps_id in range(100):
obs, info = env.reset()
prev_action = np.zeros_like(env.action_space.sample())
prev_reward = 0
terminated = truncated = False
t = 0
while not terminated and not truncated:
action = env.action_space.sample()
new_obs, rew, terminated, truncated, info = env.step(action)
batch_builder.add_values(
t=t,
eps_id=eps_id,
agent_index=0,
obs=prep.transform(obs),
actions=action,
action_prob=1.0, # put the true action probability here
action_logp=0.0,
rewards=rew,
prev_actions=prev_action,
prev_rewards=prev_reward,
terminateds=terminated,
truncateds=truncated,
infos=info,
new_obs=prep.transform(new_obs),
)
obs = new_obs
prev_action = action
prev_reward = rew
t += 1
writer.write(batch_builder.build_and_reset())
# __sphinx_doc_end__
@@ -0,0 +1,121 @@
# @OldAPIStack
"""Example of handling variable length or parametric action spaces.
This toy example demonstrates the action-embedding based approach for handling large
discrete action spaces (potentially infinite in size), similar to this example:
https://neuro.cs.ut.ee/the-use-of-embeddings-in-openai-five/
This example works with RLlib's policy gradient style algorithms
(e.g., PG, PPO, IMPALA, A2C) and DQN.
Note that since the model outputs now include "-inf" tf.float32.min
values, not all algorithm options are supported. For example,
algorithms might crash if they don't properly ignore the -inf action scores.
Working configurations are given below.
"""
import argparse
import os
import ray
from ray import tune
from ray.rllib.examples._old_api_stack.models.parametric_actions_model import (
ParametricActionsModel,
TorchParametricActionsModel,
)
from ray.rllib.examples.envs.classes.parametric_actions_cartpole import (
ParametricActionsCartPole,
)
from ray.rllib.models import ModelCatalog
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.rllib.utils.test_utils import check_learning_achieved
from ray.tune.registry import register_env
from ray.tune.result import TRAINING_ITERATION
parser = argparse.ArgumentParser()
parser.add_argument(
"--run", type=str, default="PPO", help="The RLlib-registered algorithm to use."
)
parser.add_argument(
"--framework",
choices=["tf", "tf2", "torch"],
default="torch",
help="The DL framework specifier.",
)
parser.add_argument(
"--as-test",
action="store_true",
help="Whether this script should be run as a test: --stop-reward must "
"be achieved within --stop-timesteps AND --stop-iters.",
)
parser.add_argument(
"--stop-iters", type=int, default=200, help="Number of iterations to train."
)
parser.add_argument(
"--stop-timesteps", type=int, default=100000, help="Number of timesteps to train."
)
parser.add_argument(
"--stop-reward", type=float, default=150.0, help="Reward at which we stop training."
)
if __name__ == "__main__":
args = parser.parse_args()
ray.init()
register_env("pa_cartpole", lambda _: ParametricActionsCartPole(10))
ModelCatalog.register_custom_model(
"pa_model",
TorchParametricActionsModel
if args.framework == "torch"
else ParametricActionsModel,
)
if args.run == "DQN":
cfg = {
# TODO(ekl) we need to set these to prevent the masked values
# from being further processed in DistributionalQModel, which
# would mess up the masking. It is possible to support these if we
# defined a custom DistributionalQModel that is aware of masking.
"hiddens": [],
"dueling": False,
"enable_rl_module_and_learner": False,
"enable_env_runner_and_connector_v2": False,
}
else:
cfg = {}
config = dict(
{
"env": "pa_cartpole",
"model": {
"custom_model": "pa_model",
},
# Use GPUs iff `RLLIB_NUM_GPUS` env var set to > 0.
"num_gpus": int(os.environ.get("RLLIB_NUM_GPUS", "0")),
"num_env_runners": 0,
"framework": args.framework,
},
**cfg,
)
stop = {
TRAINING_ITERATION: args.stop_iters,
f"{NUM_ENV_STEPS_SAMPLED_LIFETIME}": args.stop_timesteps,
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": args.stop_reward,
}
results = tune.Tuner(
args.run,
run_config=tune.RunConfig(stop=stop, verbose=1),
param_space=config,
).fit()
if args.as_test:
check_learning_achieved(results, args.stop_reward)
ray.shutdown()
@@ -0,0 +1,107 @@
# @OldAPIStack
"""Example of handling variable length or parametric action spaces.
This is a toy example of the action-embedding based approach for handling large
discrete action spaces (potentially infinite in size), similar to this:
https://neuro.cs.ut.ee/the-use-of-embeddings-in-openai-five/
This currently works with RLlib's policy gradient style algorithms
(e.g., PG, PPO, IMPALA, A2C) and also DQN.
Note that since the model outputs now include "-inf" tf.float32.min
values, not all algorithm options are supported at the moment. For example,
algorithms might crash if they don't properly ignore the -inf action scores.
Working configurations are given below.
"""
import argparse
import os
import ray
from ray import tune
from ray.rllib.examples._old_api_stack.models.parametric_actions_model import (
ParametricActionsModelThatLearnsEmbeddings,
)
from ray.rllib.examples.envs.classes.parametric_actions_cartpole import (
ParametricActionsCartPoleNoEmbeddings,
)
from ray.rllib.models import ModelCatalog
from ray.rllib.utils.metrics import (
ENV_RUNNER_RESULTS,
EPISODE_RETURN_MEAN,
NUM_ENV_STEPS_SAMPLED_LIFETIME,
)
from ray.rllib.utils.test_utils import check_learning_achieved
from ray.tune.registry import register_env
from ray.tune.result import TRAINING_ITERATION
parser = argparse.ArgumentParser()
parser.add_argument("--run", type=str, default="PPO")
parser.add_argument(
"--framework",
choices=["tf", "tf2"],
default="tf",
help="The DL framework specifier (Torch not supported "
"due to the lack of a model).",
)
parser.add_argument("--as-test", action="store_true")
parser.add_argument("--stop-iters", type=int, default=200)
parser.add_argument("--stop-reward", type=float, default=150.0)
parser.add_argument("--stop-timesteps", type=int, default=100000)
if __name__ == "__main__":
args = parser.parse_args()
ray.init()
register_env("pa_cartpole", lambda _: ParametricActionsCartPoleNoEmbeddings(10))
ModelCatalog.register_custom_model(
"pa_model", ParametricActionsModelThatLearnsEmbeddings
)
if args.run == "DQN":
cfg = {
# TODO(ekl) we need to set these to prevent the masked values
# from being further processed in DistributionalQModel, which
# would mess up the masking. It is possible to support these if we
# defined a custom DistributionalQModel that is aware of masking.
"hiddens": [],
"dueling": False,
"enable_rl_module_and_learner": False,
"enable_env_runner_and_connector_v2": False,
}
else:
cfg = {}
config = dict(
{
"env": "pa_cartpole",
"model": {
"custom_model": "pa_model",
},
# Use GPUs iff `RLLIB_NUM_GPUS` env var set to > 0.
"num_gpus": int(os.environ.get("RLLIB_NUM_GPUS", "0")),
"num_env_runners": 0,
"framework": args.framework,
"action_mask_key": "valid_avail_actions_mask",
},
**cfg,
)
stop = {
TRAINING_ITERATION: args.stop_iters,
NUM_ENV_STEPS_SAMPLED_LIFETIME: args.stop_timesteps,
f"{ENV_RUNNER_RESULTS}/{EPISODE_RETURN_MEAN}": args.stop_reward,
}
results = tune.Tuner(
args.run,
run_config=tune.RunConfig(stop=stop, verbose=2),
param_space=config,
).fit()
if args.as_test:
check_learning_achieved(results, args.stop_reward)
ray.shutdown()
@@ -0,0 +1,101 @@
# @OldAPIStack
from typing import Dict, List, Optional, Tuple, Union
import gymnasium as gym
import numpy as np
from ray.rllib.models.torch.torch_action_dist import TorchCategorical
from ray.rllib.policy.policy import Policy, ViewRequirement
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.utils.annotations import override
from ray.rllib.utils.debug import update_global_seed_if_necessary
from ray.rllib.utils.typing import AlgorithmConfigDict, TensorStructType, TensorType
class CliffWalkingWallPolicy(Policy):
"""Optimal RLlib policy for the CliffWalkingWallEnv environment, defined in
ray/rllib/examples/env/cliff_walking_wall_env.py, with epsilon-greedy exploration.
The policy takes a random action with probability epsilon, specified
by `config["epsilon"]`, and the optimal action with probability 1 - epsilon.
"""
@override(Policy)
def __init__(
self,
observation_space: gym.Space,
action_space: gym.Space,
config: AlgorithmConfigDict,
):
update_global_seed_if_necessary(seed=config.get("seed"))
super().__init__(observation_space, action_space, config)
# Known optimal action dist for each of the 48 states and 4 actions
self.action_dist = np.zeros((48, 4), dtype=float)
# Starting state: go up
self.action_dist[36] = (1, 0, 0, 0)
# Cliff + Goal: never actually used, set to random
self.action_dist[37:] = (0.25, 0.25, 0.25, 0.25)
# Row 2; always go right
self.action_dist[24:36] = (0, 1, 0, 0)
# Row 0 and Row 1; go down or go right
self.action_dist[0:24] = (0, 0.5, 0.5, 0)
# Col 11; always go down, supercedes previous values
self.action_dist[[11, 23, 35]] = (0, 0, 1, 0)
assert np.allclose(self.action_dist.sum(-1), 1)
# Epsilon-Greedy action selection
epsilon = config.get("epsilon", 0.0)
self.action_dist = self.action_dist * (1 - epsilon) + epsilon / 4
assert np.allclose(self.action_dist.sum(-1), 1)
# Attributes required for RLlib; note that while CliffWalkingWallPolicy
# inherits from Policy, it actually implements TorchPolicyV2.
self.view_requirements[SampleBatch.ACTION_PROB] = ViewRequirement()
self.device = "cpu"
self.model = None
self.dist_class = TorchCategorical
@override(Policy)
def compute_actions(
self,
obs_batch: Union[List[TensorStructType], TensorStructType],
state_batches: Optional[List[TensorType]] = None,
**kwargs,
) -> Tuple[TensorType, List[TensorType], Dict[str, TensorType]]:
obs = np.array(obs_batch, dtype=int)
action_probs = self.action_dist[obs]
actions = np.zeros(len(obs), dtype=int)
for i in range(len(obs)):
actions[i] = np.random.choice(4, p=action_probs[i])
return (
actions,
[],
{SampleBatch.ACTION_PROB: action_probs[np.arange(len(obs)), actions]},
)
@override(Policy)
def compute_log_likelihoods(
self,
actions: Union[List[TensorType], TensorType],
obs_batch: Union[List[TensorType], TensorType],
**kwargs,
) -> TensorType:
obs = np.array(obs_batch, dtype=int)
actions = np.array(actions, dtype=int)
# Compute action probs for all possible actions
action_probs = self.action_dist[obs]
# Take the action_probs corresponding to the specified actions
action_probs = action_probs[np.arange(len(obs)), actions]
# Ignore RuntimeWarning thrown by np.log(0) if action_probs is 0
with np.errstate(divide="ignore"):
return np.log(action_probs)
def action_distribution_fn(
self, model, obs_batch: TensorStructType, **kwargs
) -> Tuple[TensorType, type, List[TensorType]]:
obs = np.array(obs_batch[SampleBatch.OBS], dtype=int)
action_probs = self.action_dist[obs]
# Ignore RuntimeWarning thrown by np.log(0) if action_probs is 0
with np.errstate(divide="ignore"):
return np.log(action_probs), TorchCategorical, None
@@ -0,0 +1,102 @@
# @OldAPIStack
import random
from typing import (
List,
Optional,
Union,
)
import numpy as np
import tree # pip install dm_tree
from gymnasium.spaces import Box
from ray.rllib.policy.policy import Policy
from ray.rllib.policy.sample_batch import SampleBatch
from ray.rllib.utils.annotations import override
from ray.rllib.utils.typing import ModelWeights, TensorStructType, TensorType
class RandomPolicy(Policy):
"""Hand-coded policy that returns random actions."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Whether for compute_actions, the bounds given in action_space
# should be ignored (default: False). This is to test action-clipping
# and any Env's reaction to bounds breaches.
if self.config.get("ignore_action_bounds", False) and isinstance(
self.action_space, Box
):
self.action_space_for_sampling = Box(
-float("inf"),
float("inf"),
shape=self.action_space.shape,
dtype=self.action_space.dtype,
)
else:
self.action_space_for_sampling = self.action_space
@override(Policy)
def init_view_requirements(self):
super().init_view_requirements()
# Disable for_training and action attributes for SampleBatch.INFOS column
# since it can not be properly batched.
vr = self.view_requirements[SampleBatch.INFOS]
vr.used_for_training = False
vr.used_for_compute_actions = False
@override(Policy)
def compute_actions(
self,
obs_batch: Union[List[TensorStructType], TensorStructType],
state_batches: Optional[List[TensorType]] = None,
prev_action_batch: Union[List[TensorStructType], TensorStructType] = None,
prev_reward_batch: Union[List[TensorStructType], TensorStructType] = None,
**kwargs,
):
# Alternatively, a numpy array would work here as well.
# e.g.: np.array([random.choice([0, 1])] * len(obs_batch))
obs_batch_size = len(tree.flatten(obs_batch)[0])
return (
[self.action_space_for_sampling.sample() for _ in range(obs_batch_size)],
[],
{},
)
@override(Policy)
def learn_on_batch(self, samples):
"""No learning."""
return {}
@override(Policy)
def compute_log_likelihoods(
self,
actions,
obs_batch,
state_batches=None,
prev_action_batch=None,
prev_reward_batch=None,
**kwargs,
):
return np.array([random.random()] * len(obs_batch))
@override(Policy)
def get_weights(self) -> ModelWeights:
"""No weights to save."""
return {}
@override(Policy)
def set_weights(self, weights: ModelWeights) -> None:
"""No weights to set."""
pass
@override(Policy)
def _get_dummy_batch_from_view_requirements(self, batch_size: int = 1):
return SampleBatch(
{
SampleBatch.OBS: tree.map_structure(
lambda s: s[None], self.observation_space.sample()
),
}
)
@@ -0,0 +1,82 @@
# @OldAPIStack
# __sphinx_doc_replay_buffer_api_example_script_begin__
"""Simple example of how to modify replay buffer behaviour.
We modify DQN to utilize prioritized replay but supplying it with the
PrioritizedMultiAgentReplayBuffer instead of the standard MultiAgentReplayBuffer.
This is possible because DQN uses the DQN training iteration function,
which includes and a priority update, given that a fitting buffer is provided.
"""
import argparse
import ray
from ray import tune
from ray.rllib.algorithms.dqn import DQNConfig
from ray.rllib.utils.framework import try_import_tf
from ray.rllib.utils.metrics import NUM_ENV_STEPS_SAMPLED_LIFETIME
from ray.rllib.utils.replay_buffers.replay_buffer import StorageUnit
from ray.tune.result import TRAINING_ITERATION
tf1, tf, tfv = try_import_tf()
parser = argparse.ArgumentParser()
parser.add_argument("--num-cpus", type=int, default=0)
parser.add_argument(
"--framework",
choices=["tf", "tf2", "torch"],
default="torch",
help="The DL framework specifier.",
)
parser.add_argument(
"--stop-iters", type=int, default=50, help="Number of iterations to train."
)
parser.add_argument(
"--stop-timesteps", type=int, default=100000, help="Number of timesteps to train."
)
if __name__ == "__main__":
args = parser.parse_args()
ray.init(num_cpus=args.num_cpus or None)
# This is where we add prioritized experiences replay
# The training iteration function that is used by DQN already includes a priority
# update step.
replay_buffer_config = {
"type": "MultiAgentPrioritizedReplayBuffer",
# Although not necessary, we can modify the default constructor args of
# the replay buffer here
"prioritized_replay_alpha": 0.5,
"storage_unit": StorageUnit.SEQUENCES,
"replay_burn_in": 20,
"zero_init_states": True,
}
config = (
DQNConfig()
.environment("CartPole-v1")
.framework(framework=args.framework)
.env_runners(num_env_runners=4)
.training(
model=dict(use_lstm=True, lstm_cell_size=64, max_seq_len=20),
replay_buffer_config=replay_buffer_config,
)
)
stop_config = {
NUM_ENV_STEPS_SAMPLED_LIFETIME: args.stop_timesteps,
TRAINING_ITERATION: args.stop_iters,
}
results = tune.Tuner(
config.algo_class,
param_space=config,
run_config=tune.RunConfig(stop=stop_config),
).fit()
ray.shutdown()
# __sphinx_doc_replay_buffer_api_example_script_end__