Skip to main content
This example deploys Matrix-Game 2.0, a real-time world model from Skywork (MIT license, 1.8B parameters), on one H100. The model starts from a seed image and generates video that reacts to movement and camera keys. The app uses fal WMA (World Model Accelerator) (fal.wma) to stream the video over WebRTC.
Want a hosted world model instead of deploying your own? Try Lingbot World 2 or Abot World 0 on fal. Both let you explore an interactive world in real time with movement and camera controls, with nothing to deploy. Use this example when you want to run your own world model.
The clip above is a recording of this app on one H100. The caption shows each commands message as the client sent it. The example shows the CommandWorld interaction profile:
  • The world starts when the session starts. The first video frame tells the client that the world is ready.
  • The client sends the commands that are held and the commands that were tapped.
  • An optional setup message chooses the seed image. A reset message restarts the world.
WMA is experimental. This page pins fal 1.82.1. Read the WMA guide before you deploy.

What Is a World Model

A world model generates video that responds to what you do. A video model turns a prompt into a clip of fixed length. A world model produces the next frames one chunk at a time, and each chunk depends on the frames before it and on the latest actions from the user, such as movement and camera keys. Because the user’s input shapes every new frame, the output works like a game: you can walk around, look around, and see the world change in response. That loop has two consequences for how you serve it:
  • Frames must arrive in real time. The app streams the generated frames to the client as a video track, not as a finished file. This is why the app streams over WebRTC.
  • Each session holds state. The model keeps the world it has already generated, so a session belongs to one user and runs on one GPU for as long as it lasts.
WMA handles the streaming, the control messages, and the session lifecycle, so the app only has to implement the generation loop.

Key Facts

Prerequisites

  • A fal account with permission to deploy and call the app. The browser client connects through fal’s WMA service at wma.fal.run. It needs a fal API key on your server, or a signed-in dashboard session for the playground.
  • Access to the fal WMA TURN servers for the account that owns the app (the X in fal.run/X/app). Request it from fal before you rely on the app. Without it, a session connects only when the runner is directly reachable, which fails on many runners and networks. See If the Session Does Not Start.
  • Python 3.12 on your computer. The app image uses Python 3.12, and the Python versions must match for deployment.
  • fal 1.82.1 and authentication. Run these commands:
  • An H100 machine type. On one H100 the app uses about 14 GB of GPU memory.
The app downloads two pinned sources on the first start of a runner and stores them in /data:
  • The upstream code from SkyworkAI/Matrix-Game at commit c7a88bf. Only the Matrix-Game-2 folder is checked out.
  • The weights from Skywork/Matrix-Game-2.0 at revision 08c42d1, about 12 GB for the files that the app needs.

The App

View the complete source code on GitHub. This page shows the parts that use the WMA SDK.

The Contract

The contract declares one video track and the CommandWorld profile. The Commands message holds the eight semantic commands. The contract describes the app to the playground and to clients. The handlers do the real work. The values "/active" and "/activated" are JSON Pointers into the commands message.

The Control Handlers

The app registers one handler for each message type. The handlers run on the session event loop.

Billing in the Generation Loop

The generation thread bills each frame after it queues the frame. If billing is already final, the session is closing and the thread stops.

The App Class

The class sets the runner, the pinned requirements, and a long startup_timeout for the weight download. The keep_alive value keeps a runner up for 300 seconds after its last session, and fal bills that idle time. The value min_concurrency=0 scales the app to zero, so the first session waits for a cold start. The max_concurrency value is the most runners, and max_multiplexing is the most sessions on one runner. Both are example limits. Adjust them for your workload and cost. See Scale your application.

The Session Backend

The create_backend method accepts one session at a time. It then starts the generation thread and the WebRTC peer. The lines that build the ICE servers (the RunnerIceConfig call) are boilerplate. Copy them as they are.
The module imports on a computer without a GPU. The app imports torch and the upstream code inside Engine.load().

How the Contract Maps to Controls

The app declares eight semantic commands. The client does not send physical key names. The browser, a gamepad, or an agent can map any input to these commands. Each commands message reports the complete held state in active. It reports newly tapped commands in activated. fal.wma.KeyState merges both lists. It keeps a tap for one generation step and a held command until the client releases it. The generation thread calls KeyState.sample() once for each chunk. A new tap wins over an older held command in the same conflict group. If the client holds both commands of a group, KeyState keeps both, and the app cancels them. The model works in chunks. One chunk is three latent frames, which decode to 12 video frames. The first chunk decodes to 9 frames. All frames of a chunk use the same keyboard and mouse values. This is how the upstream inference_streaming.py script conditions the model. The app replaces its terminal prompt with the sampled state. The other messages follow the CommandWorld profile:

Run and Deploy

Run these commands from the root of the cloned repository. Use fal run as the development loop. It starts the app on an ephemeral runner.
Deploy the app as a private app. Use the deployed app for the playground and for the browser client in the next section.

Cost and First Start

You pay per second for the runner, including setup time and the idle keep_alive time. See Pricing. The billable units in the app matter only if you list the app on the fal Marketplace. Callers then pay for these units at a price that the fal team sets. The first session starts a runner, and that start is slow. The runner installs the requirements, then downloads and loads about 12 GB of weights. In our tests, this took 5 to 20 minutes, so the app sets startup_timeout to 1800 seconds. You can follow the progress in the runner logs in the dashboard. Later starts are faster because the weights stay on the /data volume across runners. To skip the cold start, set min_concurrency = 1 to keep one runner warm. You then pay for that runner while it is idle. See cold starts.

Clean Up

To stop paying for the app, scale it to zero with fal apps scale <app-name> --min-concurrency 0. To remove it, run fal apps delete <app-name>.

Try It

Open the deployed app in the fal dashboard. In the Playground tab, select the /start-session endpoint.
  1. Select Connect to start a session. The first frame shows that the world is ready.
  2. Hold a movement or look control, or its keyboard key, to move. Release it to stop.
  3. Use the Restart world control to reset the world.

Browser Client

For your own page, connect with the experimental WMA browser client. There is no Python or Node client today. The deploy output shows the app ID as your-account/<app-name>. The name comes from the class name, so MatrixGameApp becomes matrix-game-app. Install the client and the server proxy in your web application. Set up the proxy route as the browser client page describes.
The client sends the WMA signaling requests through your proxy route. The proxy adds your API key on the server, so the key never reaches the browser. The video then flows over WebRTC, not through your proxy. The following code sends the held and tapped keys of the keyboard. Replace the application ID with your own.
The onData callback receives the raw JSON string. Parse it before you use it. Enable the controls after the video element fires playing. The blur handler releases all held keys when the tab loses focus. A session can end on its own. The stream_exhausted message closes the session when the world reaches its length limit. In that case, open a new session. To reconnect, wrap fal.realtime.open(...) and the ended flag in a function. The try and catch around await session.ready handle a failed start. A failure reaches both onError and onState. The ended flag makes endSession run once.

Server Messages

If the Session Does Not Start

Use Your Own Model

The model is the Engine class. It has three parts.
  • load() loads the weights once for each runner.
  • new_world(scene) starts a world from a seed image.
  • step(world, keys) returns a chunk of uint8 frames with the shape [T, H, W, 3].
The rest is the same WMA shape for any model:
  • A generation thread calls step in a loop. It reads the sampled keys and checks for a restart before each chunk.
  • The thread puts each frame in a bounded queue, with the push method. The method blocks while the queue is full.
  • make_video_queue_track plays the queue as the video track, and AiortcPeer sends it to the client.

Billing

This section applies if you list the app on the fal Marketplace. For your own private app, you pay only for runner time, and the units do not change the cost. The app calls session.add_billable_units(1) for each frame that it queues for playback. One unit is one frame. Some frames never play. The app drops the queued frames when a reset or setup message restarts the world, and some frames remain queued when the session ends. Before billing is final, a deferred cleanup calls session.cap_billable_units(total - undelivered). The argument is the new total, so the undelivered frames are not billed. The WMA guide describes this billing pattern. fal reports the total when the session closes. The fal team configures the price per unit when it lists the app.

Limits

  • Each runner serves one session at a time. With max_concurrency = 2, the example serves at most two sessions at once.
  • A world lasts at most 1437 video frames, about 57 seconds at playback speed. The app then sends stream_exhausted and closes the session. Use reset to start again before that point.
  • The app plays video at 25 frames per second, the rate that upstream states for real-time generation. In our tests on one H100, generation ran at about 19 to 23 fps at 640x352, which is slower than playback, so motion can look bursty. The first frame arrived 1 to 4 seconds after we connected.
  • A key change takes effect at the next chunk. The delay is the time to generate a chunk plus the frames that wait in the queue.
  • Upstream notes that an upward camera movement can cause brief rendering glitches, such as black frames.
  • The example supports the universal model only. The upstream templerun model uses different weights and a different command set.