@@ -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
298364class 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