Pretrained Visual Models and DINO-WM › Code Implementation

Michael BrenndoerferJuly 20, 202621 min read
World Models HandbookAll chapters
Part 9 of 12Foundation and World-Action Models
Chapter 50Pretrained Visual Models and DINO-WM › Code Implementation
Reading section 7 of 10 · Code Implementation · 21 minSection 7 of 10 · 21 min

Book contents

Section 7 of 10 · 21 min read

Pretrained Visual Models and DINO-WM › Code ImplementationOne section · Sections in this chapter7 / 10 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

We'll build the pipeline in five stages: the world, the frozen encoder, the feature-space dynamics model, the rollout diagnostics, and the planner. Every piece is small enough to read in one sitting, and every number in the output is computed at run time. Keeping the pieces separate makes it clear which part of the system is responsible for which behavior, which is exactly the clarity a full-scale system makes hard to achieve.

Imports and configuration

In[3]:
Code
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
from book_plot_style import PALETTE, polish_axes, use_book_style

torch.manual_seed(0)
rng = np.random.default_rng(0)

The synthetic world

The world is deliberately simple so that we can check the learned model against ground truth. A 32×32, three-channel image shows a red key disk and a blue cursor disk. The cursor moves by the commanded displacement inside a box. The frame shows approximate disk positions, but rasterization aliases nearby cursor locations and the blue disk can occlude the red key. Information loss can therefore occur in the renderer as well as the learned encoder; the diagnostics below do not isolate one cause perfectly.

In[4]:
Code
IMG = 32


def render(cursor_xy, key_xy):
    """Paint a 32x32 RGB frame: red key disk plus blue cursor disk."""
    yy, xx = np.mgrid[0:IMG, 0:IMG].astype(np.float32)

    def disk(center, radius):
        cx = float(center[0]) * (IMG - 1)
        cy = float(center[1]) * (IMG - 1)
        return ((xx - cx) ** 2 + (yy - cy) ** 2) <= radius**2

    img = np.zeros((3, IMG, IMG), dtype=np.float32)
    key_mask = disk(key_xy, 2.2)
    img[0][key_mask] = 0.95
    img[1][key_mask] = 0.25
    img[2][key_mask] = 0.25
    cursor_mask = disk(cursor_xy, 2.2)
    img[0][cursor_mask] = 0.25
    img[1][cursor_mask] = 0.45
    img[2][cursor_mask] = 1.00
    return img


def step(state, action, low=0.05, high=0.95):
    """Displacement dynamics inside a box; the rasterized frame is lossy."""
    return np.clip(state + action, low, high)

Before learning an encoder, check what the renderer itself can hide. With the cursor drawn over the key, two different key positions can produce exactly the same 32×32 frame:

In[5]:
Code
occluding_cursor = np.array([0.50, 0.50], dtype=np.float32)
hidden_key_a = np.array([0.50, 0.50], dtype=np.float32)
hidden_key_b = np.array([0.51, 0.50], dtype=np.float32)
print(
    "distinct key positions render identically:",
    np.array_equal(
        render(occluding_cursor, hidden_key_a),
        render(occluding_cursor, hidden_key_b),
    ),
)
Out[5]:
Console
distinct key positions render identically: True

No visual encoder can recover a distinction absent from identical input pixels. This is renderer occlusion and rasterization, not a failure introduced by frozen features.

Let's render one frame and look at it, to fix the scale of the problem: a disk of radius 2.2 pixels corresponds to about 7% of the image width.

Out[6]:
Console
frame shape: (3, 32, 32)
pixel values in [0, 1]: 0.0 to 1.0
fraction of pixels that are background: 0.970703125

The reported background fraction shows why uniform pixel MSE can be a poor emphasis for this task. It does not by itself establish where another model spends its capacity.

Collecting unlabeled trajectories

We collect random-walk trajectories. Each episode gets a fresh key position and a fresh start, so the dataset covers a range of scene layouts. The first 32 episodes are the "training" split for the dynamics model; the last 8 are held out. Holding out entire episodes rather than random frames is important, because frames within an episode are highly correlated; a random-frame split would leak near-duplicate frames across the boundary and make the evaluation optimistic.

In[7]:
Code
N_EPISODES = 40
EP_LEN = 100
N_TRAIN_EPISODES = 32
ACTION_SCALE = 0.06
MAX_ACTION = 0.08

frames, states, actions, keys, ep_ids = [], [], [], [], []
for ep in range(N_EPISODES):
    key_xy = rng.uniform(0.15, 0.85, size=2)
    state = rng.uniform(0.10, 0.90, size=2)
    for _ in range(EP_LEN):
        frames.append(render(state, key_xy))
        states.append(state.copy())
        keys.append(key_xy.copy())
        ep_ids.append(ep)
        action = np.clip(
            rng.normal(scale=ACTION_SCALE, size=2), -MAX_ACTION, MAX_ACTION
        )
        actions.append(action)
        state = step(state, action)

frames = np.stack(frames).astype(np.float32)
states = np.stack(states).astype(np.float32)
actions = np.stack(actions).astype(np.float32)
keys = np.stack(keys).astype(np.float32)
ep_ids = np.array(ep_ids)
train_episode_mask = ep_ids < N_TRAIN_EPISODES
Out[8]:
Console
frames:  (4000, 3, 32, 32)
actions: (4000, 2)
training episodes: 32, held-out episodes: 8
training frames: 3200
max displacement between consecutive frames: 0.080

Consecutive states differ by at most about 0.08 in each coordinate, roughly 2.5 pixels on this raster. That small physical step motivates trying a residual predictor. It does not prove that encoded features change smoothly at a patch boundary or that residual training beats a direct predictor; those are empirical questions for the diagnostics below.

A frozen patch encoder

This is a pedagogical substitute, not a DINOv2 reproduction. We pretrain a small convolutional encoder with a reconstruction objective and a foreground-weighted loss, then freeze it before dynamics training. DINOv2 uses a different pretraining objective and data scale; the shared feature of the two systems is only that a fixed visual encoder supplies the predictor's coordinates.

The encoder produces a 4×4 grid of 32-dimensional tokens, mirroring the patch-token structure of a ViT. The grid permits spatially indexed feature comparison for the goal-image cost; whether that cost tracks task success must be tested.

In[9]:
Code
TOKEN_DIM = 32
N_TOKENS = 16
LATENT_DIM = TOKEN_DIM * N_TOKENS


class PatchEncoder(nn.Module):
    """Frozen visual backbone: image -> 4x4 grid of patch tokens."""

    def __init__(self, token_dim=TOKEN_DIM):
        super().__init__()
        self.net = nn.Sequential(
            nn.Conv2d(3, 32, 3, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(32, 64, 3, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, 64, 3, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, token_dim, 1),
        )

    def forward(self, x):
        features = self.net(x)  # (B, token_dim, 4, 4)
        return features.flatten(2).transpose(1, 2)  # (B, 16, token_dim)


class PatchDecoder(nn.Module):
    """Pretrains the encoder; excluded from dynamics and planning, retained for diagnostics."""

    def __init__(self, token_dim=TOKEN_DIM):
        super().__init__()
        self.net = nn.Sequential(
            nn.ConvTranspose2d(token_dim, 64, 4, stride=2, padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(64, 32, 4, stride=2, padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(32, 16, 4, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(16, 3, 3, padding=1),
            nn.Sigmoid(),
        )

    def forward(self, tokens):
        b, n, d = tokens.shape
        side = int(round(n**0.5))
        grid = tokens.transpose(1, 2).reshape(b, d, side, side)
        return self.net(grid)

Now we pretrain the encoder on the training episodes' frames, using no actions or goal/outcome labels. Roughly 97% of pixels are black background, so plain pixel MSE could favor a nearly black output. We threshold the rendered RGB frames to identify nonblack foreground pixels and upweight them. This is a hand-designed synthetic-domain foreground prior, not a separately supplied renderer disk mask or a DINOv2-style self-supervised objective.

In[10]:
Code
imgs = torch.tensor(frames)
imgs_train = imgs[train_episode_mask]

encoder, decoder = PatchEncoder(), PatchDecoder()
ae_optimizer = torch.optim.Adam(
    list(encoder.parameters()) + list(decoder.parameters()), lr=2e-3
)

AE_STEPS, AE_BATCH = 600, 64
ae_history = []
for _ in range(AE_STEPS):
    batch = imgs_train[torch.randint(0, imgs_train.shape[0], (AE_BATCH,))]
    reconstruction = decoder(encoder(batch))
    foreground = (batch.amax(dim=1, keepdim=True) > 0.1).float()
    pixel_weight = 1.0 + 20.0 * foreground
    loss = (pixel_weight * (reconstruction - batch).square()).mean()
    ae_optimizer.zero_grad()
    loss.backward()
    ae_optimizer.step()
    ae_history.append(loss.item())

Now freeze. From this point forward, no gradient ever flows into encoder.

Out[12]:
Console
encoder pretraining loss: 0.31562 -> 0.00231
frozen parameters in encoder: 58,400
tokens per frame: 16, token dim: 32, latent size: 512

The decoder remains in memory for the reconstruction diagnostic below, but is excluded from dynamics training and planning. We do not use it again after that diagnostic.

Out[13]:
Visualization
Sample one original red-key and blue-cursor scene above the decoded approximation.
Sample 1: original frame above its decoder reconstruction from frozen 4×4 patch tokens, on the same 0–1 pixel scale. The decoder is a diagnostic, not part of the transition model.
Sample two original red-key and blue-cursor scene above the decoded approximation.
Sample 2: original frame above its decoder reconstruction, using the same fixed 0–1 display scale.
Sample three original red-key and blue-cursor scene above the decoded approximation.
Sample 3: original frame above its decoder reconstruction, using the same fixed 0–1 display scale.
Sample four original red-key and blue-cursor scene above the decoded approximation.
Sample 4: original frame above its decoder reconstruction, using the same fixed 0–1 display scale.

The common display scale lets us check whether the 512-number latent retains approximate disk positions, color, and intensity instead of merely inspecting a contrast-enhanced image. Even a visually plausible reconstruction would not establish that these features are sufficient for control; that is what the following rollout and planning tests examine.

Extracting and normalizing features

We encode every frame once and apply a global normalization. This changes the numerical scale of the regression loss; unlike per-dimension whitening, it does not change relative feature weights or candidate-plan rankings under Euclidean distance.

We estimate the normalization statistics from training frames only. Per-dimension whitening is an alternative, but it can amplify nearly constant noisy dimensions. With one scalar scale, high-variance directions retain their relative influence; whether that influence is useful requires a task-level check.

In[14]:
Code
with torch.no_grad():
    Z = encoder(imgs).numpy()  # (N, 16, TOKEN_DIM)

Z_train = Z[train_episode_mask]
feature_mean = Z_train.reshape(-1, TOKEN_DIM).mean(axis=0)
feature_scale = (Z_train - feature_mean).std()
Z_norm = (Z - feature_mean) / feature_scale
Z_flat = Z_norm.reshape(len(Z_norm), LATENT_DIM)
Out[15]:
Console
raw feature tensor: (4000, 16, 32)
global feature scale: 1.3882
per-dimension std after normalization:
  min 0.587, median 0.885, max 1.982

The spread of per-dimension standard deviations after global scaling is a diagnostic of relative variation. Dominance in variation can affect a squared-error cost, but variance alone does not tell us whether the changing directions matter for the goal.

The cost is easier to inspect as a landscape. We render the cursor over a fixed grid with a fixed key, encode each frame, and evaluate feature distance to a goal image drawn at the key. Rasterization can make nearby positions identical, and the learned Conv–ReLU encoder is not known to be invertible or globally smooth. The observed landscape is a diagnostic for this trained toy, not a general theorem.

Out[17]:
Visualization
Heatmap of feature-space cost to a goal over cursor positions with goal star.
Feature-space cost to a goal image over a sampled grid of cursor positions. The measured minimum is near the key; rasterization and a learned encoder do not guarantee an exact or unique physical-state minimum.

Building the transition dataset and learning dynamics

Transition pairs are consecutive frames within the same episode. Cross-episode pairs must be excluded, since they are not real transitions. This is the same leakage concern as the train/test split: joining the last frame of one episode to the first frame of the next would create a pair that no action could produce, and it would corrupt the learned dynamics.

In[18]:
Code
same_episode = np.r_[ep_ids[1:] == ep_ids[:-1], False]
train_pairs = np.where(train_episode_mask & same_episode)[0]

Z_t = torch.tensor(Z_flat[train_pairs])
Z_next = torch.tensor(Z_flat[train_pairs + 1])
A_t = torch.tensor(actions[train_pairs])
Out[19]:
Console
training transitions: 3,168
input dim: 514 (latent plus action), output dim: 512

The predictor is a residual MLP: it predicts a change in feature space rather than the next feature directly. The code does not zero-initialize its final layer, so the initial residual is not guaranteed to be small. We measure improvement against persistence below.

The action enters by concatenation. The physical update is action-affine only away from clipping boundaries; whether this MLP handles boundary transitions is empirical.

In[20]:
Code
class LatentDynamics(nn.Module):
    """Residual predictor in normalized feature space."""

    def __init__(self, latent_dim=LATENT_DIM, action_dim=2, hidden=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(latent_dim + action_dim, hidden),
            nn.SiLU(),
            nn.Linear(hidden, hidden),
            nn.SiLU(),
            nn.Linear(hidden, latent_dim),
        )

    def forward(self, z, a):
        return z + self.net(torch.cat([z, a], dim=-1))


dynamics = LatentDynamics()
dyn_optimizer = torch.optim.Adam(dynamics.parameters(), lr=1e-3)

DYN_STEPS, DYN_BATCH = 800, 256
dyn_history = []
for _ in range(DYN_STEPS):
    idx = torch.randint(0, Z_t.shape[0], (DYN_BATCH,))
    prediction = dynamics(Z_t[idx], A_t[idx])
    loss = nn.functional.mse_loss(prediction, Z_next[idx])
    dyn_optimizer.zero_grad()
    loss.backward()
    dyn_optimizer.step()
    dyn_history.append(loss.item())

dynamics.eval()
Out[21]:
Console
dynamics loss: 0.31021 -> 0.16331
predicting no change at all would give: 0.28829
full-train fitted MSE: 0.16575
paired full-train reduction over persistence: 42.5%

The paired full-training-set comparison against persistence checks whether the predictor improves on a simple baseline for this one-step metric. The preceding last-minibatch loss is a training trace, not the numerator of that comparison. Held-out transition error and closed-loop task success are needed before calling the learned dynamics useful.

Rollout diagnostics: one-step versus recursive error

Now the key measurement. We roll the predictor forward on held-out episodes and compare two error curves:

  • One-step error: always start from a true feature vector and predict one step ahead. This measures the model in the regime it was trained on.
  • Recursive error: start from a true feature vector and then feed predictions back in. This measures the model in the regime the planner will actually use it.
  • Persistence error: compare each future observation with the fixed starting feature. This is a reference curve, not a mathematical lower bound for a learned model or a policy.
In[22]:
Code
HORIZON_EVAL = 15
STRIDE = 12

start_idx = []
for ep in range(N_TRAIN_EPISODES, N_EPISODES):
    for t in range(0, EP_LEN - HORIZON_EVAL, STRIDE):
        start_idx.append(ep * EP_LEN + t)
start_idx = np.array(start_idx)

z0_eval = torch.tensor(Z_flat[start_idx])
src_idx = start_idx[:, None] + np.arange(HORIZON_EVAL)[None, :]
targets_eval = torch.tensor(Z_flat[src_idx + 1])

with torch.no_grad():
    rolling = []
    z = z0_eval
    for h in range(HORIZON_EVAL):
        z = dynamics(z, torch.tensor(actions[src_idx[:, h]]))
        rolling.append(z)
    rollout_pred = torch.stack(rolling, dim=1)

    one_step_pred = dynamics(
        torch.tensor(Z_flat[src_idx]),
        torch.tensor(actions[src_idx]),
    )

recursive_mse = (
    ((rollout_pred - targets_eval) ** 2).mean(dim=-1).mean(dim=0).numpy()
)
one_step_mse = (
    ((one_step_pred - targets_eval) ** 2).mean(dim=-1).mean(dim=0).numpy()
)
persistence_curve = (
    ((z0_eval[:, None, :] - targets_eval) ** 2).mean(dim=-1).mean(dim=0).numpy()
)
Out[23]:
Console
held-out start states: 64
one-step MSE at horizon 1:      0.33500
recursive MSE at horizon 1:     0.33500
recursive MSE at horizon 15:     2.22436
persistence MSE at horizon 15:   1.14052
recursive error growth factor (h=15 compared with h=1): 6.6x
Out[24]:
Visualization
Line chart of three error curves rising from a low flat line to a higher compounding curve.
Feature-space error against rollout horizon on held-out episodes. One-step predictions use observed features at each step; recursive predictions feed back model outputs. In this run, recursive error rises and crosses the fixed-start persistence reference between horizons six and seven.

The gap between the one-step and recursive curves shows how feedback of model predictions affects this held-out set. Where recursive MSE crosses the fixed-start persistence reference, the model predicts these recorded open-loop trajectories less accurately under this feature metric. That is not a comparison of action policies, a safe-horizon certificate, or a statement about every initial state.

This diagnostic motivates testing shorter horizons, alternative models, and closed-loop control. A CEM planner optimizes goal cost rather than the random-trajectory prediction MSE plotted here, so the crossing cannot establish that doing nothing is better or that search cannot help.

Planning to a goal image with the cross-entropy method

The toy planner searches over action sequences and sums feature distance to the encoded goal at every predicted step. This favors earlier approach to the goal, but it differs from DINO-WM's terminal-cost objective stated above.

In[25]:
Code
def cem_plan(
    z_start, z_goal, horizon=8, n_samples=128, n_elite=12, n_iters=3, seed=0
):
    """Cross-entropy method over action sequences, scored in feature space."""
    plan_rng = np.random.default_rng(seed)
    mean = np.zeros((horizon, 2), dtype=np.float32)
    std = np.full((horizon, 2), MAX_ACTION, dtype=np.float32)
    z0 = torch.tensor(z_start[None, :])
    zg = torch.tensor(z_goal[None, :])

    for _ in range(n_iters):
        noise = plan_rng.standard_normal((n_samples, horizon, 2)).astype(
            np.float32
        )
        candidates = np.clip(
            mean[None] + std[None] * noise, -MAX_ACTION, MAX_ACTION
        )
        candidate_t = torch.tensor(candidates)

        with torch.no_grad():
            z = z0.repeat(n_samples, 1)
            cost = torch.zeros(n_samples)
            for h in range(horizon):
                z = dynamics(z, candidate_t[:, h])
                cost = cost + ((z - zg) ** 2).mean(dim=1)

        elite = candidates[torch.argsort(cost)[:n_elite].numpy()]
        mean = elite.mean(axis=0)
        std = elite.std(axis=0) + 1e-3

    return mean

The MPC loop re-encodes a rendered observation at every real step, so the next search starts from the encoder's output rather than the previous imagined feature. That observation is still lossy because of rasterization and possible occlusion.

In[26]:
Code
def encode_state(state_xy, key_xy):
    """Render, encode with the frozen backbone, and normalize."""
    frame = render(state_xy, key_xy)
    with torch.no_grad():
        z = encoder(torch.tensor(frame[None])).numpy()[0]
    return ((z - feature_mean) / feature_scale).reshape(-1).astype(np.float32)


def run_mpc(key_xy, start_xy, n_steps=20, horizon=8, seed=0):
    """Receding-horizon control. Execute one action, observe, replan."""
    goal_latent = encode_state(key_xy, key_xy)
    state = np.asarray(start_xy, dtype=np.float32).copy()
    trajectory = [state.copy()]
    for k in range(n_steps):
        current_latent = encode_state(state, key_xy)
        plan = cem_plan(
            current_latent, goal_latent, horizon=horizon, seed=seed + k
        )
        state = step(state, plan[0])
        trajectory.append(state.copy())
    return np.array(trajectory)

Evaluating closed-loop success

This is where proxy and task outcomes can be compared. We track final physical distance from cursor to key and final observed-frame feature distance to the goal. The latter is related to, but not identical to, the summed predicted feature cost CEM optimized. A random-action baseline runs on the same scenarios with the same number of steps.

In[27]:
Code
N_SCENARIOS = 10
eval_rng = np.random.default_rng(1234)
eval_keys = eval_rng.uniform(0.20, 0.80, size=(N_SCENARIOS, 2)).astype(
    np.float32
)
eval_starts = eval_rng.uniform(0.15, 0.85, size=(N_SCENARIOS, 2)).astype(
    np.float32
)

mpc_trajectories, mpc_final_distance = [], []
for i in range(N_SCENARIOS):
    traj = run_mpc(
        eval_keys[i], eval_starts[i], n_steps=20, horizon=8, seed=100 * i
    )
    mpc_trajectories.append(traj)
    mpc_final_distance.append(float(np.linalg.norm(traj[-1] - eval_keys[i])))
mpc_final_distance = np.array(mpc_final_distance)

random_final_distance = []
for i in range(N_SCENARIOS):
    state = eval_starts[i].copy()
    for _ in range(20):
        noise = np.clip(
            eval_rng.normal(scale=ACTION_SCALE, size=2), -MAX_ACTION, MAX_ACTION
        )
        state = step(state, noise)
    random_final_distance.append(float(np.linalg.norm(state - eval_keys[i])))
random_final_distance = np.array(random_final_distance)
Out[28]:
Console
initial distance to key, mean:      0.397
latent MPC final distance, mean:    0.378
random walk final distance, mean:   0.396
latent MPC success rate (<= 0.10):  10%
random success rate (<= 0.10):      0%

The toy model was trained without goal/outcome labels or demonstrations, but its encoder pretraining used a foreground mask derived by thresholding rendered RGB pixels. It does not consistently reach the key in these held-out scenarios. It sometimes approaches the target and sometimes stalls far away. The predictor learned from random-walk transitions in 32 episodes; the test goal was supplied as an image. Reporting misses alongside successes matters because feature-space fit is not task success.

For each held-out scenario we compare final physical distance with final observed-frame feature cost. This is a descriptive cross-scenario check: each point has a different key and start, and the plotted final-state cost is not the exact summed predicted cost optimized within that scenario. Rank inversions across different goals do not prove that CEM misranks candidate plans for one goal. A within-goal candidate study would be needed for that conclusion.

Out[30]:
Visualization
Scatter of final task distance versus feature-space cost across held-out scenarios.
Final observed-frame feature cost against final physical distance for ten distinct held-out scenarios. Each point has a different goal and start; cross-scenario ranks do not test candidate-plan ranking within one goal.
Out[31]:
Visualization
Scenario one square plot with cursor path, start circle, and goal star separated at the end.
Held-out scenario 1: latent MPC cursor trajectory from the circled start toward the starred goal supplied as an image. The trajectory finishes visibly short of the goal.
Scenario two square plot with cursor path, start circle, and goal star.
Held-out scenario 2: latent MPC cursor trajectory from the circled start toward the starred goal supplied as an image.
Scenario three square plot with cursor path, start circle, and goal star separated at the end.
Held-out scenario 3: latent MPC cursor trajectory from the circled start toward the starred goal supplied as an image. The trajectory finishes visibly short of the goal.
Scenario four square plot with cursor path, start circle, and goal star.
Held-out scenario 4: latent MPC cursor trajectory from the circled start toward the starred goal supplied as an image.
Out[32]:
Visualization
Bar chart of mean final goal distance for latent MPC and random actions, with ten horizontally jittered individual outcomes over each bar.
Final distance to the goal key for the learned latent planner and a random-action baseline with the same number of steps on ten scenarios. Bars show sample means; dots show individual outcomes. The small mean gap and wide spread do not establish a general advantage.

A note on what just happened

The encoder in this example is not DINOv2. It is a tiny convolutional network trained by foreground-weighted reconstruction on a few thousand 32×32 RGB images. What it shares with DINOv2 is a system role: a frozen visual map supplies features for dynamics learning and goal comparison. The toy uses a one-frame MLP, global normalization, and a summed-horizon CEM cost; a real-backbone implementation must revisit those choices rather than copy them uncritically.

Scaling up changes the data distribution, patch grid, feature dimension, action dynamics, and compute budget. A pretrained backbone may improve useful visual invariance, but that must be verified for the task. A Transformer is one way to model token interactions, not a requirement imposed by dimension alone. The toy remains useful for understanding the interfaces and diagnostics, not for predicting real-robot performance.