System info helper (#613)

* Add `system_env_info`

* Add `print_system_info` to load
and store system info at save time

* Remove TODO

* Rename to `get_system_info`

* Import as sb3 for consistency

* Update changelog

* Add warning for old SB3 versions

* Use underscore litteral for more clarity
This commit is contained in:
Antonin RAFFIN 2021-10-18 10:43:56 +02:00 committed by GitHub
parent 09e9fc42eb
commit 1564a85081
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 111 additions and 18 deletions

View file

@ -50,6 +50,12 @@ Describe the characteristic of your environment:
* Gym version * Gym version
* Versions of any other relevant libraries * Versions of any other relevant libraries
You can use `sb3.get_system_info()` to print relevant packages info:
```python
import stable_baselines3 as sb3
sb3.get_system_info()
```
### Additional context ### Additional context
Add any other context about the problem here. Add any other context about the problem here.

View file

@ -78,6 +78,12 @@ Describe the characteristic of your environment:
* Gym version * Gym version
* Versions of any other relevant libraries * Versions of any other relevant libraries
You can use `sb3.get_system_info()` to print relevant packages info:
```python
import stable_baselines3 as sb3
sb3.get_system_info()
```
### Additional context ### Additional context
Add any other context about the problem here. Add any other context about the problem here.

View file

@ -81,6 +81,9 @@ In the following example, we will train, save and load a DQN model on the Lunar
del model # delete trained model to demonstrate loading del model # delete trained model to demonstrate loading
# Load the trained agent # Load the trained agent
# NOTE: if you have loading issue, you can pass `print_system_info=True`
# to compare the system on which the model was trained vs the current one
# model = DQN.load("dqn_lunar", env=env, print_system_info=True)
model = DQN.load("dqn_lunar", env=env) model = DQN.load("dqn_lunar", env=env)
# Evaluate the agent # Evaluate the agent

View file

@ -30,8 +30,15 @@ inspecting stored objects without deserializing the object itself.
This format allows skipping elements in the file, i.e. we can skip deserializing objects that are This format allows skipping elements in the file, i.e. we can skip deserializing objects that are
broken/non-serializable. broken/non-serializable.
This can be done via ``custom_objects`` argument to load functions.
.. This can be done via ``custom_objects`` argument to load functions. .. note::
If you encounter loading issue, for instance pickle issues or error after loading
(see `#171 <https://github.com/DLR-RM/stable-baselines3/issues/171>`_ or `#573 <https://github.com/DLR-RM/stable-baselines3/issues/573>`_),
you can pass ``print_system_info=True``
to compare the system on which the model was trained vs the current one
``model = PPO.load("ppo_saved", print_system_info=True)``
File structure: File structure:
@ -44,6 +51,7 @@ File structure:
├── policy.pth PyTorch state dictionary of the policy saved ├── policy.pth PyTorch state dictionary of the policy saved
├── pytorch_variables.pth Additional PyTorch variables ├── pytorch_variables.pth Additional PyTorch variables
├── _stable_baselines3_version contains the SB3 version with which the model was saved ├── _stable_baselines3_version contains the SB3 version with which the model was saved
├── system_info.txt contains system info (os, python version, ...) on which the model was saved
Pros: Pros:

View file

@ -4,10 +4,9 @@ Changelog
========== ==========
Release 1.2.1a3 (WIP) Release 1.2.1a4 (WIP)
--------------------------- ---------------------------
Breaking Changes: Breaking Changes:
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
- ``sde_net_arch`` argument in policies is deprecated and will be removed in a future version. - ``sde_net_arch`` argument in policies is deprecated and will be removed in a future version.
@ -22,6 +21,8 @@ New Features:
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
- Added methods ``get_distribution`` and ``predict_values`` for ``ActorCriticPolicy`` for A2C/PPO/TRPO (@cyprienc) - Added methods ``get_distribution`` and ``predict_values`` for ``ActorCriticPolicy`` for A2C/PPO/TRPO (@cyprienc)
- Added methods ``forward_actor`` and ``forward_critic`` for ``MlpExtractor`` - Added methods ``forward_actor`` and ``forward_critic`` for ``MlpExtractor``
- Added ``sb3.get_system_info()`` helper function to gather version information relevant to SB3 (e.g., Python and PyTorch version)
- Saved models now store system information where agent was trained, and load functions have ``print_system_info`` parameter to help debugging load issues.
Bug Fixes: Bug Fixes:
^^^^^^^^^^ ^^^^^^^^^^

View file

@ -1,6 +1,7 @@
import os import os
from stable_baselines3.a2c import A2C from stable_baselines3.a2c import A2C
from stable_baselines3.common.utils import get_system_info
from stable_baselines3.ddpg import DDPG from stable_baselines3.ddpg import DDPG
from stable_baselines3.dqn import DQN from stable_baselines3.dqn import DQN
from stable_baselines3.her.her_replay_buffer import HerReplayBuffer from stable_baselines3.her.her_replay_buffer import HerReplayBuffer

View file

@ -25,6 +25,7 @@ from stable_baselines3.common.utils import (
check_for_correct_spaces, check_for_correct_spaces,
get_device, get_device,
get_schedule_fn, get_schedule_fn,
get_system_info,
set_random_seed, set_random_seed,
update_learning_rate, update_learning_rate,
) )
@ -634,6 +635,7 @@ class BaseAlgorithm(ABC):
env: Optional[GymEnv] = None, env: Optional[GymEnv] = None,
device: Union[th.device, str] = "auto", device: Union[th.device, str] = "auto",
custom_objects: Optional[Dict[str, Any]] = None, custom_objects: Optional[Dict[str, Any]] = None,
print_system_info: bool = False,
**kwargs, **kwargs,
) -> "BaseAlgorithm": ) -> "BaseAlgorithm":
""" """
@ -650,9 +652,17 @@ class BaseAlgorithm(ABC):
will be used instead. Similar to custom_objects in will be used instead. Similar to custom_objects in
``keras.models.load_model``. Useful when you have an object in ``keras.models.load_model``. Useful when you have an object in
file that can not be deserialized. file that can not be deserialized.
:param print_system_info: Whether to print system info from the saved model
and the current system info (useful to debug loading issues)
:param kwargs: extra arguments to change the model when loading :param kwargs: extra arguments to change the model when loading
""" """
data, params, pytorch_variables = load_from_zip_file(path, device=device, custom_objects=custom_objects) if print_system_info:
print("== CURRENT SYSTEM INFO ==")
get_system_info()
data, params, pytorch_variables = load_from_zip_file(
path, device=device, custom_objects=custom_objects, print_system_info=print_system_info
)
# Remove stored device information and replace with ours # Remove stored device information and replace with ours
if "policy_kwargs" in data: if "policy_kwargs" in data:

View file

@ -79,7 +79,7 @@ class OffPolicyAlgorithm(BaseAlgorithm):
env: Union[GymEnv, str], env: Union[GymEnv, str],
policy_base: Type[BasePolicy], policy_base: Type[BasePolicy],
learning_rate: Union[float, Schedule], learning_rate: Union[float, Schedule],
buffer_size: int = 1000000, # 1e6 buffer_size: int = 1_000_000, # 1e6
learning_starts: int = 100, learning_starts: int = 100,
batch_size: int = 256, batch_size: int = 256,
tau: float = 0.005, tau: float = 0.005,

View file

@ -16,9 +16,9 @@ from typing import Any, Dict, Optional, Tuple, Union
import cloudpickle import cloudpickle
import torch as th import torch as th
import stable_baselines3 import stable_baselines3 as sb3
from stable_baselines3.common.type_aliases import TensorDict from stable_baselines3.common.type_aliases import TensorDict
from stable_baselines3.common.utils import get_device from stable_baselines3.common.utils import get_device, get_system_info
def recursive_getattr(obj: Any, attr: str, *args) -> Any: def recursive_getattr(obj: Any, attr: str, *args) -> Any:
@ -321,7 +321,9 @@ def save_to_zip_file(
with archive.open(file_name + ".pth", mode="w") as param_file: with archive.open(file_name + ".pth", mode="w") as param_file:
th.save(dict_, param_file) th.save(dict_, param_file)
# Save metadata: library version when file was saved # Save metadata: library version when file was saved
archive.writestr("_stable_baselines3_version", stable_baselines3.__version__) archive.writestr("_stable_baselines3_version", sb3.__version__)
# Save system info about the current python env
archive.writestr("system_info.txt", get_system_info(print_info=False)[1])
def save_to_pkl(path: Union[str, pathlib.Path, io.BufferedIOBase], obj: Any, verbose: int = 0) -> None: def save_to_pkl(path: Union[str, pathlib.Path, io.BufferedIOBase], obj: Any, verbose: int = 0) -> None:
@ -362,6 +364,7 @@ def load_from_zip_file(
custom_objects: Optional[Dict[str, Any]] = None, custom_objects: Optional[Dict[str, Any]] = None,
device: Union[th.device, str] = "auto", device: Union[th.device, str] = "auto",
verbose: int = 0, verbose: int = 0,
print_system_info: bool = False,
) -> (Tuple[Optional[Dict[str, Any]], Optional[TensorDict], Optional[TensorDict]]): ) -> (Tuple[Optional[Dict[str, Any]], Optional[TensorDict], Optional[TensorDict]]):
""" """
Load model data from a .zip archive Load model data from a .zip archive
@ -376,6 +379,9 @@ def load_from_zip_file(
``keras.models.load_model``. Useful when you have an object in ``keras.models.load_model``. Useful when you have an object in
file that can not be deserialized. file that can not be deserialized.
:param device: Device on which the code should run. :param device: Device on which the code should run.
:param verbose: Verbosity level, 0 means only warnings, 2 means debug information.
:param print_system_info: Whether to print or not the system info
about the saved model.
:return: Class parameters, model state_dicts (aka "params", dict of state_dict) :return: Class parameters, model state_dicts (aka "params", dict of state_dict)
and dict of pytorch variables and dict of pytorch variables
""" """
@ -395,6 +401,17 @@ def load_from_zip_file(
pytorch_variables = None pytorch_variables = None
params = {} params = {}
# Debug system info first
if print_system_info:
if "system_info.txt" in namelist:
print("== SAVED MODEL SYSTEM INFO ==")
print(archive.read("system_info.txt").decode())
else:
warnings.warn(
"The model was saved with SB3 <= 1.2.0 and thus cannot print system information.",
UserWarning,
)
if "data" in namelist and load_data: if "data" in namelist and load_data:
# Load class parameters that are stored # Load class parameters that are stored
# with either JSON or pickle (not PyTorch variables). # with either JSON or pickle (not PyTorch variables).

View file

@ -1,14 +1,17 @@
import glob import glob
import os import os
import platform
import random import random
from collections import deque from collections import deque
from itertools import zip_longest from itertools import zip_longest
from typing import Dict, Iterable, Optional, Union from typing import Dict, Iterable, Optional, Tuple, Union
import gym import gym
import numpy as np import numpy as np
import torch as th import torch as th
import stable_baselines3 as sb3
# Check if tensorboard is available for pytorch # Check if tensorboard is available for pytorch
try: try:
from torch.utils.tensorboard import SummaryWriter from torch.utils.tensorboard import SummaryWriter
@ -460,3 +463,28 @@ def should_collect_more_steps(
"The unit of the `train_freq` must be either TrainFrequencyUnit.STEP " "The unit of the `train_freq` must be either TrainFrequencyUnit.STEP "
f"or TrainFrequencyUnit.EPISODE not '{train_freq.unit}'!" f"or TrainFrequencyUnit.EPISODE not '{train_freq.unit}'!"
) )
def get_system_info(print_info: bool = True) -> Tuple[Dict[str, str], str]:
"""
Retrieve system and python env info for the current system.
:param print_info: Whether to print or not those infos
:return: Dictionary summing up the version for each relevant package
and a formatted string.
"""
env_info = {
"OS": f"{platform.platform()} {platform.version()}",
"Python": platform.python_version(),
"Stable-Baselines3": sb3.__version__,
"PyTorch": th.__version__,
"GPU Enabled": str(th.cuda.is_available()),
"Numpy": np.__version__,
"Gym": gym.__version__,
}
env_info_str = ""
for key, value in env_info.items():
env_info_str += f"{key}: {value}\n"
if print_info:
print(env_info_str)
return env_info, env_info_str

View file

@ -58,7 +58,7 @@ class DDPG(TD3):
policy: Union[str, Type[TD3Policy]], policy: Union[str, Type[TD3Policy]],
env: Union[GymEnv, str], env: Union[GymEnv, str],
learning_rate: Union[float, Schedule] = 1e-3, learning_rate: Union[float, Schedule] = 1e-3,
buffer_size: int = 1000000, # 1e6 buffer_size: int = 1_000_000, # 1e6
learning_starts: int = 100, learning_starts: int = 100,
batch_size: int = 100, batch_size: int = 100,
tau: float = 0.005, tau: float = 0.005,

View file

@ -63,7 +63,7 @@ class DQN(OffPolicyAlgorithm):
policy: Union[str, Type[DQNPolicy]], policy: Union[str, Type[DQNPolicy]],
env: Union[GymEnv, str], env: Union[GymEnv, str],
learning_rate: Union[float, Schedule] = 1e-4, learning_rate: Union[float, Schedule] = 1e-4,
buffer_size: int = 1000000, # 1e6 buffer_size: int = 1_000_000, # 1e6
learning_starts: int = 50000, learning_starts: int = 50000,
batch_size: Optional[int] = 32, batch_size: Optional[int] = 32,
tau: float = 1.0, tau: float = 1.0,

View file

@ -193,8 +193,6 @@ class PPO(OnPolicyAlgorithm):
actions = rollout_data.actions.long().flatten() actions = rollout_data.actions.long().flatten()
# Re-sample the noise matrix because the log_std has changed # Re-sample the noise matrix because the log_std has changed
# TODO: investigate why there is no issue with the gradient
# if that line is commented (as in SAC)
if self.use_sde: if self.use_sde:
self.policy.reset_noise(self.batch_size) self.policy.reset_noise(self.batch_size)

View file

@ -77,7 +77,7 @@ class SAC(OffPolicyAlgorithm):
policy: Union[str, Type[SACPolicy]], policy: Union[str, Type[SACPolicy]],
env: Union[GymEnv, str], env: Union[GymEnv, str],
learning_rate: Union[float, Schedule] = 3e-4, learning_rate: Union[float, Schedule] = 3e-4,
buffer_size: int = 1000000, # 1e6 buffer_size: int = 1_000_000, # 1e6
learning_starts: int = 100, learning_starts: int = 100,
batch_size: int = 256, batch_size: int = 256,
tau: float = 0.005, tau: float = 0.005,

View file

@ -65,7 +65,7 @@ class TD3(OffPolicyAlgorithm):
policy: Union[str, Type[TD3Policy]], policy: Union[str, Type[TD3Policy]],
env: Union[GymEnv, str], env: Union[GymEnv, str],
learning_rate: Union[float, Schedule] = 1e-3, learning_rate: Union[float, Schedule] = 1e-3,
buffer_size: int = 1000000, # 1e6 buffer_size: int = 1_000_000, # 1e6
learning_starts: int = 100, learning_starts: int = 100,
batch_size: int = 100, batch_size: int = 100,
tau: float = 0.005, tau: float = 0.005,

View file

@ -1 +1 @@
1.2.1a3 1.2.1a4

View file

@ -222,7 +222,11 @@ def test_exclude_include_saved_params(tmp_path, model_class):
del model del model
# Load with custom objects # Load with custom objects
custom_objects = dict(learning_rate=2e-5, dummy=1.0) custom_objects = dict(learning_rate=2e-5, dummy=1.0)
model = model_class.load(str(tmp_path / "test_save.zip"), custom_objects=custom_objects) model = model_class.load(
str(tmp_path / "test_save.zip"),
custom_objects=custom_objects,
print_system_info=True,
)
assert model.verbose == 2 assert model.verbose == 2
# Check that the custom object was taken into account # Check that the custom object was taken into account
assert model.learning_rate == custom_objects["learning_rate"] assert model.learning_rate == custom_objects["learning_rate"]

View file

@ -6,13 +6,14 @@ import numpy as np
import pytest import pytest
import torch as th import torch as th
import stable_baselines3 as sb3
from stable_baselines3 import A2C, PPO from stable_baselines3 import A2C, PPO
from stable_baselines3.common.atari_wrappers import ClipRewardEnv, MaxAndSkipEnv from stable_baselines3.common.atari_wrappers import ClipRewardEnv, MaxAndSkipEnv
from stable_baselines3.common.env_util import is_wrapped, make_atari_env, make_vec_env, unwrap_wrapper from stable_baselines3.common.env_util import is_wrapped, make_atari_env, make_vec_env, unwrap_wrapper
from stable_baselines3.common.evaluation import evaluate_policy from stable_baselines3.common.evaluation import evaluate_policy
from stable_baselines3.common.monitor import Monitor from stable_baselines3.common.monitor import Monitor
from stable_baselines3.common.noise import ActionNoise, OrnsteinUhlenbeckActionNoise, VectorizedActionNoise from stable_baselines3.common.noise import ActionNoise, OrnsteinUhlenbeckActionNoise, VectorizedActionNoise
from stable_baselines3.common.utils import polyak_update, zip_strict from stable_baselines3.common.utils import get_system_info, polyak_update, zip_strict
from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv
@ -376,3 +377,13 @@ def test_ppo_warnings():
# Truncated mini-batch # Truncated mini-batch
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
PPO("MlpPolicy", "Pendulum-v0", n_steps=6, batch_size=8) PPO("MlpPolicy", "Pendulum-v0", n_steps=6, batch_size=8)
def test_get_system_info():
info, info_str = get_system_info(print_info=True)
assert info["Stable-Baselines3"] == str(sb3.__version__)
assert "Python" in info_str
assert "PyTorch" in info_str
assert "GPU Enabled" in info_str
assert "Numpy" in info_str
assert "Gym" in info_str