View on GitHub
Full source for this example in fal-serverless-examples.
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
- Install fal:
- Authenticate (if not already done):
- Clone the examples repository and enter this example’s directory:
- Run the app:
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.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
EveryField 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
Loading the model and enabling parallel attention
OneWanWorker 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.
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: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
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=300holds 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.
Next steps
- Multi-GPU Workloads: the full strategy taxonomy - data, sequence, tensor, and hybrid parallelism.
- Event Streaming: send preview
frames from rank 0 with
add_streaming_result. - fal.distributed API Reference:
DistributedRunnerandDistributedWorkerin depth. - Deploy a Text-to-Video Model: the single-GPU serving patterns this example builds on.