Modular RSSM#
The Modular RSSM lets you mix and match encoder, backbone, and decoder components for world-model research. The standard Dreamer RSSM is a fixed architecture; the modular variant exposes every piece as a pluggable component.
Overview#
The architecture has three slots:
Observation → Encoder → Backbone → Decoder → Reconstruction
│
Reward Decoder → Reward prediction
Slot |
Built-in options |
Custom |
|---|---|---|
Encoder |
|
Subclass |
Backbone |
|
Subclass |
Decoder |
|
Subclass |
A reward_decoder (always an MLPDecoder) is attached automatically when using
the factory, or can be provided manually.
Quick start#
Use the factory for the most common combinations:
from world_models.models.modular_rssm import create_modular_rssm
rssm = create_modular_rssm(
encoder_type="conv", # "conv" | "mlp" | "vit"
decoder_type="conv", # "conv" | "mlp"
backbone_type="gru", # "gru" | "lstm" | "transformer"
obs_shape=(3, 64, 64), # (C, H, W) for images, (D,) for state vectors
action_size=6,
stoch_size=32,
deter_size=200,
embed_size=1024,
)
The factory creates the three components plus a reward decoder and wraps them
in a ModularRSSM container. All components are also importable individually
for direct construction.
Components#
Encoders#
from world_models.models.modular_rssm import ConvEncoder, MLPEncoder, ViTEncoder
# Convolutional encoder — Dreamer-style, for image observations
enc = ConvEncoder(input_shape=(3, 64, 64), embed_size=1024, depth=32)
# MLP encoder — for low-dimensional state observations
enc = MLPEncoder(input_dim=10, embed_size=256, hidden_sizes=[256, 256])
# Vision Transformer encoder — for image observations with global context
enc = ViTEncoder(input_shape=(3, 64, 64), embed_size=512, patch_size=8, depth=6)
Encoder |
Input |
Embedding |
|---|---|---|
|
|
|
|
|
|
|
|
|
Backbones#
from world_models.models.modular_rssm import GRUBackbone, LSTMBackbone, TransformerBackbone
# GRU — standard RSSM recurrent dynamics
bb = GRUBackbone(action_size=6, stoch_size=32, deter_size=200,
hidden_size=200, embed_size=1024)
# LSTM — longer memory than GRU, at higher compute cost
bb = LSTMBackbone(action_size=6, stoch_size=32, deter_size=200,
hidden_size=200, embed_size=1024)
# Transformer — global dependencies, no recurrent state
bb = TransformerBackbone(action_size=6, stoch_size=32, deter_size=200,
embed_size=256, num_heads=4, num_layers=2)
Backbone |
State keys |
Use when |
|---|---|---|
|
|
Standard Dreamer-style dynamics |
|
|
Longer horizons, slower drift |
|
|
Non-recurrent, parallel training |
Decoders#
from world_models.models.modular_rssm import ConvDecoder, MLPDecoder
# Convolutional decoder — reconstructs images from latent features
dec = ConvDecoder(stoch_size=32, deter_size=200,
output_shape=(3, 64, 64), depth=32)
# MLP decoder — reconstructs low-dimensional observations
dec = MLPDecoder(stoch_size=32, deter_size=200,
output_dim=10, hidden_sizes=[256, 256])
Both decoders return a torch.distributions object (the convolutional decoder
returns a pixel-wise Independent(Normal(mean, 1))).
Direct construction#
When you need fine-grained control over component configuration, construct
each piece and pass them to ModularRSSM directly:
from world_models.models.modular_rssm import (
ModularRSSM, ConvEncoder, ConvDecoder, GRUBackbone, MLPDecoder,
)
encoder = ConvEncoder((3, 64, 64), embed_size=1024)
decoder = ConvDecoder(32, 200, (3, 64, 64))
backbone = GRUBackbone(6, 32, 200, 200, 1024)
reward_decoder = MLPDecoder(32, 200, 1)
rssm = ModularRSSM(encoder, decoder, backbone, reward_decoder)
When reward_decoder is omitted, calling rssm.decode_reward(...) raises
a clear error.
Forward pass#
The ModularRSSM operates in two modes — observation (encode + posterior) and
imagination (prior only):
Single-step operations#
state = rssm.init_state(batch_size=4, device="cpu")
# state = {"mean": (4, 32), "std": (4, 32), "stoch": (4, 32), "deter": (4, 200)}
action = torch.randn(4, 6)
# Observe step: encode observation, compute posterior
prior, posterior = rssm.observe_step(state, action, observation)
# Imagine step: prior only (no observation available)
next_state = rssm.imagine_step(state, action)
Rollout operations#
# Observe a full trajectory — returns stacked states
priors, posteriors = rssm.observe_rollout(
obs=observations, # (T, B, C, H, W)
actions=actions, # (T, B, action_size)
nonterms=nonterms, # (T, B) or (T, B, 1)
prev_state=state,
horizon=T,
)
# priors["stoch"]: (T, B, 32)
# posteriors["stoch"]: (T, B, 32)
# Imagine a rollout using an actor policy
imagined = rssm.imagine_rollout(
actor=policy_network, # callable: features → action
prev_state=state,
horizon=15,
)
# imagined["stoch"]: (15, B, 32)
Decoding#
# Decode observations from latent features
features = torch.cat([posterior["stoch"], posterior["deter"]], dim=-1)
obs_dist = rssm.decode_observation(features)
reconstruction = obs_dist.mean # or obs_dist.sample()
# Decode rewards
reward_dist = rssm.decode_reward(features)
Ablation studies#
Swapping one component at a time isolates its contribution:
# Same encoder and decoder, different backbone
bb_gru = GRUBackbone(6, 32, 200, 200, 1024)
rssm_gru = ModularRSSM(enc, dec, bb_gru)
bb_lstm = LSTMBackbone(6, 32, 200, 200, 1024)
rssm_lstm = ModularRSSM(enc, dec, bb_lstm)
bb_transformer = TransformerBackbone(6, 32, 200, 256)
rssm_transformer = ModularRSSM(enc, dec, bb_transformer)
Or swap the encoder while keeping the backbone fixed:
enc_conv = ConvEncoder((3, 64, 64), 1024)
enc_vit = ViTEncoder((3, 64, 64), 512, patch_size=8, depth=6)
rssm_conv = ModularRSSM(enc_conv, dec, bb)
rssm_vit = ModularRSSM(enc_vit, dec, bb)
Custom components#
Subclass EncoderBase, DecoderBase, or BackboneBase and implement the
required interface:
from world_models.models.modular_rssm import EncoderBase
class MyEncoder(EncoderBase):
def __init__(self, input_dim: int, embed_size: int):
super().__init__()
self.embed_size = embed_size
self.net = nn.Sequential(
nn.Linear(input_dim, 512),
nn.ReLU(),
nn.Linear(512, embed_size),
)
def forward(self, obs: torch.Tensor) -> torch.Tensor:
return self.net(obs)
rssm = ModularRSSM(MyEncoder(64, 256), decoder, backbone)
The base classes enforce the contract via ABC:
Base class |
Contract |
|---|---|
|
|
|
|
|
|
Integration with training#
The ModularRSSM works with any Dreamer-style training loop. The standard
pattern is:
state = rssm.init_state(batch_size, device)
for t in range(horizon):
prior, posterior = rssm.observe_step(state, actions[t], observations[t])
state = posterior
# Train on the posterior states
features = torch.cat([posterior["stoch"], posterior["deter"]], dim=-1)
recon_loss = -rssm.decode_observation(features).log_prob(observations).mean()
reward_loss = -rssm.decode_reward(features).log_prob(rewards).mean()
# Imagine for actor-critic
imagined = rssm.imagine_rollout(actor, state, horizon=15)
The seq_to_batch and detach_state helpers are available for managing
sequence dimensions and gradient flow in recurrent training.
Available via the public API#
import torchwm
rssm = torchwm.create_modular_rssm(
encoder_type="vit",
backbone_type="transformer",
obs_shape=(3, 64, 64),
action_size=6,
)
# Or import directly
from torchwm import ModularRSSM, create_modular_rssm
## See Also
- {doc}`vision_guide` — available encoders (ConvEncoder, ViTEncoder, MLPEncoder) and decoders (ConvDecoder, MLPDecoder)
- {doc}`dreamer` — using the ModularRSSM inside a full Dreamer training loop