Diffusion Continuous Batching¶
This document describes the unified diffusion execution architecture, including request execution, request-level batching, step execution, continuous batching, and output delivery. For user-facing configuration and CLI examples, see Diffusion Execution Modes.
Overview¶
DiffusionEngine selects an execution mode from step_execution and uses max_num_seqs as that mode's scheduler capacity:
| Configuration | Engine mode | Scheduler | Execution |
|---|---|---|---|
step_execution=False, max_num_seqs=1 | REQUEST_BATCH | RequestScheduler | One complete request-level forward() |
step_execution=False, max_num_seqs>1 | REQUEST_BATCH | RequestScheduler | One fused forward() over compatible requests |
step_execution=True, max_num_seqs=1 | STEP_BATCH | StepScheduler | One request advanced one denoise step per scheduler tick |
step_execution=True, max_num_seqs>1 | STEP_BATCH | StepScheduler | Compatible requests advanced together in step waves |
Serial request execution is handled inside REQUEST_BATCH; it is not a separate engine mode. Similarly, continuous batching is the multi-request configuration of STEP_BATCH, not an independent execution mode.
The engine performs the following initialization:
- Resolve model-specific pre- and post-processing hooks.
- Select
REQUEST_BATCHorSTEP_BATCH. - Construct the configured
DiffusionExecutor. - Construct
RequestSchedulerorStepScheduler, unless a scheduler was explicitly injected. - Bind
execute_batchorexecute_step.
DiffusionEngine.make_engine() owns startup warmup after engine construction. This keeps construction, backend selection, and warmup failure cleanup in one lifecycle.
Unified Output Stream¶
All asynchronous diffusion requests use the same per-request output stream, regardless of whether the caller exposes streaming results to the user.
When a request is admitted, the engine creates an asyncio.Queue[DiffusionOutput] keyed by its request ID. The busy loop sends both intermediate chunks and final results through that queue:
request
-> scheduler.add_request()
-> scheduler.schedule()
-> execute_batch() or execute_step()
-> scheduler.update_from_output()
-> per-request output queue
-> step_streaming()
Streaming callers forward every yielded output. Non-streaming callers consume the same generator to completion and return only its final output. This avoids maintaining separate future-based and queue-based result paths.
DiffusionEngine.step() and async_add_req_and_wait_for_response() remain deprecated compatibility wrappers. New integrations should consume step_streaming() or async_add_req_and_stream_response().
Completion and Cancellation¶
Output delivery and scheduler lifecycle are related but separate:
- A terminal scheduler result always finalizes request state.
- A disconnected or cancelled consumer removes only its delivery queue.
- The scheduler may still finish and clean up the request after its queue has been removed.
- Engine shutdown sends an error output to all remaining streams.
This separation prevents cancelled consumers from leaving active request IDs in the scheduler.
Request-Batch Execution¶
Request-batch execution runs the complete diffusion pipeline for each scheduler wave. Each OmniDiffusionRequest remains an independent logical request with its own prompt, sampling parameters, seed, request ID, output, error, and abort state.
With max_num_seqs=1, execute_batch() handles the serial request path. With a larger value, a pipeline can opt into fused execution by declaring:
Its forward() method must accept DiffusionRequestBatch and return one DiffusionOutput per request. The runner validates the result count and maps outputs back to their original request IDs with BatchRunnerOutput.
Current in-tree request-batch implementations include the Flux, LTX-2, SD3, and Qwen-Image pipelines. Treat the pipeline capability flag in the source as authoritative because support continues to expand.
Request-Batch Data Flow¶
DiffusionSchedulerOutputcarries scheduled request IDs and payloads.DiffusionRequestBatchprovides the pipeline-facing static batch.BatchRunnerOutputcarries per-request results back to the engine.
Request-local setup, such as seed handling and output/error mapping, remains per request. Cache refresh, homogeneous LoRA activation, and pipeline.forward(req_batch) can be shared by the batch.
Compatibility and Admission¶
RequestBatchSamplingParamsKey contains fields that affect the tensor contract, including shape, guidance, output count, and LoRA identity. Only compatible requests can share a fused forward call.
Admission is FIFO and conservative. An incompatible request at the front of the waiting queue can prevent later compatible requests from joining the current wave.
request_batch_max_wait_ms optionally delays the first schedule operation of a new wave so bursty arrivals can accumulate. It applies only when:
- the pipeline supports request batching;
- step execution is disabled;
- no requests are currently running; and
- the configured wait is greater than zero.
The wait ends when the queue reaches capacity, stabilizes briefly, reaches its deadline, or the engine stops.
Step-Batch Execution¶
Step execution exposes denoising progress to the scheduler. A supporting pipeline implements four stateful operations:
| Operation | Responsibility |
|---|---|
prepare_encode(state) | Validate input, encode prompts, initialize latents and timesteps, and create request-local scheduler state |
denoise_step(input_batch, *, states=...) | Run one denoise forward for the scheduler-provided request states |
step_scheduler(state, noise_pred) | Update latents and advance request progress |
post_decode(state) | Decode and postprocess a completed request or output boundary |
Persistent request state lives in StepRequestState. Pipeline-specific fields that do not belong in the shared contract should be stored in state.extra. Queueing and lifecycle metadata remain in the scheduler's request state.
Current native pipelines that explicitly enable step execution include Qwen-Image, HunyuanImage3, and Helios. Step execution alone does not imply continuous-batching support: Qwen-Image accepts batched step states. HunyuanImage3 accepts batched step states only when its resolved self-attention backend is TORCH_SDPA; otherwise it rejects groups larger than one request. Configure DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA or diffusion_attention_config.default.backend=TORCH_SDPA when max_num_seqs>1. See the HunyuanImage-3.0 recipe for its validated configuration. Helios supports only a single active step request and must use max_num_seqs=1.
Continuous Batching¶
When max_num_seqs>1, StepScheduler can keep multiple compatible requests active. The runner gathers their state into InputBatch, performs one batched denoise forward, then applies scheduler updates per request:
- Receive transferred KV payloads, then run
prepare_encode()for newly admitted requests. - Build or refresh
InputBatch. - Run one batched
denoise_step(input_batch, states=states). - Slice noise predictions back to each request.
- Run each request's
step_scheduler(). - Run
post_decode()at a chunk boundary or request completion. - Scatter updated latents back into persistent request state.
StepBatchSamplingParamsKey protects the batched tensor contract. Requests can have different total step counts and current step indices, but shape-sensitive, CFG-sensitive, and LoRA fields must be compatible. FIFO head-of-line blocking also applies here.
Chunk-capable pipelines may emit intermediate results. Final-only step pipelines emit when the request finishes even if streaming_output=True.
Model Author Guidelines¶
When converting a request-level pipeline:
- Reuse the same helpers as
forward()to avoid behavior drift. - Copy request-scoped scheduler state into
state.scheduler. - Advance
state.step_indexonly instep_scheduler(). - Keep model forward work in
denoise_step(). - Keep latent mutation in
step_scheduler(). - Keep final decoding equivalent to the tail of
forward(). - Store masks, condition latents, and other request-local tensors in the request state rather than on the shared pipeline.
Before setting supports_step_execution = True, validate output parity for the same seed and sampling parameters, scheduler isolation across concurrent requests, abort cleanup, progress reporting, and all supported CFG paths.
Before setting supports_request_batch = True, validate the batch input contract, one-output-per-request result shape, request identity and error mapping, seeded concurrency, LoRA compatibility, and tensor IPC.
Limitations¶
- Request batching requires an explicit pipeline capability when
max_num_seqs>1. - Step execution requires the four-method stateful pipeline contract.
- Both batching paths currently require homogeneous compatibility keys.
- FIFO scheduling can reduce batching opportunities.
request_batch_max_wait_mstrades first-request latency for burst coalescing.- All diffusion cache backends are currently unsupported in step mode.
- KV transfer is supported for newly admitted step requests; some other request-mode extras remain unsupported.
- Step continuous batching remains experimental; use
max_num_seqs=1for the conservative step path.
Related Implementation¶
- Engine:
vllm_omni/diffusion/diffusion_engine.py - Model contract:
vllm_omni/diffusion/models/interface.py - Scheduler interface:
vllm_omni/diffusion/sched/interface.py - Request scheduler:
vllm_omni/diffusion/sched/request_scheduler.py - Step scheduler:
vllm_omni/diffusion/sched/step_scheduler.py - Runner:
vllm_omni/diffusion/worker/diffusion_model_runner.py - Request batch:
vllm_omni/diffusion/worker/request_batch.py - Step input batch:
vllm_omni/diffusion/worker/input_batch.py