[Perf] Support continuous batching for MammothModa2 (#7954)

Signed-off-by: nagisa-kun <1434936049@qq.com>
Signed-off-by: nagisa <1434936049@qq.com>
This commit is contained in:
hurukawa authored and GitHub committed 2026-10-05 02:22:37 +08:00
1 parent c9c478dd07
commit b9cf74352a
5 files changed
+684 -72

No files matched your search

+18 -6
View File
@@ -18,12 +18,24 @@ The generic example formats the AR prompt, drives the AR → DiT stage pipeline,
and forwards MammothModa2-specific generation parameters through the and forwards MammothModa2-specific generation parameters through the
pipeline-declared `extra_body` contract. pipeline-declared `extra_body` contract.
MammothModa2's DiT stage runs in the shared diffusion runtime in request mode. MammothModa2's DiT stage runs in the shared diffusion runtime. The default
The first integration intentionally supports one request and one image per `mammoth_moda2.yaml` uses request-level batching with up to eight compatible
forward only (`max_num_seqs: 1`, `num_outputs_per_prompt: 1`). Request-level requests (`max_num_seqs: 8`) and one image per request
batching, step execution, continuous batching, compilation, quantization, (`num_outputs_per_prompt: 1`). TeaCache and Cache-DiT are optional alternative
parallelism, and offload are not enabled by this recipe. TeaCache acceleration cache backends in request mode. Enable TeaCache with `--cache-backend tea_cache`
is supported for the DiT stage. as shown below, or enable Cache-DiT using the commented settings in that YAML.
Compilation, DiT quantization, parallelism, and offload are not enabled by
these presets.
To use step execution and continuous batching, select
[`mammoth_moda2_step.yaml`](../../vllm_omni/deploy/mammoth_moda2_step.yaml)
with `--deploy-config vllm_omni/deploy/mammoth_moda2_step.yaml` in the
text-to-image commands below. This preset inherits the same stage placement
and capacity, enables step execution for Stage 1, and disables request-batch
admission waiting and diffusion cache acceleration. Compatible requests can
join between denoising steps and finish independently, including requests
with different inference-step counts. Step mode cannot be combined with
TeaCache, Cache-DiT, or other diffusion cache backends.
Image size, seed, guidance, and denoising steps use the standard diffusion Image size, seed, guidance, and denoising steps use the standard diffusion
request fields. `cfg_range` remains a MammothModa2-specific `extra_body` request fields. `cfg_range` remains a MammothModa2-specific `extra_body`
@@ -33,7 +33,10 @@ from vllm_omni.diffusion.models.mammoth_moda2.pipeline_mammothmoda2_dit import (
get_mammoth_moda2_pre_process_func, get_mammoth_moda2_pre_process_func,
) )
from vllm_omni.diffusion.request import OmniDiffusionRequest from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.sched.step_scheduler import StepScheduler
from vllm_omni.diffusion.worker.input_batch import InputBatch
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
from vllm_omni.diffusion.worker.utils import StepRequestState
from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.platforms import current_omni_platform from vllm_omni.platforms import current_omni_platform
@@ -90,12 +93,12 @@ def test_root_weight_source_forwards_revision() -> None:
assert _root_weight_source(config).revision == "rev-7" assert _root_weight_source(config).revision == "rev-7"
def test_pipeline_declares_native_components_and_batch_request_mode() -> None: def test_pipeline_declares_native_components_and_batch_modes() -> None:
assert MammothModa2DiTPipeline._dit_modules == ["gen_transformer"] assert MammothModa2DiTPipeline._dit_modules == ["gen_transformer"]
assert MammothModa2DiTPipeline._encoder_modules == ["gen_image_condition_refiner"] assert MammothModa2DiTPipeline._encoder_modules == ["gen_image_condition_refiner"]
assert MammothModa2DiTPipeline._vae_modules == ["gen_vae"] assert MammothModa2DiTPipeline._vae_modules == ["gen_vae"]
assert MammothModa2DiTPipeline.supports_request_batch is True assert MammothModa2DiTPipeline.supports_request_batch is True
assert MammothModa2DiTPipeline.supports_step_execution is False assert MammothModa2DiTPipeline.supports_step_execution is True
def test_mammoth_postprocess_denormalizes_nonnegative_raw_vae_output() -> None: def test_mammoth_postprocess_denormalizes_nonnegative_raw_vae_output() -> None:
@@ -342,6 +345,21 @@ class _FakeTransformer(nn.Module):
return torch.zeros_like(hidden_states) return torch.zeros_like(hidden_states)
class _FakeImageRefiner(nn.Module):
def __init__(self, num_queries: int) -> None:
super().__init__()
self.anchor = nn.Parameter(torch.zeros(1))
self.num_queries = num_queries
def forward(self, image_embeds, attention_mask):
assert attention_mask.shape == image_embeds.shape[:2]
return image_embeds.new_zeros(
image_embeds.shape[0],
self.num_queries,
image_embeds.shape[-1],
)
@dataclass @dataclass
class _FakeVaeConfig: class _FakeVaeConfig:
scaling_factor: float | None = None scaling_factor: float | None = None
@@ -590,6 +608,25 @@ def test_pre_process_registers_batch_compatibility_key() -> None:
assert req.batch_compatibility_key == ("mammoth_moda2_dit", 32, 48, 7) assert req.batch_compatibility_key == ("mammoth_moda2_dit", 32, 48, 7)
def test_step_pre_process_allows_different_step_counts() -> None:
config = _od_config()
config.step_execution = True
pre_process = get_mammoth_moda2_pre_process_func(config)
first = _batch(
request_id="short",
sampling=OmniDiffusionSamplingParams(height=32, width=48, num_inference_steps=2),
).requests[0]
second = _batch(
request_id="long",
sampling=OmniDiffusionSamplingParams(height=32, width=48, num_inference_steps=7),
).requests[0]
pre_process(first)
pre_process(second)
assert first.batch_compatibility_key == second.batch_compatibility_key == ("mammoth_moda2_dit", 32, 48)
def test_pre_process_rejects_missing_ar_conditions() -> None: def test_pre_process_rejects_missing_ar_conditions() -> None:
pre_process = get_mammoth_moda2_pre_process_func(_od_config()) pre_process = get_mammoth_moda2_pre_process_func(_od_config())
req = _batch(request_id="req-bad-ar", prompt={"prompt": "test"}).requests[0] req = _batch(request_id="req-bad-ar", prompt={"prompt": "test"}).requests[0]
@@ -1020,3 +1057,367 @@ def test_admission_and_inference_threshold_resolution_matches(llm_cfg_patch: dic
text_cond, image_cond = pipeline._split_request_conditions(parsed_at) text_cond, image_cond = pipeline._split_request_conditions(parsed_at)
assert text_cond.shape == (1, 8) assert text_cond.shape == (1, 8)
assert image_cond.shape == (1, 8) assert image_cond.shape == (1, 8)
def _step_state(batch: DiffusionRequestBatch) -> StepRequestState:
request = batch.requests[0]
return StepRequestState(
request_id=request.request_id,
sampling=request.sampling_params,
prompt=request.prompt,
)
@pytest.mark.parametrize("step_execution", [False, True])
@pytest.mark.parametrize(
("standard_steps", "extra_args", "extra_info", "expected_steps"),
[
(None, {}, {}, 50),
(None, {"num_inference_steps": 30}, {}, 30),
(50, {"num_inference_steps": 30}, {}, 30),
(None, {}, {"num_inference_steps": [20]}, 20),
(10, {}, {"num_inference_steps": [20]}, 10),
],
)
def test_pre_process_normalizes_steps_before_scheduler_admission(
step_execution: bool,
standard_steps: int | None,
extra_args: dict,
extra_info: dict,
expected_steps: int,
) -> None:
config = _od_config()
config.step_execution = step_execution
request = _batch(
sampling=OmniDiffusionSamplingParams(num_inference_steps=standard_steps, extra_args=extra_args),
).requests[0]
request.prompt["additional_information"].update(extra_info)
request = get_mammoth_moda2_pre_process_func(config)(request)
assert request.sampling_params.num_inference_steps == expected_steps
parsed = _pipeline_shell()._parse_request(DiffusionRequestBatch([request]))
assert parsed.num_inference_steps == expected_steps
if step_execution:
scheduler = StepScheduler()
assert scheduler.add_request(request) == request.request_id
assert scheduler._request_progress[request.request_id].total_steps == expected_steps
else:
assert request.batch_compatibility_key[-1] == expected_steps
def test_step_protocol_matches_request_mode() -> None:
pipeline = _pipeline_shell()
pipeline.gen_transformer = _FakeTransformer()
pipeline.gen_image_condition_refiner = None
pipeline.gen_vae = _FakeVae()
pipeline.gen_freqs_cis = torch.zeros(1)
batch = _batch(
sampling=OmniDiffusionSamplingParams(
height=32,
width=48,
seed=42,
guidance_scale=1.0,
num_inference_steps=2,
)
)
module = "vllm_omni.diffusion.models.mammoth_moda2.pipeline_mammothmoda2_dit"
with (
patch(f"{module}.FlowMatchEulerDiscreteScheduler", side_effect=lambda: _FakeScheduler()),
patch(f"{module}.randn_tensor", side_effect=lambda shape, **kwargs: torch.zeros(shape, dtype=kwargs["dtype"])),
):
request_output = pipeline.forward(batch)[0]
state = pipeline.prepare_encode(_step_state(batch))
while not state.denoise_completed:
input_batch = InputBatch.make_batch([state])
noise_pred = pipeline.denoise_step(input_batch, states=[state])
pipeline.step_scheduler(state, noise_pred)
step_output = pipeline.post_decode(state)
torch.testing.assert_close(step_output.output, request_output.output)
assert state.step_index == 2
@pytest.mark.parametrize(
("guidance_scales", "expected_predictions", "expected_calls"),
[
((1.0, 4.0), (2.0, 5.0), 2),
((4.0, 1.0), (5.0, 2.0), 2),
((1.0, 1.0), (2.0, 2.0), 1),
],
)
def test_step_batch_handles_cfg_and_non_cfg_requests(
guidance_scales: tuple[float, float],
expected_predictions: tuple[float, float],
expected_calls: int,
) -> None:
pipeline = _pipeline_shell()
pipeline.gen_transformer = _FakeTransformer()
pipeline.gen_image_condition_refiner = None
pipeline.gen_freqs_cis = torch.zeros(1)
config = _od_config()
config.step_execution = True
pre_process = get_mammoth_moda2_pre_process_func(config)
scheduler = StepScheduler()
requests = [
pre_process(
_batch(
request_id=f"cfg-{index}",
sampling=OmniDiffusionSamplingParams(
height=32,
width=48,
seed=index,
guidance_scale=4.0,
num_inference_steps=2,
extra_args={"text_guidance_scale": scale},
),
).requests[0]
)
for index, scale in enumerate(guidance_scales)
]
assert scheduler._build_sampling_params_key(requests[0]) == scheduler._build_sampling_params_key(requests[1])
def predict(*, hidden_states, text_hidden_states, **kwargs):
return torch.full_like(hidden_states, 2.0 if text_hidden_states.shape[1] else 1.0)
module = "vllm_omni.diffusion.models.mammoth_moda2.pipeline_mammothmoda2_dit"
with (
patch(f"{module}.FlowMatchEulerDiscreteScheduler", side_effect=lambda: _FakeScheduler()),
patch.object(pipeline.gen_transformer, "forward", side_effect=predict) as transformer_forward,
):
states = [pipeline.prepare_encode(_step_state(DiffusionRequestBatch([request]))) for request in requests]
predictions = pipeline.denoise_step(InputBatch.make_batch(states), states=states)
for prediction, expected in zip(predictions, expected_predictions):
torch.testing.assert_close(prediction, torch.full_like(prediction, expected))
assert transformer_forward.call_count == expected_calls
@pytest.mark.parametrize(
("sampling", "extra_info", "expected_guidance"),
[
(OmniDiffusionSamplingParams(num_inference_steps=2), {}, 9.0),
(OmniDiffusionSamplingParams(num_inference_steps=2), {"text_guidance_scale": [3.5]}, 3.5),
],
)
def test_step_protocol_resolves_guidance_like_request_mode(
sampling: OmniDiffusionSamplingParams,
extra_info: dict,
expected_guidance: float,
) -> None:
"""Guidance from defaults or additional_information must survive step mode.
prepare_encode() must parse the already-initialized step state directly
instead of re-wrapping its sampling params in a fresh OmniDiffusionRequest,
whose __post_init__ promotes the auto-filled 1.0 guidance_scale to an
explicitly-provided value and silently disables model-default CFG.
"""
pipeline = _pipeline_shell()
pipeline.gen_transformer = _FakeTransformer()
pipeline.gen_image_condition_refiner = None
pipeline.gen_vae = _FakeVae()
pipeline.gen_freqs_cis = torch.zeros(1)
batch = _batch(sampling=sampling)
batch.prompts[0]["additional_information"].update(extra_info)
assert sampling.guidance_scale_provided is False
module = "vllm_omni.diffusion.models.mammoth_moda2.pipeline_mammothmoda2_dit"
with (
patch(f"{module}.FlowMatchEulerDiscreteScheduler", side_effect=lambda: _FakeScheduler()),
patch(f"{module}.randn_tensor", side_effect=lambda shape, **kwargs: torch.zeros(shape, dtype=kwargs["dtype"])),
):
request_spec = pipeline._parse_request(batch)
request_output = pipeline.forward(batch)[0]
state = pipeline.prepare_encode(_step_state(batch))
while not state.denoise_completed:
noise_pred = pipeline.denoise_step(InputBatch.make_batch([state]), states=[state])
pipeline.step_scheduler(state, noise_pred)
step_output = pipeline.post_decode(state)
assert request_spec.text_guidance_scale == expected_guidance
assert state.extra["mammoth_text_guidance_scale"] == expected_guidance
assert state.negative_prompt_embeds is not None
assert sampling.guidance_scale_provided is False
torch.testing.assert_close(step_output.output, request_output.output)
def test_refiner_output_uses_query_length_mask_in_request_and_step_modes() -> None:
pipeline = _pipeline_shell()
pipeline.gen_transformer = _FakeTransformer()
pipeline.gen_image_condition_refiner = _FakeImageRefiner(num_queries=3)
pipeline.gen_vae = _FakeVae()
pipeline.gen_freqs_cis = torch.zeros(1)
batch = _batch(
sampling=OmniDiffusionSamplingParams(
height=32,
width=48,
seed=42,
guidance_scale=1.0,
num_inference_steps=1,
)
)
module = "vllm_omni.diffusion.models.mammoth_moda2.pipeline_mammothmoda2_dit"
with (
patch(f"{module}.FlowMatchEulerDiscreteScheduler", side_effect=lambda: _FakeScheduler()),
patch(f"{module}.randn_tensor", side_effect=lambda shape, **kwargs: torch.zeros(shape, dtype=kwargs["dtype"])),
):
assert len(pipeline.forward(batch)) == 1
state = pipeline.prepare_encode(_step_state(batch))
assert state.prompt_embeds is not None
assert state.prompt_embeds_mask is not None
assert state.prompt_embeds.shape[1] == 5
assert state.prompt_embeds_mask.shape == state.prompt_embeds.shape[:2]
assert state.prompt_embeds_mask.all()
def test_step_protocol_keeps_request_schedulers_and_progress_independent() -> None:
pipeline = _pipeline_shell()
pipeline.gen_transformer = _FakeTransformer()
pipeline.gen_image_condition_refiner = None
pipeline.gen_vae = _FakeVae()
pipeline.gen_freqs_cis = torch.zeros(1)
short = _step_state(
_batch(
request_id="short",
sampling=OmniDiffusionSamplingParams(
height=32,
width=48,
seed=1,
guidance_scale=1.0,
num_inference_steps=1,
),
)
)
long = _step_state(
_batch(
request_id="long",
sampling=OmniDiffusionSamplingParams(
height=32,
width=48,
seed=2,
guidance_scale=1.0,
num_inference_steps=2,
),
)
)
module = "vllm_omni.diffusion.models.mammoth_moda2.pipeline_mammothmoda2_dit"
with (
patch(f"{module}.FlowMatchEulerDiscreteScheduler", side_effect=lambda: _FakeScheduler()),
patch(f"{module}.randn_tensor", side_effect=lambda shape, **kwargs: torch.zeros(shape, dtype=kwargs["dtype"])),
):
pipeline.prepare_encode(short)
pipeline.prepare_encode(long)
predictions = pipeline.denoise_step(InputBatch.make_batch([short, long]), states=[short, long])
pipeline.step_scheduler(short, predictions[:1])
pipeline.step_scheduler(long, predictions[1:])
assert short.scheduler is not long.scheduler
assert short.denoise_completed
assert not long.denoise_completed
assert short.step_index == long.step_index == 1
class _InputDependentTransformer(_FakeTransformer):
"""Expose dropped masks, timesteps, conditioning and latent rows in outputs."""
def forward(self, *, hidden_states, timestep, text_hidden_states, text_attention_mask, **kwargs):
self.calls += 1
conditioning = (text_hidden_states * text_attention_mask.unsqueeze(-1)).sum(dim=(1, 2)) / 100
return hidden_states * 0.125 + timestep[:, None, None, None] + conditioning[:, None, None, None]
def _step_test_pipeline() -> MammothModa2DiTPipeline:
pipeline = _pipeline_shell()
pipeline.gen_transformer = _InputDependentTransformer()
pipeline.gen_image_condition_refiner = None
pipeline.gen_vae = _FakeVae()
pipeline.gen_freqs_cis = torch.zeros(1)
return pipeline
def _step_test_request(request_id: str, *, seed: int = 11, steps: int = 4, text_tokens: int = 2):
return _batch(
request_id=request_id,
prompt={
"prompt": "",
"additional_information": {
"full_hidden_states": torch.arange((text_tokens + 2) * 8, dtype=torch.float32).reshape(-1, 8) / 10,
"full_token_ids": [10] * text_tokens + [100, 101],
"answer_start_index": text_tokens,
},
},
sampling=OmniDiffusionSamplingParams(
height=32,
width=48,
seed=seed,
guidance_scale=4.0,
num_inference_steps=steps,
extra_args={"cfg_range": [0.25, 0.75]},
),
)
def test_step_protocol_seed_conditioning_and_request_parity() -> None:
pipeline = _step_test_pipeline()
outputs = []
for seed, text_tokens in ((11, 2), (11, 2), (12, 2), (11, 5)):
batch = _step_test_request("solo", seed=seed, text_tokens=text_tokens)
state = pipeline.prepare_encode(_step_state(batch))
expected_noise = torch.randn(state.latents.shape, generator=torch.Generator().manual_seed(seed))
torch.testing.assert_close(state.latents, expected_noise, rtol=0, atol=0)
while not state.denoise_completed:
prediction = pipeline.denoise_step(InputBatch.make_batch([state]), states=[state])
pipeline.step_scheduler(state, prediction)
output = pipeline.post_decode(state).output
reference = pipeline.forward(batch)[0].output
torch.testing.assert_close(output, reference)
outputs.append(output)
torch.testing.assert_close(outputs[0], outputs[1], rtol=0, atol=0)
assert not torch.allclose(outputs[0], outputs[2])
assert not torch.allclose(outputs[0], outputs[3])
def test_step_protocol_handles_arrival_reordering_and_retirement() -> None:
pipeline = _step_test_pipeline()
long = pipeline.prepare_encode(_step_state(_step_test_request("long")))
solo_long = pipeline.prepare_encode(_step_state(_step_test_request("solo-long")))
solo_short = pipeline.prepare_encode(_step_state(_step_test_request("solo-short", seed=22, steps=2, text_tokens=5)))
short = None
cached_batch = None
completed = {}
retired_latents = None
# Match runner ordering: newly admitted requests precede existing requests.
for tick in range(4):
if tick == 1:
short = pipeline.prepare_encode(_step_state(_step_test_request("short", seed=22, steps=2, text_tokens=5)))
assert short.scheduler is not long.scheduler
active = [short, long] if tick in (1, 2) else [long]
references = [solo_short, solo_long] if tick in (1, 2) else [solo_long]
cached_batch = InputBatch.make_batch(active, cached_batch=cached_batch)
if tick == 1:
assert long.current_timestep != short.current_timestep
assert cached_batch.prompt_embeds_mask.sum(dim=1).tolist() == [7, 4]
predictions = pipeline.denoise_step(cached_batch, states=active)
for index, (state, reference) in enumerate(zip(active, references)):
solo_prediction = pipeline.denoise_step(InputBatch.make_batch([reference]), states=[reference])
torch.testing.assert_close(predictions[index : index + 1], solo_prediction)
pipeline.step_scheduler(state, predictions[index : index + 1])
pipeline.step_scheduler(reference, solo_prediction)
torch.testing.assert_close(state.latents, reference.latents)
assert state.step_index == reference.step_index == state.scheduler.step_index
if state.denoise_completed:
completed[state.request_id] = pipeline.post_decode(state).output
torch.testing.assert_close(completed[state.request_id], pipeline.post_decode(reference).output)
if tick == 2:
assert short.denoise_completed and not long.denoise_completed
assert list(completed) == ["short"]
retired_latents = short.latents.clone()
assert list(completed) == ["short", "long"]
assert (short.step_index, long.step_index) == (2, 4)
torch.testing.assert_close(short.latents, retired_latents, rtol=0, atol=0)
assert not torch.allclose(completed["short"], completed["long"])
+3 -1
View File
@@ -19,10 +19,12 @@ stages:
request_batch_max_wait_ms: 20 request_batch_max_wait_ms: 20
gpu_memory_utilization: 0.3 gpu_memory_utilization: 0.3
enforce_eager: true enforce_eager: true
step_execution: false
trust_remote_code: true trust_remote_code: true
enable_prefix_caching: false enable_prefix_caching: false
# Optional Cache-DiT acceleration for the DiT stage (~2x faster denoise, # Optional Cache-DiT acceleration for the DiT stage (~2x faster denoise,
# training-free). Uncomment to enable; guidance=1.0 requests bypass it. # training-free). Request mode only; uncomment to enable.
# guidance=1.0 requests bypass it. Use mammoth_moda2_step.yaml for step mode.
# cache_backend: cache_dit # cache_backend: cache_dit
# cache_config: # cache_config:
# Fn_compute_blocks: 1 # Fn_compute_blocks: 1
+10
View File
@@ -0,0 +1,10 @@
# MammothModa2 AR→DiT with step execution and continuous batching.
base_config: mammoth_moda2.yaml
stages:
- stage_id: 1
step_execution: true
request_batch_max_wait_ms: 0
# Step mode does not support diffusion cache backends.
cache_backend: none
@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import ClassVar from typing import ClassVar
@@ -25,7 +25,9 @@ from vllm_omni.diffusion.layers.norm import RMSNorm
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.interface import SupportsComponentDiscovery from vllm_omni.diffusion.models.interface import SupportsComponentDiscovery
from vllm_omni.diffusion.request import OmniDiffusionRequest from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.worker.input_batch import InputBatch
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
from vllm_omni.diffusion.worker.utils import StepRequestState
from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.transformers_utils.configs.mammoth_moda2 import Mammothmoda2Config from vllm_omni.transformers_utils.configs.mammoth_moda2 import Mammothmoda2Config
@@ -283,21 +285,28 @@ def get_mammoth_moda2_pre_process_func(od_config: OmniDiffusionConfig | None = N
Validates AR conditions, dimensions, and sampling knobs at admission so Validates AR conditions, dimensions, and sampling knobs at admission so
malformed requests are rejected individually with their request_id before malformed requests are rejected individually with their request_id before
entering the scheduler queue. Admitted requests are assigned a entering the scheduler queue. Request mode groups by output geometry and
batch_compatibility_key based on output geometry and inference steps. inference steps; step mode omits the step count so independently progressing
requests can share compatible denoising ticks.
""" """
gen_vocab_start_index = _resolve_gen_vocab_start_index(od_config) gen_vocab_start_index = _resolve_gen_vocab_start_index(od_config)
step_execution = bool(getattr(od_config, "step_execution", False))
def pre_process_func(request: OmniDiffusionRequest) -> OmniDiffusionRequest: def pre_process_func(request: OmniDiffusionRequest) -> OmniDiffusionRequest:
height, width, num_inference_steps = _validate_request_for_admission( height, width, num_inference_steps = _validate_request_for_admission(
request, od_config=od_config, gen_vocab_start_index=gen_vocab_start_index request, od_config=od_config, gen_vocab_start_index=gen_vocab_start_index
) )
request.batch_compatibility_key = ( # Scheduler admission and output metrics must use the model's resolved step count.
"mammoth_moda2_dit", request.sampling_params.num_inference_steps = num_inference_steps
height, if step_execution:
width, request.batch_compatibility_key = ("mammoth_moda2_dit", height, width)
num_inference_steps, else:
) request.batch_compatibility_key = (
"mammoth_moda2_dit",
height,
width,
num_inference_steps,
)
return request return request
return pre_process_func return pre_process_func
@@ -342,7 +351,7 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
_vae_modules: ClassVar[list[str]] = ["gen_vae"] _vae_modules: ClassVar[list[str]] = ["gen_vae"]
supports_request_batch = True supports_request_batch = True
supports_step_execution = False supports_step_execution = True
# Load only gen_* weights; ignore llm_model.* to prevent loading the entire LLM backbone in the DiT stage. # Load only gen_* weights; ignore llm_model.* to prevent loading the entire LLM backbone in the DiT stage.
hf_to_vllm_mapper = WeightsMapper( hf_to_vllm_mapper = WeightsMapper(
@@ -487,16 +496,23 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
def _parse_request(self, req: DiffusionRequestBatch, index: int = 0) -> _MammothRequest: def _parse_request(self, req: DiffusionRequestBatch, index: int = 0) -> _MammothRequest:
request = req.requests[index] request = req.requests[index]
request_id = request.request_id
prompt = request.prompt if isinstance(request.prompt, dict) else {} prompt = request.prompt if isinstance(request.prompt, dict) else {}
sampling = request.sampling_params return self._parse_request_inputs(prompt, request.sampling_params, request.request_id, index)
def _parse_request_inputs(
self,
prompt: dict,
sampling: OmniDiffusionSamplingParams | None,
request_id: str,
index: int = 0,
) -> _MammothRequest:
if sampling is not None and getattr(sampling, "num_outputs_per_prompt", 1) != 1: if sampling is not None and getattr(sampling, "num_outputs_per_prompt", 1) != 1:
raise ValueError( raise ValueError(
f"MammothModa2 requires num_outputs_per_prompt == 1, got {sampling.num_outputs_per_prompt} " f"MammothModa2 requires num_outputs_per_prompt == 1, got {sampling.num_outputs_per_prompt} "
f"for request {request_id}" f"for request {request_id}"
) )
info = prompt.get("additional_information") info = prompt.get("additional_information")
if request.is_dummy_run(): if OmniDiffusionRequest.is_dummy_run_request_id(request_id):
gen_start = 152064 gen_start = 152064
if hasattr(self, "config") and hasattr(self.config, "llm_config") and self.config.llm_config is not None: if hasattr(self, "config") and hasattr(self.config, "llm_config") and self.config.llm_config is not None:
val = getattr(self.config.llm_config, "gen_vocab_start_index", None) val = getattr(self.config.llm_config, "gen_vocab_start_index", None)
@@ -662,20 +678,22 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
generators.append(gen) generators.append(gen)
return generators return generators
def _denoise_group( def _model_device_and_dtype(self) -> tuple[torch.device, torch.dtype]:
self, model_device = next(self.parameters()).device
specs: list[_MammothRequest], if self.gen_image_condition_refiner is not None:
conds: list[_RequestConditioning], return model_device, next(self.gen_image_condition_refiner.parameters()).dtype
model_device: torch.device, return model_device, next(self.gen_transformer.parameters()).dtype
) -> list[torch.Tensor]:
batch = len(specs)
height = specs[0].height
width = specs[0].width
num_inference_steps = specs[0].num_inference_steps
target_dtype = conds[0].text_embeds.dtype
# Conditioning collation. Token order is text-then-image per request, def _collate_conditioning(
# so concatenate first and right-pad the combined sequence. self,
conds: list[_RequestConditioning],
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
"""Collate per-request conditions into transformer inputs.
Returns ``(prompt_embeds, prompt_attention_mask, ar_image_embeds,
ar_image_attention_mask)``. Token order is text-then-image per request,
so concatenate first and right-pad the combined sequence.
"""
nested_image_embedder = getattr(self.gen_transformer.time_caption_embed, "image_embedder", None) nested_image_embedder = getattr(self.gen_transformer.time_caption_embed, "image_embedder", None)
if self.gen_image_condition_refiner is not None: if self.gen_image_condition_refiner is not None:
image_embeds, image_mask = _pad_cond_sequence( image_embeds, image_mask = _pad_cond_sequence(
@@ -698,9 +716,8 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
seq_embeds.append(torch.cat([c.text_embeds, cur_img_embed], dim=1)) seq_embeds.append(torch.cat([c.text_embeds, cur_img_embed], dim=1))
seq_masks.append(torch.cat([c.text_mask, cur_img_mask], dim=1)) seq_masks.append(torch.cat([c.text_mask, cur_img_mask], dim=1))
prompt_embeds, prompt_attention_mask = _pad_cond_sequence(seq_embeds, seq_masks) prompt_embeds, prompt_attention_mask = _pad_cond_sequence(seq_embeds, seq_masks)
ar_image_embeds = None return prompt_embeds, prompt_attention_mask, None, None
ar_image_attention_mask = None if nested_image_embedder is not None:
elif nested_image_embedder is not None:
prompt_embeds, prompt_attention_mask = _pad_cond_sequence( prompt_embeds, prompt_attention_mask = _pad_cond_sequence(
[c.text_embeds for c in conds], [c.text_embeds for c in conds],
[c.text_mask for c in conds], [c.text_mask for c in conds],
@@ -709,13 +726,60 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
[c.image_embeds for c in conds], [c.image_embeds for c in conds],
[c.image_mask for c in conds], [c.image_mask for c in conds],
) )
else: return prompt_embeds, prompt_attention_mask, ar_image_embeds, ar_image_attention_mask
prompt_embeds, prompt_attention_mask = _pad_cond_sequence( prompt_embeds, prompt_attention_mask = _pad_cond_sequence(
[torch.cat([c.text_embeds, c.image_embeds], dim=1) for c in conds], [torch.cat([c.text_embeds, c.image_embeds], dim=1) for c in conds],
[torch.cat([c.text_mask, c.image_mask], dim=1) for c in conds], [torch.cat([c.text_mask, c.image_mask], dim=1) for c in conds],
) )
ar_image_embeds = None return prompt_embeds, prompt_attention_mask, None, None
ar_image_attention_mask = None
def _init_latents(
self,
specs: list[_MammothRequest],
height: int,
width: int,
model_device: torch.device,
target_dtype: torch.dtype,
) -> torch.Tensor:
vae_scale_factor = 16
latent_channels = int(self.gen_transformer.config.in_channels)
shape = (len(specs), latent_channels, 2 * height // vae_scale_factor, 2 * width // vae_scale_factor)
single_shape = (1, *shape[1:])
generators = self._make_latent_generators(specs, model_device)
if generators is None:
return randn_tensor(shape, device=model_device, dtype=target_dtype)
noise_list = []
for gen in generators:
gen_device = gen.device if hasattr(gen, "device") else model_device
n = randn_tensor(single_shape, generator=gen, device=gen_device, dtype=target_dtype)
noise_list.append(n.to(device=model_device))
return torch.cat(noise_list, dim=0)
@staticmethod
def _make_scheduler(num_inference_steps: int, latents: torch.Tensor) -> FlowMatchEulerDiscreteScheduler:
scheduler = FlowMatchEulerDiscreteScheduler()
scheduler.set_timesteps(
num_inference_steps=num_inference_steps,
device=latents.device,
num_tokens=latents.shape[-2] * latents.shape[-1],
)
return scheduler
def _denoise_group(
self,
specs: list[_MammothRequest],
conds: list[_RequestConditioning],
model_device: torch.device,
) -> list[torch.Tensor]:
batch = len(specs)
height = specs[0].height
width = specs[0].width
num_inference_steps = specs[0].num_inference_steps
target_dtype = conds[0].text_embeds.dtype
prompt_embeds, prompt_attention_mask, ar_image_embeds, ar_image_attention_mask = self._collate_conditioning(
conds
)
# Empty unconditional prompt for classifier-free guidance, shared by # Empty unconditional prompt for classifier-free guidance, shared by
# all rows; rows that never take the CFG branch select their cond # all rows; rows that never take the CFG branch select their cond
@@ -736,27 +800,8 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
device=model_device, device=model_device,
) )
vae_scale_factor = 16 latents = self._init_latents(specs, height, width, model_device, target_dtype)
latent_channels = int(self.gen_transformer.config.in_channels) scheduler = self._make_scheduler(num_inference_steps, latents)
shape = (batch, latent_channels, 2 * height // vae_scale_factor, 2 * width // vae_scale_factor)
single_shape = (1, latent_channels, 2 * height // vae_scale_factor, 2 * width // vae_scale_factor)
generators = self._make_latent_generators(specs, model_device)
if generators is not None:
noise_list = []
for gen in generators:
gen_device = gen.device if hasattr(gen, "device") else model_device
n = randn_tensor(single_shape, generator=gen, device=gen_device, dtype=target_dtype)
noise_list.append(n.to(device=model_device))
latents = torch.cat(noise_list, dim=0)
else:
latents = randn_tensor(shape, device=model_device, dtype=target_dtype)
scheduler = FlowMatchEulerDiscreteScheduler()
scheduler.set_timesteps(
num_inference_steps=num_inference_steps,
device=model_device,
num_tokens=latents.shape[-2] * latents.shape[-1],
)
scale_vec = torch.tensor( scale_vec = torch.tensor(
[s.text_guidance_scale for s in specs], [s.text_guidance_scale for s in specs],
@@ -853,12 +898,7 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
# requests fail fast instead of surfacing device/dtype errors. # requests fail fast instead of surfacing device/dtype errors.
raw_conds = [self._split_request_conditions(spec) for spec in specs] raw_conds = [self._split_request_conditions(spec) for spec in specs]
model_device = next(self.parameters()).device model_device, target_dtype = self._model_device_and_dtype()
if self.gen_image_condition_refiner is not None:
target_dtype = next(self.gen_image_condition_refiner.parameters()).dtype
else:
target_dtype = next(self.gen_transformer.parameters()).dtype
conds = [ conds = [
self._build_conditioning(text_cond, image_cond, model_device, target_dtype) self._build_conditioning(text_cond, image_cond, model_device, target_dtype)
for text_cond, image_cond in raw_conds for text_cond, image_cond in raw_conds
@@ -882,6 +922,153 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
raise RuntimeError("DiT batching produced no image for at least one scheduled request") raise RuntimeError("DiT batching produced no image for at least one scheduled request")
return [output for output in outputs if output is not None] return [output for output in outputs if output is not None]
def prepare_encode(self, state: StepRequestState, **kwargs) -> StepRequestState:
del kwargs
prompt = state.prompt if isinstance(state.prompt, dict) else {}
spec = self._parse_request_inputs(prompt, state.sampling, state.request_id)
text_cond, image_cond = self._split_request_conditions(spec)
model_device, target_dtype = self._model_device_and_dtype()
cond = self._build_conditioning(text_cond, image_cond, model_device, target_dtype)
prompt_embeds, prompt_attention_mask, ar_image_embeds, ar_image_attention_mask = self._collate_conditioning(
[cond]
)
# InputBatch requires consistent negative fields for CFG and non-CFG requests.
# denoise_step decides whether the current batch needs an unconditional pass.
negative_prompt_embeds = prompt_embeds.new_zeros((1, 0, int(prompt_embeds.shape[-1])))
negative_prompt_attention_mask = torch.zeros(
(1, 0),
dtype=torch.bool,
device=model_device,
)
latents = self._init_latents([spec], spec.height, spec.width, model_device, target_dtype)
scheduler = self._make_scheduler(spec.num_inference_steps, latents)
state.prompt_embeds = prompt_embeds
state.prompt_embeds_mask = prompt_attention_mask
state.negative_prompt_embeds = negative_prompt_embeds
state.negative_prompt_embeds_mask = negative_prompt_attention_mask
state.latents = latents
state.timesteps = scheduler.timesteps
# Mid-denoise resume (sampling_params.step_index > 0) is not supported;
# like other step pipelines, encoding always restarts from step 0.
state.step_index = 0
state.scheduler = scheduler
state.do_true_cfg = False
state.txt_seq_lens = [int(prompt_embeds.shape[1])]
state.negative_txt_seq_lens = [0]
state.extra.update(
{
"mammoth_ar_image_embeds": ar_image_embeds,
"mammoth_ar_image_attention_mask": ar_image_attention_mask,
"mammoth_text_guidance_scale": spec.text_guidance_scale,
"mammoth_cfg_range": spec.cfg_range,
}
)
return state
def denoise_step(
self,
input_batch: InputBatch,
*,
states: Sequence[StepRequestState] | None = None,
**kwargs,
) -> torch.Tensor:
del kwargs
batch_states = list(states if states is not None else input_batch.states)
if input_batch.prompt_embeds is None or input_batch.prompt_embeds_mask is None:
raise ValueError("MammothModa2 step batch is missing prompt conditioning")
if len(batch_states) != int(input_batch.latents.shape[0]):
raise ValueError("MammothModa2 supports one latent row per step request")
ar_embeds = [state.extra["mammoth_ar_image_embeds"] for state in batch_states]
ar_masks = [state.extra["mammoth_ar_image_attention_mask"] for state in batch_states]
if all(value is None for value in ar_embeds):
ar_image_embeds = None
ar_image_attention_mask = None
elif all(value is not None for value in ar_embeds) and all(value is not None for value in ar_masks):
ar_image_embeds, ar_image_attention_mask = _pad_cond_sequence(
[value for value in ar_embeds if value is not None],
[value for value in ar_masks if value is not None],
)
else:
raise ValueError("Mixed MammothModa2 AR image-conditioning layouts in one step batch")
batch_size = int(input_batch.latents.shape[0])
timesteps = input_batch.timesteps.reshape(-1).expand(batch_size).to(input_batch.latents.dtype)
model_pred = self.gen_transformer(
hidden_states=input_batch.latents,
timestep=timesteps,
text_hidden_states=input_batch.prompt_embeds,
text_attention_mask=input_batch.prompt_embeds_mask,
ref_image_hidden_states=None,
ar_image_hidden_states=ar_image_embeds,
ar_image_attention_mask=ar_image_attention_mask,
freqs_cis=self.gen_freqs_cis,
)
guidance_scales = [float(state.extra["mammoth_text_guidance_scale"]) for state in batch_states]
active_cfg = []
for state, guidance_scale in zip(batch_states, guidance_scales):
cfg_start, cfg_end = state.extra["mammoth_cfg_range"]
fraction = state.step_index / max(1, state.total_steps)
active_cfg.append(guidance_scale > 1.0 and cfg_start <= fraction <= cfg_end)
if not any(active_cfg):
return model_pred
if input_batch.negative_prompt_embeds is None or input_batch.negative_prompt_embeds_mask is None:
raise ValueError("MammothModa2 CFG batch is missing negative conditioning")
model_pred_uncond = self.gen_transformer(
hidden_states=input_batch.latents,
timestep=timesteps,
text_hidden_states=input_batch.negative_prompt_embeds,
text_attention_mask=input_batch.negative_prompt_embeds_mask,
ref_image_hidden_states=None,
freqs_cis=self.gen_freqs_cis,
)
scale = model_pred.new_tensor(guidance_scales).view(batch_size, 1, 1, 1)
blended = torch.lerp(model_pred_uncond, model_pred, scale)
if all(active_cfg):
return blended
active_tensor = torch.tensor(
active_cfg,
device=model_pred.device,
dtype=torch.bool,
).view(batch_size, 1, 1, 1)
return torch.where(active_tensor, blended, model_pred)
def step_scheduler(self, state: StepRequestState, noise_pred: torch.Tensor, **kwargs) -> None:
del kwargs
if (
state.scheduler is None
or state.latents is None
or state.current_timestep is None
or state.prompt_embeds is None
):
raise ValueError(f"MammothModa2 scheduler state is incomplete for request {state.request_id}")
state.latents = state.scheduler.step(
noise_pred,
state.current_timestep,
state.latents,
return_dict=False,
)[0].to(dtype=state.prompt_embeds.dtype)
state.step_index += 1
def post_decode(self, state: StepRequestState, **kwargs) -> DiffusionOutput:
del kwargs
if state.latents is None:
raise ValueError(f"MammothModa2 has no final latents for request {state.request_id}")
latents = state.latents
if self.gen_vae.config.scaling_factor is not None:
latents = latents / self.gen_vae.config.scaling_factor
if self.gen_vae.config.shift_factor is not None:
latents = latents + self.gen_vae.config.shift_factor
vae_dtype = next(self.gen_vae.parameters()).dtype
image = self.gen_vae.decode(latents.to(dtype=vae_dtype), return_dict=False)[0]
return DiffusionOutput(output=image)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loader = AutoWeightsLoader(self) loader = AutoWeightsLoader(self)
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper) return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)