rlmodule 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rlmodule-0.2.0/rlmodule.egg-info → rlmodule-0.2.1}/PKG-INFO +1 -1
- {rlmodule-0.2.0 → rlmodule-0.2.1}/pyproject.toml +1 -1
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/output_layer.py +18 -3
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/rlmodel.py +21 -2
- {rlmodule-0.2.0 → rlmodule-0.2.1/rlmodule.egg-info}/PKG-INFO +1 -1
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_rnn_memory_and_correctness.py +31 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_squashed_gaussian.py +34 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/LICENSE +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/README.md +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/__init__.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/__init__.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/__init__.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/cnn_experimental.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_mlp_separate_model.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_mlp_shared_model.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_rnn_forwarded_input_shared_model.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_rnn_separate_model.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_rnn_shared_model.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_sac_mlp_separate_model.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/skrl_no_module.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/skrl_no_module_rnn.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/skrl_sac.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/temp.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/network/__init__.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/output_layer/__init__.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/__init__.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/build.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/log_std.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/log_std_cfg.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/model.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/network.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/network_cfg.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/__init__.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/configclass.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/dict.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/string.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/output_layer_cfg.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/rlmodel_cfg.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/utils.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/SOURCES.txt +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/dependency_links.txt +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/requires.txt +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/top_level.txt +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/setup.cfg +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_build_model_integration.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_cnn_backbone.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_combined_log_std.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_graph_network.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_head_mlp.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_mlp_layer_norm.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_residual_mlp.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_skrl_startup.py +0 -0
- {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_skrl_train_smoke.py +0 -0
|
@@ -120,6 +120,11 @@ class GaussianLayer(OutputLayer):
|
|
|
120
120
|
>>> print(actions.shape, log_prob.shape, outputs["mean_actions"].shape)
|
|
121
121
|
torch.Size([4096, 8]) torch.Size([4096, 1]) torch.Size([4096, 8])
|
|
122
122
|
"""
|
|
123
|
+
# ``RLModel.forward`` passes ``inputs["taken_pre_squash"]`` here (``_squashed_forward``).
|
|
124
|
+
taken_pre_squash = outputs_dict.pop("taken_pre_squash", None)
|
|
125
|
+
if taken_pre_squash is not None and not self._squash:
|
|
126
|
+
raise ValueError("taken_pre_squash is the pre-tanh sample of a squashed head, but squash=False")
|
|
127
|
+
|
|
123
128
|
# Optional per-head pre-projection (see OutputLayerCfg.head).
|
|
124
129
|
head_out = self._head(input)
|
|
125
130
|
|
|
@@ -134,7 +139,7 @@ class GaussianLayer(OutputLayer):
|
|
|
134
139
|
self._distribution = Normal(mean_actions, self._cached_log_std.exp())
|
|
135
140
|
|
|
136
141
|
if self._squash:
|
|
137
|
-
return self._squashed_forward(mean_actions, taken_actions, outputs_dict)
|
|
142
|
+
return self._squashed_forward(mean_actions, taken_actions, outputs_dict, taken_pre_squash)
|
|
138
143
|
|
|
139
144
|
# sample using the reparametrization trick
|
|
140
145
|
actions = self._distribution.rsample()
|
|
@@ -180,7 +185,7 @@ class GaussianLayer(OutputLayer):
|
|
|
180
185
|
# Keeps atanh finite for actions stored at the bound (float32 tanh saturates to 1).
|
|
181
186
|
_SQUASH_EPS = 1e-6
|
|
182
187
|
|
|
183
|
-
def _squashed_forward(self, mean_actions, taken_actions, outputs_dict):
|
|
188
|
+
def _squashed_forward(self, mean_actions, taken_actions, outputs_dict, taken_pre_squash=None):
|
|
184
189
|
"""Tanh-squashed Gaussian: ``u ~ N(mean, std)``, ``a = output_scale * tanh(u)``.
|
|
185
190
|
|
|
186
191
|
``self._distribution`` stays the pre-squash Normal over ``u``. ``mean_actions``
|
|
@@ -190,11 +195,20 @@ class GaussianLayer(OutputLayer):
|
|
|
190
195
|
``log_prob`` applies the change of variables per action dimension before the
|
|
191
196
|
reduction: ``log p(a) = log N(u) - log(output_scale) - log(1 - tanh(u)^2)``,
|
|
192
197
|
with ``log(1 - tanh(u)^2) = 2 * (log 2 - u - softplus(-2u))`` for stability.
|
|
198
|
+
|
|
199
|
+
A sample's ``u`` is returned as ``outputs_dict["pre_squash"]``. Re-evaluating a
|
|
200
|
+
taken action recovers ``u = atanh(a / output_scale)``, which float32 loses once
|
|
201
|
+
``tanh(u)`` rounds to 1 (``|u|`` above about 8): at a std of 0.08 a ``u`` of 12
|
|
202
|
+
comes back 4.9 off, thousands of nats in ``log_prob``. ``taken_pre_squash``
|
|
203
|
+
(the stored ``u``) skips the inversion; PPO stores it (``PPOSquash``).
|
|
193
204
|
"""
|
|
194
205
|
scale = self._output_scale
|
|
195
206
|
self._distribution = Normal(mean_actions / scale, self._cached_log_std.exp())
|
|
196
207
|
|
|
197
|
-
if
|
|
208
|
+
if taken_pre_squash is not None:
|
|
209
|
+
pre_squash = taken_pre_squash
|
|
210
|
+
actions = scale * torch.tanh(pre_squash)
|
|
211
|
+
elif taken_actions is None:
|
|
198
212
|
pre_squash = self._distribution.rsample()
|
|
199
213
|
actions = scale * torch.tanh(pre_squash)
|
|
200
214
|
else:
|
|
@@ -211,6 +225,7 @@ class GaussianLayer(OutputLayer):
|
|
|
211
225
|
|
|
212
226
|
outputs_dict["mean_actions"] = scale * torch.tanh(mean_actions / scale)
|
|
213
227
|
outputs_dict["log_prob"] = log_prob
|
|
228
|
+
outputs_dict["pre_squash"] = pre_squash
|
|
214
229
|
return actions, outputs_dict
|
|
215
230
|
|
|
216
231
|
def get_entropy(self, role: str = "") -> torch.Tensor:
|
|
@@ -45,6 +45,21 @@ def _select_trunk_input(inputs: Mapping[str, Union[torch.Tensor, Any]], role: st
|
|
|
45
45
|
return trunk_input
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _episode_ends(inputs: Mapping[str, Union[torch.Tensor, Any]]) -> Optional[torch.Tensor]:
|
|
49
|
+
"""Where a sampled sequence's episodes end, for the recurrent trunk's resets.
|
|
50
|
+
|
|
51
|
+
skrl's RNN agents zero the hidden state after a ``terminated`` *or* a
|
|
52
|
+
``truncated`` step while collecting, and pass both with every sampled
|
|
53
|
+
sequence; resetting on ``terminated`` alone would carry a timed-out
|
|
54
|
+
episode's hidden state into the next episode of the same sequence.
|
|
55
|
+
"""
|
|
56
|
+
terminated = inputs.get("terminated", None)
|
|
57
|
+
truncated = inputs.get("truncated", None)
|
|
58
|
+
if terminated is None or truncated is None:
|
|
59
|
+
return terminated if terminated is not None else truncated
|
|
60
|
+
return terminated | truncated
|
|
61
|
+
|
|
62
|
+
|
|
48
63
|
class RLModel(Model):
|
|
49
64
|
def __init__(
|
|
50
65
|
self,
|
|
@@ -75,11 +90,13 @@ class RLModel(Model):
|
|
|
75
90
|
if self._input_actions:
|
|
76
91
|
trunk_input = torch.cat((trunk_input, inputs["taken_actions"]), dim=1)
|
|
77
92
|
if self._rnn:
|
|
78
|
-
output, output_dict = self._net(trunk_input, inputs
|
|
93
|
+
output, output_dict = self._net(trunk_input, _episode_ends(inputs), inputs["rnn"])
|
|
79
94
|
else:
|
|
80
95
|
output = self._net(trunk_input)
|
|
81
96
|
output_dict = {}
|
|
82
97
|
|
|
98
|
+
if "taken_pre_squash" in inputs:
|
|
99
|
+
output_dict = {**output_dict, "taken_pre_squash": inputs["taken_pre_squash"]}
|
|
83
100
|
return self._output_layer(output, inputs.get("taken_actions", None), output_dict)
|
|
84
101
|
|
|
85
102
|
|
|
@@ -156,7 +173,7 @@ class SharedRLModel(Model):
|
|
|
156
173
|
self._reset_cache()
|
|
157
174
|
|
|
158
175
|
if self._rnn:
|
|
159
|
-
output, output_dict = self._net(states, inputs
|
|
176
|
+
output, output_dict = self._net(states, _episode_ends(inputs), inputs["rnn"])
|
|
160
177
|
else:
|
|
161
178
|
output = self._net(states)
|
|
162
179
|
output_dict = {}
|
|
@@ -167,6 +184,8 @@ class SharedRLModel(Model):
|
|
|
167
184
|
output, output_dict = self._cached_output
|
|
168
185
|
|
|
169
186
|
if role == "policy":
|
|
187
|
+
if "taken_pre_squash" in inputs:
|
|
188
|
+
output_dict = {**output_dict, "taken_pre_squash": inputs["taken_pre_squash"]}
|
|
170
189
|
return self._policy_output_layer(output, inputs.get("taken_actions", None), output_dict)
|
|
171
190
|
elif role == "value":
|
|
172
191
|
return self._value_output_layer(output, inputs.get("taken_actions", None), output_dict)
|
|
@@ -442,3 +442,34 @@ def test_num_envs_happy_path_returns_expected_shape():
|
|
|
442
442
|
spec = model.get_specification()
|
|
443
443
|
assert spec["rnn"]["sequence_length"] == L
|
|
444
444
|
assert spec["rnn"]["sizes"] == [(NUM_LAYERS, N, HOUT), (NUM_LAYERS, N, HOUT)]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def test_a_truncated_step_resets_the_hidden_state_like_a_terminated_one():
|
|
448
|
+
"""skrl's RNN agents zero the hidden state after a truncated step too, so
|
|
449
|
+
the model's training forward must reset there: a time-out in the middle
|
|
450
|
+
of a sampled sequence starts a new episode.
|
|
451
|
+
"""
|
|
452
|
+
torch.manual_seed(0)
|
|
453
|
+
model = _build_shared_lstm_model()
|
|
454
|
+
model.train(True)
|
|
455
|
+
states = torch.randn(N * L, HIN)
|
|
456
|
+
h, c = (torch.randn(NUM_LAYERS, N * L, HOUT) for _ in range(2))
|
|
457
|
+
none = torch.zeros(N * L, 1, dtype=torch.bool)
|
|
458
|
+
at_1 = none.view(N, L).clone()
|
|
459
|
+
at_1[:, 1] = True
|
|
460
|
+
at_1 = at_1.view(N * L, 1)
|
|
461
|
+
|
|
462
|
+
def mean_actions(terminated, truncated):
|
|
463
|
+
model._reset_cache()
|
|
464
|
+
inputs = {
|
|
465
|
+
"states": states,
|
|
466
|
+
"rnn": [h, c],
|
|
467
|
+
"terminated": terminated,
|
|
468
|
+
"truncated": truncated,
|
|
469
|
+
}
|
|
470
|
+
with torch.no_grad():
|
|
471
|
+
return model(inputs, role="policy")[1]["mean_actions"]
|
|
472
|
+
|
|
473
|
+
truncated = mean_actions(none, at_1)
|
|
474
|
+
assert torch.allclose(truncated, mean_actions(at_1, none))
|
|
475
|
+
assert not torch.allclose(truncated, mean_actions(none, none))
|
|
@@ -68,6 +68,40 @@ def test_squashed_log_prob_is_finite_at_the_bound() -> None:
|
|
|
68
68
|
assert torch.isfinite(out["log_prob"]).all()
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def _saturated(mean: float, std: float) -> GaussianLayer:
|
|
72
|
+
"""A squashed layer whose pre-squash mean is *mean* everywhere, at *std*."""
|
|
73
|
+
layer = _squashed(log_std=ParameterLogStdCfg(initial_log_std=math.log(std)))
|
|
74
|
+
with torch.no_grad():
|
|
75
|
+
layer._net[0].weight.zero_()
|
|
76
|
+
layer._net[0].bias.fill_(mean)
|
|
77
|
+
return layer
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_the_sample_returns_its_pre_squash_value() -> None:
|
|
81
|
+
layer = _squashed(output_scale=2.0)
|
|
82
|
+
actions, out = layer(torch.randn(64, N_IN), None, {})
|
|
83
|
+
torch.testing.assert_close(2.0 * torch.tanh(out["pre_squash"]), actions)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_taken_pre_squash_gives_the_sampled_log_prob_where_tanh_saturates() -> None:
|
|
87
|
+
# Run 89b's failure: at |u| ~ 12 float32 tanh is 1, so atanh(a) cannot recover u,
|
|
88
|
+
# and at a std of 0.08 the re-evaluated log_prob is thousands of nats off.
|
|
89
|
+
layer = _saturated(mean=12.0, std=0.08)
|
|
90
|
+
x = torch.randn(256, N_IN)
|
|
91
|
+
actions, out = layer(x, None, {})
|
|
92
|
+
assert out["pre_squash"].abs().min() > 11.0
|
|
93
|
+
_, exact = layer(x, actions, {"taken_pre_squash": out["pre_squash"]})
|
|
94
|
+
torch.testing.assert_close(exact["log_prob"], out["log_prob"])
|
|
95
|
+
_, inverted = layer(x, actions, {})
|
|
96
|
+
assert (inverted["log_prob"] - out["log_prob"]).abs().max() > 1000.0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_taken_pre_squash_on_an_unsquashed_layer_is_refused() -> None:
|
|
100
|
+
layer = _layer()
|
|
101
|
+
with pytest.raises(ValueError, match="squash=False"):
|
|
102
|
+
layer(torch.randn(3, N_IN), torch.zeros(3, N_ACT), {"taken_pre_squash": torch.zeros(3, N_ACT)})
|
|
103
|
+
|
|
104
|
+
|
|
71
105
|
def test_squashed_mean_action_is_tanh_of_the_mean() -> None:
|
|
72
106
|
layer = _squashed(output_scale=2.0)
|
|
73
107
|
x = torch.randn(8, N_IN)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/cnn_experimental.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/skrl_no_module_rnn.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|