Skip to content

Commit eb164ad

Browse files
committed
[Feature] Add noise argument and scan mode to RSSMRollout
- Add noise argument to RSSMPrior.forward and RSSMPosterior.forward for deterministic testing - Add use_scan parameter to RSSMRollout for torch.compile compatibility (uses torch._higher_order_ops.scan) - Add compile_step, compile_backend, compile_mode parameters for per-step compilation - Refactor forward to support both loop and scan implementations ghstack-source-id: 200a6da Pull-Request: #3307
1 parent 9aa5446 commit eb164ad

1 file changed

Lines changed: 109 additions & 16 deletions

File tree

torchrl/modules/models/model_based.py

Lines changed: 109 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,55 @@ class RSSMRollout(TensorDictModuleBase):
236236
Args:
237237
rssm_prior (TensorDictModule): Prior network.
238238
rssm_posterior (TensorDictModule): Posterior network.
239+
use_scan (bool, optional): If True, uses torch._higher_order_ops.scan for
240+
the rollout loop. This is more torch.compile friendly but may have
241+
different performance characteristics. Defaults to False.
242+
compile_step (bool, optional): If True, compiles the individual step function.
243+
Only used when use_scan=False. Defaults to False.
244+
compile_backend (str, optional): Backend to use for compilation.
245+
Defaults to "inductor".
246+
compile_mode (str, optional): Mode to use for compilation.
247+
Defaults to None (uses PyTorch default).
239248
240249
241250
"""
242251

243-
def __init__(self, rssm_prior: TensorDictModule, rssm_posterior: TensorDictModule):
252+
def __init__(
253+
self,
254+
rssm_prior: TensorDictModule,
255+
rssm_posterior: TensorDictModule,
256+
use_scan: bool = False,
257+
compile_step: bool = False,
258+
compile_backend: str = "inductor",
259+
compile_mode: str | None = None,
260+
):
244261
super().__init__()
245262
_module = TensorDictSequential(rssm_prior, rssm_posterior)
246263
self.in_keys = _module.in_keys
247264
self.out_keys = _module.out_keys
248265
self.rssm_prior = rssm_prior
249266
self.rssm_posterior = rssm_posterior
267+
self.use_scan = use_scan
268+
self.compile_step = compile_step
269+
self.compile_backend = compile_backend
270+
self.compile_mode = compile_mode
271+
self._compiled_step = None
272+
273+
def _get_step_fn(self):
274+
"""Get the step function, optionally compiled."""
275+
if self.compile_step and self._compiled_step is None:
276+
self._compiled_step = torch.compile(
277+
self._step,
278+
backend=self.compile_backend,
279+
mode=self.compile_mode,
280+
)
281+
return self._compiled_step if self.compile_step else self._step
282+
283+
def _step(self, _tensordict):
284+
"""Single RSSM step: prior + posterior."""
285+
self.rssm_prior(_tensordict)
286+
self.rssm_posterior(_tensordict)
287+
return _tensordict
250288

251289
def forward(self, tensordict):
252290
"""Runs a rollout of simulated transitions in the latent space given a sequence of actions and environment observations.
@@ -267,25 +305,23 @@ def forward(self, tensordict):
267305
which amends to q(s_{t+1} | s_t, a_t, o_{t+1})
268306
269307
"""
270-
# from torchrl.envs.utils import step_mdp
308+
if self.use_scan:
309+
return self._forward_scan(tensordict)
310+
return self._forward_loop(tensordict)
311+
312+
def _forward_loop(self, tensordict):
313+
"""Traditional loop-based forward."""
271314
tensordict_out = []
272315
*batch, time_steps = tensordict.shape
273316

274317
update_values = tensordict.exclude(*self.out_keys).unbind(-1)
275318
_tensordict = update_values[0]
276-
for t in range(time_steps):
277-
# samples according to p(s_{t+1} | s_t, a_t, b_t)
278-
# ["state", "belief", "action"] -> [("next", "prior_mean"), ("next", "prior_std"), "_", ("next", "belief")]
279-
with timeit("rssm_rollout/time-rssm_prior"):
280-
self.rssm_prior(_tensordict)
319+
step_fn = self._get_step_fn()
281320

282-
# samples according to p(s_{t+1} | s_t, a_t, o_{t+1}) = p(s_t | b_t, o_t)
283-
# [("next", "belief"), ("next", "encoded_latents")] -> [("next", "posterior_mean"), ("next", "posterior_std"), ("next", "state")]
284-
with timeit("rssm_rollout/time-rssm_posterior"):
285-
self.rssm_posterior(_tensordict)
321+
for t in range(time_steps):
322+
_tensordict = step_fn(_tensordict)
286323

287324
tensordict_out.append(_tensordict)
288-
# _tensordict = step_mdp(_tensordict, keep_other=True)
289325
if t < time_steps - 1:
290326
# Translate ("next", *) to the non-next key required for the current step input
291327
_tensordict = _tensordict.select(*self.in_keys, strict=False)
@@ -294,6 +330,36 @@ def forward(self, tensordict):
294330
out = torch.stack(tensordict_out, tensordict.ndim - 1)
295331
return out
296332

333+
def _forward_scan(self, tensordict):
334+
"""Scan-based forward using torch._higher_order_ops.scan.
335+
336+
This is more torch.compile friendly as it avoids Python control flow.
337+
"""
338+
from torch._higher_order_ops.scan import scan
339+
340+
*batch, time_steps = tensordict.shape
341+
342+
update_values = tensordict.exclude(*self.out_keys).unbind(-1)
343+
init_td = update_values[0]
344+
345+
# Stack the update values for scan input
346+
stacked_updates = torch.stack(list(update_values), dim=0)
347+
348+
def scan_fn(carry, x):
349+
# carry is the current tensordict, x is the update for this step
350+
_td = x.update(carry.select(*self.in_keys, strict=False))
351+
self.rssm_prior(_td)
352+
self.rssm_posterior(_td)
353+
# Return output and new carry
354+
return _td, _td
355+
356+
# Run scan
357+
_, outputs = scan(scan_fn, [init_td], [stacked_updates])
358+
359+
# outputs is stacked along dim 0, move to time dimension
360+
out = outputs.transpose(0, tensordict.ndim - 1)
361+
return out
362+
297363

298364
class RSSMPrior(nn.Module):
299365
"""The prior network of the RSSM.
@@ -356,7 +422,19 @@ def __init__(
356422
self.rnn_hidden_dim = rnn_hidden_dim
357423
self.action_shape = action_spec.shape
358424

359-
def forward(self, state, belief, action):
425+
def forward(self, state, belief, action, noise=None):
426+
"""Forward pass through the prior network.
427+
428+
Args:
429+
state: Previous stochastic state.
430+
belief: Previous deterministic belief.
431+
action: Action to condition on.
432+
noise: Optional pre-sampled noise for the prior state.
433+
If None, samples from standard normal. Used for deterministic testing.
434+
435+
Returns:
436+
Tuple of (prior_mean, prior_std, state, belief).
437+
"""
360438
projector_input = torch.cat([state, action], dim=-1)
361439
action_state = self.action_state_projector(projector_input)
362440
unsqueeze = False
@@ -377,7 +455,9 @@ def forward(self, state, belief, action):
377455
belief = belief.squeeze(0)
378456

379457
prior_mean, prior_std = self.rnn_to_prior_projector(belief)
380-
state = prior_mean + torch.randn_like(prior_std) * prior_std
458+
if noise is None:
459+
noise = torch.randn_like(prior_std)
460+
state = prior_mean + noise * prior_std
381461
return prior_mean, prior_std, state, belief
382462

383463

@@ -424,9 +504,22 @@ def __init__(self, hidden_dim=200, state_dim=30, scale_lb=0.1, rnn_hidden_dim=No
424504
)
425505
self.hidden_dim = hidden_dim
426506

427-
def forward(self, belief, obs_embedding):
507+
def forward(self, belief, obs_embedding, noise=None):
508+
"""Forward pass through the posterior network.
509+
510+
Args:
511+
belief: Deterministic belief from the prior.
512+
obs_embedding: Encoded observation.
513+
noise: Optional pre-sampled noise for the posterior state.
514+
If None, samples from standard normal. Used for deterministic testing.
515+
516+
Returns:
517+
Tuple of (posterior_mean, posterior_std, state).
518+
"""
428519
posterior_mean, posterior_std = self.obs_rnn_to_post_projector(
429520
torch.cat([belief, obs_embedding], dim=-1)
430521
)
431-
state = posterior_mean + torch.randn_like(posterior_std) * posterior_std
522+
if noise is None:
523+
noise = torch.randn_like(posterior_std)
524+
state = posterior_mean + noise * posterior_std
432525
return posterior_mean, posterior_std, state

0 commit comments

Comments
 (0)