Skip to content

[Perf] Add profiling-based diffusion batching and keyed DRR scheduling - #3860

Closed
xuan58 wants to merge 1 commit into
vllm-project:mainfrom
xuan58:perf/diffusion-compute-budget-drr
Closed

xuan58 wants to merge 1 commit into
vllm-project:mainfrom
xuan58:perf/diffusion-compute-budget-drr

Conversation

@xuan58

@xuan58 xuan58 commented May 25, 2026

Copy link
Copy Markdown

Summary

This PR optimizes diffusion step-wise continuous batching for request-shape- and hardware-dependent workloads.

It adds:

  • A profiling-based diffusion batching policy that derives per-request-type batch caps from a profiled compute-unit budget.
  • A profiler tool that finds the 512x512 reference request sweet spot from denoise-step profiling logs.
  • Keyed DRR scheduling across SamplingParamsKey queues to reduce head-of-line blocking in mixed-resolution traffic.
  • Documentation for profiling a new setup, using the built-in Qwen-Image/Ascend 910B2 reference profile, and enabling DRR-related options.

Closes #3831.

Motivation

In vLLM-Omni 0.20.0, I validated the release version performance on a single Ascend 910B NPU with the Qwen/Qwen-Image model.

PR #2729 introduced step-wise continuous batching for diffusion models, and the PR benchmark showed performance gains on a single A100 GPU with max-num-seqs=8. However, when running the same configuration on a single Ascend 910B NPU, directly using max-num-seqs=8 does not bring a performance improvement. In my validation, it slightly regressed end-to-end benchmark duration and average latency.

This suggests that the optimal max-num-seqs for diffusion continuous batching is hardware- and request-shape-dependent. A fixed value such as 8 may work well on A100, but is not necessarily optimal on Ascend 910B.

Problems

I ran the text-to-image benchmark with:

  • vLLM-Omni version: 0.20.0
  • Backend: vllm-omni
  • Model: Qwen/Qwen-Image
  • Hardware: single Ascend 910B NPU
  • Dataset: vbench
  • Number of prompts: 100
  • Resolution: 1024x1024
  • Inference steps: 50
  • Max concurrency: 8

Benchmark command:

python benchmarks/diffusion/diffusion_benchmark_serving.py \
  --backend vllm-omni \
  --dataset vbench \
  --task t2i \
  --model Qwen/Qwen-Image \
  --port 8080 \
  --num-prompts 100 \
  --max-concurrency 8 \
  --width 1024 \
  --height 1024 \
  --num-inference-steps 50

Baseline: max-num-seqs=1

Server command:

python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen-Image \
  --omni \
  --port 8080 \
  --step-execution \
  --max-num-seqs 1

Result:

================= Serving Benchmark Result =================
Backend:                         vllm-omni
Model:                           Qwen/Qwen-Image
Dataset:                         vbench
Task:                            t2i
--------------------------------------------------
Benchmark duration (s):          2387.01
Request rate:                    inf
Max request concurrency:         8
Successful requests:             100/100
--------------------------------------------------
Request throughput (req/s):      0.04
Latency Mean (s):                184.2836
Latency Median (s):              190.7596
Latency P99 (s):                 192.1131
Latency P95 (s):                 191.8620

Continuous batching: max-num-seqs=8

Server command:

python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen-Image \
  --omni \
  --port 8080 \
  --step-execution \
  --max-num-seqs 8

Result:

================= Serving Benchmark Result =================
Backend:                         vllm-omni
Model:                           Qwen/Qwen-Image
Dataset:                         vbench
Task:                            t2i
--------------------------------------------------
Benchmark duration (s):          2444.79
Request rate:                    inf
Max request concurrency:         8
Successful requests:             100/100
--------------------------------------------------
Request throughput (req/s):      0.04
Latency Mean (s):                191.9775
Latency Median (s):              197.1824
Latency P99 (s):                 197.8310
Latency P95 (s):                 197.7747

Compared with max-num-seqs=1, max-num-seqs=8 regressed benchmark duration by about 2.42% and average latency by about 4.17%.

This suggests that a single fixed max-num-seqs is not robust across hardware and request shapes. A good batch size for A100 is not necessarily a good batch size for Ascend 910B, and a good batch size for 512x512 requests is not necessarily a good batch size for 1024x1024 requests.

Profiling-Based Batch Size

This PR adds a small policy layer for diffusion continuous batching:

  • fixed: preserves the existing fixed max_num_seqs behavior.
  • profile: ramps the effective batch cap and logs [DiffusionBatchProfiler] records.
  • compute_budget: derives the effective homogeneous batch cap from a profiled compute-unit budget.

The profiling workflow is: first profile a reference request shape, find the batch-size sweet spot from execution-time behavior, then use that profiled parameter for normal serving.

For diffusion, the reference shape is 512x512. The profiler records:

batch_size
request_compute_units
total_compute_units
denoise_step_time

benchmarks/diffusion/diffusion_batching_profiler.py parses these logs, groups 512x512 records by batch size, filters outliers with IQR, uses median denoise-step time for each batch size, and applies a two-segment linear fit to locate the 512x512 sweet-spot batch size.

The generated policy parameter is:

compute_unit_budget = reference_compute_units * sweet_spot_batch_size

During serving, the scheduler estimates each request type by visual compute units:

request_compute_units ~= latent_h * latent_w * latent_frames * cfg_multiplier

Then it derives:

effective_max_num_seqs = ceil(compute_unit_budget / request_compute_units)

max_num_seqs remains a hard upper bound.

For the built-in Qwen-Image reference profile on one Ascend 910B2 NPU:

512x512 sweet-spot batch size = 10
compute_unit_budget = (512 / 16) * (512 / 16) * 10 = 10240
1024x1024 effective batch size = ceil(10240 / ((1024 / 16) * (1024 / 16))) = 3

The built-in 10240 profile should only be used when the model, backend, hardware, and reference profiling shape match this setup. Other environments should run the profiler first and pass the generated compute_unit_budget.

Example profiling run:

python3 -m vllm_omni.entrypoints.cli.main serve /data/Qwen-Image \
  --omni \
  --step-execution \
  --max-num-seqs 128 \
  --diffusion-batching-policy profile

Generate the serving config:

python3 benchmarks/diffusion/diffusion_batching_profiler.py \
  --log-file /path/to/server.log \
  --output-json /tmp/diffusion_batching_config.json

Example serving command on Ascend NPU:

COMPUTE_UNIT_BUDGET=<generated_compute_unit_budget>

python3 -m vllm_omni.entrypoints.cli.main serve /data/Qwen-Image \
  --served-model-name Qwen/Qwen-Image \
  --omni \
  --port 8081 \
  --step-execution \
  --max-num-seqs 128 \
  --diffusion-batching-policy compute_budget \
  --diffusion-batching-config "{\"compute_unit_budget\":${COMPUTE_UNIT_BUDGET},\"drr_arrival_window_size\":256,\"log_stats\":true}" \
  --vae-use-tiling \
  --vae-use-slicing

drr_arrival_window_size and log_stats are optional. The former controls the sliding window used to estimate request arrival probabilities for DRR; the latter only enables verbose batching-policy logs.

Keyed DRR Scheduling

Step-wise continuous batching still requires requests in one running batch to share the same SamplingParamsKey. With a single FIFO waiting queue, a high-cost or incompatible request at the head can block later lower-cost requests, even when those later requests could form an efficient homogeneous batch.

This PR changes the waiting side of the scheduler to keep per-key queues:

SamplingParamsKey -> deque[request]

The scheduler then selects the next homogeneous queue with a cost-aware DRR policy:

  • cost_i is estimated from the request shape as visual compute units.
  • prob_i is estimated online from a sliding arrival window.
  • Non-empty queues accrue normalized budget proportional to prob_i / cost_i.
  • The first selection starts from the lowest-cost queue.
  • Later selections scan queues in ascending cost order from the DRR cursor and skip queues whose budget is insufficient.

After a key is selected, the existing homogeneous batching rule is preserved: only requests with the same SamplingParamsKey can join the running batch. The compute_budget policy then caps the batch size for that selected request type.

Performance

I evaluated a mixed-resolution Qwen-Image workload on one Ascend 910B NPU. Requests arrive at high concurrency with a 50/50 mix of 512x512 and 1024x1024 requests, each using 50 inference steps.

Benchmark command:

python benchmarks/diffusion/diffusion_benchmark_serving.py \
  --backend vllm-omni \
  --dataset random \
  --task t2i \
  --model Qwen/Qwen-Image \
  --port 8080 \
  --num-prompts 50 \
  --max-concurrency 100 \
  --random-request-config '[
    {"width":512, "height":512, "num_inference_steps":50, "weight":0.5},
    {"width":1024, "height":1024, "num_inference_steps":50, "weight":0.5}
  ]'

Fixed continuous batching, max-num-seqs=8:

Benchmark duration (s):          830.84
Request throughput (req/s):      0.06
Latency Mean (s):                468.4024
Latency Median (s):              473.7393
Latency P99 (s):                 819.0828
Latency P95 (s):                 806.8418
Successful requests:             50/50

Profiled compute-budget batching plus keyed DRR scheduling:

Benchmark duration (s):          679.94
Request throughput (req/s):      0.07
Latency Mean (s):                475.3142
Latency Median (s):              578.9919
Latency P99 (s):                 679.9409
Latency P95 (s):                 679.9369
Successful requests:             50/50

The optimized strategy improves benchmark duration from 830.84s to 679.94s, which is about an 18.16% speedup.

Documentation

This PR adds docs/user_guide/diffusion/compute_budget_batching.md, which documents:

  • How to profile a new device or model with 512x512 reference requests.
  • How the profiler derives compute_unit_budget.
  • When the built-in Qwen-Image/Ascend 910B2 reference profile is valid.
  • How keyed DRR handles mixed request types.
  • The Ascend 910B performance result.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds configurable diffusion continuous-batching support (compute-budget + profiling) and improves mixed-shape scheduling fairness by grouping waiting requests by sampling key.

Changes:

  • Add CLI flags for diffusion batching config/policy and plumb them into engine/stage args.
  • Introduce diffusion batching policy plugins (fixed/compute_budget/profile) plus profiler tooling + docs.
  • Rework diffusion scheduler waiting-queue handling using keyed queues + cost-aware DRR selection; expand tests accordingly.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
vllm_omni/entrypoints/cli/serve.py Adds CLI flags to configure diffusion batching policy and JSON config.
vllm_omni/engine/async_omni_engine.py Passes diffusion batching config/policy into default diffusion stage args.
vllm_omni/diffusion/worker/diffusion_model_runner.py Emits profiler log lines for denoise-step timing under profiler policy.
vllm_omni/diffusion/sched/batching_policy.py New batching policy abstractions + implementations (fixed/compute budget/profiler).
vllm_omni/diffusion/sched/base_scheduler.py Implements keyed waiting queues and DRR-based key selection; integrates batching policy cap.
vllm_omni/diffusion/data.py Adds DiffusionBatchingConfig and wires it into OmniDiffusionConfig.
tests/diffusion/test_diffusion_scheduler.py Updates/extends scheduler tests for keyed DRR + compute-budget/profile policies.
docs/user_guide/diffusion/compute_budget_batching.md New user guide for compute-budget batching, profiling workflow, and mixed-shape scheduling.
benchmarks/diffusion/diffusion_batching_profiler.py New script to derive compute-unit budgets from profiler logs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

devices = ",".join(str(i) for i in range(num_devices))
model_class_name = kwargs.get("model_class_name", None)
final_output_type = "audio" if model_class_name and supports_audio_output(model_class_name) else "image"
diffusion_batching_config = dict(kwargs.get("diffusion_batching_config") or {})
Comment on lines +318 to +322
key = self._select_next_waiting_key()
if key is None and key not in self._waiting_by_key:
return None
return self._peek_waiting_state_for_key(key)

Comment on lines +383 to +385
# All non-empty queues were visited but did not have enough budget.
self._drr_cursor_index = start
return None
Comment on lines +323 to +332
def _peek_waiting_state_for_key(self, key: SamplingParamsKey | None) -> DiffusionRequestState | None:
queue = self._waiting_by_key.get(key)
while queue:
state = self._request_states.get(queue[0])
if state is not None and not state.is_finished():
return state
queue.popleft()
if key in self._waiting_by_key:
self._waiting_by_key.pop(key, None)
return None
Comment on lines +323 to +348
def _peek_waiting_state_for_key(self, key: SamplingParamsKey | None) -> DiffusionRequestState | None:
queue = self._waiting_by_key.get(key)
while queue:
state = self._request_states.get(queue[0])
if state is not None and not state.is_finished():
return state
queue.popleft()
if key in self._waiting_by_key:
self._waiting_by_key.pop(key, None)
return None

def _pop_waiting_state(self, state: DiffusionRequestState) -> None:
self._remove_waiting_id(state.sched_req_id)

def _remove_waiting_id(self, sched_req_id: str) -> None:
self._waiting = deque(req_id for req_id in self._waiting if req_id != sched_req_id)
state = self._request_states.get(sched_req_id)
if state is None:
return
key = state.sampling_params_key
queue = self._waiting_by_key.get(key)
if queue is None:
return
self._waiting_by_key[key] = deque(req_id for req_id in queue if req_id != sched_req_id)
if not self._waiting_by_key[key]:
self._waiting_by_key.pop(key, None)
Signed-off-by: xuan58 <xuanai2-c@my.cityu.edu.hk>
@xuan58
xuan58 force-pushed the perf/diffusion-compute-budget-drr branch from 2f0cf1b to 48115f3 Compare May 26, 2026 06:12
@hsliuustc0106

Copy link
Copy Markdown
Collaborator

fix conflicts

@asukaqaq-s

asukaqaq-s commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

Hello, thank you for your contribution. We are going to remove most fields from SamplingParamsKey. You may need to adapt the changes for #3846
The related draft will be released this week.

@yJader

yJader commented May 27, 2026

Copy link
Copy Markdown
Contributor

request_compute_units ~= latent_h * latent_w * latent_frames * cfg_multiplier

The current request_compute_units is linear in the number of latent visual tokens. This is a useful proxy, but DiT step latency usually contains both linear terms from MLP/projections and quadratic terms from attention.
Therefore, extrapolating a 512×512 profiling budget to larger resolutions may underestimate the cost of high-resolution requests and produce an overly optimistic batch cap.
It may be worth either fitting a mixed aN^2 + bN cost model or profiling multiple reference resolutions to calibrate the scaling.

@asukaqaq-s

Copy link
Copy Markdown
Contributor

Hello, thank you for your contribution. We are going to remove most fields from SamplingParamsKey. You may need to adapt the changes for #3846 The related draft will be released this week.

#4024

@hsliuustc0106 hsliuustc0106 added diffusion codes related to diffusion models enhancement New feature or request labels Jul 17, 2026 — with ChatGPT Codex Connector
@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot triage note

Automated triage of commit 48115f3aec8b produced:

  • Priority: high. Prompt maintainer attention is suggested.

These are automated triage suggestions only — the final decision belongs to the maintainers.

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot: three questions on the performance claim

@xuan58 this PR reads as a performance or value claim:

  • claim: The optimized strategy improves benchmark duration from 830.84s to 679.94s, which is about an 18.16% speedup.

Before the full evidence checklist, three short questions:

  1. Bottleneck — what is the current bottleneck, and which profile, trace or per-stage measurement shows it?
  2. Value — what does the change buy the user or the system (latency, throughput, memory, cost), and at which workload?
  3. A/B or ablation — is there a same-workload, same-head/config comparison that isolates each main claim on its own? For stacked optimizations, one number per item rather than a blended delta.

When you answer, the evidence that settles it is: base and head SHA, hardware, model, workload, warm-up and repeat count, mean or percentiles with their spread, and a correctness/quality-equivalence signal; an end-to-end claim also needs stage attribution.

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot: no human activity for 15 days

@xuan58 this pull request has had no human commit, comment or review since 2026-08-31. Per repository policy it may be closed if it stays inactive.

To keep it moving, any one of these is enough: push an update, reply to the open blocker, or post the current plan and timeline.

@vllm-omni-review-bot

Copy link
Copy Markdown

Omni ReviewBot: no human activity for 23 days

@xuan58 this pull request has had no human commit, comment or review since 2026-08-31. Please consider marking this PR as draft until work can resume. The author or a maintainer decides whether to change the PR state.

To keep it moving, any one of these is enough: push an update, reply to the open blocker, or post the current plan and timeline.

@hsliuustc0106 hsliuustc0106 added the stale No new commits for at least 60 days label Sep 30, 2026 — with ChatGPT Codex Connector
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diffusion codes related to diffusion models enhancement New feature or request stale No new commits for at least 60 days

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Performance]: Optimize continuous batching max-num-seqs and scheduling for Qwen-Image on Ascend 910B

6 participants