Skip to main content
Split a monolithic text-to-video/image pipeline into independent Encoder, Denoiser, and Decoder roles, each running on its own GPU(s). A central DiffusionServer routes requests through the pipeline.

Quick Start

Disaggregation is controlled by a single flag: --disagg-role. Each component is launched independently, just like LLM PD disaggregation.
—disagg-roleWhat it runs
monolithic(Default) Standard single-server mode
encoderAll stages with the default RoleType.ENCODER affinity: InputValidationStage, TextEncodingStage (plus ImageEncodingStage / ImageVAEEncodingStage for image-conditioned pipelines), LatentPreparationStage, TimestepPreparationStage, and any model-specific “before denoising” stage (e.g. QwenImageLayeredBeforeDenoisingStage, GlmImageBeforeDenoisingStage).
prompt_encoder / image_encoderSeparate text and reference-image encoding stages (currently FLUX.3 Image).
denoiserDenoisingStage (and its subclasses: CausalDMDDenoisingStage, DmdDenoisingStage, LTX2AVDenoisingStage, LTX2RefinementStage, Hunyuan3DShapeDenoisingStage, …) — the DiT forward loop plus the scheduler stepping it drives.
decoderDecodingStage (VAE decode) and its subclasses (LTX2AVDecodingStage, HeliosDecodingStage, …).
serverDiffusionServer head node + HTTP server (no GPU)
Each stage declares its role via the role_affinity property on PipelineStage (default ENCODER). Non-monolithic pipelines only instantiate matching stages. The combined encoder role also includes stages with PROMPT_ENCODER or IMAGE_ENCODER affinity.

Single-Machine Example (Verified)

The following commands have been tested end-to-end on an 8×H200 machine with Wan-AI/Wan2.1-T2V-1.3B-Diffusers. Each role runs on a separate GPU via --base-gpu-id; the server head node requires no GPU.
Tested result (8×H200): Encoder 2.3 s (TextEncoding) → Denoiser 312.8 s (50 steps, layerwise offload) → Decoder 7.1 s (VAE decode). Total ~322 s for 81-frame 1024×1024 video.
Tip: --base-gpu-id controls which physical GPU the role uses. Encoder and Decoder can share a GPU (e.g. both --base-gpu-id 0) to save resources, but make sure the combined GPU memory is sufficient.

Multi-Machine Example

The exact same CLI pattern — just replace 127.0.0.1 with actual IPs and add RDMA flags for direct transfer:
ZMQ handles startup order gracefully — instances and head can start in any order.

FLUX.3 split encoding

FLUX.3 Image supports both the three-role topology above and four independently deployable worker pools:
The prompt encoder loads only the text encoder. The image encoder loads only the VAE for reference-image encoding; the decoder has its own VAE instance. The denoiser loads only the DiT and creates noise from the request seed locally. T2I bypasses the image encoder and goes directly from prompt encoder to denoiser. Stages within one request run sequentially; different requests can occupy different worker pools concurrently. This is not a latency optimization for a single request: separation adds transfers and allows independent placement and scaling. The head still serves complete image-generation requests; this is not a standalone API for returning training embeddings or VAE latents. Use the same FLUX.3 package, variant, and numerical settings on every process. Each host currently needs a complete package on disk, even though a worker loads only its own components into memory. Follow the FLUX.3 cookbook for model access and licensing. For gated checkpoints, authenticate on each host with an authorized account using hf auth login, or provide an accessible local package path. Install the transfer dependencies described below on every worker. For long prompts, increase --disagg-transfer-pool-size on the encoding and denoising workers to cover their largest incoming/outgoing tensors, as well as reserved receive slots; for example, 1073741824 reserves a 1 GiB pool. For a Linux NVIDIA deployment with four GPUs, start the workers in one shell:
Then start the GPU-less head in another shell:
The existing image API is unchanged: use /v1/images/generations for T2I and multipart /v1/images/edits for references:
Replace reference.png with your input image. Omit size on edits to keep the reference dimensions, including the model’s padding/crop behavior. The head inlines locally uploaded reference files before dispatch, so GPU workers do not need access to the head’s upload directory. URL inputs must remain reachable from the image encoder. Specify both --prompt-encoder-urls and --image-encoder-urls, instead of --encoder-urls. Each accepts semicolon-separated worker endpoints, with the same dispatch policy as the existing pools. Alternatively, use the existing encoder role and --encoder-urls to colocate both encoders in one worker. Image encoder and decoder processes can also share a GPU if their combined weights, transfer pools, and concurrent activations fit. Across machines, replace loopback addresses with routable IPs. Advertise each worker’s IP with --disagg-p2p-hostname, and use that exact work endpoint in the head’s URL list. The head also listens on scheduler port +4 for prompt encoder results and +5 for image encoder results; +1/+2/+3 retain their existing meanings. Keep worker work/control ports (+1), internal scheduler ports, and distributed master ports distinct. Run the internal transport on a trusted private network, not a public interface. The BF16 flow-map package has been validated on H200 with same-node, multi-process Mooncake RDMA: concurrent T2I, reference-size-preserving i2i, and both three-role and four-role topologies matched the monolithic API’s output pixels with quality=exact. Cross-node execution has not been validated.

Multiple Instances per Role

Use semicolons in --*-urls to register multiple instances:

GLM-Image distributed mode

GLM-Image can batch AR generation in the head and dispatch the resulting prior tokens to distributed denoiser workers. Each worker runs prompt/glyph preparation, DiT, and VAE decoding locally; no latent or embedding tensors are transferred. The following 16-device deployment uses devices 0-1 for the external AR server and devices 2-15 for 14 independent batch-1 Cache-DiT denoisers.
Run the external AR server:
Run the public head. --encoder-urls and --decoder-urls are intentionally omitted for this topology.
Workers return decoded pixels to the head, which saves and serves the final files from --output-path. The PR benchmark used longtext-bench.zip:

Port Convention

Result endpoints are derived deterministically from the head node’s --scheduler-port (default: 5555):
SocketPort
DS frontend (ROUTER)scheduler_port
Encoder result (PULL)scheduler_port + 1
Denoiser result (PULL)scheduler_port + 2
Decoder result (PULL)scheduler_port + 3
Prompt encoder result (PULL, split encoding)scheduler_port + 4
Image encoder result (PULL, split encoding)scheduler_port + 5
Role instances derive their result endpoint automatically from --disagg-server-addr. No manual endpoint configuration needed.

Transfer Mechanism

Tensor data between adjacent roles is transferred via a P2P transfer engine. The DiffusionServer routes control messages and request metadata; tensors flow directly between workers. Uploaded reference files are forwarded inline as request metadata. mooncake-transfer-engine is required for disaggregated diffusion. It provides RDMA for direct GPU-to-GPU data movement. The GLM-Image distributed mode is an exception: it relays only prior token IDs and request metadata over ZMQ and does not require Mooncake.

Transfer Flow

  1. Sender stages output tensors in its transfer buffer, then notifies DiffusionServer.
  2. DiffusionServer selects a downstream worker and reserves one of its preallocated receive slots. If none fits, it sends transfer_alloc and waits for transfer_allocated.
  3. DiffusionServer sends transfer_push to the sender with the receiver’s session and destination address.
  4. Sender pushes the tensors through Mooncake and reports transfer_pushed after completion.
  5. DiffusionServer sends transfer_ready to the receiver and releases the sender’s scheduling slot.
  6. Receiver loads the tensors on its transfer stream and executes its stages. Receive prefetch can overlap the previous request’s compute.
Decoder results (final output) flow back through DiffusionServer as raw ZMQ frames to the HTTP client.

Failure Isolation

The head rejects malformed scheduler requests and duplicate active request IDs without replacing the original request. Each scheduler message carries one request; use concurrent API calls for independent requests, or n for multiple outputs from one request. Tensor decoding errors fail only the associated request when its ID can be recovered. Results without an identifiable request are logged and discarded; affected requests still rely on --disagg-timeout. Reported worker errors release their scheduling capacity and reserved receive slots so later requests can run. Keep scheduler and worker ports on a trusted private network. This error handling does not make the pickle-based scheduler protocol safe for untrusted clients, or provide durable jobs and transparent recovery after a head restart.

RDMA Flags

FlagDefaultDescription
—disagg-p2p-hostname127.0.0.1RDMA-reachable hostname/IP of this instance
—disagg-ib-deviceNoneInfiniBand device (e.g., mlx5_0, mlx5_roce0)
—disagg-transfer-pool-size256 MiBPinned memory pool per instance
Set --disagg-p2p-hostname to the actual IP on each machine. For multi-machine, --disagg-ib-device specifies the RDMA NIC.

Per-Role Parallelism

FlagDescription
—encoder-tpEncoder tensor parallelism
—denoiser-tp / —denoiser-sp / —denoiser-ulysses / —denoiser-ringDenoiser parallelism
—decoder-spDecoder sequence parallelism
Role-specific flags take precedence over explicitly set generic flags such as --tp-size and --sp-degree. Otherwise, parallelism is auto-derived for each role’s --num-gpus (or its GPU list in the Python pool API). Automatic CFG, sequence-parallel and HSDP choices from the parent configuration are not reused for a role with a different device count. Explicit incompatible settings still raise an error; they are not silently reduced to fit.

Other Options

FlagDefaultDescription
—disagg-timeout600Timeout (seconds) for pending requests
—disagg-dispatch-policyround_robinround_robin or max_free_slots

Python API

For programmatic single-machine deployment, launch_pool_disagg_server() is available. It spawns its workers, and each child re-executes this script’s module scope before initializing its platform, so keep the diffusion imports inside the guard:

Architecture

Request State Machine

Any state can transition to FAILED or TIMED_OUT.