[Performance] Optimize MammothModa2 AR → DiT hidden-state transfer (#7102)
Signed-off-by: kunkunblueberry <1833921874@qq.com> Signed-off-by: kunkun <72174834+kunkunblueberry@users.noreply.github.com>
This commit is contained in:
1 parent
9f85b3722e
commit
c68eadbbe0
4 files changed
+212
-5
No files matched your search
@@ -33,6 +33,18 @@ def test_tensor_serialization():
|
||||
assert torch.equal(tensor, deserialized)
|
||||
|
||||
|
||||
def test_bfloat16_tensor_serialization():
|
||||
"""The raw tensor serializer preserves the Mammoth BF16 wire dtype."""
|
||||
import torch
|
||||
|
||||
tensor = torch.tensor([[1.0, -2.5], [3.25, 4.5]], dtype=torch.bfloat16)
|
||||
serialized = OmniSerializer.serialize({"text_prompt_embeds": tensor})
|
||||
deserialized = OmniSerializer.deserialize(serialized)
|
||||
|
||||
assert deserialized["text_prompt_embeds"].dtype == torch.bfloat16
|
||||
assert torch.equal(deserialized["text_prompt_embeds"], tensor)
|
||||
|
||||
|
||||
def test_noncontiguous_tensor_serialization():
|
||||
"""Test torch.Tensor serialization for strided views."""
|
||||
import torch
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""CPU regression tests for MammothModa2's completed-AR to DiT bridge."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.models.mammoth_moda2.pipeline_mammothmoda2_dit import (
|
||||
MammothModa2DiTPipeline,
|
||||
)
|
||||
from vllm_omni.engine.serialization import (
|
||||
deserialize_additional_information,
|
||||
serialize_additional_information,
|
||||
)
|
||||
from vllm_omni.engine.stage_engine_core_client import StageEngineCoreClient
|
||||
from vllm_omni.model_executor.models.mammoth_moda2.pipeline import MAMMOTH_MODA2_PIPELINE
|
||||
from vllm_omni.model_executor.stage_input_processors.mammoth_moda2 import ar2dit
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
_PROMPT_TOKEN_IDS = [7, 8, 9]
|
||||
_GENERATED_TOKEN_IDS = [101, 102, 103]
|
||||
|
||||
|
||||
def _hidden_states(rows: int = 5, dtype: torch.dtype = torch.bfloat16) -> torch.Tensor:
|
||||
return torch.arange(rows * 4, dtype=dtype).reshape(rows, 4)
|
||||
|
||||
|
||||
def _ar_output(hidden_states: torch.Tensor | None = None) -> SimpleNamespace:
|
||||
multimodal_output = {} if hidden_states is None else {"latent": hidden_states}
|
||||
return SimpleNamespace(
|
||||
request_id="mammoth-request",
|
||||
prompt_token_ids=list(_PROMPT_TOKEN_IDS),
|
||||
outputs=[
|
||||
SimpleNamespace(
|
||||
cumulative_token_ids=list(_GENERATED_TOKEN_IDS),
|
||||
multimodal_output=multimodal_output,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _prompt(
|
||||
*,
|
||||
target_h: int | None = None,
|
||||
target_w: int | None = None,
|
||||
) -> dict[str, object]:
|
||||
mm_processor_kwargs: dict[str, int] = {}
|
||||
if target_h is not None:
|
||||
mm_processor_kwargs["target_h"] = target_h
|
||||
if target_w is not None:
|
||||
mm_processor_kwargs["target_w"] = target_w
|
||||
return {
|
||||
"additional_information": {
|
||||
"image_height": [384],
|
||||
"image_width": [640],
|
||||
"text_guidance_scale": [4.5],
|
||||
"cfg_range": [0.1, 0.9],
|
||||
"num_inference_steps": [20],
|
||||
},
|
||||
"mm_processor_kwargs": mm_processor_kwargs,
|
||||
}
|
||||
|
||||
|
||||
def _assert_complete_dit_input(dit_inputs: list[dict[str, object]], hidden_states: torch.Tensor) -> None:
|
||||
assert len(dit_inputs) == 1
|
||||
dit_input = dit_inputs[0]
|
||||
assert dit_input["prompt_token_ids"] == [0]
|
||||
|
||||
additional_information = dit_input["additional_information"]
|
||||
assert isinstance(additional_information, dict)
|
||||
full_hidden_states = additional_information["full_hidden_states"]
|
||||
assert isinstance(full_hidden_states, torch.Tensor)
|
||||
assert full_hidden_states.dtype == torch.bfloat16
|
||||
assert full_hidden_states.is_contiguous()
|
||||
assert torch.equal(full_hidden_states, hidden_states)
|
||||
assert additional_information["full_token_ids"] == [7, 8, 9, 101, 102]
|
||||
assert additional_information["answer_start_index"] == [3]
|
||||
assert additional_information["image_height"] == [384]
|
||||
assert additional_information["image_width"] == [640]
|
||||
assert additional_information["text_guidance_scale"] == [4.5]
|
||||
assert additional_information["cfg_range"] == [0.1, 0.9]
|
||||
assert additional_information["num_inference_steps"] == [20]
|
||||
|
||||
|
||||
def test_ar2dit_builds_one_complete_dit_input() -> None:
|
||||
hidden_states = _hidden_states()
|
||||
|
||||
dit_inputs = ar2dit([_ar_output(hidden_states)], _prompt())
|
||||
|
||||
_assert_complete_dit_input(dit_inputs, hidden_states)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", (torch.float16, torch.bfloat16))
|
||||
def test_ar2dit_preserves_low_precision_through_engine_core_payload(dtype: torch.dtype) -> None:
|
||||
hidden_states = _hidden_states(dtype=dtype)
|
||||
dit_input = ar2dit([_ar_output(hidden_states)], _prompt())[0]
|
||||
|
||||
wire_payload = serialize_additional_information(dit_input["additional_information"])
|
||||
assert wire_payload is not None
|
||||
restored = deserialize_additional_information(wire_payload)
|
||||
restored_hidden_states = restored["full_hidden_states"]
|
||||
|
||||
assert isinstance(restored_hidden_states, torch.Tensor)
|
||||
assert restored_hidden_states.dtype == dtype
|
||||
assert restored_hidden_states.is_contiguous()
|
||||
assert torch.equal(restored_hidden_states, hidden_states)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", (torch.float16, torch.bfloat16))
|
||||
def test_dit_condition_split_preserves_compact_transfer_dtype(dtype: torch.dtype) -> None:
|
||||
# Construct only the config read by _split_ar_conditions. This keeps the
|
||||
# test CPU-only and avoids loading the DiT/VAE weights.
|
||||
pipeline = object.__new__(MammothModa2DiTPipeline)
|
||||
object.__setattr__(
|
||||
pipeline,
|
||||
"config",
|
||||
SimpleNamespace(
|
||||
llm_config=SimpleNamespace(gen_vocab_start_index=100),
|
||||
image_token_id=20,
|
||||
video_token_id=21,
|
||||
vision_start_token_id=22,
|
||||
vision_end_token_id=23,
|
||||
),
|
||||
)
|
||||
hidden_states = _hidden_states(dtype=dtype)
|
||||
|
||||
text_cond, image_cond = pipeline._split_ar_conditions(
|
||||
full_hidden_states=hidden_states,
|
||||
full_token_ids=[7, 20, 8, 101, 102],
|
||||
answer_start_index=3,
|
||||
)
|
||||
|
||||
assert text_cond.dtype == dtype
|
||||
assert image_cond.dtype == dtype
|
||||
assert text_cond.is_contiguous()
|
||||
assert image_cond.is_contiguous()
|
||||
assert torch.equal(text_cond, hidden_states[[0, 2]])
|
||||
assert torch.equal(image_cond, hidden_states[[3, 4]])
|
||||
|
||||
|
||||
def test_ar2dit_prefers_processor_image_dimensions() -> None:
|
||||
hidden_states = _hidden_states()
|
||||
|
||||
dit_inputs = ar2dit([_ar_output(hidden_states)], _prompt(target_h=512, target_w=768))
|
||||
|
||||
additional_information = dit_inputs[0]["additional_information"]
|
||||
assert additional_information["image_height"] == [512]
|
||||
assert additional_information["image_width"] == [768]
|
||||
|
||||
|
||||
def test_ar2dit_rejects_missing_latent_output() -> None:
|
||||
with pytest.raises(ValueError, match="missing latent multimodal output"):
|
||||
ar2dit([_ar_output()], _prompt())
|
||||
|
||||
|
||||
def test_ar2dit_rejects_hidden_token_row_mismatch() -> None:
|
||||
with pytest.raises(AssertionError, match="Hidden states length mismatch"):
|
||||
ar2dit([_ar_output(_hidden_states(rows=4))], _prompt())
|
||||
|
||||
|
||||
def test_mammoth_pipeline_uses_standard_completed_ar_forwarding() -> None:
|
||||
stage0, stage1 = MAMMOTH_MODA2_PIPELINE.stages
|
||||
|
||||
assert stage0.custom_process_next_stage_input_func is None
|
||||
assert stage1.custom_process_input_func.endswith(".ar2dit")
|
||||
assert stage1.sync_process_input_func is None
|
||||
assert stage1.requires_full_payload_input is False
|
||||
|
||||
|
||||
def test_stage_client_forwards_completed_ar_output_to_mammoth_adapter() -> None:
|
||||
hidden_states = _hidden_states()
|
||||
client = object.__new__(StageEngineCoreClient)
|
||||
client.custom_process_input_func = ar2dit
|
||||
client.requires_multimodal_data = False
|
||||
|
||||
dit_inputs = client.process_engine_inputs(
|
||||
[_ar_output(hidden_states)],
|
||||
_prompt(),
|
||||
)
|
||||
|
||||
_assert_complete_dit_input(dit_inputs, hidden_states)
|
||||
@@ -1,3 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
@@ -191,8 +193,12 @@ class MammothModa2DiTPipeline(nn.Module, SupportsComponentDiscovery):
|
||||
text_mask = questions_mask & ~(visual_token_mask | gen_token_mask)
|
||||
image_mask = answers_mask & gen_token_mask
|
||||
|
||||
text_cond = full_hidden_states[text_mask].to(dtype=torch.float32).contiguous()
|
||||
image_cond = full_hidden_states[image_mask].to(dtype=torch.float32).contiguous()
|
||||
# Keep the transferred representation compact until the final device/dtype
|
||||
# conversion in ``forward``. The masks are dtype-independent, and an
|
||||
# unconditional host-side float32 expansion here would otherwise double both
|
||||
# the selected-condition staging footprint and BF16 H2D traffic.
|
||||
text_cond = full_hidden_states[text_mask].contiguous()
|
||||
image_cond = full_hidden_states[image_mask].contiguous()
|
||||
return text_cond, image_cond
|
||||
|
||||
@torch.inference_mode()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Stage input processor for MammothModa2 (AR -> DiT)."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
@@ -91,9 +93,11 @@ def ar2dit(
|
||||
# from the model config. Pass through the raw AR hidden states + token ids and
|
||||
# the question/answer boundary so the pipeline can reconstruct the masks.
|
||||
additional_information = {
|
||||
# float32 so the tensor crosses the stage boundary (the serializer uses
|
||||
# numpy, which has no bf16); the DiT re-casts to the model dtype.
|
||||
"full_hidden_states": full_hidden_states.float().contiguous(),
|
||||
# The EngineCore payload serializer preserves bfloat16 as raw bytes.
|
||||
# Keep the AR representation compact across the CPU/IPC boundary;
|
||||
# the DiT selects its conditioning rows and casts them only when it
|
||||
# consumes them.
|
||||
"full_hidden_states": full_hidden_states.contiguous(),
|
||||
"full_token_ids": full_token_ids,
|
||||
"answer_start_index": [len(prompt_token_ids)],
|
||||
"image_height": [int(image_height)],
|
||||
|
||||
Reference in new issue
Block a user