[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:
authored and GitHub committed 2026-06-15 20:36:41 +08:00
1 parent 11829ccd75
commit ae937320ff
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
+3 -2
View File
@@ -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.")
+13 -5
View File
@@ -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),
) )