[Feature] Add HunyuanImage3 DiT grouped step batching (#4041)
Signed-off-by: TaffyOfficial <2324465096@qq.com> Signed-off-by: zuiho <2324465096@qq.com> Co-authored-by: TaffyOfficial <2324465096@qq.com> Co-authored-by: Gao Han <hgaoaf@connect.ust.hk> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
7 files changed
+1337
-76
No files matched your search
@@ -64,7 +64,8 @@ container_name="rocm_${BUILDKITE_COMMIT}_$(tr -dc A-Za-z0-9 < /dev/urandom | hea
|
|||||||
docker pull "${image_name}"
|
docker pull "${image_name}"
|
||||||
|
|
||||||
remove_docker_container() {
|
remove_docker_container() {
|
||||||
docker rm -f "${container_name}" || docker image rm -f "${image_name}" || true
|
docker ps -aq --filter "name=${container_name}" | xargs -r docker rm -f || true
|
||||||
|
docker image rm -f "${image_name}" || true
|
||||||
}
|
}
|
||||||
trap remove_docker_container EXIT
|
trap remove_docker_container EXIT
|
||||||
|
|
||||||
@@ -107,7 +108,6 @@ if [[ $commands == *"--shard-id="* ]]; then
|
|||||||
--network=host \
|
--network=host \
|
||||||
--shm-size=16gb \
|
--shm-size=16gb \
|
||||||
--group-add "$render_gid" \
|
--group-add "$render_gid" \
|
||||||
--rm \
|
|
||||||
-e MIOPEN_DEBUG_CONV_DIRECT=0 \
|
-e MIOPEN_DEBUG_CONV_DIRECT=0 \
|
||||||
-e MIOPEN_DEBUG_CONV_GEMM=0 \
|
-e MIOPEN_DEBUG_CONV_GEMM=0 \
|
||||||
-e VLLM_ROCM_USE_AITER=0 \
|
-e VLLM_ROCM_USE_AITER=0 \
|
||||||
@@ -140,7 +140,6 @@ else
|
|||||||
--network=host \
|
--network=host \
|
||||||
--shm-size=16gb \
|
--shm-size=16gb \
|
||||||
--group-add "$render_gid" \
|
--group-add "$render_gid" \
|
||||||
--rm \
|
|
||||||
-e MIOPEN_DEBUG_CONV_DIRECT=0 \
|
-e MIOPEN_DEBUG_CONV_DIRECT=0 \
|
||||||
-e MIOPEN_DEBUG_CONV_GEMM=0 \
|
-e MIOPEN_DEBUG_CONV_GEMM=0 \
|
||||||
-e VLLM_ROCM_USE_AITER=0 \
|
-e VLLM_ROCM_USE_AITER=0 \
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
import vllm_omni.diffusion.models.hunyuan_image3.pipeline_hunyuan_image3 as hy3_module
|
||||||
|
from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec
|
||||||
|
from vllm_omni.diffusion.models.hunyuan_image3.pipeline_hunyuan_image3 import (
|
||||||
|
_STEP_AR_KV,
|
||||||
|
_STEP_CFG_FACTOR,
|
||||||
|
_STEP_GENERATOR,
|
||||||
|
_STEP_GUIDANCE_SCALE,
|
||||||
|
_STEP_INPUT_IDS,
|
||||||
|
_STEP_MODEL_KWARGS,
|
||||||
|
_STEP_PROMPT_KV,
|
||||||
|
HunyuanImage3Pipeline,
|
||||||
|
)
|
||||||
|
from vllm_omni.diffusion.worker.input_batch import InputBatch
|
||||||
|
from vllm_omni.diffusion.worker.utils import DiffusionRequestState
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
def _pipeline():
|
||||||
|
pipeline = object.__new__(HunyuanImage3Pipeline)
|
||||||
|
pipeline._tkwrapper = SimpleNamespace(pad_token_id=0)
|
||||||
|
pipeline.od_config = SimpleNamespace(
|
||||||
|
diffusion_attention_config=AttentionConfig(default=AttentionSpec(backend="TORCH_SDPA")),
|
||||||
|
parallel_config=SimpleNamespace(sequence_parallel_size=1, cfg_parallel_size=1),
|
||||||
|
cache_backend=None,
|
||||||
|
diffusion_kv_cache_skip_step_indices=None,
|
||||||
|
)
|
||||||
|
pipeline._pipeline = SimpleNamespace()
|
||||||
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
def _state(request_id: str, step_index: int) -> DiffusionRequestState:
|
||||||
|
state = DiffusionRequestState(
|
||||||
|
request_id=request_id,
|
||||||
|
sampling=SimpleNamespace(),
|
||||||
|
prompts=["prompt"],
|
||||||
|
)
|
||||||
|
state.step_index = step_index
|
||||||
|
state.timesteps = torch.tensor([1.0, 0.5, 0.25, 0.0])
|
||||||
|
state.latents = torch.zeros(1, 4, 8, 8)
|
||||||
|
state.extra = {
|
||||||
|
_STEP_CFG_FACTOR: 1,
|
||||||
|
_STEP_AR_KV: None,
|
||||||
|
_STEP_INPUT_IDS: None,
|
||||||
|
_STEP_GUIDANCE_SCALE: 1.0,
|
||||||
|
_STEP_MODEL_KWARGS: {
|
||||||
|
"num_image_tokens": 17,
|
||||||
|
"ar_kv_reuse_len": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def _sampling_params(**extra_args):
|
||||||
|
return SimpleNamespace(
|
||||||
|
timesteps=None,
|
||||||
|
sigmas=None,
|
||||||
|
num_outputs_per_prompt=None,
|
||||||
|
extra_args=extra_args,
|
||||||
|
height=512,
|
||||||
|
width=512,
|
||||||
|
num_inference_steps=4,
|
||||||
|
guidance_scale=1.0,
|
||||||
|
guidance_scale_provided=True,
|
||||||
|
guidance_rescale=0.0,
|
||||||
|
generator=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hunyuan_step_group_key_ignores_step_index_for_later_steps():
|
||||||
|
pipeline = _pipeline()
|
||||||
|
states = [_state("req-0", 1), _state("req-1", 3)]
|
||||||
|
|
||||||
|
groups = pipeline._split_step_groups(states)
|
||||||
|
|
||||||
|
assert len(groups) == 1
|
||||||
|
assert [state.request_id for state in groups[0]] == ["req-0", "req-1"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("sampling", "prompt_item", "expected_model_bot_task", "expected_system_bot_task"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
_sampling_params(bot_task="think_recaption", use_system_prompt="dynamic"),
|
||||||
|
{"prompt": "prompt", "bot_task": "vanilla"},
|
||||||
|
"think",
|
||||||
|
"think",
|
||||||
|
id="extra-args-precedence",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
_sampling_params(use_system_prompt="dynamic"),
|
||||||
|
{"prompt": "prompt", "bot_task": "vanilla"},
|
||||||
|
"image",
|
||||||
|
"image",
|
||||||
|
id="prompt-dict-fallback",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
_sampling_params(use_system_prompt="dynamic"),
|
||||||
|
{"prompt": "prompt"},
|
||||||
|
"auto",
|
||||||
|
"image",
|
||||||
|
id="default-auto-system-prompt",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_prepare_encode_preserves_normal_hunyuan_bot_task_semantics(
|
||||||
|
monkeypatch,
|
||||||
|
sampling,
|
||||||
|
prompt_item,
|
||||||
|
expected_model_bot_task,
|
||||||
|
expected_system_bot_task,
|
||||||
|
):
|
||||||
|
pipeline = _pipeline()
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
def fake_get_system_prompt(sys_type, bot_task, system_prompt=None):
|
||||||
|
del sys_type, system_prompt
|
||||||
|
captured["system_prompt_bot_task"] = bot_task
|
||||||
|
return "system prompt"
|
||||||
|
|
||||||
|
def fake_prepare_model_inputs(**kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
raise RuntimeError("stop after prepare_model_inputs")
|
||||||
|
|
||||||
|
monkeypatch.setattr(hy3_module, "get_system_prompt", fake_get_system_prompt)
|
||||||
|
pipeline.prepare_model_inputs = fake_prepare_model_inputs
|
||||||
|
state = DiffusionRequestState(
|
||||||
|
request_id="req-bot-task",
|
||||||
|
sampling=sampling,
|
||||||
|
prompts=[prompt_item],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="stop after prepare_model_inputs"):
|
||||||
|
pipeline.prepare_encode(state)
|
||||||
|
|
||||||
|
assert captured["bot_task"] == expected_model_bot_task
|
||||||
|
assert captured["system_prompt_bot_task"] == expected_system_bot_task
|
||||||
|
|
||||||
|
|
||||||
|
def test_forward_uses_same_hunyuan_bot_task_semantics(monkeypatch):
|
||||||
|
pipeline = _pipeline()
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
def fake_get_system_prompt(sys_type, bot_task, system_prompt=None):
|
||||||
|
del sys_type, system_prompt
|
||||||
|
captured["system_prompt_bot_task"] = bot_task
|
||||||
|
return "system prompt"
|
||||||
|
|
||||||
|
def fake_prepare_model_inputs(**kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
raise RuntimeError("stop after prepare_model_inputs")
|
||||||
|
|
||||||
|
monkeypatch.setattr(hy3_module, "get_system_prompt", fake_get_system_prompt)
|
||||||
|
pipeline.prepare_model_inputs = fake_prepare_model_inputs
|
||||||
|
req = SimpleNamespace(
|
||||||
|
request_id="req-forward-bot-task",
|
||||||
|
sampling_params=_sampling_params(bot_task="think_recaption", use_system_prompt="dynamic"),
|
||||||
|
prompts=[{"prompt": "prompt", "bot_task": "vanilla"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="stop after prepare_model_inputs"):
|
||||||
|
pipeline.forward(req)
|
||||||
|
|
||||||
|
assert captured["bot_task"] == "think"
|
||||||
|
assert captured["system_prompt_bot_task"] == "think"
|
||||||
|
|
||||||
|
|
||||||
|
def test_grouped_denoise_rejects_non_sdpa_attention_backend():
|
||||||
|
pipeline = _pipeline()
|
||||||
|
pipeline.od_config.diffusion_attention_config = AttentionConfig(default=AttentionSpec(backend="FLASH_ATTN"))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="only supports TORCH_SDPA"):
|
||||||
|
pipeline._ensure_grouped_attention_backend_supported(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_denoise_allows_non_sdpa_attention_backend():
|
||||||
|
pipeline = _pipeline()
|
||||||
|
pipeline.od_config.diffusion_attention_config = AttentionConfig(default=AttentionSpec(backend="FLASH_ATTN"))
|
||||||
|
|
||||||
|
pipeline._ensure_grouped_attention_backend_supported(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_grouped_denoise_allows_sdpa_attention_backend():
|
||||||
|
pipeline = _pipeline()
|
||||||
|
|
||||||
|
pipeline._ensure_grouped_attention_backend_supported(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_step_scheduler_preserves_latent_dtype_for_mixed_progress_batches():
|
||||||
|
pipeline = _pipeline()
|
||||||
|
pipeline._pipeline = SimpleNamespace(prepare_extra_func_kwargs=lambda step, kwargs: {})
|
||||||
|
|
||||||
|
class FakeScheduler:
|
||||||
|
def step(self, noise_pred, timestep, latents, **kwargs):
|
||||||
|
del timestep, kwargs
|
||||||
|
return (latents.float() + noise_pred.float(),)
|
||||||
|
|
||||||
|
state = _state("req", 0)
|
||||||
|
state.timesteps = torch.tensor([1.0])
|
||||||
|
state.scheduler = FakeScheduler()
|
||||||
|
state.latents = torch.zeros(1, 4, 8, 8, dtype=torch.bfloat16)
|
||||||
|
state.extra[_STEP_GENERATOR] = None
|
||||||
|
|
||||||
|
pipeline.step_scheduler(state, torch.ones_like(state.latents, dtype=torch.float32))
|
||||||
|
|
||||||
|
assert state.latents.dtype == torch.bfloat16
|
||||||
|
assert state.step_index == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_later_step_merge_shifts_spans_without_polluting_request_state():
|
||||||
|
pipeline = _pipeline()
|
||||||
|
states = [_state("short", 2), _state("long", 4)]
|
||||||
|
states[0].extra[_STEP_MODEL_KWARGS].update(
|
||||||
|
{
|
||||||
|
"attention_mask": torch.ones(1, 1, 3, 5, dtype=torch.bool),
|
||||||
|
"full_attn_spans": [[(2, 5)]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
states[1].extra[_STEP_MODEL_KWARGS].update(
|
||||||
|
{
|
||||||
|
"attention_mask": torch.ones(1, 1, 3, 7, dtype=torch.bool),
|
||||||
|
"full_attn_spans": [[(4, 7)]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
states[0].extra[_STEP_PROMPT_KV] = [{"lens": torch.tensor([2])}]
|
||||||
|
states[1].extra[_STEP_PROMPT_KV] = [{"lens": torch.tensor([4])}]
|
||||||
|
|
||||||
|
row_state_indexes = [0, 1]
|
||||||
|
row_branches = [0, 0]
|
||||||
|
_, merged = pipeline._merge_step_model_inputs(
|
||||||
|
states,
|
||||||
|
row_state_indexes,
|
||||||
|
row_branches,
|
||||||
|
first_step=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert merged["attention_mask"].shape == (2, 1, 3, 7)
|
||||||
|
assert merged["full_attn_spans"] == [[(4, 7)], [(4, 7)]]
|
||||||
|
|
||||||
|
pipeline._split_merged_kwargs_to_states(states, merged, row_state_indexes, row_branches)
|
||||||
|
|
||||||
|
assert states[0].extra[_STEP_MODEL_KWARGS]["attention_mask"].shape == (1, 1, 3, 5)
|
||||||
|
assert states[1].extra[_STEP_MODEL_KWARGS]["attention_mask"].shape == (1, 1, 3, 7)
|
||||||
|
assert states[0].extra[_STEP_MODEL_KWARGS]["full_attn_spans"] == [[(2, 5)]]
|
||||||
|
assert states[1].extra[_STEP_MODEL_KWARGS]["full_attn_spans"] == [[(4, 7)]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_later_step_merge_allows_request_local_step_counts_and_guidance_values():
|
||||||
|
pipeline = _pipeline()
|
||||||
|
states = [_state("req-0", 1), _state("req-1", 3)]
|
||||||
|
for idx, state in enumerate(states):
|
||||||
|
state.extra[_STEP_MODEL_KWARGS].update(
|
||||||
|
{
|
||||||
|
"attention_mask": torch.ones(1, 1, 2, 4, dtype=torch.bool),
|
||||||
|
"full_attn_spans": [[(2, 4)]],
|
||||||
|
"guidance_scale": 3.0 + idx,
|
||||||
|
"num_inference_steps": 20 + idx,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
state.extra[_STEP_PROMPT_KV] = [{"lens": torch.tensor([2])}]
|
||||||
|
|
||||||
|
_, merged = pipeline._merge_step_model_inputs(
|
||||||
|
states,
|
||||||
|
row_state_indexes=[0, 1],
|
||||||
|
row_branches=[0, 0],
|
||||||
|
first_step=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "guidance_scale" not in merged
|
||||||
|
assert "num_inference_steps" not in merged
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("request_id", "mutate_state", "error_match"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"broken-req",
|
||||||
|
lambda state: state.extra.pop(_STEP_MODEL_KWARGS),
|
||||||
|
"broken-req",
|
||||||
|
id="missing-model-kwargs",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"bad-cfg",
|
||||||
|
lambda state: state.extra.__setitem__(_STEP_CFG_FACTOR, 3),
|
||||||
|
"bad-cfg",
|
||||||
|
id="unsupported-cfg-factor",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_denoise_step_reports_invalid_group_state_with_request_id(request_id, mutate_state, error_match):
|
||||||
|
pipeline = _pipeline()
|
||||||
|
state = _state(request_id, 0)
|
||||||
|
mutate_state(state)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=error_match):
|
||||||
|
pipeline.denoise_step(InputBatch.make_batch([state]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_denoise_step_uses_input_batch_group_order_and_splits_back(monkeypatch):
|
||||||
|
pipeline = _pipeline()
|
||||||
|
monkeypatch.setattr(HunyuanImage3Pipeline, "device", property(lambda self: torch.device("cpu")))
|
||||||
|
states = [_state("req-0", 1), _state("req-1", 3)]
|
||||||
|
for idx, state in enumerate(states):
|
||||||
|
prefix_len = 2 + idx * 2
|
||||||
|
state.latents = torch.full((1, 1), float(idx))
|
||||||
|
state.extra[_STEP_CFG_FACTOR] = 2
|
||||||
|
state.extra[_STEP_GUIDANCE_SCALE] = 1.0
|
||||||
|
state.extra[_STEP_INPUT_IDS] = None
|
||||||
|
state.extra[_STEP_MODEL_KWARGS].update(
|
||||||
|
{
|
||||||
|
"attention_mask": torch.ones(2, 1, 2, prefix_len + 2, dtype=torch.bool),
|
||||||
|
"full_attn_spans": [[(prefix_len, prefix_len + 2)], [(prefix_len, prefix_len + 2)]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
state.extra[_STEP_PROMPT_KV] = [
|
||||||
|
{
|
||||||
|
"key": torch.zeros(2, prefix_len, 1, 1),
|
||||||
|
"value": torch.zeros(2, prefix_len, 1, 1),
|
||||||
|
"lens": torch.tensor([prefix_len, prefix_len]),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_restore_prompt_kv_cache(states_arg, row_state_indexes, row_branches):
|
||||||
|
del states_arg
|
||||||
|
captured["row_state_indexes"] = list(row_state_indexes)
|
||||||
|
captured["row_branches"] = list(row_branches)
|
||||||
|
|
||||||
|
def fake_prepare_inputs_for_generation(input_ids, images, timestep, **model_kwargs):
|
||||||
|
captured["input_ids"] = input_ids
|
||||||
|
captured["images"] = images.clone()
|
||||||
|
captured["timestep"] = timestep.clone()
|
||||||
|
captured["merged_attention_mask_shape"] = tuple(model_kwargs["attention_mask"].shape)
|
||||||
|
captured["merged_full_attn_spans"] = model_kwargs["full_attn_spans"]
|
||||||
|
return {"model_kwargs": model_kwargs}
|
||||||
|
|
||||||
|
pipeline._restore_prompt_kv_cache = fake_restore_prompt_kv_cache
|
||||||
|
pipeline.prepare_inputs_for_generation = fake_prepare_inputs_for_generation
|
||||||
|
pipeline.forward_call = lambda **kwargs: {"diffusion_prediction": torch.tensor([[10.0], [20.0], [1.0], [2.0]])}
|
||||||
|
pipeline._update_model_kwargs_for_generation = lambda model_output, model_kwargs: model_kwargs
|
||||||
|
pipeline._pipeline = SimpleNamespace(cfg_operator=lambda cond, uncond, scale, step: cond + uncond)
|
||||||
|
|
||||||
|
batch = InputBatch.make_batch(states)
|
||||||
|
out = pipeline.denoise_step(batch)
|
||||||
|
|
||||||
|
assert captured["row_state_indexes"] == [0, 1, 0, 1]
|
||||||
|
assert captured["row_branches"] == [0, 0, 1, 1]
|
||||||
|
assert captured["input_ids"] is None
|
||||||
|
assert tuple(captured["images"].shape) == (4, 1)
|
||||||
|
assert captured["timestep"].tolist() == [0.5, 0.0, 0.5, 0.0]
|
||||||
|
assert captured["merged_attention_mask_shape"] == (4, 1, 2, 6)
|
||||||
|
assert captured["merged_full_attn_spans"] == [[(4, 6)], [(4, 6)], [(4, 6)], [(4, 6)]]
|
||||||
|
torch.testing.assert_close(out, torch.tensor([[11.0], [22.0]]))
|
||||||
|
assert states[0].extra[_STEP_MODEL_KWARGS]["attention_mask"].shape == (2, 1, 2, 4)
|
||||||
|
assert states[1].extra[_STEP_MODEL_KWARGS]["attention_mask"].shape == (2, 1, 2, 6)
|
||||||
|
assert states[0].extra[_STEP_MODEL_KWARGS]["full_attn_spans"] == [[(2, 4)], [(2, 4)]]
|
||||||
|
assert states[1].extra[_STEP_MODEL_KWARGS]["full_attn_spans"] == [[(4, 6)], [(4, 6)]]
|
||||||
@@ -39,7 +39,8 @@ from vllm_omni.diffusion.sched.interface import (
|
|||||||
)
|
)
|
||||||
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
|
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
|
||||||
from vllm_omni.diffusion.worker.diffusion_worker import DiffusionWorker
|
from vllm_omni.diffusion.worker.diffusion_worker import DiffusionWorker
|
||||||
from vllm_omni.diffusion.worker.utils import RunnerOutput
|
from vllm_omni.diffusion.worker.input_batch import InputBatch
|
||||||
|
from vllm_omni.diffusion.worker.utils import DiffusionRequestState, RunnerOutput
|
||||||
from vllm_omni.engine.async_omni_engine import AsyncOmniEngine
|
from vllm_omni.engine.async_omni_engine import AsyncOmniEngine
|
||||||
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
|
||||||
@@ -254,7 +255,9 @@ def _make_runner():
|
|||||||
runner.cache_backend = None
|
runner.cache_backend = None
|
||||||
runner.offload_backend = None
|
runner.offload_backend = None
|
||||||
runner.state_cache = {}
|
runner.state_cache = {}
|
||||||
runner.kv_transfer_manager = SimpleNamespace()
|
runner.kv_transfer_manager = SimpleNamespace(
|
||||||
|
receive_multi_kv_cache_distributed=lambda req, cfg_kv_collect_func=None, target_device=None: None
|
||||||
|
)
|
||||||
return runner
|
return runner
|
||||||
|
|
||||||
|
|
||||||
@@ -270,7 +273,9 @@ def _make_distributed_runner(mode: str, device: torch.device):
|
|||||||
runner.cache_backend = None
|
runner.cache_backend = None
|
||||||
runner.offload_backend = None
|
runner.offload_backend = None
|
||||||
runner.state_cache = {}
|
runner.state_cache = {}
|
||||||
runner.kv_transfer_manager = SimpleNamespace()
|
runner.kv_transfer_manager = SimpleNamespace(
|
||||||
|
receive_multi_kv_cache_distributed=lambda req, cfg_kv_collect_func=None, target_device=None: None
|
||||||
|
)
|
||||||
return runner
|
return runner
|
||||||
|
|
||||||
|
|
||||||
@@ -298,6 +303,17 @@ def _make_batch_scheduler_output(reqs, *, step_id=0, finished_req_ids=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_input_batch_state(request_id: str, latent_value: float) -> DiffusionRequestState:
|
||||||
|
state = DiffusionRequestState(
|
||||||
|
request_id=request_id,
|
||||||
|
sampling=SimpleNamespace(),
|
||||||
|
prompts=None,
|
||||||
|
)
|
||||||
|
state.latents = torch.tensor([[latent_value]])
|
||||||
|
state.timesteps = torch.tensor([1.0])
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
def _make_cached_scheduler_output(request_id="req-1", step_id=1, finished_req_ids=None):
|
def _make_cached_scheduler_output(request_id="req-1", step_id=1, finished_req_ids=None):
|
||||||
return DiffusionSchedulerOutput(
|
return DiffusionSchedulerOutput(
|
||||||
step_id=step_id,
|
step_id=step_id,
|
||||||
@@ -374,6 +390,38 @@ def _distributed_step_worker(local_rank: int, world_size: int, mode: str, master
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.cpu
|
||||||
|
def test_input_batch_cached_repack_refreshes_state_references_without_prompt_embeds():
|
||||||
|
first_state = _make_input_batch_state("req-1", 1.0)
|
||||||
|
batch = InputBatch.make_batch([first_state])
|
||||||
|
assert batch.prompt_embeds is None
|
||||||
|
|
||||||
|
replacement_state = _make_input_batch_state("req-1", 2.0)
|
||||||
|
repacked = InputBatch.make_batch([replacement_state], cached_batch=batch)
|
||||||
|
|
||||||
|
assert repacked is batch
|
||||||
|
assert repacked.states[0] is replacement_state
|
||||||
|
assert repacked.prompt_embeds is None
|
||||||
|
torch.testing.assert_close(repacked.latents, replacement_state.latents)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.cpu
|
||||||
|
def test_input_batch_cached_repack_keeps_static_prompt_fields_for_same_composition():
|
||||||
|
first_state = _make_input_batch_state("req-1", 1.0)
|
||||||
|
first_state.prompt_embeds = torch.ones(1, 2, 3)
|
||||||
|
first_state.prompt_embeds_mask = torch.ones(1, 2, dtype=torch.bool)
|
||||||
|
batch = InputBatch.make_batch([first_state])
|
||||||
|
|
||||||
|
replacement_state = _make_input_batch_state("req-1", 2.0)
|
||||||
|
replacement_state.prompt_embeds = torch.full((1, 2, 3), 2.0)
|
||||||
|
replacement_state.prompt_embeds_mask = torch.ones(1, 2, dtype=torch.bool)
|
||||||
|
repacked = InputBatch.make_batch([replacement_state], cached_batch=batch)
|
||||||
|
|
||||||
|
assert repacked.states[0] is replacement_state
|
||||||
|
torch.testing.assert_close(repacked.latents, replacement_state.latents)
|
||||||
|
torch.testing.assert_close(repacked.prompt_embeds, torch.ones(1, 2, 3))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.cpu
|
@pytest.mark.cpu
|
||||||
class TestRunner:
|
class TestRunner:
|
||||||
"""DiffusionModelRunner.execute_stepwise"""
|
"""DiffusionModelRunner.execute_stepwise"""
|
||||||
@@ -427,6 +475,36 @@ class TestRunner:
|
|||||||
result = DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
|
result = DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_receives_kv_payload_before_prepare_encode(self, monkeypatch):
|
||||||
|
runner = _make_runner()
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
kv_payload = object()
|
||||||
|
|
||||||
|
class _CapturingStepPipeline(_StepPipeline):
|
||||||
|
def prepare_encode(self, state, **kwargs):
|
||||||
|
captured["past_key_values"] = getattr(state.sampling, "past_key_values", None)
|
||||||
|
return super().prepare_encode(state, **kwargs)
|
||||||
|
|
||||||
|
class _KVTransferManager:
|
||||||
|
def receive_multi_kv_cache_distributed(self, req, cfg_kv_collect_func=None, target_device=None):
|
||||||
|
captured["cfg_kv_collect_func"] = cfg_kv_collect_func
|
||||||
|
captured["target_device"] = target_device
|
||||||
|
req.sampling_params.past_key_values = kv_payload
|
||||||
|
|
||||||
|
runner.pipeline = _CapturingStepPipeline()
|
||||||
|
runner.pipeline.device = torch.device("cpu")
|
||||||
|
runner.od_config.cfg_kv_collect_func = "collect-cfg"
|
||||||
|
runner.kv_transfer_manager = _KVTransferManager()
|
||||||
|
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
|
||||||
|
req = _make_step_request()
|
||||||
|
|
||||||
|
DiffusionModelRunner.execute_stepwise(runner, _make_scheduler_output(req, step_id=0))
|
||||||
|
|
||||||
|
assert captured["past_key_values"] is kv_payload
|
||||||
|
assert captured["cfg_kv_collect_func"] == "collect-cfg"
|
||||||
|
assert captured["target_device"] == torch.device("cpu")
|
||||||
|
assert getattr(req.sampling_params, "past_key_values", None) is None
|
||||||
|
|
||||||
def test_rejects_missing_cached_state(self):
|
def test_rejects_missing_cached_state(self):
|
||||||
runner = _make_runner()
|
runner = _make_runner()
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from vllm_omni.diffusion.data import DiffusionOutput
|
from vllm_omni.diffusion.data import DiffusionOutput
|
||||||
|
from vllm_omni.diffusion.worker.input_batch import InputBatch
|
||||||
from vllm_omni.diffusion.worker.utils import DiffusionRequestState
|
from vllm_omni.diffusion.worker.utils import DiffusionRequestState
|
||||||
|
|
||||||
|
|
||||||
@@ -56,8 +57,8 @@ class SupportsStepExecution(Protocol):
|
|||||||
def prepare_encode(self, state: DiffusionRequestState, **kwargs: Any) -> DiffusionRequestState:
|
def prepare_encode(self, state: DiffusionRequestState, **kwargs: Any) -> DiffusionRequestState:
|
||||||
"""Prepare request-level inputs and return initialized state."""
|
"""Prepare request-level inputs and return initialized state."""
|
||||||
|
|
||||||
def denoise_step(self, state: DiffusionRequestState, **kwargs: Any) -> torch.Tensor | None:
|
def denoise_step(self, input_batch: InputBatch, **kwargs: Any) -> torch.Tensor | None:
|
||||||
"""Run one denoise step."""
|
"""Run one denoise forward on the runner-assembled batch."""
|
||||||
|
|
||||||
def step_scheduler(self, state: DiffusionRequestState, noise_pred: torch.Tensor, **kwargs: Any) -> None:
|
def step_scheduler(self, state: DiffusionRequestState, noise_pred: torch.Tensor, **kwargs: Any) -> None:
|
||||||
"""Run one scheduler step."""
|
"""Run one scheduler step."""
|
||||||
|
|||||||
@@ -374,6 +374,13 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
|||||||
sampling=copy.deepcopy(req.sampling_params),
|
sampling=copy.deepcopy(req.sampling_params),
|
||||||
prompts=req.prompts,
|
prompts=req.prompts,
|
||||||
)
|
)
|
||||||
|
state_req = copy.copy(req)
|
||||||
|
state_req.sampling_params = new_state.sampling
|
||||||
|
self.kv_transfer_manager.receive_multi_kv_cache_distributed(
|
||||||
|
state_req,
|
||||||
|
cfg_kv_collect_func=getattr(self.od_config, "cfg_kv_collect_func", None),
|
||||||
|
target_device=getattr(self.pipeline, "device", None),
|
||||||
|
)
|
||||||
self.state_cache[request_id] = new_state
|
self.state_cache[request_id] = new_state
|
||||||
resolved.append(new_state)
|
resolved.append(new_state)
|
||||||
|
|
||||||
@@ -452,8 +459,8 @@ class DiffusionModelRunner(OmniConnectorModelRunnerMixin):
|
|||||||
if not self.supports_step_mode():
|
if not self.supports_step_mode():
|
||||||
raise ValueError("Current pipeline does not support step execution.")
|
raise ValueError("Current pipeline does not support step execution.")
|
||||||
# Stepwise mode only supports the basic state-driven denoise path for now.
|
# Stepwise mode only supports the basic state-driven denoise path for now.
|
||||||
# Request-mode extras such as cache backends, KV transfer, editing inputs,
|
# Request-mode extras such as cache backends, editing inputs, and
|
||||||
# and similar features are not supported here yet.
|
# similar features are not supported here yet.
|
||||||
if self.od_config.cache_backend not in (None, "none"):
|
if self.od_config.cache_backend not in (None, "none"):
|
||||||
raise ValueError("Step mode does not support cache_backend yet.")
|
raise ValueError("Step mode does not support cache_backend yet.")
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ step, and step outputs are scattered back into request states by
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
@@ -496,7 +496,7 @@ def _prepare_prompt_embeds(
|
|||||||
*,
|
*,
|
||||||
embeds_out: torch.Tensor | None = None,
|
embeds_out: torch.Tensor | None = None,
|
||||||
mask_out: torch.Tensor | None = None,
|
mask_out: torch.Tensor | None = None,
|
||||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
||||||
prompt_embeds, prompt_embeds_mask = _prepare_padded_prompt_fields(
|
prompt_embeds, prompt_embeds_mask = _prepare_padded_prompt_fields(
|
||||||
states,
|
states,
|
||||||
embeds_attr="prompt_embeds",
|
embeds_attr="prompt_embeds",
|
||||||
@@ -505,8 +505,6 @@ def _prepare_prompt_embeds(
|
|||||||
embeds_out=embeds_out,
|
embeds_out=embeds_out,
|
||||||
mask_out=mask_out,
|
mask_out=mask_out,
|
||||||
)
|
)
|
||||||
if prompt_embeds is None:
|
|
||||||
raise ValueError("All requests must have `prompt_embeds` initialized.")
|
|
||||||
return prompt_embeds, prompt_embeds_mask
|
return prompt_embeds, prompt_embeds_mask
|
||||||
|
|
||||||
|
|
||||||
@@ -581,6 +579,10 @@ class InputBatch:
|
|||||||
source of truth. ``InputBatch`` only assembles a contiguous view for the
|
source of truth. ``InputBatch`` only assembles a contiguous view for the
|
||||||
current step and refreshes dynamic fields in-place when composition is
|
current step and refreshes dynamic fields in-place when composition is
|
||||||
unchanged.
|
unchanged.
|
||||||
|
|
||||||
|
``states`` is a narrow escape hatch for Hunyuan-style state-driven
|
||||||
|
pipelines that need request-private KV/cache metadata during denoise.
|
||||||
|
Other pipelines should continue to use the standard batch fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
request_ids: list[str]
|
request_ids: list[str]
|
||||||
@@ -591,7 +593,7 @@ class InputBatch:
|
|||||||
|
|
||||||
latents: torch.Tensor
|
latents: torch.Tensor
|
||||||
timesteps: torch.Tensor
|
timesteps: torch.Tensor
|
||||||
prompt_embeds: torch.Tensor
|
prompt_embeds: torch.Tensor | None
|
||||||
prompt_embeds_mask: torch.Tensor | None
|
prompt_embeds_mask: torch.Tensor | None
|
||||||
negative_prompt_embeds: torch.Tensor | None
|
negative_prompt_embeds: torch.Tensor | None
|
||||||
negative_prompt_embeds_mask: torch.Tensor | None
|
negative_prompt_embeds_mask: torch.Tensor | None
|
||||||
@@ -604,6 +606,7 @@ class InputBatch:
|
|||||||
img_shapes: list | None = None
|
img_shapes: list | None = None
|
||||||
txt_seq_lens: list[int] | None = None
|
txt_seq_lens: list[int] | None = None
|
||||||
negative_txt_seq_lens: list[int] | None = None
|
negative_txt_seq_lens: list[int] | None = None
|
||||||
|
states: Sequence[DiffusionRequestState] = field(default_factory=tuple)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if len(self.request_ids) != int(self.idx_mapping.numel()):
|
if len(self.request_ids) != int(self.idx_mapping.numel()):
|
||||||
@@ -648,6 +651,9 @@ class InputBatch:
|
|||||||
self,
|
self,
|
||||||
selected_states: Sequence[DiffusionRequestState],
|
selected_states: Sequence[DiffusionRequestState],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# Same-composition cache hits reuse static prompt fields; request ids
|
||||||
|
# must keep the same encoded prompt metadata for the batch lifetime.
|
||||||
|
self.states = tuple(selected_states)
|
||||||
self._refresh_dynamic_fields(selected_states)
|
self._refresh_dynamic_fields(selected_states)
|
||||||
|
|
||||||
def _rebuild(
|
def _rebuild(
|
||||||
@@ -662,6 +668,7 @@ class InputBatch:
|
|||||||
self.num_reqs_after_padding = len(request_ids)
|
self.num_reqs_after_padding = len(request_ids)
|
||||||
self.idx_mapping = idx_mapping
|
self.idx_mapping = idx_mapping
|
||||||
self.idx_mapping_np = idx_mapping_np
|
self.idx_mapping_np = idx_mapping_np
|
||||||
|
self.states = tuple(selected_states)
|
||||||
self.latents = _prepare_latents(selected_states, out=self.latents)
|
self.latents = _prepare_latents(selected_states, out=self.latents)
|
||||||
self.timesteps = _prepare_timesteps(selected_states, out=self.timesteps)
|
self.timesteps = _prepare_timesteps(selected_states, out=self.timesteps)
|
||||||
self._refresh_static_fields(selected_states)
|
self._refresh_static_fields(selected_states)
|
||||||
@@ -718,6 +725,7 @@ class InputBatch:
|
|||||||
selected_states,
|
selected_states,
|
||||||
"negative_txt_seq_lens",
|
"negative_txt_seq_lens",
|
||||||
),
|
),
|
||||||
|
states=tuple(selected_states),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user