memmap-replay-buffer 0.2.3__tar.gz → 0.2.4__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 (25) hide show
  1. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/PKG-INFO +41 -1
  2. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/README.md +40 -0
  3. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer.py +11 -3
  4. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/pyproject.toml +1 -1
  5. memmap_replay_buffer-0.2.4/tests/test_advantage_filtering.py +143 -0
  6. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/.gitignore +0 -0
  7. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/LICENSE +0 -0
  8. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/__init__.py +0 -0
  9. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/concat_replay_buffer.py +0 -0
  10. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/episode_collector.py +0 -0
  11. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/frontend/index.html +0 -0
  12. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/frontend.py +0 -0
  13. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer_h5py.py +0 -0
  14. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/video.py +0 -0
  15. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_episode_collector.py +0 -0
  16. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_frontend.py +0 -0
  17. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_get_all_data.py +0 -0
  18. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_n_step.py +0 -0
  19. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_replay_buffer.py +0 -0
  20. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_replay_buffer_h5py.py +0 -0
  21. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_scalar_flexibility.py +0 -0
  22. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_store_episode_parity.py +0 -0
  23. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_time_window.py +0 -0
  24. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_uint8_storage.py +0 -0
  25. {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_video.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memmap-replay-buffer
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Simple Replay Buffer for RL
5
5
  Project-URL: Homepage, https://pypi.org/project/memmap-replay-buffer/
6
6
  Project-URL: Repository, https://codeberg.org/lucidrains/memmap-replay-buffer
@@ -205,6 +205,46 @@ buffer.update(episode_ids, returns = returns_for_ids) # specific episodes
205
205
  buffer.update(0, returns = returns_for_one) # scalar index
206
206
  ```
207
207
 
208
+ ### Computed fields and selective timestep training
209
+
210
+ Derived fields must be declared in `fields` when the buffer is created, then written after collection. Use `timestep_start` to write contiguous time chunks without loading a whole episode into memory. A common self-play recipe is to compute per-timestep advantages, threshold them into a boolean field, and then let the timestep-level dataset fetch only the selected timesteps.
211
+
212
+ ```python
213
+ buffer = ReplayBuffer(
214
+ './replay_data',
215
+ max_episodes = 1000,
216
+ max_timesteps = 500,
217
+ fields = dict(
218
+ state = ('float', (8,)),
219
+ reward = 'float',
220
+ advantage = 'float',
221
+ high_advantage = ('bool', (), False), # declares the derived mask
222
+ )
223
+ )
224
+
225
+ # compute advantages in episode batches
226
+ buffer.update(episode_ids, advantage = advantages)
227
+
228
+ # or write contiguous time chunks for all episodes
229
+ for start in range(0, max_len, 128):
230
+ buffer.update(
231
+ timestep_start = start,
232
+ advantage = advantage_chunk
233
+ )
234
+
235
+ # set the mask from a comparison of the computed field
236
+ buffer.update(high_advantage = advantage > threshold)
237
+
238
+ # only selected timesteps are returned
239
+ dataloader = buffer.dataloader(
240
+ batch_size = 64,
241
+ timestep_level = True,
242
+ filter_fields = dict(high_advantage = True)
243
+ )
244
+ ```
245
+
246
+ `demo_advantage_filtering.py` contains a complete self-play loop that computes advantages in batches, marks extreme positive advantages, and trains only on those timesteps.
247
+
208
248
  ### Pulling everything at once
209
249
 
210
250
  ```python
@@ -155,6 +155,46 @@ buffer.update(episode_ids, returns = returns_for_ids) # specific episodes
155
155
  buffer.update(0, returns = returns_for_one) # scalar index
156
156
  ```
157
157
 
158
+ ### Computed fields and selective timestep training
159
+
160
+ Derived fields must be declared in `fields` when the buffer is created, then written after collection. Use `timestep_start` to write contiguous time chunks without loading a whole episode into memory. A common self-play recipe is to compute per-timestep advantages, threshold them into a boolean field, and then let the timestep-level dataset fetch only the selected timesteps.
161
+
162
+ ```python
163
+ buffer = ReplayBuffer(
164
+ './replay_data',
165
+ max_episodes = 1000,
166
+ max_timesteps = 500,
167
+ fields = dict(
168
+ state = ('float', (8,)),
169
+ reward = 'float',
170
+ advantage = 'float',
171
+ high_advantage = ('bool', (), False), # declares the derived mask
172
+ )
173
+ )
174
+
175
+ # compute advantages in episode batches
176
+ buffer.update(episode_ids, advantage = advantages)
177
+
178
+ # or write contiguous time chunks for all episodes
179
+ for start in range(0, max_len, 128):
180
+ buffer.update(
181
+ timestep_start = start,
182
+ advantage = advantage_chunk
183
+ )
184
+
185
+ # set the mask from a comparison of the computed field
186
+ buffer.update(high_advantage = advantage > threshold)
187
+
188
+ # only selected timesteps are returned
189
+ dataloader = buffer.dataloader(
190
+ batch_size = 64,
191
+ timestep_level = True,
192
+ filter_fields = dict(high_advantage = True)
193
+ )
194
+ ```
195
+
196
+ `demo_advantage_filtering.py` contains a complete self-play loop that computes advantages in batches, marks extreme positive advantages, and trains only on those timesteps.
197
+
158
198
  ### Pulling everything at once
159
199
 
160
200
  ```python
@@ -987,10 +987,18 @@ class ReplayBufferBase:
987
987
 
988
988
  @can_write
989
989
  @beartype
990
- def update(self, indices: int | list | ndarray | slice | None = None, **data):
990
+ def update(
991
+ self,
992
+ indices: int | list | ndarray | slice | None = None,
993
+ timestep_start: int = 0,
994
+ **data
995
+ ):
991
996
  if len(data) == 0:
992
997
  raise ValueError('No data provided to `update`')
993
998
 
999
+ if timestep_start < 0:
1000
+ raise ValueError(f'timestep_start must be non-negative, got {timestep_start}')
1001
+
994
1002
  indices, scalar_index = expand_indices(indices, self.max_episodes)
995
1003
 
996
1004
  if not exists(indices):
@@ -1028,11 +1036,11 @@ class ReplayBufferBase:
1028
1036
 
1029
1037
  time_dim = value.shape[1]
1030
1038
 
1031
- if time_dim > self.max_timesteps:
1039
+ if timestep_start + time_dim > self.max_timesteps:
1032
1040
  raise ValueError(f'You exceeded the `max_timesteps` ({self.max_timesteps}) set on the replay buffer. Please increase it on init.')
1033
1041
 
1034
1042
  value = np.broadcast_to(value, (len(indices), time_dim, *self.shapes[name]))
1035
- self.data[name][indices, :time_dim] = value[order]
1043
+ self.data[name][indices, timestep_start:timestep_start + time_dim] = value[order]
1036
1044
 
1037
1045
  else:
1038
1046
  value = cast_to_target_shape(value, self.meta_shapes[name], num_leading_dims = 1)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memmap-replay-buffer"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Simple Replay Buffer for RL"
5
5
  authors = [
6
6
  { name = "Phil Wang", email = "lucidrains@gmail.com" }
@@ -0,0 +1,143 @@
1
+ import numpy as np
2
+ import pytest
3
+ import torch
4
+
5
+ from memmap_replay_buffer import ReplayBuffer
6
+ from memmap_replay_buffer.replay_buffer_h5py import ReplayBufferH5PY
7
+
8
+
9
+ @pytest.mark.parametrize('buffer_class', [ReplayBuffer, ReplayBufferH5PY])
10
+ def test_update_timestep_start(buffer_class, tmp_path):
11
+ buffer = buffer_class(
12
+ tmp_path,
13
+ max_episodes = 2,
14
+ max_timesteps = 6,
15
+ fields = dict(advantage = 'float'),
16
+ )
17
+
18
+ buffer.store_episode(advantage = np.zeros(6, dtype = np.float32))
19
+ buffer.store_episode(advantage = np.ones(6, dtype = np.float32))
20
+
21
+ # unsorted indices -> values must follow the passed order
22
+ buffer.update(
23
+ indices = [1, 0],
24
+ timestep_start = 2,
25
+ advantage = torch.tensor([[20., 21.], [10., 11.]])
26
+ )
27
+
28
+ data = buffer.get_all_data(fields = ('advantage',))['advantage']
29
+
30
+ assert torch.equal(data[0], torch.tensor([0., 0., 10., 11., 0., 0.]))
31
+ assert torch.equal(data[1], torch.tensor([1., 1., 20., 21., 1., 1.]))
32
+
33
+ # scalar episode index writes (time, *shape) at the offset
34
+ buffer.update(0, timestep_start = 4, advantage = torch.tensor([40., 41.]))
35
+ data = buffer.get_all_data(fields = ('advantage',))['advantage']
36
+ assert torch.equal(data[0], torch.tensor([0., 0., 10., 11., 40., 41.]))
37
+
38
+ with pytest.raises(ValueError, match = 'non-negative'):
39
+ buffer.update(timestep_start = -1, advantage = torch.zeros(2, 1))
40
+
41
+ with pytest.raises(ValueError, match = 'max_timesteps'):
42
+ buffer.update(timestep_start = 5, advantage = torch.zeros(2, 2))
43
+
44
+
45
+ @pytest.mark.parametrize('buffer_class', [ReplayBuffer, ReplayBufferH5PY])
46
+ def test_advantage_threshold_filtering_end_to_end(buffer_class, tmp_path):
47
+ buffer = buffer_class(
48
+ tmp_path / 'advantage',
49
+ max_episodes = 6,
50
+ max_timesteps = 8,
51
+ fields = dict(
52
+ reward = 'float',
53
+ advantage = 'float',
54
+ high_advantage = ('bool', (), False),
55
+ ),
56
+ circular = False,
57
+ overwrite = True,
58
+ )
59
+
60
+ rewards = [
61
+ [1., 1., 1., -1., 1., 1.],
62
+ [-1., -1., -1., -1.],
63
+ [1., -1., 1., -1., 1., -1., 1.],
64
+ [-1., 1., -1.],
65
+ [1., 1., 1., 1., 1.],
66
+ [-1., -1.],
67
+ ]
68
+
69
+ for episode_rewards in rewards:
70
+ buffer.store_episode(reward = np.asarray(episode_rewards, dtype = np.float32))
71
+
72
+ num_episodes = buffer.num_episodes
73
+ episode_lens = torch.from_numpy(buffer.episode_lens[:num_episodes].copy())
74
+ max_len = int(episode_lens.max())
75
+
76
+ # 1. compute advantage in episode batches, writing each batch back
77
+
78
+ gamma = 0.9
79
+
80
+ for start in range(0, num_episodes, 2):
81
+ episode_indices = np.arange(start, min(start + 2, num_episodes))
82
+ batch_len = int(episode_lens[episode_indices].max())
83
+
84
+ rewards_batch = torch.from_numpy(buffer.data['reward'][episode_indices, :batch_len].copy())
85
+ valid = torch.arange(batch_len)[None] < episode_lens[episode_indices][:, None]
86
+
87
+ returns = torch.zeros_like(rewards_batch)
88
+ running = torch.zeros(len(episode_indices))
89
+
90
+ for t in reversed(range(batch_len)):
91
+ running = rewards_batch[:, t] + gamma * running
92
+ returns[:, t] = running
93
+
94
+ buffer.update(episode_indices, advantage = returns * valid)
95
+
96
+ # 2. set a mask from a threshold comparison, in time chunks
97
+
98
+ threshold = 1.
99
+
100
+ for start in range(0, max_len, 4):
101
+ end = min(start + 4, max_len)
102
+
103
+ advantages = torch.from_numpy(buffer.data['advantage'][:num_episodes, start:end].copy())
104
+ valid = torch.arange(start, end)[None] < episode_lens[:, None]
105
+ high_advantage = (advantages > threshold) & valid
106
+
107
+ buffer.update(timestep_start = start, high_advantage = high_advantage)
108
+
109
+ # 3. verify the mask matches the comparison exactly
110
+
111
+ advantages = torch.from_numpy(buffer.data['advantage'][:num_episodes].copy())
112
+ high_advantage = torch.from_numpy(buffer.data['high_advantage'][:num_episodes].copy()).bool()
113
+
114
+ expected = {
115
+ (episode, timestep)
116
+ for episode in range(num_episodes)
117
+ for timestep in range(int(episode_lens[episode]))
118
+ if advantages[episode, timestep].item() > threshold
119
+ }
120
+
121
+ assert len(expected) > 0
122
+ assert len(expected) < int(episode_lens.sum())
123
+
124
+ for episode in range(num_episodes):
125
+ for timestep in range(int(episode_lens[episode])):
126
+ assert high_advantage[episode, timestep].item() == ((episode, timestep) in expected)
127
+
128
+ # 4. timestep-level learning only receives the selected timesteps
129
+
130
+ dataset = buffer.dataset(
131
+ timestep_level = True,
132
+ fields = ('advantage',),
133
+ filter_fields = dict(high_advantage = True),
134
+ return_indices = True,
135
+ )
136
+
137
+ fetched = {
138
+ tuple(dataset[index]['_indices'].tolist())
139
+ for index in range(len(dataset))
140
+ }
141
+
142
+ assert fetched == expected
143
+ assert all(dataset[index]['advantage'].item() > threshold for index in range(len(dataset)))