memmap-replay-buffer 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.
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/PKG-INFO +1 -1
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/__init__.py +2 -0
- memmap_replay_buffer-0.2.1/memmap_replay_buffer/episode_collector.py +284 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/replay_buffer.py +6 -2
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/replay_buffer_h5py.py +6 -2
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/pyproject.toml +1 -1
- memmap_replay_buffer-0.2.1/tests/test_rollout_collector.py +187 -0
- memmap_replay_buffer-0.2.0/memmap_replay_buffer/episode_collector.py +0 -105
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/.gitignore +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/LICENSE +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/README.md +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/concat_replay_buffer.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_e2e.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_edge_cases.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_episode_collector.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_get_all_data.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_lazy_init.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_mixed_storage.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_n_step.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_nstep_timestep_gotcha.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_replay_buffer.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_replay_buffer_h5py.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_scalar_flexibility.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_store_episode_parity.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_time_window.py +0 -0
- {memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_uint8_storage.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.1
|
|
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
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict, namedtuple
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from beartype import beartype
|
|
8
|
+
from numpy import ndarray
|
|
9
|
+
import torch
|
|
10
|
+
from torch import is_tensor
|
|
11
|
+
from torch.utils.data import DataLoader, Dataset, default_collate
|
|
12
|
+
|
|
13
|
+
from memmap_replay_buffer.replay_buffer import tree_map_to_device
|
|
14
|
+
|
|
15
|
+
# helpers
|
|
16
|
+
|
|
17
|
+
def exists(v):
|
|
18
|
+
return v is not None
|
|
19
|
+
|
|
20
|
+
def default(v, d):
|
|
21
|
+
return v if exists(v) else d
|
|
22
|
+
|
|
23
|
+
def is_group_batched(value, num_groups):
|
|
24
|
+
if is_tensor(value) or isinstance(value, ndarray):
|
|
25
|
+
return value.ndim > 0 and value.shape[0] == num_groups
|
|
26
|
+
|
|
27
|
+
if isinstance(value, (list, tuple)):
|
|
28
|
+
return len(value) == num_groups
|
|
29
|
+
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def stack(values, dim = 0):
|
|
33
|
+
if is_tensor(values[0]):
|
|
34
|
+
return torch.stack(values, dim = dim)
|
|
35
|
+
|
|
36
|
+
return np.stack(values, axis = dim)
|
|
37
|
+
|
|
38
|
+
# dataset
|
|
39
|
+
|
|
40
|
+
class TensorDictDataset(Dataset):
|
|
41
|
+
def __init__(self, data: dict[str, torch.Tensor]):
|
|
42
|
+
self.data = data
|
|
43
|
+
self.num_samples = len(next(iter(data.values())))
|
|
44
|
+
|
|
45
|
+
def __len__(self):
|
|
46
|
+
return self.num_samples
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, index):
|
|
49
|
+
return {name: value[index] for name, value in self.data.items()}
|
|
50
|
+
|
|
51
|
+
# class
|
|
52
|
+
|
|
53
|
+
class EpisodeCollector:
|
|
54
|
+
""" accumulates a group-batched rollout step by step, then either stores it or trains on it in memory
|
|
55
|
+
|
|
56
|
+
layouts are explicit, so the time and group axes can never be silently swapped
|
|
57
|
+
|
|
58
|
+
`append` - one step, group-batched (num_groups, *shape) or (num_groups, 1, *shape)
|
|
59
|
+
`extend` - whole trajectories, time-major (time, num_groups, *shape), e.g. returns / advantages
|
|
60
|
+
`env_major` / `time_major` - read back (num_groups, time, ...) / (time, num_groups, ...)
|
|
61
|
+
`all_data` / `dataloader` - env-major tensors / flattened minibatches, entirely in memory
|
|
62
|
+
`store` - write one episode per group, then `clear`
|
|
63
|
+
`clear` - discard the current rollout
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@beartype
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
replay_buffer: Any, # ReplayBuffer | ReplayBufferH5PY, without a hard dependency on h5py
|
|
70
|
+
num_groups: int,
|
|
71
|
+
fieldnames: tuple[str, ...] | None = None,
|
|
72
|
+
meta_fieldnames: tuple[str, ...] | None = None,
|
|
73
|
+
strict: bool = True
|
|
74
|
+
):
|
|
75
|
+
self.replay_buffer = replay_buffer
|
|
76
|
+
self.num_groups = num_groups
|
|
77
|
+
|
|
78
|
+
self.fieldnames = set(default(fieldnames, tuple(replay_buffer.fieldnames)))
|
|
79
|
+
self.meta_fieldnames = set(default(meta_fieldnames, tuple(replay_buffer.meta_fieldnames)))
|
|
80
|
+
|
|
81
|
+
self.shapes = replay_buffer.shapes
|
|
82
|
+
self.meta_shapes = replay_buffer.meta_shapes
|
|
83
|
+
self.strict = strict
|
|
84
|
+
|
|
85
|
+
self.clear()
|
|
86
|
+
|
|
87
|
+
def __len__(self):
|
|
88
|
+
return self.step_count
|
|
89
|
+
|
|
90
|
+
def clear(self):
|
|
91
|
+
self.episode_data = [defaultdict(list) for _ in range(self.num_groups)]
|
|
92
|
+
self.step_count = 0
|
|
93
|
+
|
|
94
|
+
# fields
|
|
95
|
+
|
|
96
|
+
def field_kind(self, name):
|
|
97
|
+
if name in self.fieldnames:
|
|
98
|
+
return 'data'
|
|
99
|
+
|
|
100
|
+
if name in self.meta_fieldnames:
|
|
101
|
+
return 'meta'
|
|
102
|
+
|
|
103
|
+
if self.strict:
|
|
104
|
+
raise KeyError(f'unknown field {name} - valid fields are {sorted(self.fieldnames | self.meta_fieldnames)}')
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def field_shape(self, name):
|
|
109
|
+
return self.meta_shapes[name] if name in self.meta_fieldnames else self.shapes[name]
|
|
110
|
+
|
|
111
|
+
def per_group(self, name, value):
|
|
112
|
+
# (num_groups, *shape) / (num_groups, 1, *shape) -> num_groups values of shape (*shape)
|
|
113
|
+
|
|
114
|
+
if isinstance(value, (list, tuple)):
|
|
115
|
+
return list(value)
|
|
116
|
+
|
|
117
|
+
shape = tuple(self.field_shape(name))
|
|
118
|
+
rest = tuple(value.shape[1:])
|
|
119
|
+
|
|
120
|
+
assert rest in (shape, (1, *shape)), f'field {name} - per-group shape {rest} should be {shape} or (1, *shape)'
|
|
121
|
+
|
|
122
|
+
if rest != shape:
|
|
123
|
+
value = value[:, 0]
|
|
124
|
+
|
|
125
|
+
return list(value)
|
|
126
|
+
|
|
127
|
+
# adding data
|
|
128
|
+
|
|
129
|
+
@beartype
|
|
130
|
+
def append(self, **data):
|
|
131
|
+
# one step, group-batched values
|
|
132
|
+
|
|
133
|
+
for name, value in data.items():
|
|
134
|
+
kind = self.field_kind(name)
|
|
135
|
+
|
|
136
|
+
if not exists(kind):
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
if kind == 'meta':
|
|
140
|
+
values = [value] * self.num_groups
|
|
141
|
+
else:
|
|
142
|
+
shape = tuple(self.field_shape(name))
|
|
143
|
+
|
|
144
|
+
assert is_group_batched(value, self.num_groups), (
|
|
145
|
+
f'field {name} - `append` expects per-step group-batched values of shape ({self.num_groups}, *{shape}) '
|
|
146
|
+
f'or ({self.num_groups}, 1, *{shape}) - for whole trajectories in time-major (time, {self.num_groups}, *{shape}), use `extend`'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
values = self.per_group(name, value)
|
|
150
|
+
|
|
151
|
+
for group_index, group_value in enumerate(values):
|
|
152
|
+
self.episode_data[group_index][name].append(group_value)
|
|
153
|
+
|
|
154
|
+
self.step_count += 1
|
|
155
|
+
|
|
156
|
+
@beartype
|
|
157
|
+
def extend(self, **trajectories):
|
|
158
|
+
# whole trajectories, time-major (time, num_groups, *shape)
|
|
159
|
+
|
|
160
|
+
for name, value in trajectories.items():
|
|
161
|
+
kind = self.field_kind(name)
|
|
162
|
+
|
|
163
|
+
if not exists(kind):
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
assert is_tensor(value) or isinstance(value, ndarray), f'field {name} - `extend` expects a tensor / ndarray of shape (time, {self.num_groups}, *shape)'
|
|
167
|
+
|
|
168
|
+
time = value.shape[0]
|
|
169
|
+
|
|
170
|
+
assert value.ndim > 1 and value.shape[1] == self.num_groups, (
|
|
171
|
+
f'field {name} - `extend` expects time-major (time, {self.num_groups}, *shape), got {tuple(value.shape)}'
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if len(self) == 0:
|
|
175
|
+
self.step_count = time
|
|
176
|
+
else:
|
|
177
|
+
assert time == len(self), f'field {name} - time dim {time} does not match the {len(self)} collected steps'
|
|
178
|
+
|
|
179
|
+
shape = tuple(self.field_shape(name))
|
|
180
|
+
assert tuple(value.shape[2:]) == shape, f'field {name} - feature shape {tuple(value.shape[2:])} does not match {shape}'
|
|
181
|
+
|
|
182
|
+
for group_index in range(self.num_groups):
|
|
183
|
+
self.episode_data[group_index][name].extend(list(value[:, group_index]))
|
|
184
|
+
|
|
185
|
+
# reading back
|
|
186
|
+
|
|
187
|
+
def groups(self, name):
|
|
188
|
+
# one value per group, i.e. a list of num_groups tensors / arrays
|
|
189
|
+
|
|
190
|
+
if self.field_kind(name) == 'meta':
|
|
191
|
+
return [data[name][-1] for data in self.episode_data]
|
|
192
|
+
|
|
193
|
+
return [stack(data[name], dim = 0) for data in self.episode_data]
|
|
194
|
+
|
|
195
|
+
def time_major(self, name):
|
|
196
|
+
# (time, num_groups, *shape) for data fields, (num_groups,) for meta fields
|
|
197
|
+
|
|
198
|
+
return stack(self.groups(name), dim = 1 if self.field_kind(name) == 'data' else 0)
|
|
199
|
+
|
|
200
|
+
def env_major(self, name):
|
|
201
|
+
# (num_groups, time, *shape), the layout generalized advantage estimation expects
|
|
202
|
+
|
|
203
|
+
values = self.time_major(name)
|
|
204
|
+
|
|
205
|
+
if self.field_kind(name) == 'meta':
|
|
206
|
+
return values
|
|
207
|
+
|
|
208
|
+
return values.swapaxes(0, 1)
|
|
209
|
+
|
|
210
|
+
# in-memory training - no storing required
|
|
211
|
+
|
|
212
|
+
def all_data(self, fields = None, device: torch.device | str | None = None):
|
|
213
|
+
# dict of env-major tensors, (num_groups, time, *shape), for the collected data fields
|
|
214
|
+
|
|
215
|
+
if len(self) == 0:
|
|
216
|
+
raise ValueError('collector is empty')
|
|
217
|
+
|
|
218
|
+
collected = tuple(name for name in self.episode_data[0] if name in self.fieldnames)
|
|
219
|
+
|
|
220
|
+
data = {name: self.env_major(name) for name in default(fields, collected)}
|
|
221
|
+
return tree_map_to_device(data, device)
|
|
222
|
+
|
|
223
|
+
def dataloader(
|
|
224
|
+
self,
|
|
225
|
+
batch_size,
|
|
226
|
+
fields = None,
|
|
227
|
+
filter_fields = None,
|
|
228
|
+
to_named_tuple = None,
|
|
229
|
+
shuffle = False,
|
|
230
|
+
device: torch.device | str | None = None,
|
|
231
|
+
**kwargs
|
|
232
|
+
):
|
|
233
|
+
# minibatches flattened over (num_groups, time) for the current rollout, entirely in memory
|
|
234
|
+
|
|
235
|
+
data = {name: torch.as_tensor(value).reshape(-1, *value.shape[2:]) for name, value in self.all_data().items()}
|
|
236
|
+
|
|
237
|
+
if exists(filter_fields) and len(filter_fields) > 0:
|
|
238
|
+
mask = None
|
|
239
|
+
for name, filter_value in filter_fields.items():
|
|
240
|
+
field_mask = data[name] == filter_value
|
|
241
|
+
mask = field_mask if not exists(mask) else (mask & field_mask)
|
|
242
|
+
|
|
243
|
+
data = {name: value[mask] for name, value in data.items()}
|
|
244
|
+
|
|
245
|
+
fields = default(fields, tuple(data.keys()))
|
|
246
|
+
data = {name: data[name] for name in fields}
|
|
247
|
+
|
|
248
|
+
dataset = TensorDictDataset(data)
|
|
249
|
+
|
|
250
|
+
NamedTupleCls = None
|
|
251
|
+
if exists(to_named_tuple):
|
|
252
|
+
sanitized_fields = tuple(f.lstrip('_') if f.startswith('_') else f for f in to_named_tuple)
|
|
253
|
+
NamedTupleCls = namedtuple('Batch', sanitized_fields)
|
|
254
|
+
|
|
255
|
+
def collate_fn(data):
|
|
256
|
+
batch = default_collate(data)
|
|
257
|
+
|
|
258
|
+
if exists(NamedTupleCls):
|
|
259
|
+
for field in to_named_tuple:
|
|
260
|
+
if field not in batch:
|
|
261
|
+
raise ValueError(f'field `{field}` not found in batch. available fields: {list(batch.keys())}')
|
|
262
|
+
|
|
263
|
+
batch = NamedTupleCls(**{san: batch[orig] for orig, san in zip(to_named_tuple, sanitized_fields)})
|
|
264
|
+
|
|
265
|
+
return tree_map_to_device(batch, device)
|
|
266
|
+
|
|
267
|
+
return DataLoader(dataset, batch_size = batch_size, shuffle = shuffle, collate_fn = collate_fn, **kwargs)
|
|
268
|
+
|
|
269
|
+
# storing
|
|
270
|
+
|
|
271
|
+
@beartype
|
|
272
|
+
def store(self, **meta_data):
|
|
273
|
+
for group_index, group_data in enumerate(self.episode_data):
|
|
274
|
+
store_kwargs = {name: stack(values, dim = 0) if name in self.fieldnames else values[-1] for name, values in group_data.items()}
|
|
275
|
+
|
|
276
|
+
for name, value in meta_data.items():
|
|
277
|
+
if name not in self.meta_fieldnames:
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
store_kwargs[name] = value[group_index] if is_group_batched(value, self.num_groups) else value
|
|
281
|
+
|
|
282
|
+
self.replay_buffer.store_episode(**store_kwargs)
|
|
283
|
+
|
|
284
|
+
self.clear()
|
{memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/replay_buffer.py
RENAMED
|
@@ -1394,12 +1394,16 @@ class ReplayBuffer:
|
|
|
1394
1394
|
self,
|
|
1395
1395
|
num_groups: int,
|
|
1396
1396
|
fieldnames: tuple[str, ...] | None = None,
|
|
1397
|
-
meta_fieldnames: tuple[str, ...] | None = None
|
|
1397
|
+
meta_fieldnames: tuple[str, ...] | None = None,
|
|
1398
|
+
**collector_kwargs
|
|
1398
1399
|
):
|
|
1399
1400
|
from memmap_replay_buffer.episode_collector import EpisodeCollector
|
|
1400
1401
|
return EpisodeCollector(
|
|
1401
1402
|
self,
|
|
1402
1403
|
num_groups,
|
|
1403
1404
|
fieldnames = fieldnames,
|
|
1404
|
-
meta_fieldnames = meta_fieldnames
|
|
1405
|
+
meta_fieldnames = meta_fieldnames,
|
|
1406
|
+
**collector_kwargs
|
|
1405
1407
|
)
|
|
1408
|
+
|
|
1409
|
+
create_rollout_collector = create_collector
|
{memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/memmap_replay_buffer/replay_buffer_h5py.py
RENAMED
|
@@ -917,16 +917,20 @@ class ReplayBufferH5PY:
|
|
|
917
917
|
self,
|
|
918
918
|
num_groups: int,
|
|
919
919
|
fieldnames: tuple[str, ...] | None = None,
|
|
920
|
-
meta_fieldnames: tuple[str, ...] | None = None
|
|
920
|
+
meta_fieldnames: tuple[str, ...] | None = None,
|
|
921
|
+
**collector_kwargs
|
|
921
922
|
):
|
|
922
923
|
from memmap_replay_buffer.episode_collector import EpisodeCollector
|
|
923
924
|
return EpisodeCollector(
|
|
924
925
|
self,
|
|
925
926
|
num_groups,
|
|
926
927
|
fieldnames = fieldnames,
|
|
927
|
-
meta_fieldnames = meta_fieldnames
|
|
928
|
+
meta_fieldnames = meta_fieldnames,
|
|
929
|
+
**collector_kwargs
|
|
928
930
|
)
|
|
929
931
|
|
|
932
|
+
create_rollout_collector = create_collector
|
|
933
|
+
|
|
930
934
|
@can_write
|
|
931
935
|
def _store_episodes_batch(self, data: dict[str, np.ndarray]):
|
|
932
936
|
batch_size = next(iter(data.values())).shape[0]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
from memmap_replay_buffer import ReplayBuffer
|
|
5
|
+
|
|
6
|
+
FIELDS = dict(
|
|
7
|
+
obs = ('float', (3,)),
|
|
8
|
+
action = 'int',
|
|
9
|
+
reward = 'float',
|
|
10
|
+
next_obs = ('float', (3,)),
|
|
11
|
+
value = 'float',
|
|
12
|
+
returns = 'float',
|
|
13
|
+
advantage = 'float',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def make_buffer(tmp_path):
|
|
17
|
+
return ReplayBuffer(
|
|
18
|
+
tmp_path / 'buffer',
|
|
19
|
+
max_episodes = 4,
|
|
20
|
+
max_timesteps = 16,
|
|
21
|
+
fields = FIELDS,
|
|
22
|
+
circular = True,
|
|
23
|
+
overwrite = True
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def test_rollout_collector_alignment_roundtrip(tmp_path):
|
|
27
|
+
T, N = 5, 2
|
|
28
|
+
|
|
29
|
+
buffer = make_buffer(tmp_path)
|
|
30
|
+
collector = buffer.create_collector(num_groups = N)
|
|
31
|
+
|
|
32
|
+
obs_all = torch.randn(T + 1, N, 3)
|
|
33
|
+
obs, next_obs = obs_all[:-1], obs_all[1:]
|
|
34
|
+
action = torch.randint(0, 4, (T, N))
|
|
35
|
+
reward = torch.randn(T, N)
|
|
36
|
+
value = torch.randn(T, N)
|
|
37
|
+
|
|
38
|
+
for t in range(T):
|
|
39
|
+
collector.append(
|
|
40
|
+
obs = obs[t],
|
|
41
|
+
action = action[t],
|
|
42
|
+
reward = reward[t],
|
|
43
|
+
next_obs = next_obs[t],
|
|
44
|
+
value = value[t]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# whole-trajectory fields, explicitly time-major
|
|
48
|
+
|
|
49
|
+
returns = torch.randn(T, N)
|
|
50
|
+
advantage = torch.randn(T, N)
|
|
51
|
+
collector.extend(returns = returns, advantage = advantage)
|
|
52
|
+
|
|
53
|
+
assert len(collector) == T
|
|
54
|
+
|
|
55
|
+
# read-back views must agree with the inputs
|
|
56
|
+
|
|
57
|
+
assert torch.equal(collector.time_major('obs'), obs)
|
|
58
|
+
assert torch.equal(collector.env_major('obs'), obs.transpose(0, 1))
|
|
59
|
+
assert torch.equal(collector.time_major('returns'), returns)
|
|
60
|
+
assert torch.equal(collector.env_major('advantage'), advantage.transpose(0, 1))
|
|
61
|
+
|
|
62
|
+
collector.store()
|
|
63
|
+
|
|
64
|
+
# stored rows are per group and time-ordered, never scrambled
|
|
65
|
+
|
|
66
|
+
for i in range(N):
|
|
67
|
+
assert torch.allclose(torch.tensor(buffer.data['obs'][i, :T]), obs[:, i])
|
|
68
|
+
assert torch.allclose(torch.tensor(buffer.data['next_obs'][i, :T]), next_obs[:, i])
|
|
69
|
+
assert torch.allclose(torch.tensor(buffer.data['returns'][i, :T]), returns[:, i])
|
|
70
|
+
assert torch.allclose(torch.tensor(buffer.data['advantage'][i, :T]), advantage[:, i])
|
|
71
|
+
assert torch.allclose(torch.tensor(buffer.data['value'][i, :T]), value[:, i])
|
|
72
|
+
|
|
73
|
+
# the transition continuity invariant that catches any permutation
|
|
74
|
+
|
|
75
|
+
assert torch.allclose(torch.tensor(buffer.data['next_obs'][i, :T - 1]), torch.tensor(buffer.data['obs'][i, 1:T]))
|
|
76
|
+
|
|
77
|
+
def test_collector_in_memory_training(tmp_path):
|
|
78
|
+
T, N = 5, 3
|
|
79
|
+
|
|
80
|
+
buffer = make_buffer(tmp_path)
|
|
81
|
+
collector = buffer.create_collector(num_groups = N)
|
|
82
|
+
|
|
83
|
+
obs = torch.randn(T, N, 3)
|
|
84
|
+
action = torch.randint(0, 4, (T, N))
|
|
85
|
+
value = torch.randn(T, N)
|
|
86
|
+
returns = torch.randn(T, N)
|
|
87
|
+
advantage = torch.randn(T, N)
|
|
88
|
+
|
|
89
|
+
for t in range(T):
|
|
90
|
+
collector.append(obs = obs[t], action = action[t], value = value[t])
|
|
91
|
+
|
|
92
|
+
collector.extend(returns = returns, advantage = advantage)
|
|
93
|
+
|
|
94
|
+
# env-major snapshot, no disk access
|
|
95
|
+
|
|
96
|
+
all_data = collector.all_data()
|
|
97
|
+
assert torch.equal(all_data['obs'], obs.transpose(0, 1))
|
|
98
|
+
|
|
99
|
+
# flattened minibatches, optionally filtered
|
|
100
|
+
|
|
101
|
+
dataloader = collector.dataloader(
|
|
102
|
+
batch_size = 4,
|
|
103
|
+
filter_fields = dict(action = 1),
|
|
104
|
+
to_named_tuple = ('obs', 'action', 'returns', 'advantage')
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
batches = list(dataloader)
|
|
108
|
+
assert sum(len(batch.obs) for batch in batches) == (action == 1).sum().item()
|
|
109
|
+
|
|
110
|
+
expected = obs.transpose(0, 1).reshape(-1, 3)[(action.transpose(0, 1) == 1).reshape(-1)]
|
|
111
|
+
assert torch.allclose(torch.cat([batch.obs for batch in batches]), expected)
|
|
112
|
+
|
|
113
|
+
# nothing was stored
|
|
114
|
+
|
|
115
|
+
assert buffer.num_episodes == 0
|
|
116
|
+
|
|
117
|
+
# clearing resets the rollout
|
|
118
|
+
|
|
119
|
+
collector.clear()
|
|
120
|
+
assert len(collector) == 0
|
|
121
|
+
|
|
122
|
+
with pytest.raises(ValueError, match = 'empty'):
|
|
123
|
+
collector.all_data()
|
|
124
|
+
|
|
125
|
+
def test_rollout_collector_rejects_time_major_append(tmp_path):
|
|
126
|
+
T, N = 5, 2
|
|
127
|
+
|
|
128
|
+
buffer = make_buffer(tmp_path)
|
|
129
|
+
collector = buffer.create_collector(num_groups = N)
|
|
130
|
+
|
|
131
|
+
with pytest.raises(AssertionError, match = 'extend'):
|
|
132
|
+
collector.append(obs = torch.randn(T, N, 3))
|
|
133
|
+
|
|
134
|
+
def test_rollout_collector_rejects_wrong_time_dim_in_extend(tmp_path):
|
|
135
|
+
T, N = 4, 3
|
|
136
|
+
|
|
137
|
+
buffer = make_buffer(tmp_path)
|
|
138
|
+
collector = buffer.create_collector(num_groups = N)
|
|
139
|
+
|
|
140
|
+
for t in range(T):
|
|
141
|
+
collector.append(
|
|
142
|
+
obs = torch.randn(N, 3),
|
|
143
|
+
action = torch.zeros(N, dtype = torch.long),
|
|
144
|
+
reward = torch.zeros(N),
|
|
145
|
+
next_obs = torch.randn(N, 3),
|
|
146
|
+
value = torch.zeros(N)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
with pytest.raises(AssertionError, match = 'time dim'):
|
|
150
|
+
collector.extend(returns = torch.randn(T + 1, N))
|
|
151
|
+
|
|
152
|
+
def test_rollout_collector_unit_time_dim_and_strict(tmp_path):
|
|
153
|
+
N = 2
|
|
154
|
+
|
|
155
|
+
buffer = make_buffer(tmp_path)
|
|
156
|
+
collector = buffer.create_collector(num_groups = N)
|
|
157
|
+
|
|
158
|
+
# the (N, 1, *shape) form is accepted and stored identically to (N, *shape)
|
|
159
|
+
|
|
160
|
+
for t in range(3):
|
|
161
|
+
collector.append(
|
|
162
|
+
obs = torch.full((N, 1, 3), float(t)),
|
|
163
|
+
action = torch.full((N, 1), t),
|
|
164
|
+
reward = torch.full((N, 1), float(t)),
|
|
165
|
+
next_obs = torch.zeros(N, 1, 3),
|
|
166
|
+
value = torch.zeros(N, 1)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
collector.store()
|
|
170
|
+
|
|
171
|
+
data = buffer.get_all_data()
|
|
172
|
+
assert data['obs'].shape[:2] == (N, 3)
|
|
173
|
+
assert data['action'][0].tolist() == [0, 1, 2]
|
|
174
|
+
assert data['action'][1].tolist() == [0, 1, 2]
|
|
175
|
+
|
|
176
|
+
with pytest.raises(KeyError, match = 'unknown field'):
|
|
177
|
+
collector.append(typo_field = torch.zeros(N))
|
|
178
|
+
|
|
179
|
+
collector_lax = buffer.create_collector(num_groups = N, strict = False)
|
|
180
|
+
collector_lax.append(
|
|
181
|
+
obs = torch.zeros(N, 3),
|
|
182
|
+
action = torch.zeros(N, dtype = torch.long),
|
|
183
|
+
reward = torch.zeros(N),
|
|
184
|
+
next_obs = torch.zeros(N, 3),
|
|
185
|
+
value = torch.zeros(N),
|
|
186
|
+
typo_field = torch.zeros(N)
|
|
187
|
+
)
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections import defaultdict
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
from beartype import beartype
|
|
7
|
-
from numpy import ndarray
|
|
8
|
-
from torch import cat, is_tensor, stack
|
|
9
|
-
|
|
10
|
-
from memmap_replay_buffer.replay_buffer import ReplayBuffer
|
|
11
|
-
from memmap_replay_buffer.replay_buffer_h5py import ReplayBufferH5PY
|
|
12
|
-
|
|
13
|
-
# helpers
|
|
14
|
-
|
|
15
|
-
def exists(v):
|
|
16
|
-
return v is not None
|
|
17
|
-
|
|
18
|
-
def default(v, d):
|
|
19
|
-
return v if exists(v) else d
|
|
20
|
-
|
|
21
|
-
def is_group_batched(value, num_groups):
|
|
22
|
-
if is_tensor(value) or isinstance(value, ndarray):
|
|
23
|
-
return value.shape[0] == num_groups
|
|
24
|
-
|
|
25
|
-
if isinstance(value, (list, tuple)):
|
|
26
|
-
return len(value) == num_groups
|
|
27
|
-
|
|
28
|
-
return False
|
|
29
|
-
|
|
30
|
-
def combine_trajectory(values):
|
|
31
|
-
first = values[0]
|
|
32
|
-
if is_tensor(first):
|
|
33
|
-
return stack(values) if first.ndim == 0 else cat(values, dim = 0)
|
|
34
|
-
|
|
35
|
-
if isinstance(first, ndarray):
|
|
36
|
-
return np.stack(values) if first.ndim == 0 else np.concatenate(values, axis = 0)
|
|
37
|
-
|
|
38
|
-
return np.array(values)
|
|
39
|
-
|
|
40
|
-
class EpisodeCollector:
|
|
41
|
-
@beartype
|
|
42
|
-
def __init__(
|
|
43
|
-
self,
|
|
44
|
-
replay_buffer: ReplayBuffer | ReplayBufferH5PY,
|
|
45
|
-
num_groups: int,
|
|
46
|
-
fieldnames: tuple[str, ...] | None = None,
|
|
47
|
-
meta_fieldnames: tuple[str, ...] | None = None
|
|
48
|
-
):
|
|
49
|
-
self.replay_buffer = replay_buffer
|
|
50
|
-
self.num_groups = num_groups
|
|
51
|
-
|
|
52
|
-
self.fieldnames = default(fieldnames, tuple(replay_buffer.fieldnames))
|
|
53
|
-
self.meta_fieldnames = default(meta_fieldnames, tuple(replay_buffer.meta_fieldnames))
|
|
54
|
-
|
|
55
|
-
self.reset()
|
|
56
|
-
|
|
57
|
-
def reset(self):
|
|
58
|
-
self.episode_data = [defaultdict(list) for _ in range(self.num_groups)]
|
|
59
|
-
|
|
60
|
-
@beartype
|
|
61
|
-
def append(
|
|
62
|
-
self,
|
|
63
|
-
**data
|
|
64
|
-
):
|
|
65
|
-
for name, value in data.items():
|
|
66
|
-
if name not in self.fieldnames and name not in self.meta_fieldnames:
|
|
67
|
-
continue
|
|
68
|
-
|
|
69
|
-
if is_group_batched(value, self.num_groups):
|
|
70
|
-
for i in range(self.num_groups):
|
|
71
|
-
self.episode_data[i][name].append(value[i])
|
|
72
|
-
else:
|
|
73
|
-
for i in range(self.num_groups):
|
|
74
|
-
self.episode_data[i][name].append(value)
|
|
75
|
-
|
|
76
|
-
@beartype
|
|
77
|
-
def store(self, **meta_data):
|
|
78
|
-
for i in range(self.num_groups):
|
|
79
|
-
group_data = self.episode_data[i]
|
|
80
|
-
|
|
81
|
-
store_kwargs = dict()
|
|
82
|
-
|
|
83
|
-
# Handle collected data
|
|
84
|
-
for name, values in group_data.items():
|
|
85
|
-
if name in self.fieldnames:
|
|
86
|
-
combined = combine_trajectory(values)
|
|
87
|
-
else:
|
|
88
|
-
# Meta fields: take the last appended value
|
|
89
|
-
combined = values[-1]
|
|
90
|
-
|
|
91
|
-
store_kwargs[name] = combined
|
|
92
|
-
|
|
93
|
-
# Merge with extra meta data provided to store()
|
|
94
|
-
for name, value in meta_data.items():
|
|
95
|
-
if name not in self.meta_fieldnames:
|
|
96
|
-
continue
|
|
97
|
-
|
|
98
|
-
if is_group_batched(value, self.num_groups):
|
|
99
|
-
store_kwargs[name] = value[i]
|
|
100
|
-
else:
|
|
101
|
-
store_kwargs[name] = value
|
|
102
|
-
|
|
103
|
-
self.replay_buffer.store_episode(**store_kwargs)
|
|
104
|
-
|
|
105
|
-
self.reset()
|
|
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
|
{memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_nstep_timestep_gotcha.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memmap_replay_buffer-0.2.0 → memmap_replay_buffer-0.2.1}/tests/test_store_episode_parity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|