Loading…
Loading…
An interactive world model. You give it one start image, then hold keys; it generates the video that follows, one frame at a time, and streams it to a browser while you play.
The architecture is Summaryformer. It is my own design: I arrived at it after testing a lot of ideas on consumer hardware, and I trained it from scratch. This checkpoint was trained on GG800, my new 800-hour GTA5 dataset.
This repository holds the weights and the complete inference runtime: the model, its memory, the sampler and the streaming server. There is no training code here. What I took from other people's work is listed under Credits.
The part of Summaryformer I most want people to read about is how a frame remembers the past.
A frame generated a second ago matters in detail: the next frame has to continue its motion pixel for pixel. A frame generated many seconds ago matters only in outline: what was there, roughly where. Keeping every past frame at full resolution spends almost all of the attention budget on detail nobody needs. Keeping only a short window forgets everything else.
So every frame in this model is stored twice, at two sizes, and the model itself writes both.
Each frame enters the transformer as one record:
record(t) = [ raw(t) : H*W tokens ] ++ [ sum(t) : X tokens ]
raw(t) is the frame's latent grid, one token per cell. At the served 384x672 that is 24x42 = 1008 tokens.sum(t) is X = 96 summary tokens. They start from a learned init (the same for every frame), have no
position on the grid, and go through the same 28 blocks in the same forward pass as the raw tokens. By the last
block they hold a compact description of the frame.There is no separate summarizer network and no extra pass. The summary is a by-product of generating the frame.
While frame t is being generated, every one of its tokens attends to:
t;Recent frames give local detail. Summaries give the long horizon. The start image anchors the scene.
All of this is one preallocated key/value buffer per block:
| Z0 : record(0) | RAW : (Y+1) records, rotating | SUM : (W-Y) x X summary rows, rotating |
read = buffer[0 : end]
Z0 holds the start image's record.RAW holds the frame being generated and the Y frames before it.SUM holds one X-row summary for each older frame in the window.When a frame falls out of the raw window, the keys and values of its 96 summary tokens are copied from RAW
into SUM, and its 1008 raw rows are overwritten by the next frame. That copy of 96 rows per block is the entire
cost of moving a frame into long-term memory.
Attention is one dense read of the filled prefix of that buffer per block. There is no attention mask and no concatenation: what a frame may see is decided by what is in the buffer.
At the served resolution with the trained window, the buffer holds 6,528 keys. The same span with every frame kept at full resolution would be 26,208.
| summary tokens per frame (X) | 96 |
| raw tokens per frame at 384x672 | 1008 |
| frames kept whole (Y) | 2 |
| memory window in frames (W) | 24 trained; the server default serves 16 |
One body frame of latents decodes to 4 RGB frames, so a window of 24 covers 96 RGB frames.
Raw tokens and summary tokens of the same frame share a temporal position, so rotary position embeddings alone cannot tell them apart. I did not add a token-type embedding. Instead the two lowest-frequency planes of the temporal RoPE band are used as a level axis.
Each attention head has 12 temporal planes. The two slowest rotate by less than half a radian over 1024 frames, and by a small fraction of that over the distances that occur inside the memory window, so for raw tokens they carry no usable position. Summary tokens get an exact quarter turn on those two planes. A query can then weight "summary of frame t" and "pixel of frame t" independently, using dimensions that were otherwise idle. The model checks at construction that those planes really are idle and refuses to build if they are not.
Frame ids keep growing as the session goes on, but the start image was written at id 0. Left alone, the temporal distance between the start image and the live window would grow without bound.
RoPE keys are rotations, and rotation(id + T) = rotation(id) * rotation(T). So a stored key can be moved
forward in time by multiplying it, in place, by one more rotation. Whenever the gap between the start image and
the oldest frame in memory reaches 5, the start image's keys are rotated forward to bring the gap back to 1.
Values are untouched. Nothing is recomputed.
rope.py are mine.noise_schedule.py.flash_attn_with_kvcache. The server uses
FlexAttention.serve_ckpts/
gta_model.pt the model weights
sae_compact.pt the autoencoder weights
latent_stats_*.pt per-channel mean / std of the autoencoder's latent
summary_server.py FastAPI server: start image in, H.264 out, keys in
summary_streaming_inference.py one session: the per-frame loop
summaryformer.py the model
summaryformer_wrapper.py the model's public surface
gta_streaming_kv_cache.py the memory buffer
summaryformer_utils/
transformer_block.py the block
rope.py rotary positions and level planes
streaming_sink.py moving the start image's keys
latent_head.py output head
visual_mlp.py SwiGLU feed-forward
z0_focus_mask.py optional focus region on the start image
rollout_flex_attention.py FlexAttention kernel
fast_flash_kv.py FlashAttention kernel
recency_bias.py optional recency bias on summary keys
streaming_server_utils/
ckpt_name_schema.py checkpoint file names
controls.py keys to the model's 5-number action
sae.py the autoencoder (parallel and autoregressive encode / decode)
noise_schedule.py the noise schedule and the timesteps a frame is denoised at
inference_common.py small shared helpers
Requirements: a CUDA GPU; Python with torch (FlexAttention and torch.compile), numpy, fastapi, uvicorn,
pillow, av, imageio. flash_attn is optional.
Everything the server needs is in this repository, at the paths its settings point to. The settings are the
CONFIG dict at the top of summary_server.py:
checkpoints_dir (default serve_ckpts/): serve_ckpts/gta_model.pt;sae_ckpt, default sae_compact.pt): the encoder that turns the start image
into a latent and the decoder that turns latents into frames;latent_stats_z0.pt (start image) and latent_stats_z_rest.pt (generated
frames). They hold the per-channel mean and standard deviation of the autoencoder's latent.Download the repository and start the server from its root:
hf download lucidml/gta --local-dir gta
cd gta
python summary_server.py # serves on 0.0.0.0:3002
At startup the server loads the weights, compiles the model and plays a short warm-up session, so the first start takes a while. One GPU process serves one player at a time.
The browser client is not in this repository. The protocol is small:
POST /z0/{user_id} |
upload the start image (form field z0_image) |
WS /ws/{user_id} |
the session |
GET /status, GET /models |
occupancy and the available checkpoints |
On the WebSocket the server sends binary messages, one byte (0x01 key frame, 0x00 delta frame) followed by an
H.264 Annex-B access unit at 384x672. The client sends JSON with the keys currently held:
{"type": "keys", "pressed": {"w": true, "a": false, "s": false, "d": false, "shift": false, "space": false, "left": false, "right": true}}
ws_session_limit_s, default 180 s;
0 disables it).Summaryformer is my own architecture and this model was trained from scratch. These are the pieces that came from other people:
conditional_dict name at the model's boundary is from there.summaryformer_utils/rope.py lists what I changed.noise_schedule.py is my own
implementation of those formulas.Apache-2.0. See LICENSE.