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.
Files changed (53) hide show
  1. {rlmodule-0.2.0/rlmodule.egg-info → rlmodule-0.2.1}/PKG-INFO +1 -1
  2. {rlmodule-0.2.0 → rlmodule-0.2.1}/pyproject.toml +1 -1
  3. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/output_layer.py +18 -3
  4. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/rlmodel.py +21 -2
  5. {rlmodule-0.2.0 → rlmodule-0.2.1/rlmodule.egg-info}/PKG-INFO +1 -1
  6. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_rnn_memory_and_correctness.py +31 -0
  7. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_squashed_gaussian.py +34 -0
  8. {rlmodule-0.2.0 → rlmodule-0.2.1}/LICENSE +0 -0
  9. {rlmodule-0.2.0 → rlmodule-0.2.1}/README.md +0 -0
  10. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/__init__.py +0 -0
  11. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/__init__.py +0 -0
  12. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/__init__.py +0 -0
  13. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/cnn_experimental.py +0 -0
  14. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_mlp_separate_model.py +0 -0
  15. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_mlp_shared_model.py +0 -0
  16. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_rnn_forwarded_input_shared_model.py +0 -0
  17. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_rnn_separate_model.py +0 -0
  18. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_ppo_rnn_shared_model.py +0 -0
  19. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/pendulum_sac_mlp_separate_model.py +0 -0
  20. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/skrl_no_module.py +0 -0
  21. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/skrl_no_module_rnn.py +0 -0
  22. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/skrl_sac.py +0 -0
  23. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/examples/gymnasium/temp.py +0 -0
  24. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/network/__init__.py +0 -0
  25. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/skrl/torch/output_layer/__init__.py +0 -0
  26. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/__init__.py +0 -0
  27. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/build.py +0 -0
  28. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/log_std.py +0 -0
  29. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/log_std_cfg.py +0 -0
  30. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/model.py +0 -0
  31. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/network.py +0 -0
  32. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/network_cfg.py +0 -0
  33. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/__init__.py +0 -0
  34. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/configclass.py +0 -0
  35. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/dict.py +0 -0
  36. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/nvidia_utils/string.py +0 -0
  37. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/output_layer_cfg.py +0 -0
  38. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/rlmodel_cfg.py +0 -0
  39. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule/source/utils.py +0 -0
  40. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/SOURCES.txt +0 -0
  41. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/dependency_links.txt +0 -0
  42. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/requires.txt +0 -0
  43. {rlmodule-0.2.0 → rlmodule-0.2.1}/rlmodule.egg-info/top_level.txt +0 -0
  44. {rlmodule-0.2.0 → rlmodule-0.2.1}/setup.cfg +0 -0
  45. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_build_model_integration.py +0 -0
  46. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_cnn_backbone.py +0 -0
  47. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_combined_log_std.py +0 -0
  48. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_graph_network.py +0 -0
  49. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_head_mlp.py +0 -0
  50. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_mlp_layer_norm.py +0 -0
  51. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_residual_mlp.py +0 -0
  52. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_skrl_startup.py +0 -0
  53. {rlmodule-0.2.0 → rlmodule-0.2.1}/tests/test_skrl_train_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rlmodule
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Flexible reinforcement learning models instantiators library
5
5
  Author: Lopatovsky
6
6
  Author-email: lopatovsky@gmail.com
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rlmodule"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Flexible reinforcement learning models instantiators library"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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 taken_actions is None:
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.get("terminated", None), inputs["rnn"])
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.get("terminated", None), inputs["rnn"])
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rlmodule
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Flexible reinforcement learning models instantiators library
5
5
  Author: Lopatovsky
6
6
  Author-email: lopatovsky@gmail.com
@@ -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