Skip to content

Commit c7af13d

Browse files
authored
Merge pull request #649 from unilabsim/fix/appo-collector-param-and-replay-add
perf: avoid replay transition concatenation
2 parents 840e4d2 + a56fcfd commit c7af13d

4 files changed

Lines changed: 105 additions & 24 deletions

File tree

src/unilab/algos/torch/appo/runner.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,11 @@ def __init__(
5757
sim_backend: str = "mujoco",
5858
num_envs: int = 1024,
5959
steps_per_env: int = 24,
60-
num_workers: int = 1, # kept for API compat, but only 1 collector used
6160
replay_queue_size: int = 3,
6261
seed: int | None = None,
6362
resume_path: str | None = None,
6463
nan_guard_cfg: NanGuardCfg | None = None,
6564
):
66-
del num_workers
6765
super().__init__(
6866
env_name=env_name,
6967
env_cfg_overrides=env_cfg_overrides,

src/unilab/ipc/replay_buffer.py

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -104,44 +104,67 @@ def add(
104104
if self._critic_dim > 0 and (critic is None or next_critic is None):
105105
raise ValueError("ReplayBuffer with critic_dim > 0 requires critic and next_critic")
106106

107-
parts = [
108-
obs,
109-
next_obs,
110-
actions,
111-
rewards.unsqueeze(1),
112-
dones.unsqueeze(1),
113-
truncated.unsqueeze(1),
114-
]
115-
if has_critic:
116-
assert next_critic is not None
117-
parts.extend([critic, next_critic])
118-
row = torch.cat(parts, dim=1)
119-
120107
if idx + n <= self.capacity:
121-
self._storage[idx : idx + n] = row
108+
target = self._storage[idx : idx + n]
109+
self._write_transition_rows(
110+
target,
111+
obs,
112+
actions,
113+
rewards,
114+
next_obs,
115+
dones,
116+
truncated,
117+
critic,
118+
next_critic,
119+
has_critic=has_critic,
120+
)
122121
self._patch_terminal_next_observations(
123-
self._storage[idx : idx + n, self._nobs_sl],
122+
target[:, self._nobs_sl],
124123
terminal_mask,
125124
terminal_next_obs,
126-
self._storage[idx : idx + n, self._ncritic_sl] if has_critic else None,
125+
target[:, self._ncritic_sl] if has_critic else None,
127126
terminal_next_critic,
128127
)
129128
else:
130129
split = self.capacity - idx
131-
self._storage[idx:] = row[:split]
132-
self._storage[: n - split] = row[split:]
130+
first = self._storage[idx:]
131+
second = self._storage[: n - split]
132+
self._write_transition_rows(
133+
first,
134+
obs[:split],
135+
actions[:split],
136+
rewards[:split],
137+
next_obs[:split],
138+
dones[:split],
139+
truncated[:split],
140+
critic[:split] if critic is not None else None,
141+
next_critic[:split] if next_critic is not None else None,
142+
has_critic=has_critic,
143+
)
144+
self._write_transition_rows(
145+
second,
146+
obs[split:],
147+
actions[split:],
148+
rewards[split:],
149+
next_obs[split:],
150+
dones[split:],
151+
truncated[split:],
152+
critic[split:] if critic is not None else None,
153+
next_critic[split:] if next_critic is not None else None,
154+
has_critic=has_critic,
155+
)
133156
self._patch_terminal_next_observations(
134-
self._storage[idx:, self._nobs_sl],
157+
first[:, self._nobs_sl],
135158
terminal_mask[:split] if terminal_mask is not None else None,
136159
terminal_next_obs[:split] if terminal_next_obs is not None else None,
137-
self._storage[idx:, self._ncritic_sl] if has_critic else None,
160+
first[:, self._ncritic_sl] if has_critic else None,
138161
terminal_next_critic[:split] if terminal_next_critic is not None else None,
139162
)
140163
self._patch_terminal_next_observations(
141-
self._storage[: n - split, self._nobs_sl],
164+
second[:, self._nobs_sl],
142165
terminal_mask[split:] if terminal_mask is not None else None,
143166
terminal_next_obs[split:] if terminal_next_obs is not None else None,
144-
self._storage[: n - split, self._ncritic_sl] if has_critic else None,
167+
second[:, self._ncritic_sl] if has_critic else None,
145168
terminal_next_critic[split:] if terminal_next_critic is not None else None,
146169
)
147170

@@ -156,6 +179,32 @@ def add(
156179
args={"batch_size": int(n), "device": self.device},
157180
)
158181

182+
def _write_transition_rows(
183+
self,
184+
target,
185+
obs,
186+
actions,
187+
rewards,
188+
next_obs,
189+
dones,
190+
truncated,
191+
critic,
192+
next_critic,
193+
*,
194+
has_critic: bool,
195+
) -> None:
196+
target[:, self._obs_sl] = obs
197+
target[:, self._nobs_sl] = next_obs
198+
target[:, self._act_sl] = actions
199+
target[:, self._rew_col] = rewards
200+
target[:, self._done_col] = dones
201+
target[:, self._trunc_col] = truncated
202+
if has_critic:
203+
assert critic is not None
204+
assert next_critic is not None
205+
target[:, self._critic_sl] = critic
206+
target[:, self._ncritic_sl] = next_critic
207+
159208
@staticmethod
160209
def _patch_terminal_next_observations(
161210
target_next_obs,

tests/ipc/test_replay_buffer.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import multiprocessing as mp
66

77
import numpy as np
8+
import pytest
89
import torch
910

1011
from unilab.ipc.replay_buffer import ReplayBuffer
@@ -190,6 +191,37 @@ def test_add_stores_combined_dones_and_truncated_contract():
190191
torch.testing.assert_close(buf._storage[:3, buf._trunc_col], torch.tensor([0.0, 1.0, 0.0]))
191192

192193

194+
def test_add_writes_packed_columns_without_cat(monkeypatch: pytest.MonkeyPatch):
195+
"""Collector hot path should not allocate a full concatenated transition batch."""
196+
buf = ReplayBuffer(
197+
capacity=8,
198+
obs_dim=_OBS_DIM,
199+
action_dim=_ACTION_DIM,
200+
critic_dim=5,
201+
device=_DEVICE,
202+
)
203+
obs = torch.randn(4, _OBS_DIM)
204+
act = torch.randn(4, _ACTION_DIM)
205+
rew = torch.randn(4)
206+
nobs = torch.randn(4, _OBS_DIM)
207+
done = torch.zeros(4)
208+
trunc = torch.zeros(4)
209+
critic = torch.randn(4, 5)
210+
ncritic = torch.randn(4, 5)
211+
212+
def _fail_cat(*args, **kwargs):
213+
del args, kwargs
214+
raise AssertionError("ReplayBuffer.add should write packed columns directly")
215+
216+
monkeypatch.setattr(torch, "cat", _fail_cat)
217+
218+
buf.add(obs, act, rew, nobs, done, trunc, critic=critic, next_critic=ncritic)
219+
220+
torch.testing.assert_close(buf._storage[:4, buf._obs_sl], obs)
221+
torch.testing.assert_close(buf._storage[:4, buf._act_sl], act)
222+
torch.testing.assert_close(buf._storage[:4, buf._critic_sl], critic)
223+
224+
193225
# ---------------------------------------------------------------------------
194226
# Multiprocess test
195227
# ---------------------------------------------------------------------------

tests/scripts/test_train_scripts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,8 @@ def test_build_appo_runner_kwargs_forwards_sim_backend():
11981198
assert runner_kwargs["collector_device"] == "cpu"
11991199
assert runner_kwargs["num_envs"] == cfg.algo.num_envs
12001200
assert runner_kwargs["steps_per_env"] == cfg.algo.steps_per_env
1201+
assert "num_workers" not in runner_kwargs
1202+
assert "num_collectors" not in runner_kwargs
12011203
assert runner_kwargs["env_cfg_overrides"]["reward_config"]["scales"] == {}
12021204

12031205

0 commit comments

Comments
 (0)