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.
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/PKG-INFO +41 -1
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/README.md +40 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer.py +11 -3
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/pyproject.toml +1 -1
- memmap_replay_buffer-0.2.4/tests/test_advantage_filtering.py +143 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/.gitignore +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/LICENSE +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/__init__.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/concat_replay_buffer.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/episode_collector.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/frontend/index.html +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/frontend.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer_h5py.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/video.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_episode_collector.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_frontend.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_get_all_data.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_n_step.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_replay_buffer.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_replay_buffer_h5py.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_scalar_flexibility.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_store_episode_parity.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_time_window.py +0 -0
- {memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_uint8_storage.py +0 -0
- {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
|
+
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
|
{memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer.py
RENAMED
|
@@ -987,10 +987,18 @@ class ReplayBufferBase:
|
|
|
987
987
|
|
|
988
988
|
@can_write
|
|
989
989
|
@beartype
|
|
990
|
-
def update(
|
|
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)
|
|
@@ -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)))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/episode_collector.py
RENAMED
|
File without changes
|
{memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/frontend/index.html
RENAMED
|
File without changes
|
|
File without changes
|
{memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/memmap_replay_buffer/replay_buffer_h5py.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
|
{memmap_replay_buffer-0.2.3 → memmap_replay_buffer-0.2.4}/tests/test_store_episode_parity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|