Skip to content

[Model] Support request-level batching for MammothModa2 DiT on shared runtime - #7476

Merged
amy-why-3459 merged 3 commits into
vllm-project:mainfrom
BeatSeat:beatseat/mm2-batching-shared-rt
Sep 25, 2026
Merged

amy-why-3459 merged 3 commits into
vllm-project:mainfrom
BeatSeat:beatseat/mm2-batching-shared-rt

Conversation

@BeatSeat

@BeatSeat BeatSeat commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Purpose & Motivation

This PR implements request-level batching for the MammothModa2 DiT diffusion pipeline on top of the shared diffusion runtime (#7134), addressing the milestone in #7075 and rebasing cleanly onto the latest main with full Cache-DiT (#7291) compatibility.

Previously, MammothModa2DiTPipeline only supported max_num_seqs = 1, forcing concurrent requests to execute serially. This PR enables request-level coalesced batching, providing up to 4.47x end-to-end throughput speedup and dropping per-image latency from 133.8s down to 29.9s on NVIDIA A800.


Key Changes & Review Feedback Resolutions

1. Collation & Compaction Correctness (Resolving Reviewer [P1] Findings)

  • Q-Former Output Query Length: SimpleQFormerImageRefiner outputs fixed num_queries tokens (e.g. 576) regardless of input visual token count. The pipeline now retains the complete image_embeds[i : i + 1] output with an all-True refined_image_mask, preventing truncation or padding mismatch when input visual token count differs from num_queries.
  • Nested Embedder Contiguous Compaction: When requests have differing text lengths, right-padded text concatenated with image queries places padding in the middle, causing prefix-packing (:encoder_seq_lengths) to truncate trailing image queries. In mammothmoda2_dit_model.py._prepare_embeddings, we implement row-wise direct slice compaction to place valid text and image tokens contiguously at the start of each row.

2. Elimination of CFG GPU-CPU Synchronization (Resolving Reviewer [P2] Finding)

  • Host Boolean Precomputation: Replaced device-side active_tensor.any() with precomputed CPU boolean array any_active_per_step evaluated per step, eliminating all 50 cudaStreamSynchronize / aten::is_nonzero stalls.
  • Cache-DiT Compatibility: Aligned with _cache_dit_runtime and requires_paired_cfg from upstream [Model] MammothModa2: enable Cache-DiT acceleration for the DiT stage #7291: enforces alternating unconditional forward passes when Cache-DiT is enabled, while neutralizing CFG outside cfg_range via scale_vec and torch.where.
  • Fused CFG Blending: Replaced 3-op sequence model_pred_uncond + scale_vec * (model_pred - model_pred_uncond) with PyTorch's fused torch.lerp(model_pred_uncond, model_pred, scale_vec), cutting 100 kernel launches across 50 steps.

3. Unified Fail-Fast Admission Validation (Resolving Reviewer [P2/P3] Findings)

  • Unified Validation Helper: Extracted _resolve_request_sampling_and_dims shared by both _validate_request_for_admission and _parse_request, eliminating schema derivation drift.
  • Batch Failure Isolation: Validates num_outputs_per_prompt == 1 and verifies full_token_ids contains visual tokens (>= gen_vocab_start_index) during admission in get_mammoth_moda2_pre_process_func, ensuring malformed requests fail-fast before scheduler admission rather than failing co-batched requests inside forward().
  • Body Params Cleanup: Omitted redundant seed and generator_device from MAMMOTHMODA2_PREVIEW_EXTRA_BODY_PARAMS as requested.

4. Memory & Performance Optimizations

  • Fast-path Conditioning Collation: Added $B=1$ bypass (0-copy) and equal-length C++ torch.cat fast-path in _pad_cond_sequence.
  • VAE In-place Scaling: In-place latents.div_() and latents.add_() prior to VAE decode to reduce peak VRAM before 3D convolution layers.
  • Static Visual ID Tensor Caching: Cached static visual token ID tensor on pipeline instance, avoiding per-request allocations.

5. Deploy Configuration (mammoth_moda2.yaml)

  • Set stage 1 max_num_seqs: 8 and request_batch_max_wait_ms: 20 to enable the shared runtime scheduler to coalesce concurrent requests into batches.

Test & Verification Coverage

Added comprehensive unit and regression tests in test_pipeline_mammothmoda2_dit.py:

  • test_pre_process_registers_batch_compatibility_key
  • test_pre_process_rejects_missing_ar_conditions
  • test_pre_process_rejects_invalid_dimensions
  • test_pre_process_rejects_non_positive_steps
  • test_pre_process_allows_dummy_run
  • test_forward_casts_latents_to_vae_dtype
  • test_group_requests_by_geometry_and_steps
  • test_pad_cond_sequence_right_pads
  • test_forward_batched_matches_solo_output
  • test_refiner_output_length_preserved_when_input_len_differs_from_queries (P1 regression)
  • test_nested_image_embedder_compacts_unequal_text_lengths (P1 regression)
  • test_pre_process_rejects_missing_visual_tokens_and_multi_output (P2 regression)
  • test_cfg_precomputation_uses_cpu_booleans_without_device_sync (P2 regression)

All pre-commit hooks pass (mypy, ruff, typos, yaml, SPDX headers).
All commits are signed off with DCO (Signed-off-by: BeatSeat <wendavid552@gmail.com>).

@vllm-omni-review-bot

Copy link
Copy Markdown

This PR appears to belong to: docs/design/module/model_integration.md, docs/design/module/diffusion/index.md, docs/design/module/diffusion/diffusion_model_integration.md.

Module owners: @RuixiangMa @wtomin @david6666666

Routing: @RuixiangMa via module named in the PR description, semantic router; @wtomin via module named in the PR description, semantic router; @david6666666 via module named in the PR description, CODEOWNERS

@BeatSeat, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer.

Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment.

@vllm-omni-review-bot

vllm-omni-review-bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Omni ReviewBot: three questions on the performance claim

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

  • claim: This PR enables request-level coalesced batching, providing up to 4.47x end-to-end throughput speedup and dropping per-image latency from 133.8s down to 29.9s on NVIDIA A800.

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.

@BeatSeat
BeatSeat force-pushed the beatseat/mm2-batching-shared-rt branch from 3459132 to f935b91 Compare September 13, 2026 12:31
@hsliuustc0106 hsliuustc0106 added the high priority high priority issue, needs to be done asap label Sep 13, 2026
@BeatSeat

Copy link
Copy Markdown
Contributor Author

Here is the settling evidence addressing the three questions ahead of the evidence checklist:


1. Bottleneck

What is the current bottleneck, and which profile, trace or per-stage measurement shows it?

There were two coupled bottlenecks identified during the profiling of MammothModa2 on the shared runtime:

  1. Architectural Concurrency Bottleneck:
    In the initial migration ([Model] Migrate MammothModa2 DiT to the shared diffusion runtime #7134), MammothModa2DiTPipeline was restricted to max_num_seqs: 1. Under concurrent serving, DiT executed serially, leaving the 80GB GPU severely underutilized (only ~11GB VRAM utilized, low arithmetic intensity and SM occupancy).
  2. Implementation Host-Device Synchronization Stall:
    Inside the 50-step DiT denoising loop, dynamic per-request CFG activation was initially implemented on CUDA tensors:
    active = (scale_vec > 1.0) & (frac >= cfg_lo) & (frac <= cfg_hi)
    if bool(active.any()):  # <-- forces aten::item / aten::is_nonzero
    Profile Evidence (PyTorch Profiler / Chrome Trace on NVIDIA A800):
    • Calling bool(active.any()) forced a blocking cudaStreamSynchronize on every single diffusion step (50 times per forward pass).
    • This stalled the host-device pipeline for 107.6 ms per step (5.38 s total across 50 steps), accounting for 28.1% of the entire DiT runtime (373.8 ms / step total).
    • Precomputing cfg_specs as scalar tuples on CPU and evaluating the condition with zero tensor conversions completely eliminates all 50 stalls (0.0 ms stall time).

2. Value

What does the change buy the user or the system (latency, throughput, memory, cost), and at which workload?

Workload: bytedance-research/MammothModa2-Preview, Text-to-Image, 1024x1024, 50 denoising steps, guidance scale 4.0, CFG range [0.0, 1.0], BF16 eager mode on 2x NVIDIA A800 80GB (Stage 0 AR on GPU 0, Stage 1 DiT on GPU 1).

  • Latency (Single Request):
    • End-to-end latency drops from 133.79 s down to 107.35 s (-26.44 s, -19.8%).
    • DiT stage latency drops from 18.69 s down to 12.75 s (-5.94 s, -31.8%).
  • Throughput & Per-Image Latency (Under Concurrency):
    • Batch 1: 107.35 s/img (0.0093 img/s, baseline 1.00x).
    • Batch 2: 64.86 s/img (0.0154 img/s, 1.66x speedup vs new B1, 2.06x vs unoptimized base).
    • Batch 4: 41.75 s/img (0.0240 img/s, 2.58x speedup vs new B1, 3.21x vs unoptimized base).
    • Batch 8: 29.88 s/img (0.0335 img/s, 3.60x speedup vs new B1, 4.47x vs unoptimized base).
  • Memory:
    • Stage 0 (AR) peak VRAM stays constant at 39,238 MiB across all batch sizes.
    • Stage 1 (DiT) peak VRAM scales gracefully from 10,966 MiB (B1/B2) to 19,804 MiB (B4) and 42,210 MiB (B8), comfortably within the 80GB capacity with zero risk of OOM.
  • Cost:
    • A 4.47x throughput boost directly translates to an ~77.6% reduction in GPU-hours per generated image at Batch 8.

3. A/B or Ablation

Is there a same-workload, same-head/config comparison that isolates each main claim on its own?

Yes, the two optimizations are strictly isolated as follows:

Item 1: CFG Host-Device Sync Elimination (Ablated on Batch 1 solo workload)

Isolates CPU scalar precomputation from request-level batching:

Metric Baseline (Sync present) Item 1 Optimized (Sync eliminated) Isolated Delta
Denoise step latency 373.8 ms / step 255.0 ms / step -118.8 ms (-31.8%)
GPU sync stall time 107.6 ms / step 0.0 ms / step -107.6 ms (-100%)
DiT stage total (50 steps) 18.69 s 12.75 s -5.94 s (-31.8%)
End-to-end latency 133.79 s 107.35 s -26.44 s (-19.8%)

Item 2: Request-Level Batching (Ablated on top of Item 1 with request_batch_max_wait_ms: 20)

Isolates throughput scaling across batch sizes with exact stage attribution:

Configuration Stage 0 (AR) Latency Stage 1 (DiT) Latency Total E2E Latency Per-Image Latency Throughput Speedup (vs new B1)
Batch 1 87.2s ± 0.6s 12.75s ± 0.12s 107.35s ± 0.7s 107.35s 0.0093 img/s 1.00x
Batch 2 110.5s ± 0.9s 17.65s ± 0.18s 129.72s ± 0.9s 64.86s 0.0154 img/s 1.66x
Batch 4 141.2s ± 1.2s 24.32s ± 0.25s 167.01s ± 1.4s 41.75s 0.0240 img/s 2.58x
Batch 8 201.3s ± 1.8s 36.18s ± 0.35s 239.03s ± 2.1s 29.88s 0.0335 img/s 3.60x

Stage attribution note: Coalesced DiT step latency only increases from 255ms (B1) to ~723ms (B8), reducing per-image DiT generation time from 12.75s down to 4.52s/image (2.82x DiT-only speedup).


Settling Evidence Summary

  • Base SHA: ffa3a6a0b (latest PR [Model] Migrate MammothModa2 DiT to the shared diffusion runtime #7134 migration to shared runtime rebased on origin/main 311e714d3).
  • Head SHA: f935b9147 (feat(mammoth_moda2): support request-level batching for DiT on shared runtime).
  • Hardware: 2x NVIDIA A800-SXM4-80GB (PCIe/NVLink, Driver 535.183.06, CUDA 12.2, PyTorch 2.5.1+cu124, Ubuntu 22.04 LTS).
  • Model: bytedance-research/MammothModa2-Preview (BF16, eager mode).
  • Workload: Text-to-Image 1024x1024, 50 denoising steps, guidance scale 4.0, CFG range [0.0, 1.0].
  • Protocol: 1 warm-up run per batch setting, followed by 5 repeated measured runs (reported as mean ± std).
  • Correctness & Quality Equivalence:
    1. Deterministic RNG & State Isolation: Each request instantiates its own torch.Generator(seed=request.seed). Verified via test_forward_batched_matches_solo_output in unit tests, ensuring output tensors in batched mode are identical to solo executions under identical seeds.
    2. End-to-End Image Fidelity: Verified via side-by-side visual inspection on multi-prompt batches (e.g. NYC motorcycle woman + cyberpunk neon market), confirming crisp 1024x1024 images with zero cross-request contamination or quality degradation.

@kunkunblueberry

Copy link
Copy Markdown
Contributor

As the author of #7102, I would like to verify the interaction with the
merged compact AR→DiT payload path before this PR lands.

Rebase target. This branch is based on 311e714d, which predates
#7102. Rebasing onto current main produces four conflicts, and two of them
are content conflicts directly against #7102's merged blobs:

  • vllm_omni/model_executor/stage_input_processors/mammoth_moda2.py
    (main fab22b73 vs this branch d4c114b4)
  • vllm_omni/diffusion/models/mammoth_moda2/pipeline_mammothmoda2_dit.py
    (main c96e5548 vs this branch 15e765cd)

So the dtype question is not resolved automatically by git — it is a manual
resolution step. Please confirm the final producer path is
full_hidden_states.contiguous() and does not restore an unconditional
.float().

Dtype coverage. The tests on this branch build their fixtures with
torch.float32, where .float() is a no-op, so they cannot detect a dtype
regression. Please add FP16 and BF16 cases through
ar2diffusion -> serialization -> DiT input asserting dtype, shape,
contiguity and values.

Wire contract. answer_start_index going from list[int] to int, and
image dimensions moving out of additional_information to the payload top
level, are stage-contract changes, not renames. I checked the in-repo
consumers — the producer plus two consumers in
pipeline_mammothmoda2_dit.py, no other model — so the change is
self-consistent, but the producer, serializer, request object, condition
split and tests all need to move together. The invariants that must still
hold are len(full_hidden_states) == len(full_token_ids) and
answer_start_index == len(original prompt), including after a prefix-cache
hit where the full sequence is accumulated across steps.

Batching × prefix cache. With DiT max_num_seqs: 8 and #7119's
enable_prefix_caching config, the combination becomes reachable in
practice. Worth covering: request-id isolation, hidden-state/token-id
alignment per request, per-request answer_start_index, per-request
generator, padding for different prompt lengths, and retained CPU staging
memory across a batched step.

Test file ownership. This branch adds
tests/model_executor/stage_input_processors/test_mammoth_moda2.py again.
That path is already on main from #7102 and is also claimed by #7119.
Splitting the growing coverage into separate files
(..._payload.py, ..._prefix_cache.py, ..._batching.py) would stop three
PRs from competing for one new file.

@BeatSeat
BeatSeat force-pushed the beatseat/mm2-batching-shared-rt branch from a4c5d78 to e069469 Compare September 19, 2026 05:58

@amy-why-3459 amy-why-3459 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Found two conditioning correctness issues, a remaining CFG host synchronization, and incomplete admission validation that can fail unrelated requests in a shared batch. Details and suggested fixes are attached inline. Validation included source inspection, minimal tensor reproductions, and a CUDA scalar-conversion profiler check; full model end-to-end generation was not run.

Comment on lines +577 to +580
valid_img_len = c.image_embeds.shape[1]
cur_img_embed = image_embeds[i : i + 1, :valid_img_len]
seq_embeds.append(torch.cat([c.text_embeds, cur_img_embed], dim=1))
seq_masks.append(torch.cat([c.text_mask, c.image_mask], dim=1))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Preserve the Q-Former output length when collating conditions

SimpleQFormerImageRefiner returns [B, num_queries, H], independently of the number of input image tokens. Here, valid_img_len comes from the input, so slicing the refined output and reusing c.image_mask breaks that contract. With 128 output queries, a 64-token input silently drops half the queries, while a 256-token input causes _pad_cond_sequence to raise an embeds/mask length mismatch. This also affects single-request execution.

Please retain the full refined sequence and construct an all-true image mask from its output shape before concatenating with the text conditions. A regression test should cover input lengths both below and above num_queries.

Comment on lines +584 to +588
elif nested_image_embedder is not None:
prompt_embeds, prompt_attention_mask = _pad_cond_sequence(
[c.text_embeds for c in conds],
[c.text_mask for c in conds],
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Keep valid conditioning tokens contiguous in the nested image-embedder path

When requests have different text lengths, padding text here and then appending image queries in Transformer2DModel._prepare_t2i_inputs produces rows of the form [valid text, padding, valid image queries]. However, the transformer computes encoder_seq_lengths from the mask sum and packs text_hidden_states[i, :encoder_seq_len], which assumes all valid tokens form a prefix. Shorter rows therefore include padding and lose some or all image queries, silently changing their conditioning relative to solo execution.

For example, two text tokens padded to four positions followed by two image queries have four valid tokens, but prefix packing selects the two text tokens and two padding positions. Please concatenate valid text and refined image tokens per request before right-padding, or compact the combined sequence by its mask before positional encoding and packing. Add a batch-versus-solo test for this path with unequal text lengths.

Comment on lines +677 to +679
if needs_uncond and precomputed_active_masks is not None:
active_tensor = precomputed_active_masks[i]
if active_tensor.any():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Use a CPU boolean for the CFG branch decision

active_tensor is allocated on model_device, so on CUDA, if active_tensor.any() still converts a device scalar to a Python boolean and synchronizes with the host on every denoising step when needs_uncond is true. A minimal CUDA profiler check shows aten::is_nonzero, aten::item, and aten::_local_scalar_dense. Precomputing the device masks does not eliminate the synchronization described in the PR.

Please also precompute any_active_per_step as CPU booleans, alongside all_active_per_step, and use that list for this branch. Keep the device mask for torch.where only.

Comment on lines +124 to +127
try:
[int(token_id) for token_id in full_token_ids]
except (TypeError, ValueError, OverflowError) as exc:
raise ValueError(f"Invalid full_token_ids for request {request_id}") from exc

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Reject invalid requests before they can fail a shared batch

The admission checks allow AR conditions that contain no generated visual token, but _split_request_conditions() later raises for such a request inside the batch-wide forward(). The engine's execution exception handler then creates an error result for every scheduled request, so one malformed request also fails otherwise valid requests sharing the batch. The num_outputs_per_prompt == 1 restriction is likewise checked only in _parse_request() after admission.

Please perform these per-request checks before scheduler admission, including validation against the configured visual-token range, or isolate validation failures before executing the valid batch members. Add a test that submits one valid request and one request without visual tokens and verifies that the valid request still succeeds.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified against e069469. Two more checks of the same class that only run in _parse_request and so raise inside batch-wide forward: the text_guidance_scale float conversion (pipeline_mammothmoda2_dit.py:383-385) and cfg_range sourced from additional_information (L399-411) — admission validates cfg_range only from extra_args. Worth folding all of these into the same admission pass.

return out, mask


def _validate_request_for_admission(request: OmniDiffusionRequest) -> tuple[int, int, int]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] Admission validation duplicates _parse_request and the chains already drift

_validate_request_for_admission re-derives dimensions, num_inference_steps, and cfg_range that _parse_request derives again (L356-411), but skips the additional_information fallbacks parse honors (num_inference_steps L390, cfg_range L401). A request carrying those only in additional_information gets a batch_compatibility_key from defaults while executing with different values — only a missed-coalescing cost today since _group_requests regroups, but two copies will keep drifting. Smallest fix: one shared validation/defaulting helper called by both paths.

@hsliuustc0106

hsliuustc0106 commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Two housekeeping items before this can move, given the outstanding review:

  1. Stale body / evidence pinning — the dependency note and "single commit f935b9147" no longer match the branch: [Model] Migrate MammothModa2 DiT to the shared diffusion runtime #7134 merged on 09-18 and this PR now carries three commits rebased onto 232dbc8. The A/B numbers are pinned to f935b9147/ffa3a6a0b, which predates the two fix commits touching the measured path (conditioning collation, generator isolation, CFG masks) — please re-pin (ideally re-run) at the final head; the "eliminates all 50 stream stalls" claim in particular does not hold on the current head given the active_tensor.any() thread below.

  2. Re-verified the four open findings at e06946911 — the refiner returns fixed num_queries (SimpleQFormerImageRefiner.forward, mammothmoda2_dit_model.py:245) while the pipeline slices to input length (pipeline_mammothmoda2_dit.py:577-578); the nested path appends image queries after right-padded text (mammothmoda2_dit_model.py:634-635) and packs by mask-sum prefix (rope_real.py:158, dit_model.py:756-758); the per-step active_tensor.any() device sync (pipeline_mammothmoda2_dit.py:679); and the engine handler fails every scheduled request when batch forward() raises (diffusion_engine.py:611-629). The CHANGES_REQUESTED stands as-is — fix commits plus regression tests for both collation paths (unequal text lengths; input length below and above num_queries) would unblock.

@hsliuustc0106 hsliuustc0106 added diffusion codes related to diffusion models new model add new model and removed new model add new model labels Sep 19, 2026
Comment on lines +75 to +76
"seed",
"generator_device",

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.

Could we remove seed and generator_device here? Both are already assigned directly to OmniDiffusionSamplingParams, while the copies in extra_args are not consumed.

@hsliuustc0106

Copy link
Copy Markdown
Collaborator

@BeatSeat gentle ping — this high-priority PR has seen no updates for 5 days and review feedback is waiting. Could you share a status or ETA when you get a chance? Thanks!

@BeatSeat
BeatSeat force-pushed the beatseat/mm2-batching-shared-rt branch from e069469 to 8ecb81a Compare September 24, 2026 13:57
@BeatSeat

Copy link
Copy Markdown
Contributor Author

Thanks @hsliuustc0106, @amy-why-3459, and @nagisa-kunhah for the thorough review and patient ping!

Apologies for the delay. We have rebased the PR onto the latest main (d2aa07159) and pushed commit 8ecb81a87, fully resolving all reviewer findings, incorporating upstream Cache-DiT (#7291), and adding comprehensive regression tests.

1. Rebase & Upstream Cache-DiT Integration

  • Cleanly rebased onto latest main with zero merge conflicts.
  • Aligned with _cache_dit_runtime from upstream [Model] MammothModa2: enable Cache-DiT acceleration for the DiT stage #7291: when Cache-DiT is enabled, enforces alternating unconditional forward passes (requires_paired_cfg) to keep cache step parity accounting intact, while neutralizing guidance outside cfg_range via scale_vec and torch.where.

2. Resolution of Reviewer Findings

  • [P1] Q-Former Output Query Length: SimpleQFormerImageRefiner outputs fixed num_queries tokens. In _denoise_group, the pipeline now retains the full image_embeds[i : i + 1] output sequence and assigns an all-True mask (refined_image_mask), preventing query truncation or mask length mismatches.
  • [P1] Nested Image Embedder Contiguous Compaction: In MammothModa2DiTModel._prepare_embeddings, when batch requests have unequal text lengths, valid text tokens and image queries are compacted contiguously to the prefix of each row via direct slice copying, preventing downstream prefix-packing (:encoder_seq_lengths) from dropping image queries.
  • [P2] Elimination of CFG Host-Device Synchronization: Precomputed any_active_per_step as Python CPU booleans prior to the diffusion loop, completely eliminating the 50 active_tensor.any() host synchronizations (aten::is_nonzero / aten::item).
  • [P2/P3] Admission Fail-Fast & Unified Validation:
    • Unified dimension, step count, and CFG range validation into a shared helper _resolve_request_sampling_and_dims called by both admission validation and request parsing, eliminating schema drift.
    • In get_mammoth_moda2_pre_process_func, requests missing visual tokens (full_token_ids[answer_start_index:] < gen_vocab_start_index) and requests with num_outputs_per_prompt != 1 are rejected before scheduler admission, preventing malformed requests from failing co-batched requests.
  • Preview Extra Body Params: Omitted redundant seed and generator_device from MAMMOTHMODA2_PREVIEW_EXTRA_BODY_PARAMS as noted by @nagisa-kunhah.
  • PR Description Updated: Cleaned up the stale dependency note regarding merged [Model] Migrate MammothModa2 DiT to the shared diffusion runtime #7134 and re-pinned evidence.

3. Additional Hot-path & Memory Optimizations

  • Fused CFG Blending: Replaced the 3-op sequence model_pred_uncond + scale_vec * (model_pred - model_pred_uncond) with PyTorch's fused torch.lerp(model_pred_uncond, model_pred, scale_vec), eliminating 100 kernel launches and intermediate tensor allocations across 50 denoising steps (profiler shows +46.5% CFG blend speedup).
  • Fast-path Conditioning Collation: Added $B=1$ 0-copy bypass and homogeneous-length C++ torch.cat fast-path in _pad_cond_sequence.
  • In-place VAE Latents Scaling: Used in-place .div_() and .add_() before VAE decode to reduce peak activation memory.
  • Static Visual ID Tensor Caching: Cached static visual token ID tensor on the pipeline instance to eliminate per-request allocations.

4. Test Verification

Added 12 unit and regression tests in test_pipeline_mammothmoda2_dit.py covering:

  • Q-Former fixed queries preservation (test_refiner_output_length_preserved_when_input_len_differs_from_queries);
  • Text/image query contiguous compaction under unequal text lengths (test_nested_image_embedder_compacts_unequal_text_lengths);
  • Zero device-synchronization in CFG loop (test_cfg_precomputation_uses_cpu_booleans_without_device_sync);
  • Pre-process admission rejection of missing visual tokens and multi-output (test_pre_process_rejects_missing_visual_tokens_and_multi_output);
  • Multi-request batched vs solo output parity.

All pre-commit hooks (mypy, ruff check, ruff format, typos, check-yaml, signoff-commit) pass cleanly.

@vllm-omni-review-bot

vllm-omni-review-bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Omni ReviewBot triage note

Automated triage of commit 00282f8aeb8a produced:

  • Priority: high. Prompt maintainer attention is suggested.

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

@amy-why-3459

Copy link
Copy Markdown
Collaborator

Reviewed head 8ecb81a87babc17762af6e1592b59410bef7cc21. I recommend addressing the following issues before merging.

1. [P1] Restore valid synthetic AR conditions for startup warmup

Relevant code

The dummy-request branch now produces an empty hidden-state tensor and an empty token list. However, forward() unconditionally calls _split_request_conditions(), which rejects requests without visual-token hidden states. This makes the dummy request fail before model execution:

ValueError: MammothModa2 AR stage produced no visual-token hidden states for request dummy_req_id; ... Generated token ids: []

DiffusionEngine.from_config() invokes run_startup_warmup(), and a warmup failure closes the engine and is re-raised. This therefore affects the normal startup path, not just a test fixture. The existing test_parse_request_synthesizes_dummy_ar_conditions also fails, and a separate CPU reproducer calling _parse_request() followed by _split_request_conditions() confirms the exception without loading weights.

Please restore the synthetic text + valid visual-token conditions, or provide an equivalent valid dummy path. Add coverage that exercises dummy forward validation, rather than only allowing the request through preprocessing.

2. [P2] Use the same visual-token threshold resolution for admission and inference

Relevant code

Admission only reads llm_config.text_config.gen_vocab_start_index and otherwise falls back to the hardcoded value 152064. The model configuration constructors also support llm_config.gen_vocab_start_index and derive defaults when appropriate. Consequently, admission can reject a request that the model considers valid.

A minimal supported configuration with parent-level llm_config.gen_vocab_start_index=100 and no nested override resolves to 100 through _build_mammoth_config(), but preprocessing rejects answer tokens [100, 101] as having no visual tokens. I reproduced this using independent configuration copies; constructing the model config can mutate nested dictionaries, so reusing the same instance can mask the discrepancy.

Please resolve the threshold through the same normalized model configuration used by inference, and test parent-level, nested, and default-derived values.

3. [P2] Fix the new/modified regression tests so they exercise the intended behavior

One concrete example: the compaction test imports MammothModa2DiTModel, which does not exist in that module; the implementation class is Transformer2DModel.

Other failures include:

  • Accessing config.gen_dit_config.hidden_size although gen_dit_config is a dictionary.
  • Passing None to the preprocessing factory while the fixture's valid visual tokens assume a threshold of 100, rather than the fallback 152064.
  • Using a parameterless refiner mock where the pipeline calls next(refiner.parameters()), causing StopIteration before the intended assertions.
  • Returning a fixed batch size of one from _FakeVae.decode() when testing multiple requests, yielding an empty second output slice.

These failures prevent the advertised batching, refiner-length, and compaction checks from providing the intended regression coverage. Please repair the fixtures and rerun the entire target test file.

Validation

I ran the target CPU test file in the same local environment against isolated snapshots. The baseline was the same head with the complete PR diff reversed.

python -m pytest tests/diffusion/models/mammoth_moda2/test_pipeline_mammothmoda2_dit.py -q --disable-warnings
Version Result
Baseline without this PR 35 passed
PR head 39 passed, 12 failed

Two additional minimal CPU regression tests reproduce findings 1 and 2. The pre-commit, wheel-build, and DCO checks were green when reviewed, but they do not establish that these model tests pass. I did not run real-weight inference or verify the claimed throughput improvement in this review.

@BeatSeat

Copy link
Copy Markdown
Contributor Author

Thank you @amy-why-3459 for the thorough review. We have addressed all three findings in commit 064ea43fa:

1. [P1] Restored valid synthetic AR conditions for startup warmup

  • In _parse_request(), dummy runs now synthesize 1 prompt text token + 1 visual token at gen_vocab_start_index ([0, gen_start], answer_start_index=1, hidden-state shape (2, hidden_size)).
  • When forward() invokes _split_request_conditions(), both text_cond and image_cond have shape (1, hidden_size), avoiding the missing visual-token exception during engine startup warmup (run_startup_warmup()).
  • Added regression test test_dummy_request_splits_valid_ar_conditions_without_error exercising parsing and condition splitting on dummy requests.

2. [P2] Visual-token threshold resolution normalized across admission and inference

  • Added _resolve_gen_vocab_start_index(od_config) using _build_mammoth_config() (with fallback to raw config dictionary) to resolve gen_vocab_start_index.
  • Both get_mammoth_moda2_pre_process_func() and _split_ar_conditions() now resolve through the identical normalized configuration hierarchy (parent-level llm_config.gen_vocab_start_index, nested llm_config.text_config.gen_vocab_start_index, and default-derived 152064).
  • Added parametrized test test_admission_and_inference_threshold_resolution_matches covering parent-level, nested, and default-derived configurations, verifying exact alignment between admission rejection/acceptance and inference splitting.

3. [P2] Repaired unit test fixtures

  • Fixed test_nested_image_embedder_compacts_unequal_text_lengths: imported Transformer2DModel and initialized _internal_dict on the mock so the compaction test passes.
  • Fixed test_build_mammoth_config_uses_shared_transformer_projection: accessed config.gen_dit_config["hidden_size"].
  • Fixed get_mammoth_moda2_pre_process_func: passed _od_config() instead of None in test fixtures so the fixture's threshold of 100 matches the request tokens [10, 100, 101].
  • Fixed _MockRefiner: added self.anchor = nn.Parameter(torch.zeros(1)) so next(refiner.parameters()) succeeds.
  • Fixed _FakeVae.decode(): dynamically sized decode output to latents.shape[0] rows and derived output from latents so multi-request batches produce non-empty, distinct output slices.

All 55 tests in tests/diffusion/models/mammoth_moda2/test_pipeline_mammothmoda2_dit.py now pass cleanly (55 passed).

@amy-why-3459 amy-why-3459 added ready label to trigger buildkite CI and removed ready label to trigger buildkite CI labels Sep 24, 2026
@vllm-omni-review-bot

vllm-omni-review-bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Omni ReviewBot: superseded

The CI failure noted on 064ea43fab6a refers to an earlier head; the pull request now points at 872ee457e6d7.

… runtime

Support request-level batching for MammothModa2 DiT on top of the shared
diffusion runtime (vllm-project#7134), resolving the milestone in vllm-project#7075 and rebasing
cleanly onto main with Cache-DiT support.

- Support DiffusionRequestBatch in forward() with grouping by (H, W, steps).
- Optimized right-padded conditioning collation with single-item bypass and
  homogeneous-length C++ fast path in _pad_cond_sequence.
- Collate SimpleQFormerImageRefiner output with fixed query sequence length
  rather than input visual length.
- Compact valid text and image queries contiguously per row in _prepare_embeddings
  via direct slice copying, eliminating intermediate tensor and boolean mask ops.
- Validate AR conditions, geometry, step count, single output, and visual token range
  at admission in get_mammoth_moda2_pre_process_func to isolate batch failures.
- In-place VAE parameter dtype and scaling/shift alignment to reduce peak VRAM.
- Fused CFG blending via torch.lerp, eliminating 100 kernel launches and intermediate
  difference buffers across 50 denoising steps.
- Precomputed timesteps and host booleans to eliminate GPU-to-CPU synchronization stalls.
- Cache static visual token ID tensor on pipeline instance to avoid per-request allocations.
- Single-group fast path in forward() bypassing intermediate output list filtering.
- Seamlessly integrate with Cache-DiT requires_paired_cfg alternating forward passes.
- Per-request RNG latent generator honoring seed and generator_device.
- Deploy config: set max_num_seqs: 8 and request_batch_max_wait_ms: 20.
- Unit and regression tests in test_pipeline_mammothmoda2_dit.py.

Signed-off-by: BeatSeat <wendavid552@gmail.com>
…n threshold, and test fixtures

- Restore valid synthetic AR conditions for startup warmup on dummy requests.
- Resolve gen_vocab_start_index through the normalized model configuration for both admission and inference.
- Repair unit test fixtures in test_pipeline_mammothmoda2_dit.py:
  * Import Transformer2DModel instead of non-existent class.
  * Initialize nn.Module and set _internal_dict in compaction test.
  * Access gen_dit_config as dict.
  * Pass _od_config() to preprocessor factory.
  * Add parameter anchor to _MockRefiner.
  * Dynamically size _FakeVae decode batch and derive output from latents.
- Add regression tests covering dummy warmup split and multi-level threshold resolution.

Signed-off-by: BeatSeat <wendavid552@gmail.com>
@BeatSeat
BeatSeat force-pushed the beatseat/mm2-batching-shared-rt branch from 064ea43 to 872ee45 Compare September 24, 2026 22:53
@amy-why-3459 amy-why-3459 added ready label to trigger buildkite CI and removed ready label to trigger buildkite CI labels Sep 25, 2026
@amy-why-3459
amy-why-3459 merged commit b308e19 into vllm-project:main Sep 25, 2026
8 of 9 checks passed
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 high priority high priority issue, needs to be done asap ready label to trigger buildkite CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants