Files
ray-project--ray/rllib/algorithms/dreamerv3/tests/test_dreamerv3.py
T
2026-07-13 13:17:40 +08:00

325 lines
12 KiB
Python

"""
[1] Mastering Diverse Domains through World Models - 2023
D. Hafner, J. Pasukonis, J. Ba, T. Lillicrap
https://arxiv.org/pdf/2301.04104v1.pdf
[2] Mastering Atari with Discrete World Models - 2021
D. Hafner, T. Lillicrap, M. Norouzi, J. Ba
https://arxiv.org/pdf/2010.02193.pdf
[3]
D. Hafner's (author) original code repo (for JAX):
https://github.com/danijar/dreamerv3
"""
import unittest
import gymnasium as gym
import numpy as np
import tree # pip install dm_tree
import ray
from ray import tune
from ray.rllib.algorithms.dreamerv3 import dreamerv3
from ray.rllib.connectors.env_to_module import FlattenObservations
from ray.rllib.core import DEFAULT_MODULE_ID
from ray.rllib.env.wrappers.atari_wrappers import wrap_atari_for_new_api_stack
from ray.rllib.env.wrappers.dm_control_wrapper import ActionClip, DMCEnv
from ray.rllib.utils.framework import try_import_torch
from ray.rllib.utils.numpy import one_hot
from ray.rllib.utils.test_utils import check
torch, nn = try_import_torch()
class TestDreamerV3(unittest.TestCase):
@classmethod
def setUpClass(cls):
ray.init()
@classmethod
def tearDownClass(cls):
ray.shutdown()
def test_dreamerv3_compilation(self):
"""Test whether DreamerV3 can be built with all frameworks."""
# Build a DreamerV3Config object.
config = (
dreamerv3.DreamerV3Config()
.env_runners(num_env_runners=0)
.training(
# Keep things simple. Especially the long dream rollouts seem
# to take an enormous amount of time (initially).
batch_size_B=4,
horizon_H=5,
batch_length_T=16,
model_size="nano", # Use a tiny model for testing
symlog_obs=True,
use_float16=False,
)
.learners(
num_learners=2,
num_cpus_per_learner=1,
num_gpus_per_learner=0,
)
)
num_iterations = 3
for env in [
# "DMC/cartpole/swingup", # causes strange MuJoCo error(s) on CI
"FrozenLake-v1",
"CartPole-v1",
"ale_py:ALE/MsPacman-v5",
"Pendulum-v1",
]:
print("Env={}".format(env))
# Add one-hot observations for FrozenLake env.
if env == "FrozenLake-v1":
config.env_runners(
env_to_module_connector=(
lambda env, spaces, device: FlattenObservations()
)
)
else:
config.env_runners(env_to_module_connector=None)
# Add Atari preprocessing.
if env == "ale_py:ALE/MsPacman-v5":
def env_creator(cfg):
return wrap_atari_for_new_api_stack(
gym.make(env, **cfg, render_mode="rgb_array"),
# No frame-stacking. DreamerV3 processes color images with a
# GRU, so partial observability is ok.
framestack=None,
grayscale=False,
)
tune.register_env("env", env_creator)
env = "env"
elif env.startswith("DMC"):
parts = env.split("/")
assert len(parts) == 3, (
"ERROR: DMC env must be formatted as 'DMC/[task]/[domain]', e.g. "
f"'DMC/cartpole/swingup'! You provided '{env}'."
)
def env_creator(cfg):
return ActionClip(
DMCEnv(
parts[1],
parts[2],
from_pixels=True,
channels_first=False,
)
)
tune.register_env("env", env_creator)
env = "env"
config.environment(env)
algo = config.build_algo()
obs_space = algo.env_runner._env_to_module.observation_space
act_space = algo.env_runner.env.single_action_space
rl_module = algo.env_runner.module
for i in range(num_iterations):
results = algo.train()
print(results)
# Test dream trajectory w/ recreated observations.
sample = algo.replay_buffer.sample()
start_states = rl_module.dreamer_model.get_initial_state()
start_states = tree.map_structure(
# Repeat only the batch dimension (B times).
lambda s: s.unsqueeze(0).repeat(1, *([1] * len(s.shape))),
start_states,
)
dream = rl_module.dreamer_model.dream_trajectory_with_burn_in(
start_states=start_states,
timesteps_burn_in=5,
timesteps_H=45,
observations=torch.from_numpy(sample["obs"][:1]), # B=1
actions=torch.from_numpy(
one_hot(
sample["actions"],
depth=act_space.n,
)
if isinstance(act_space, gym.spaces.Discrete)
else sample["actions"]
)[
:1
], # B=1
)
check(
dream["actions_dreamed_t0_to_H_BxT"].shape,
(46, 1)
+ (
(act_space.n,)
if isinstance(act_space, gym.spaces.Discrete)
else tuple(act_space.shape)
),
)
check(dream["continues_dreamed_t0_to_H_BxT"].shape, (46, 1))
check(
dream["observations_dreamed_t0_to_H_BxT"].shape,
[46, 1] + list(obs_space.shape),
)
algo.stop()
def test_dreamerv3_dreamer_model_sizes(self):
"""Tests, whether the different model sizes match the ones reported in [1]."""
# For Atari, these are the exact numbers from the repo ([3]).
# However, for CartPole + size "S" and "M", the author's original code will not
# match for the world model count. This is due to the fact that the author uses
# encoder/decoder nets with 5x1024 nodes (which corresponds to XL) regardless of
# the `model_size` settings (iff >="S").
expected_num_params_world_model = {
# XS encoder
# kernel=[4, 256], (no bias), layernorm=[256],[256]
# XS reward_predictor
# kernel=[1280, 256], (no bias), layernorm[256],[256]
# kernel=[256, 255] bias=[255]
# 1280=1024 (z-state) + 256 (h-state)
# XS continue_predictor
# kernel=[1280, 256], (no bias), layernorm=[256],[256]
# kernel=[256, 1] bias=[1]
# XS sequence_model
# [
# pre-MLP: kernel=[1026, 256], (no bias), layernorm=[256],[256], silu
# custom GRU: kernel=[512, 768], (no bias), layernorm=[768],[768]
# ]
# XS decoder
# kernel=[1280, 256], (no bias), layernorm=[256],[256]
# kernel=[256, 4] bias=[4]
# XS posterior_mlp
# kernel=[512, 256], (no bias), layernorm=[256],[256]
# XS posterior_representation_layer
# kernel=[256, 1024], bias=[1024]
"XS_cartpole": 2435076,
"S_cartpole": 7493380,
"M_cartpole": 16206084,
"L_cartpole": 37802244,
"XL_cartpole": 108353796,
# XS encoder (atari)
# cnn kernel=[4, 4, 3, 24], (no bias), layernorm=[24],[24],
# cnn kernel=[4, 4, 24, 48], (no bias), layernorm=[48],[48],
# cnn kernel=[4, 4, 48, 96], (no bias), layernorm=[96],[96],
# cnn kernel=[4, 4, 96, 192], (no bias), layernorm=[192],[192],
# XS decoder (atari)
# init dense kernel[1280, 3072] bias=[3072] -> reshape into image
# [4, 4, 96, 192], [96], [96]
# [4, 4, 48, 96], [48], [48],
# [4, 4, 24, 48], [24], [24],
# [4, 4, 3, 24], [3] <- no layernorm at end
"XS_atari": 7538979,
"S_atari": 15687811,
"M_atari": 32461635,
"L_atari": 68278275,
"XL_atari": 181558659,
}
# All values confirmed against [3] (100% match).
expected_num_params_actor = {
# hidden=[1280, 256]
# hidden_norm=[256], [256]
# pi (2 actions)=[256, 2], [2]
"XS_cartpole": 328706,
"S_cartpole": 1051650,
"M_cartpole": 2135042,
"L_cartpole": 4136450,
"XL_cartpole": 9449474,
"XS_atari": 329734,
"S_atari": 1053702,
"M_atari": 2137606,
"L_atari": 4139526,
"XL_atari": 9453574,
}
# All values confirmed against [3] (100% match).
expected_num_params_critic = {
# hidden=[1280, 256]
# hidden_norm=[256], [256]
# vf (buckets)=[256, 255], [255]
"XS_cartpole": 393727,
"S_cartpole": 1181439,
"M_cartpole": 2297215,
"L_cartpole": 4331007,
"XL_cartpole": 9708799,
"XS_atari": 393727,
"S_atari": 1181439,
"M_atari": 2297215,
"L_atari": 4331007,
"XL_atari": 9708799,
}
config = dreamerv3.DreamerV3Config().training(
batch_length_T=16,
horizon_H=5,
symlog_obs=True,
)
# Check all model_sizes described in the paper ([1]) on matching the number
# of parameters to RLlib's implementation.
for model_size in ["XS", "S", "M", "L", "XL"]:
config.model_size = model_size
# Atari and CartPole spaces.
for obs_space, num_actions, env_name in [
(gym.spaces.Box(-1.0, 0.0, (4,), np.float32), 2, "cartpole"),
(gym.spaces.Box(-1.0, 0.0, (64, 64, 3), np.float32), 6, "atari"),
]:
print(f"Testing model_size={model_size} on env-type: {env_name} ..")
config.environment(
observation_space=obs_space,
action_space=gym.spaces.Discrete(num_actions),
)
# Create our RLModule to compute actions with.
policy_dict, _ = config.get_multi_agent_setup()
module_spec = config.get_multi_rl_module_spec(policy_dict=policy_dict)
rl_module = module_spec.build()[DEFAULT_MODULE_ID]
# Count the generated RLModule's parameters and compare to the
# paper's reported numbers ([1] and [3]).
num_params_world_model = sum(
np.prod(v.shape)
for v in rl_module.world_model.parameters()
if v.requires_grad
)
self.assertEqual(
num_params_world_model,
expected_num_params_world_model[f"{model_size}_{env_name}"],
)
num_params_actor = sum(
np.prod(v.shape)
for v in rl_module.actor.parameters()
if v.requires_grad
)
self.assertEqual(
num_params_actor,
expected_num_params_actor[f"{model_size}_{env_name}"],
)
num_params_critic = sum(
np.prod(v.shape)
for v in rl_module.critic.parameters()
if v.requires_grad
)
self.assertEqual(
num_params_critic,
expected_num_params_critic[f"{model_size}_{env_name}"],
)
print("\tok")
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main(["-v", __file__]))