Skip to main content

View on GitHub

Full source for this example in fal-serverless-examples.
Large video models may not fit on a single GPU. WAN 2.2 A14B has two 14-billion-parameter expert transformers, and the weights alone are about 70 GB in bf16. This example serves the model across four H100s with fal.distributed. The DistributedRunner starts one worker for each GPU, and diffusers’ context parallelism divides every denoising step across the four workers. One prompt, one video, four GPUs cooperating.
The clip emerges the way the sampler saw it: denoising, visualized over the real output of the example’s validation run (720p, 81 frames, 27 steps, seed 7 - generated in 282 s on 4× GPU-H100).

Running the example

  1. Install fal:
  1. Authenticate (if not already done):
  1. Clone the examples repository and enter this example’s directory:
  1. Run the app:
This starts an ephemeral app on four real GPUs and prints its playground and API URLs. fal run keeps the app alive until you press Ctrl-C.
The first-ever run downloads ~126 GB of fp32 weights and stores a bf16 copy on your account’s /data volume (about 35 minutes, once). Every run after that boots from the converted copy in about 8 minutes.

Splitting one generation across four GPUs

Data parallelism gives each GPU a full, separate generation: four GPUs make four different videos, and latency does not change. This example uses the opposite strategy. Ulysses sequence parallelism divides the token sequence of one generation across all four workers. This is the right trade-off when the model itself is the bottleneck. At every attention layer, an all-to-all exchange trades tokens for heads: each GPU sees the full sequence for 10 of the 40 attention heads, then the shards return to their own GPUs.
Ulysses sequence parallelism in three phases: the 81-frame sequence is sharded across four GPUs, an all-to-all trades frames for heads at every attention layer so each GPU holds all 81 frames for 10 of the 40 heads, then shards return homeUlysses sequence parallelism in three phases: the 81-frame sequence is sharded across four GPUs, an all-to-all trades frames for heads at every attention layer so each GPU holds all 81 frames for 10 of the 40 heads, then shards return home
See Multi-GPU Workloads for the full strategy taxonomy. The next nine sections walk through the app one file top to bottom: app.py.

Importing fal.distributed and pinning the model

fal.distributed provides the two classes this example teaches: DistributedRunner and DistributedWorker. MODEL_ID pins Wan-AI/Wan2.2-T2V-A14B-Diffusers at an exact revision; the model is Apache-2.0 and ungated, so no Hugging Face token appears anywhere. CONVERTED_DIR names the one-time bf16 copy on /data. Sequence parallelism replicates the weights on every rank, so the half-size copy halves every worker’s load time on every boot.
app.py

Defining the request schema

Every Field carries a description, and the fields with examples power the generated playground form and deterministic smoke requests. The validator enforces WAN’s frame rule: num_frames must satisfy (n - 1) % 4 == 0. The two guidance scales exist because the A14B is a mixture-of-experts pair - one expert denoises the high-noise steps, the other the low-noise steps, and each takes its own classifier-free-guidance strength. seed is optional: the server picks one when omitted, and the same seed with the same inputs reproduces the clip.
app.py

Defining the response schema

The response carries the CDN-hosted clip, the seed that produced it, and per-phase timings.
app.py
On the wire, the validation run’s response looked like this:

Loading the model and enabling parallel attention

One WanWorker process runs on each GPU. The runner initializes torch.distributed (NCCL) before this method runs, so enable_parallelism can attach to the ready process group. That single line per transformer is the whole sequence-parallelism integration. Two techniques keep the ~70 GB model inside an 80 GB H100: every rank loads the bf16 copy, and sequenced CPU offload keeps only the active MoE expert resident (the two experts run in disjoint denoising stages). Measured peak is 27-30 GB per GPU, with headroom to 720p × 81 frames.
app.py

Generating on all ranks, returning from rank 0

All four ranks execute the same call with the same seed. (Under data parallelism you would do the opposite: per-rank seeds for different outputs.) Only rank 0 writes the mp4 and returns its path; the other ranks return an empty dictionary - their contribution happened inside the pipeline through NCCL collectives.
app.py

Configuring the app for four GPUs

num_gpus = 4 is a configurable parameter: fal allocates four H100s on one machine, and they appear as cuda:0..3 (see machine types). Multi-GPU billing is gpu_count × duration, so keep_alive=300 holds the warm window to 5 minutes. startup_timeout=3600 exists for one boot in the app’s life: the first-ever download plus the bf16 conversion. The requirements list is the authoritative set of deploy-time dependencies; pyzmq is pinned explicitly because fal.distributed uses it at runtime but the fal package does not depend on it.
app.py

Converting once, starting the runner, warming up

The app process converts the fp32 weights to bf16 one time per account (with an atomic rename, so concurrent boots cannot corrupt the copy), then starts the runner. The runner spawns the four workers, initializes NCCL, and waits for a readiness signal from each. If a worker dies, await runner.start() fails with an error instead of hanging. The warmup then runs one generation through the whole distributed path at the default request shape, because attention kernels autotune for each shape and a cold shape adds about 40 s to the first request. Worker errors come back as an "error" key, not an exception, so the warmup asserts on it: a broken model never reports healthy.
DistributedRunner lifecycle: the fal.App endpoint spawns four GPU workers that init NCCL before setup() and report READY; each request goes to rank 0, which broadcasts the same payload and seed to all ranks for collective compute, and only rank 0's result returns; a worker exception comes back as an error value the endpoint maps to a 5xxDistributedRunner lifecycle: the fal.App endpoint spawns four GPU workers that init NCCL before setup() and report READY; each request goes to rank 0, which broadcasts the same payload and seed to all ranks for collective compute, and only rank 0's result returns; a worker exception comes back as an error value the endpoint maps to a 5xx
app.py

Serving requests

@fal.endpoint("/") serves generate at the app’s root URL. It picks a seed when the caller omitted one, sends the payload through the runner, and checks the "error" contract: a worker failure becomes a 5xx response, which fal does not bill. The queue does not retry a 500 automatically - only 503, 504, and connection failures are retried (see Retries) - so the caller decides whether to resubmit. The mp4 uploads to fal’s CDN, and the temp file is removed either way - warm runners live on, and clips must not accumulate in /tmp. The runner processes one request at a time (fal’s default max_multiplexing = 1 admits a single request per runner), so this endpoint scales by adding runners, not by sending more requests to one runner.
app.py

Building the worker payload

The endpoint and the warmup build the worker payload the same way: resolve the resolution to pixels and pass one explicit seed to every rank.
app.py

Deploying to production

From the example directory, deploy the app:
This registers multi-gpu-video and prints its playground and API URLs. Generations take minutes at production settings, so always call the endpoint through the queue API - never raw synchronous HTTP. The clients below authenticate with FAL_KEY (fal auth login covers the CLI only): create a key at fal.ai/dashboard/keys and export it as FAL_KEY="your_key_id:your_key_secret". Python
JavaScript
curl (queue API)
That 720p call is also this example’s validation protocol: deploy, run it once, confirm the returned mp4 plays. (Last run 2026-08-11 - it produced the clip in the response JSON above, the same output the hero visualization at the top of this page is built from.)

Tuning cost and GPU count

  • Multi-GPU bills gpu_count × duration. This app runs 4× GPU-H100, so every runner-second costs four H100-seconds, from boot through teardown; see fal pricing.
  • keep_alive=300 holds a warm four-GPU runner for 5 minutes after the last request: bursts share one boot, and an idle app scales to zero.
  • Changing num_gpus: the Ulysses degree must divide the attention head count. WAN 2.2 A14B has 40 heads, so 2, 4, or 8 GPUs work. More GPUs cut per-clip latency at a higher per-second rate.
  • Fast previews vs final quality: the defaults (480p, 33 frames, 6 steps) are the fast path; WAN’s native quality settings (720p, 81 frames, 27 steps) use the same endpoint and the same seed contract.
  • Allocations can queue at four GPUs: when capacity is tight, an allocation can wait (we have measured waits of tens of minutes at peak). This is one more reason to call through the queue with generous client timeouts.
Measured on real 4× GPU-H100 runs (2026-08-04 to 2026-08-11, fal 1.78.2, diffusers 0.39.0) - one-time measurements, not a guarantee:

Next steps

For more examples, from LoRA serving to 3D streaming, browse the fal-serverless-examples repository.