[Perf] Support request-level batching for Wan2.2 pipelines (#5676)
Signed-off-by: nagisa-kun <1434936049@qq.com> Signed-off-by: nagisa <1434936049@qq.com>
This commit is contained in:
1 parent
d1e230c95b
commit
2a315e1a2d
18 files changed
+1871
-374
No files matched your search
@@ -8,15 +8,28 @@ import torch
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
from tests.diffusion.models.wan2_2.conftest import StubTransformer, StubVAE, noop_progress_bar
|
from tests.diffusion.models.wan2_2.conftest import StubScheduler, StubTransformer, StubVAE, noop_progress_bar
|
||||||
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_i2v import (
|
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_i2v import (
|
||||||
Wan22I2VPipeline,
|
Wan22I2VPipeline,
|
||||||
|
get_wan22_i2v_post_process_func,
|
||||||
get_wan22_i2v_pre_process_func,
|
get_wan22_i2v_pre_process_func,
|
||||||
)
|
)
|
||||||
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||||
|
|
||||||
|
|
||||||
|
def test_wan22_i2v_postprocess_honors_request_output_type() -> None:
|
||||||
|
video = torch.zeros(1, 4, 1, 2, 2)
|
||||||
|
|
||||||
|
output = get_wan22_i2v_post_process_func(SimpleNamespace())(
|
||||||
|
video,
|
||||||
|
sampling_params=SimpleNamespace(output_type="latent"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert output is video
|
||||||
|
|
||||||
|
|
||||||
def _make_i2v_pipeline(*, expand_timesteps: bool) -> Wan22I2VPipeline:
|
def _make_i2v_pipeline(*, expand_timesteps: bool) -> Wan22I2VPipeline:
|
||||||
pipeline = object.__new__(Wan22I2VPipeline)
|
pipeline = object.__new__(Wan22I2VPipeline)
|
||||||
nn.Module.__init__(pipeline)
|
nn.Module.__init__(pipeline)
|
||||||
@@ -31,6 +44,29 @@ def _make_i2v_pipeline(*, expand_timesteps: bool) -> Wan22I2VPipeline:
|
|||||||
return pipeline
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
def _make_i2v_sampling(**overrides):
|
||||||
|
values = {
|
||||||
|
"height": 16,
|
||||||
|
"width": 16,
|
||||||
|
"num_frames": 5,
|
||||||
|
"num_inference_steps": 1,
|
||||||
|
"guidance_scale_provided": True,
|
||||||
|
"guidance_scale": 1.0,
|
||||||
|
"guidance_scale_2": None,
|
||||||
|
"guidance_scale_2_provided": False,
|
||||||
|
"boundary_ratio": None,
|
||||||
|
"generator": None,
|
||||||
|
"seed": None,
|
||||||
|
"num_outputs_per_prompt": 1,
|
||||||
|
"max_sequence_length": 8,
|
||||||
|
"latents": None,
|
||||||
|
"output_type": "latent",
|
||||||
|
"extra_args": {},
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return SimpleNamespace(**values)
|
||||||
|
|
||||||
|
|
||||||
def test_i2v_preprocess_requires_image_and_resizes_to_480p_aspect() -> None:
|
def test_i2v_preprocess_requires_image_and_resizes_to_480p_aspect() -> None:
|
||||||
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
||||||
request = SimpleNamespace(
|
request = SimpleNamespace(
|
||||||
@@ -53,6 +89,88 @@ def test_i2v_preprocess_requires_image_and_resizes_to_480p_aspect() -> None:
|
|||||||
preprocess(missing_image)
|
preprocess(missing_image)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_i2v_preprocess_request(last_image):
|
||||||
|
return SimpleNamespace(
|
||||||
|
prompt={
|
||||||
|
"prompt": "p",
|
||||||
|
"multi_modal_data": {
|
||||||
|
"image": Image.new("RGB", (320, 160), "red"),
|
||||||
|
"last_image": last_image,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sampling_params=SimpleNamespace(height=16, width=16),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_preprocess_treats_empty_last_image_list_as_absent() -> None:
|
||||||
|
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
||||||
|
|
||||||
|
result = preprocess(_make_i2v_preprocess_request([]))
|
||||||
|
|
||||||
|
assert result.prompt["multi_modal_data"]["last_image"] is None
|
||||||
|
assert result.batch_compatibility_key == ("wan22_i2v_last_image", False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_preprocess_unwraps_single_last_image_list() -> None:
|
||||||
|
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
||||||
|
last_image = Image.new("RGB", (16, 16), "blue")
|
||||||
|
|
||||||
|
result = preprocess(_make_i2v_preprocess_request([last_image]))
|
||||||
|
|
||||||
|
assert result.prompt["multi_modal_data"]["last_image"] is last_image
|
||||||
|
assert result.batch_compatibility_key == ("wan22_i2v_last_image", True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"last_image",
|
||||||
|
[
|
||||||
|
Image.new("RGB", (16, 16), "blue"),
|
||||||
|
torch.zeros(3, 16, 16),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_i2v_preprocess_preserves_supported_last_image(last_image) -> None:
|
||||||
|
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
||||||
|
|
||||||
|
result = preprocess(_make_i2v_preprocess_request(last_image))
|
||||||
|
|
||||||
|
assert result.prompt["multi_modal_data"]["last_image"] is last_image
|
||||||
|
assert result.batch_compatibility_key == ("wan22_i2v_last_image", True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_preprocess_loads_last_image_path(tmp_path) -> None:
|
||||||
|
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
||||||
|
path = tmp_path / "last.png"
|
||||||
|
Image.new("RGB", (16, 16), "blue").save(path)
|
||||||
|
|
||||||
|
result = preprocess(_make_i2v_preprocess_request(str(path)))
|
||||||
|
|
||||||
|
last_image = result.prompt["multi_modal_data"]["last_image"]
|
||||||
|
assert isinstance(last_image, Image.Image)
|
||||||
|
assert last_image.mode == "RGB"
|
||||||
|
assert result.batch_compatibility_key == ("wan22_i2v_last_image", True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_preprocess_rejects_multiple_last_images() -> None:
|
||||||
|
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="at most one last_image"):
|
||||||
|
preprocess(
|
||||||
|
_make_i2v_preprocess_request(
|
||||||
|
[
|
||||||
|
Image.new("RGB", (16, 16), "blue"),
|
||||||
|
Image.new("RGB", (16, 16), "green"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_preprocess_rejects_unsupported_last_image_type() -> None:
|
||||||
|
preprocess = get_wan22_i2v_pre_process_func(SimpleNamespace())
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Unsupported last_image format"):
|
||||||
|
preprocess(_make_i2v_preprocess_request({"not": "an image"}))
|
||||||
|
|
||||||
|
|
||||||
def test_i2v_diffuse_selects_stage_guidance_and_expands_timesteps() -> None:
|
def test_i2v_diffuse_selects_stage_guidance_and_expands_timesteps() -> None:
|
||||||
pipeline = _make_i2v_pipeline(expand_timesteps=True)
|
pipeline = _make_i2v_pipeline(expand_timesteps=True)
|
||||||
latents = torch.zeros(1, 4, 2, 4, 4)
|
latents = torch.zeros(1, 4, 2, 4, 4)
|
||||||
@@ -126,3 +244,169 @@ def test_i2v_prepare_latents_builds_expand_condition_and_first_frame_mask() -> N
|
|||||||
assert first_frame_mask.shape == (1, 1, 2, 2, 2)
|
assert first_frame_mask.shape == (1, 1, 2, 2, 2)
|
||||||
assert first_frame_mask[:, :, 0].sum() == 0
|
assert first_frame_mask[:, :, 0].sum() == 0
|
||||||
assert first_frame_mask[:, :, 1].sum() == 4
|
assert first_frame_mask[:, :, 1].sum() == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_prepare_latents_preserves_batched_image_conditions() -> None:
|
||||||
|
pipeline = _make_i2v_pipeline(expand_timesteps=True)
|
||||||
|
generators = [
|
||||||
|
torch.Generator(device="cpu").manual_seed(1),
|
||||||
|
torch.Generator(device="cpu").manual_seed(2),
|
||||||
|
]
|
||||||
|
|
||||||
|
latents, condition, first_frame_mask = pipeline.prepare_latents(
|
||||||
|
image=torch.zeros(2, 3, 16, 16),
|
||||||
|
batch_size=2,
|
||||||
|
num_channels_latents=4,
|
||||||
|
height=16,
|
||||||
|
width=16,
|
||||||
|
num_frames=5,
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=torch.device("cpu"),
|
||||||
|
generator=generators,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert latents.shape == (2, 4, 2, 2, 2)
|
||||||
|
assert condition.shape == (2, 4, 1, 2, 2)
|
||||||
|
assert first_frame_mask.shape == (2, 1, 2, 2, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_forward_batches_conditions_random_inputs_and_outputs(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_i2v.current_omni_platform",
|
||||||
|
SimpleNamespace(is_available=lambda: False),
|
||||||
|
)
|
||||||
|
pipeline = _make_i2v_pipeline(expand_timesteps=True)
|
||||||
|
pipeline.scheduler = StubScheduler([9])
|
||||||
|
pipeline.od_config = SimpleNamespace(flow_shift=5.0)
|
||||||
|
pipeline._sample_solver = "unipc"
|
||||||
|
pipeline._flow_shift = 5.0
|
||||||
|
pipeline.boundary_ratio = 0.875
|
||||||
|
pipeline.has_image_encoder = False
|
||||||
|
pipeline._guidance_scale = None
|
||||||
|
pipeline._guidance_scale_2 = None
|
||||||
|
pipeline._num_timesteps = None
|
||||||
|
pipeline._current_timestep = None
|
||||||
|
pipeline.check_inputs = lambda **kwargs: None
|
||||||
|
prepare_call = {}
|
||||||
|
|
||||||
|
def _fake_encode_prompt(**kwargs):
|
||||||
|
batch_size = len(kwargs["prompt"])
|
||||||
|
n = batch_size * kwargs["num_videos_per_prompt"]
|
||||||
|
return torch.zeros(n, 2, 3), None
|
||||||
|
|
||||||
|
def _fake_prepare_latents(**kwargs):
|
||||||
|
prepare_call.update(kwargs)
|
||||||
|
batch_size = kwargs["batch_size"]
|
||||||
|
return (
|
||||||
|
kwargs["latents"],
|
||||||
|
torch.zeros(batch_size, 4, 1, 2, 2),
|
||||||
|
torch.ones(batch_size, 1, 2, 2, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
pipeline.encode_prompt = _fake_encode_prompt # type: ignore[method-assign]
|
||||||
|
pipeline.prepare_latents = _fake_prepare_latents # type: ignore[method-assign]
|
||||||
|
pipeline.diffuse = lambda **kwargs: kwargs["latents"] # type: ignore[method-assign]
|
||||||
|
|
||||||
|
gen_a = torch.Generator(device="cpu").manual_seed(1)
|
||||||
|
gen_b = torch.Generator(device="cpu").manual_seed(2)
|
||||||
|
latents_a = torch.zeros(2, 4, 2, 2, 2)
|
||||||
|
latents_b = torch.ones(2, 4, 2, 2, 2)
|
||||||
|
image_a = torch.zeros(1, 3, 16, 16)
|
||||||
|
image_b = torch.ones(1, 3, 16, 16)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={"prompt": "first", "multi_modal_data": {"image": image_a}},
|
||||||
|
sampling_params=_make_i2v_sampling(
|
||||||
|
generator=gen_a,
|
||||||
|
latents=latents_a,
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={"prompt": "second", "multi_modal_data": {"image": image_b}},
|
||||||
|
sampling_params=_make_i2v_sampling(
|
||||||
|
generator=gen_b,
|
||||||
|
latents=latents_b,
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs = pipeline.forward(batch)
|
||||||
|
|
||||||
|
assert prepare_call["batch_size"] == 4
|
||||||
|
assert prepare_call["generator"] == [gen_a, gen_a, gen_b, gen_b]
|
||||||
|
torch.testing.assert_close(prepare_call["latents"], torch.cat([latents_a, latents_b]))
|
||||||
|
torch.testing.assert_close(
|
||||||
|
prepare_call["image"],
|
||||||
|
torch.cat([image_a, image_a, image_b, image_b]),
|
||||||
|
)
|
||||||
|
assert len(outputs) == 2
|
||||||
|
torch.testing.assert_close(outputs[0].output, latents_a)
|
||||||
|
torch.testing.assert_close(outputs[1].output, latents_b)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_forward_rejects_mismatched_tensor_condition_shapes(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_i2v.current_omni_platform",
|
||||||
|
SimpleNamespace(is_available=lambda: False),
|
||||||
|
)
|
||||||
|
pipeline = _make_i2v_pipeline(expand_timesteps=True)
|
||||||
|
pipeline.scheduler = StubScheduler([9])
|
||||||
|
pipeline.od_config = SimpleNamespace(flow_shift=5.0)
|
||||||
|
pipeline._sample_solver = "unipc"
|
||||||
|
pipeline._flow_shift = 5.0
|
||||||
|
pipeline.boundary_ratio = 0.875
|
||||||
|
pipeline.has_image_encoder = False
|
||||||
|
pipeline._guidance_scale = None
|
||||||
|
pipeline._guidance_scale_2 = None
|
||||||
|
pipeline._num_timesteps = None
|
||||||
|
pipeline._current_timestep = None
|
||||||
|
pipeline.check_inputs = lambda **kwargs: None
|
||||||
|
pipeline.encode_prompt = lambda **kwargs: (torch.zeros(2, 2, 3), None) # type: ignore[method-assign]
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={"prompt": "first", "multi_modal_data": {"image": torch.zeros(1, 3, 16, 16)}},
|
||||||
|
sampling_params=_make_i2v_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={"prompt": "second", "multi_modal_data": {"image": torch.zeros(1, 3, 16, 8)}},
|
||||||
|
sampling_params=_make_i2v_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="image condition"):
|
||||||
|
pipeline.forward(batch)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i2v_forward_rejects_mixed_last_image_presence() -> None:
|
||||||
|
pipeline = _make_i2v_pipeline(expand_timesteps=True)
|
||||||
|
image = torch.zeros(1, 3, 16, 16)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"multi_modal_data": {"image": image, "last_image": image},
|
||||||
|
},
|
||||||
|
sampling_params=_make_i2v_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={"prompt": "second", "multi_modal_data": {"image": image}},
|
||||||
|
sampling_params=_make_i2v_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="mix of provided and missing last_image"):
|
||||||
|
pipeline.forward(batch)
|
||||||
@@ -93,6 +93,29 @@ def _make_pipeline() -> Wan22Pipeline:
|
|||||||
return pipeline
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
def _make_sampling(**overrides):
|
||||||
|
values = {
|
||||||
|
"height": None,
|
||||||
|
"width": None,
|
||||||
|
"num_frames": 1,
|
||||||
|
"num_inference_steps": 2,
|
||||||
|
"guidance_scale_provided": True,
|
||||||
|
"guidance_scale": 1.0,
|
||||||
|
"guidance_scale_2": None,
|
||||||
|
"guidance_scale_2_provided": False,
|
||||||
|
"boundary_ratio": None,
|
||||||
|
"generator": None,
|
||||||
|
"seed": None,
|
||||||
|
"num_outputs_per_prompt": 1,
|
||||||
|
"max_sequence_length": 32,
|
||||||
|
"latents": None,
|
||||||
|
"output_type": "latent",
|
||||||
|
"extra_args": {},
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return SimpleNamespace(**values)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("sampling_params_kwargs", "expected_low", "expected_high"),
|
("sampling_params_kwargs", "expected_low", "expected_high"),
|
||||||
[
|
[
|
||||||
@@ -129,9 +152,10 @@ def test_forward_delegates_denoising_to_diffuse(
|
|||||||
)
|
)
|
||||||
batch = DiffusionRequestBatch(requests=[mock_req])
|
batch = DiffusionRequestBatch(requests=[mock_req])
|
||||||
|
|
||||||
output = pipeline.forward(batch)
|
outputs = pipeline.forward(batch)
|
||||||
|
|
||||||
assert torch.equal(output.output, torch.ones((1, 4, 1, 8, 8)))
|
assert len(outputs) == 1
|
||||||
|
assert torch.equal(outputs[0].output, torch.ones((1, 4, 1, 8, 8)))
|
||||||
assert torch.equal(captured["prompt_embeds"], torch.zeros(1, 32, 8))
|
assert torch.equal(captured["prompt_embeds"], torch.zeros(1, 32, 8))
|
||||||
assert torch.equal(captured["timesteps"], pipeline.scheduler.timesteps)
|
assert torch.equal(captured["timesteps"], pipeline.scheduler.timesteps)
|
||||||
assert captured["guidance_low"] == expected_low
|
assert captured["guidance_low"] == expected_low
|
||||||
@@ -142,6 +166,134 @@ def test_forward_delegates_denoising_to_diffuse(
|
|||||||
assert pipeline.scheduler.set_timesteps_calls == [(2, torch.device("cpu"))]
|
assert pipeline.scheduler.set_timesteps_calls == [(2, torch.device("cpu"))]
|
||||||
|
|
||||||
|
|
||||||
|
def test_forward_batches_text_generators_latents_and_splits_outputs() -> None:
|
||||||
|
pipeline = _make_pipeline()
|
||||||
|
encode_call = {}
|
||||||
|
prepare_call = {}
|
||||||
|
|
||||||
|
def _fake_encode_prompt(**kwargs):
|
||||||
|
encode_call.update(kwargs)
|
||||||
|
batch_size = len(kwargs["prompt"])
|
||||||
|
n = batch_size * kwargs["num_videos_per_prompt"]
|
||||||
|
return torch.arange(n, dtype=torch.float32).view(n, 1, 1), torch.zeros(n, 1, 1)
|
||||||
|
|
||||||
|
def _fake_prepare_latents(**kwargs):
|
||||||
|
prepare_call.update(kwargs)
|
||||||
|
return kwargs["latents"]
|
||||||
|
|
||||||
|
pipeline.encode_prompt = _fake_encode_prompt # type: ignore[method-assign]
|
||||||
|
pipeline.prepare_latents = _fake_prepare_latents # type: ignore[method-assign]
|
||||||
|
pipeline.diffuse = lambda **kwargs: kwargs["latents"] # type: ignore[method-assign]
|
||||||
|
|
||||||
|
gen_a = torch.Generator(device="cpu").manual_seed(1)
|
||||||
|
gen_b = torch.Generator(device="cpu").manual_seed(2)
|
||||||
|
latents_a = torch.zeros(2, 4, 1, 2, 2)
|
||||||
|
latents_b = torch.ones(2, 4, 1, 2, 2)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={"prompt": "first", "negative_prompt": "bad first"},
|
||||||
|
sampling_params=_make_sampling(
|
||||||
|
generator=gen_a,
|
||||||
|
latents=latents_a,
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={"prompt": "second", "negative_prompt": "bad second"},
|
||||||
|
sampling_params=_make_sampling(
|
||||||
|
generator=gen_b,
|
||||||
|
latents=latents_b,
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs = pipeline.forward(batch)
|
||||||
|
|
||||||
|
assert encode_call["prompt"] == ["first", "second"]
|
||||||
|
assert encode_call["negative_prompt"] == ["bad first", "bad second"]
|
||||||
|
assert prepare_call["batch_size"] == 4
|
||||||
|
assert prepare_call["generator"] == [gen_a, gen_a, gen_b, gen_b]
|
||||||
|
torch.testing.assert_close(prepare_call["latents"], torch.cat([latents_a, latents_b]))
|
||||||
|
assert len(outputs) == 2
|
||||||
|
torch.testing.assert_close(outputs[0].output, latents_a)
|
||||||
|
torch.testing.assert_close(outputs[1].output, latents_b)
|
||||||
|
|
||||||
|
|
||||||
|
def test_forward_batches_precomputed_prompt_embeddings() -> None:
|
||||||
|
pipeline = _make_pipeline()
|
||||||
|
diffuse_call = {}
|
||||||
|
pipeline.encode_prompt = lambda **kwargs: pytest.fail("text encoder must not run") # type: ignore[method-assign]
|
||||||
|
pipeline.prepare_latents = lambda **kwargs: torch.zeros(kwargs["batch_size"], 4, 1, 2, 2) # type: ignore[method-assign]
|
||||||
|
|
||||||
|
def _fake_diffuse(**kwargs):
|
||||||
|
diffuse_call.update(kwargs)
|
||||||
|
return kwargs["latents"]
|
||||||
|
|
||||||
|
pipeline.diffuse = _fake_diffuse # type: ignore[method-assign]
|
||||||
|
embeds_a = torch.zeros(3, 4)
|
||||||
|
embeds_b = torch.ones(3, 4)
|
||||||
|
negative_a = torch.full((3, 4), 2.0)
|
||||||
|
negative_b = torch.full((3, 4), 3.0)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={"prompt_embeds": embeds_a, "negative_prompt_embeds": negative_a},
|
||||||
|
sampling_params=_make_sampling(guidance_scale=4.0),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={"prompt_embeds": embeds_b, "negative_prompt_embeds": negative_b},
|
||||||
|
sampling_params=_make_sampling(guidance_scale=4.0),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs = pipeline.forward(batch)
|
||||||
|
|
||||||
|
torch.testing.assert_close(diffuse_call["prompt_embeds"], torch.stack([embeds_a, embeds_b]))
|
||||||
|
torch.testing.assert_close(diffuse_call["negative_prompt_embeds"], torch.stack([negative_a, negative_b]))
|
||||||
|
assert len(outputs) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_latents_with_request_generators_matches_single_generation() -> None:
|
||||||
|
pipeline = _make_pipeline()
|
||||||
|
kwargs = {
|
||||||
|
"num_channels_latents": 4,
|
||||||
|
"height": 16,
|
||||||
|
"width": 16,
|
||||||
|
"num_frames": 5,
|
||||||
|
"dtype": torch.float32,
|
||||||
|
"device": torch.device("cpu"),
|
||||||
|
}
|
||||||
|
|
||||||
|
batched = Wan22Pipeline.prepare_latents(
|
||||||
|
pipeline,
|
||||||
|
batch_size=2,
|
||||||
|
generator=[torch.Generator().manual_seed(1), torch.Generator().manual_seed(2)],
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
singles = torch.cat(
|
||||||
|
[
|
||||||
|
Wan22Pipeline.prepare_latents(
|
||||||
|
pipeline,
|
||||||
|
batch_size=1,
|
||||||
|
generator=torch.Generator().manual_seed(seed),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
for seed in (1, 2)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
torch.testing.assert_close(batched, singles)
|
||||||
|
assert not torch.equal(batched[0], batched[1])
|
||||||
|
|
||||||
|
|
||||||
def test_diffuse_runs_prediction_and_scheduler_for_each_timestep() -> None:
|
def test_diffuse_runs_prediction_and_scheduler_for_each_timestep() -> None:
|
||||||
pipeline = _make_pipeline()
|
pipeline = _make_pipeline()
|
||||||
latents = torch.zeros((1, 1, 1, 2, 2), dtype=torch.float32)
|
latents = torch.zeros((1, 1, 1, 2, 2), dtype=torch.float32)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import torch
|
|||||||
import vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 as wan22_module
|
import vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 as wan22_module
|
||||||
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
|
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
|
||||||
create_transformer_from_config,
|
create_transformer_from_config,
|
||||||
|
get_wan22_post_process_func,
|
||||||
load_transformer_config,
|
load_transformer_config,
|
||||||
retrieve_latents,
|
retrieve_latents,
|
||||||
)
|
)
|
||||||
@@ -26,6 +27,17 @@ class _LatentDist:
|
|||||||
return torch.tensor([2.0])
|
return torch.tensor([2.0])
|
||||||
|
|
||||||
|
|
||||||
|
def test_wan22_postprocess_honors_request_output_type() -> None:
|
||||||
|
video = torch.zeros(1, 4, 1, 2, 2)
|
||||||
|
|
||||||
|
output = get_wan22_post_process_func(SimpleNamespace())(
|
||||||
|
video,
|
||||||
|
sampling_params=SimpleNamespace(output_type="latent"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert output is video
|
||||||
|
|
||||||
|
|
||||||
def test_retrieve_latents_supports_sample_mode_argmax_and_direct_latents() -> None:
|
def test_retrieve_latents_supports_sample_mode_argmax_and_direct_latents() -> None:
|
||||||
generator = torch.Generator(device="cpu")
|
generator = torch.Generator(device="cpu")
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,307 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import PIL.Image
|
||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
|
|
||||||
|
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_s2v import (
|
||||||
|
Wan22S2VPipeline,
|
||||||
|
_make_clip_generators,
|
||||||
|
)
|
||||||
from vllm_omni.diffusion.models.wan2_2.wan2_2_s2v_transformer import WanS2VTransformer3DModel
|
from vllm_omni.diffusion.models.wan2_2.wan2_2_s2v_transformer import WanS2VTransformer3DModel
|
||||||
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||||
|
|
||||||
|
|
||||||
|
def _make_s2v_sampling(**overrides):
|
||||||
|
values = {
|
||||||
|
"height": 16,
|
||||||
|
"width": 16,
|
||||||
|
"num_frames": 8,
|
||||||
|
"num_inference_steps": 1,
|
||||||
|
"guidance_scale_provided": True,
|
||||||
|
"guidance_scale": 1.0,
|
||||||
|
"generator": None,
|
||||||
|
"seed": None,
|
||||||
|
"num_outputs_per_prompt": 1,
|
||||||
|
"max_sequence_length": 8,
|
||||||
|
"latents": None,
|
||||||
|
"output_type": "latent",
|
||||||
|
"extra_args": {},
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return SimpleNamespace(**values)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_s2v_validation_pipeline() -> Wan22S2VPipeline:
|
||||||
|
pipeline = object.__new__(Wan22S2VPipeline)
|
||||||
|
nn.Module.__init__(pipeline)
|
||||||
|
pipeline.device = torch.device("cpu")
|
||||||
|
pipeline.transformer = SimpleNamespace(dtype=torch.float32)
|
||||||
|
pipeline.vae_scale_factor_spatial = 8
|
||||||
|
pipeline.resolution_divisor = 16
|
||||||
|
pipeline.motion_frames = 7
|
||||||
|
pipeline.drop_first_motion = True
|
||||||
|
pipeline._DEFAULT_INFER_FRAMES = 8
|
||||||
|
pipeline._guidance_scale = None
|
||||||
|
pipeline.check_inputs = lambda *args, **kwargs: None
|
||||||
|
pipeline.encode_prompt = lambda **kwargs: (torch.zeros(2, 2, 3), None) # type: ignore[method-assign]
|
||||||
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
def test_s2v_predict_noise_keeps_batch_dimension() -> None:
|
||||||
|
pipeline = object.__new__(Wan22S2VPipeline)
|
||||||
|
nn.Module.__init__(pipeline)
|
||||||
|
pipeline.device = torch.device("cpu")
|
||||||
|
transformer = MagicMock()
|
||||||
|
transformer.parameters.return_value = iter([torch.zeros(1, dtype=torch.bfloat16)])
|
||||||
|
transformer.return_value = (torch.zeros(1, 16, 2, 2, 2),)
|
||||||
|
pipeline.transformer = transformer
|
||||||
|
|
||||||
|
result = pipeline.predict_noise(hidden_states=torch.zeros(1, 16, 2, 2, 2))
|
||||||
|
|
||||||
|
assert result.shape == (1, 16, 2, 2, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_s2v_clip_generators_preserve_main_seed_per_clip_behavior() -> None:
|
||||||
|
explicit_generator = torch.Generator(device="cpu").manual_seed(999)
|
||||||
|
|
||||||
|
generators = _make_clip_generators(
|
||||||
|
seeds=[1234, 5678, None, None],
|
||||||
|
request_generators=[
|
||||||
|
torch.Generator(device="cpu").manual_seed(1234),
|
||||||
|
torch.Generator(device="cpu").manual_seed(5678),
|
||||||
|
explicit_generator,
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
clip_index=2,
|
||||||
|
device=torch.device("cpu"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [generator.initial_seed() for generator in generators if generator is not None] == [1236, 5680, 999, 2]
|
||||||
|
assert generators[2] is explicit_generator
|
||||||
|
|
||||||
|
|
||||||
|
def test_s2v_forward_rejects_different_num_repeat() -> None:
|
||||||
|
pipeline = _make_s2v_validation_pipeline()
|
||||||
|
pipeline.encode_audio = lambda *args, **kwargs: (torch.zeros(1, 1, 2, 16), 2, 16) # type: ignore[method-assign]
|
||||||
|
image = PIL.Image.new("RGB", (16, 16))
|
||||||
|
audio = np.zeros(16000, dtype=np.float32)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"multi_modal_data": {"image": image, "audio": audio},
|
||||||
|
"additional_information": {"num_repeat": 1},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={
|
||||||
|
"prompt": "second",
|
||||||
|
"multi_modal_data": {"image": image, "audio": audio},
|
||||||
|
"additional_information": {"num_repeat": 2},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="num_repeat"):
|
||||||
|
pipeline.forward(batch)
|
||||||
|
|
||||||
|
|
||||||
|
def test_s2v_forward_rejects_different_audio_time_lengths() -> None:
|
||||||
|
pipeline = _make_s2v_validation_pipeline()
|
||||||
|
pipeline.encode_audio = lambda audio, **kwargs: ( # type: ignore[method-assign]
|
||||||
|
torch.zeros(1, 1, 2, 16 if audio[0] == 0 else 24),
|
||||||
|
1,
|
||||||
|
16 if audio[0] == 0 else 24,
|
||||||
|
)
|
||||||
|
image = PIL.Image.new("RGB", (16, 16))
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"multi_modal_data": {"image": image, "audio": np.zeros(16000, dtype=np.float32)},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={
|
||||||
|
"prompt": "second",
|
||||||
|
"multi_modal_data": {"image": image, "audio": np.ones(16000, dtype=np.float32)},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="audio embedding time lengths"):
|
||||||
|
pipeline.forward(batch)
|
||||||
|
|
||||||
|
|
||||||
|
def test_s2v_forward_rejects_different_init_first_frame() -> None:
|
||||||
|
pipeline = _make_s2v_validation_pipeline()
|
||||||
|
image = PIL.Image.new("RGB", (16, 16))
|
||||||
|
audio = np.zeros(16000, dtype=np.float32)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"multi_modal_data": {"image": image, "audio": audio},
|
||||||
|
"additional_information": {"init_first_frame": True},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={
|
||||||
|
"prompt": "second",
|
||||||
|
"multi_modal_data": {"image": image, "audio": audio},
|
||||||
|
"additional_information": {"init_first_frame": False},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="init_first_frame"):
|
||||||
|
pipeline.forward(batch)
|
||||||
|
|
||||||
|
|
||||||
|
def test_s2v_forward_rejects_different_raw_audio_shapes() -> None:
|
||||||
|
pipeline = _make_s2v_validation_pipeline()
|
||||||
|
pipeline.encode_audio = lambda *args, **kwargs: (torch.zeros(1, 1, 2, 16), 1, 16) # type: ignore[method-assign]
|
||||||
|
image = PIL.Image.new("RGB", (16, 16))
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"multi_modal_data": {"image": image, "audio": np.zeros(16000, dtype=np.float32)},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={
|
||||||
|
"prompt": "second",
|
||||||
|
"multi_modal_data": {"image": image, "audio": np.zeros(8000, dtype=np.float32)},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="raw audio shapes and sample rates"):
|
||||||
|
pipeline.forward(batch)
|
||||||
|
|
||||||
|
|
||||||
|
def test_s2v_forward_batches_request_local_inputs_and_splits_outputs() -> None:
|
||||||
|
pipeline = object.__new__(Wan22S2VPipeline)
|
||||||
|
nn.Module.__init__(pipeline)
|
||||||
|
pipeline.device = torch.device("cpu")
|
||||||
|
pipeline.transformer = MagicMock()
|
||||||
|
pipeline.transformer.dtype = torch.float32
|
||||||
|
pipeline.transformer.parameters.return_value = iter([torch.zeros(1, dtype=torch.float32)])
|
||||||
|
pipeline.transformer.casual_audio_encoder = None
|
||||||
|
pipeline.transformer.encode_audio.side_effect = lambda audio, _motion: {"audio_emb": audio}
|
||||||
|
pipeline.vae = MagicMock()
|
||||||
|
pipeline.vae.dtype = torch.float32
|
||||||
|
pipeline.vae.decode.return_value = (torch.zeros(4, 3, 8, 16, 16),)
|
||||||
|
pipeline.od_config = SimpleNamespace(
|
||||||
|
enable_cpu_offload=False,
|
||||||
|
parallel_config=SimpleNamespace(use_hsdp=False),
|
||||||
|
)
|
||||||
|
pipeline.scheduler = MagicMock(timesteps=torch.tensor([1.0]))
|
||||||
|
pipeline.vae_scale_factor_spatial = 8
|
||||||
|
pipeline.resolution_divisor = 16
|
||||||
|
pipeline.motion_frames = 7
|
||||||
|
pipeline.drop_first_motion = True
|
||||||
|
pipeline._DEFAULT_INFER_FRAMES = 8
|
||||||
|
pipeline._guidance_scale = None
|
||||||
|
pipeline._num_timesteps = None
|
||||||
|
pipeline.check_inputs = lambda *args, **kwargs: None
|
||||||
|
pipeline.encode_prompt = MagicMock(return_value=(torch.zeros(4, 2, 3), torch.ones(4, 2, 3)))
|
||||||
|
pipeline.encode_audio = MagicMock(return_value=(torch.zeros(1, 1, 2, 8), 1, 8))
|
||||||
|
pipeline.encode_ref_image = MagicMock(return_value=torch.zeros(1, 16, 1, 2, 2))
|
||||||
|
pipeline.prepare_motion_latents = MagicMock(
|
||||||
|
side_effect=lambda pixels, **_: torch.zeros(pixels.shape[0], 16, 2, 2, 2)
|
||||||
|
)
|
||||||
|
pipeline._denormalize_latents = lambda latents: latents
|
||||||
|
pipeline.diffuse = MagicMock(side_effect=lambda **kwargs: kwargs["latents"])
|
||||||
|
|
||||||
|
image = PIL.Image.new("RGB", (16, 16))
|
||||||
|
audio_a = np.zeros(16000, dtype=np.float32)
|
||||||
|
audio_b = np.ones(16000, dtype=np.float32)
|
||||||
|
latents_a = torch.zeros(2, 16, 2, 2, 2)
|
||||||
|
latents_b = torch.ones(2, 16, 2, 2, 2)
|
||||||
|
generators_a = [torch.Generator().manual_seed(1), torch.Generator().manual_seed(2)]
|
||||||
|
generators_b = [torch.Generator().manual_seed(3), torch.Generator().manual_seed(4)]
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"negative_prompt": "negative first",
|
||||||
|
"multi_modal_data": {"image": image, "audio": audio_a},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
generator=generators_a,
|
||||||
|
latents=latents_a,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={
|
||||||
|
"prompt": "second",
|
||||||
|
"negative_prompt": "negative second",
|
||||||
|
"multi_modal_data": {"image": image, "audio": audio_b},
|
||||||
|
},
|
||||||
|
sampling_params=_make_s2v_sampling(
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
generator=generators_b,
|
||||||
|
latents=latents_b,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_s2v.current_omni_platform") as platform:
|
||||||
|
platform.is_available.return_value = False
|
||||||
|
outputs = pipeline.forward(batch)
|
||||||
|
|
||||||
|
assert len(outputs) == 2
|
||||||
|
assert outputs[0].output[0].shape[0] == 2
|
||||||
|
assert outputs[1].output[0].shape[0] == 2
|
||||||
|
assert outputs[0].output[1].shape == (16000,)
|
||||||
|
assert outputs[1].output[1].shape == (16000,)
|
||||||
|
np.testing.assert_array_equal(outputs[0].output[1], audio_a)
|
||||||
|
np.testing.assert_array_equal(outputs[1].output[1], audio_b)
|
||||||
|
assert np.shares_memory(outputs[0].output[1], audio_a)
|
||||||
|
assert np.shares_memory(outputs[1].output[1], audio_b)
|
||||||
|
torch.testing.assert_close(pipeline.diffuse.call_args.kwargs["latents"], torch.cat([latents_a, latents_b]))
|
||||||
|
assert pipeline.diffuse.call_args.kwargs["clip_generator"] == generators_a + generators_b
|
||||||
|
assert pipeline.encode_prompt.call_args.kwargs["prompt"] == ["first", "second"]
|
||||||
|
assert pipeline.encode_prompt.call_args.kwargs["negative_prompt"] == ["negative first", "negative second"]
|
||||||
|
|
||||||
|
|
||||||
def test_s2v_exposes_hsdp_shard_conditions_for_transformer_blocks():
|
def test_s2v_exposes_hsdp_shard_conditions_for_transformer_blocks():
|
||||||
model = object.__new__(WanS2VTransformer3DModel)
|
model = object.__new__(WanS2VTransformer3DModel)
|
||||||
nn.Module.__init__(model)
|
nn.Module.__init__(model)
|
||||||
@@ -260,31 +553,37 @@ def test_s2v_pipeline_hsdp_forward_complete_process():
|
|||||||
pipeline._prompt_clean = Wan22S2VPipeline._prompt_clean
|
pipeline._prompt_clean = Wan22S2VPipeline._prompt_clean
|
||||||
|
|
||||||
# -- Build request --
|
# -- Build request --
|
||||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
sampling_params = _make_s2v_sampling(
|
||||||
|
height=704,
|
||||||
sampling_params = MagicMock()
|
width=1024,
|
||||||
sampling_params.height = 704
|
num_frames=80,
|
||||||
sampling_params.width = 1024
|
num_inference_steps=5,
|
||||||
sampling_params.num_frames = 80
|
guidance_scale=4.5,
|
||||||
sampling_params.num_inference_steps = 5
|
generator=torch.Generator(device="cpu").manual_seed(42),
|
||||||
sampling_params.guidance_scale = 4.5
|
max_sequence_length=512,
|
||||||
sampling_params.guidance_scale_provided = True
|
)
|
||||||
sampling_params.generator = None
|
|
||||||
sampling_params.seed = 42
|
|
||||||
|
|
||||||
ref_image = PIL.Image.new("RGB", (1024, 704))
|
ref_image = PIL.Image.new("RGB", (1024, 704))
|
||||||
audio_data = np.zeros(16000, dtype=np.float32)
|
audio_data = np.zeros(16000, dtype=np.float32)
|
||||||
|
|
||||||
req = MagicMock(spec=OmniDiffusionRequest)
|
req = DiffusionRequestBatch(
|
||||||
req.prompts = [
|
requests=[
|
||||||
{
|
SimpleNamespace(
|
||||||
"prompt": "test prompt",
|
request_id="test",
|
||||||
"negative_prompt": "bad quality",
|
prompt={
|
||||||
"multi_modal_data": {"image": ref_image, "audio": audio_data},
|
"prompt": "test prompt",
|
||||||
"additional_information": {"audio_path": audio_data, "pose_video": None, "init_first_frame": False},
|
"negative_prompt": "bad quality",
|
||||||
}
|
"multi_modal_data": {"image": ref_image, "audio": audio_data},
|
||||||
]
|
"additional_information": {
|
||||||
req.sampling_params = sampling_params
|
"audio_path": audio_data,
|
||||||
|
"pose_video": None,
|
||||||
|
"init_first_frame": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sampling_params=sampling_params,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# -- Mock methods that use complex internal state --
|
# -- Mock methods that use complex internal state --
|
||||||
pipeline.encode_audio = MagicMock(return_value=(torch.zeros(1, 25, 64, 80), 1, 80))
|
pipeline.encode_audio = MagicMock(return_value=(torch.zeros(1, 25, 64, 80), 1, 80))
|
||||||
@@ -295,7 +594,7 @@ def test_s2v_pipeline_hsdp_forward_complete_process():
|
|||||||
pipeline.progress_bar.return_value.__exit__ = MagicMock(return_value=False)
|
pipeline.progress_bar.return_value.__exit__ = MagicMock(return_value=False)
|
||||||
|
|
||||||
# Mock predict_noise_maybe_with_cfg from CFGParallelMixin
|
# Mock predict_noise_maybe_with_cfg from CFGParallelMixin
|
||||||
pipeline.predict_noise_maybe_with_cfg = MagicMock(return_value=torch.zeros(16, 20, 88, 128))
|
pipeline.predict_noise_maybe_with_cfg = MagicMock(return_value=torch.zeros(1, 16, 20, 88, 128))
|
||||||
pipeline.predict_noise = Wan22S2VPipeline.predict_noise.__get__(pipeline)
|
pipeline.predict_noise = Wan22S2VPipeline.predict_noise.__get__(pipeline)
|
||||||
|
|
||||||
# Mock platform methods
|
# Mock platform methods
|
||||||
@@ -306,7 +605,7 @@ def test_s2v_pipeline_hsdp_forward_complete_process():
|
|||||||
with patch(
|
with patch(
|
||||||
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_s2v.load_audio", return_value=(audio_data, 16000)
|
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_s2v.load_audio", return_value=(audio_data, 16000)
|
||||||
):
|
):
|
||||||
result = Wan22S2VPipeline.forward(pipeline, req=req)
|
result = Wan22S2VPipeline.forward(pipeline, req=req)[0]
|
||||||
|
|
||||||
# -- Assertions --
|
# -- Assertions --
|
||||||
# Text encoder was called
|
# Text encoder was called
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import torch
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
from torch import nn
|
from torch import nn
|
||||||
|
|
||||||
from tests.diffusion.models.wan2_2.conftest import StubTransformer, StubVAE, noop_progress_bar
|
from tests.diffusion.models.wan2_2.conftest import StubScheduler, StubTransformer, StubVAE, noop_progress_bar
|
||||||
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_vace import (
|
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_vace import (
|
||||||
Wan22VACEPipeline,
|
Wan22VACEPipeline,
|
||||||
create_vace_transformer_from_config,
|
create_vace_transformer_from_config,
|
||||||
get_wan22_vace_pre_process_func,
|
get_wan22_vace_pre_process_func,
|
||||||
)
|
)
|
||||||
from vllm_omni.diffusion.models.wan2_2.wan2_2_vace_transformer import WanVACETransformer3DModel
|
from vllm_omni.diffusion.models.wan2_2.wan2_2_vace_transformer import WanVACETransformer3DModel
|
||||||
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||||
|
|
||||||
@@ -32,6 +33,27 @@ def _make_vace_pipeline() -> Wan22VACEPipeline:
|
|||||||
return pipeline
|
return pipeline
|
||||||
|
|
||||||
|
|
||||||
|
def _make_vace_sampling(**overrides):
|
||||||
|
values = {
|
||||||
|
"height": 16,
|
||||||
|
"width": 16,
|
||||||
|
"num_frames": 5,
|
||||||
|
"num_inference_steps": 1,
|
||||||
|
"guidance_scale_provided": True,
|
||||||
|
"guidance_scale": 1.0,
|
||||||
|
"boundary_ratio": None,
|
||||||
|
"generator": None,
|
||||||
|
"seed": None,
|
||||||
|
"num_outputs_per_prompt": 1,
|
||||||
|
"max_sequence_length": 8,
|
||||||
|
"latents": None,
|
||||||
|
"output_type": "latent",
|
||||||
|
"extra_args": {},
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return SimpleNamespace(**values)
|
||||||
|
|
||||||
|
|
||||||
def test_vace_preprocess_collects_reference_video_and_mask_inputs() -> None:
|
def test_vace_preprocess_collects_reference_video_and_mask_inputs() -> None:
|
||||||
preprocess = get_wan22_vace_pre_process_func(SimpleNamespace())
|
preprocess = get_wan22_vace_pre_process_func(SimpleNamespace())
|
||||||
ref = Image.new("RGB", (320, 160), "green")
|
ref = Image.new("RGB", (320, 160), "green")
|
||||||
@@ -138,3 +160,189 @@ def test_vace_diffuse_passes_context_and_scale_to_cfg_branches() -> None:
|
|||||||
assert calls[0]["positive_kwargs"]["vace_context"] is vace_context
|
assert calls[0]["positive_kwargs"]["vace_context"] is vace_context
|
||||||
assert calls[0]["negative_kwargs"]["vace_context_scale"] == 0.75
|
assert calls[0]["negative_kwargs"]["vace_context_scale"] == 0.75
|
||||||
torch.testing.assert_close(result, torch.ones_like(latents))
|
torch.testing.assert_close(result, torch.ones_like(latents))
|
||||||
|
|
||||||
|
|
||||||
|
def test_vace_forward_batches_random_inputs_and_splits_outputs(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_vace.current_omni_platform",
|
||||||
|
SimpleNamespace(is_available=lambda: False),
|
||||||
|
)
|
||||||
|
pipeline = _make_vace_pipeline()
|
||||||
|
pipeline.transformer.vace_patch_embedding = None
|
||||||
|
pipeline.transformer_2 = None
|
||||||
|
pipeline.scheduler = StubScheduler([9])
|
||||||
|
pipeline.od_config = SimpleNamespace(flow_shift=3.0)
|
||||||
|
pipeline._sample_solver = "unipc"
|
||||||
|
pipeline._flow_shift = 3.0
|
||||||
|
pipeline.boundary_ratio = None
|
||||||
|
pipeline._guidance_scale = None
|
||||||
|
pipeline._num_timesteps = None
|
||||||
|
pipeline._current_timestep = None
|
||||||
|
pipeline.check_inputs = lambda **kwargs: None
|
||||||
|
prepare_call = {}
|
||||||
|
|
||||||
|
def _fake_encode_prompt(**kwargs):
|
||||||
|
batch_size = len(kwargs["prompt"])
|
||||||
|
n = batch_size * kwargs["num_videos_per_prompt"]
|
||||||
|
return torch.zeros(n, 2, 3), None
|
||||||
|
|
||||||
|
def _fake_prepare_latents(**kwargs):
|
||||||
|
prepare_call.update(kwargs)
|
||||||
|
return kwargs["latents"]
|
||||||
|
|
||||||
|
pipeline.encode_prompt = _fake_encode_prompt # type: ignore[method-assign]
|
||||||
|
pipeline.prepare_latents = _fake_prepare_latents # type: ignore[method-assign]
|
||||||
|
pipeline.diffuse = lambda **kwargs: kwargs["latents"] # type: ignore[method-assign]
|
||||||
|
gen_a = torch.Generator(device="cpu").manual_seed(1)
|
||||||
|
gen_b = torch.Generator(device="cpu").manual_seed(2)
|
||||||
|
latents_a = torch.zeros(2, 4, 2, 2, 2)
|
||||||
|
latents_b = torch.ones(2, 4, 2, 2, 2)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={"prompt": "first"},
|
||||||
|
sampling_params=_make_vace_sampling(
|
||||||
|
generator=gen_a,
|
||||||
|
latents=latents_a,
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={"prompt": "second"},
|
||||||
|
sampling_params=_make_vace_sampling(
|
||||||
|
generator=gen_b,
|
||||||
|
latents=latents_b,
|
||||||
|
num_outputs_per_prompt=2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs = pipeline.forward(batch)
|
||||||
|
|
||||||
|
assert prepare_call["batch_size"] == 4
|
||||||
|
assert prepare_call["generator"] == [gen_a, gen_a, gen_b, gen_b]
|
||||||
|
torch.testing.assert_close(prepare_call["latents"], torch.cat([latents_a, latents_b]))
|
||||||
|
assert len(outputs) == 2
|
||||||
|
torch.testing.assert_close(outputs[0].output, latents_a)
|
||||||
|
torch.testing.assert_close(outputs[1].output, latents_b)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vace_forward_rejects_mismatched_condition_shapes(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_vace.current_omni_platform",
|
||||||
|
SimpleNamespace(is_available=lambda: False),
|
||||||
|
)
|
||||||
|
pipeline = _make_vace_pipeline()
|
||||||
|
pipeline.transformer.vace_patch_embedding = object()
|
||||||
|
pipeline.transformer_2 = None
|
||||||
|
pipeline.scheduler = StubScheduler([9])
|
||||||
|
pipeline.od_config = SimpleNamespace(flow_shift=3.0)
|
||||||
|
pipeline._sample_solver = "unipc"
|
||||||
|
pipeline._flow_shift = 3.0
|
||||||
|
pipeline.boundary_ratio = None
|
||||||
|
pipeline._guidance_scale = None
|
||||||
|
pipeline._num_timesteps = None
|
||||||
|
pipeline._current_timestep = None
|
||||||
|
pipeline.check_inputs = lambda **kwargs: None
|
||||||
|
pipeline.encode_prompt = lambda **kwargs: (torch.zeros(2, 2, 3), None) # type: ignore[method-assign]
|
||||||
|
|
||||||
|
def _fake_preprocess(video, **kwargs):
|
||||||
|
del kwargs
|
||||||
|
mask = torch.ones_like(video)
|
||||||
|
return video, mask, [[]]
|
||||||
|
|
||||||
|
pipeline.preprocess_conditions = _fake_preprocess # type: ignore[method-assign]
|
||||||
|
pipeline.prepare_video_latents = ( # type: ignore[method-assign]
|
||||||
|
lambda video, mask, refs, generator, device: torch.zeros(1, 8, video.shape[2], 2, 2)
|
||||||
|
)
|
||||||
|
pipeline.prepare_masks = lambda mask, refs: torch.zeros(1, 4, mask.shape[2], 2, 2) # type: ignore[method-assign]
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"additional_information": {"source_video": torch.zeros(1, 3, 5, 16, 16)},
|
||||||
|
},
|
||||||
|
sampling_params=_make_vace_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={
|
||||||
|
"prompt": "second",
|
||||||
|
"additional_information": {"source_video": torch.zeros(1, 3, 3, 16, 16)},
|
||||||
|
},
|
||||||
|
sampling_params=_make_vace_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="VACE condition"):
|
||||||
|
pipeline.forward(batch)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vace_forward_rejects_mismatched_reference_image_counts(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2_vace.current_omni_platform",
|
||||||
|
SimpleNamespace(is_available=lambda: False),
|
||||||
|
)
|
||||||
|
pipeline = _make_vace_pipeline()
|
||||||
|
pipeline.transformer.vace_patch_embedding = object()
|
||||||
|
pipeline.transformer_2 = None
|
||||||
|
pipeline.scheduler = StubScheduler([9])
|
||||||
|
pipeline.od_config = SimpleNamespace(flow_shift=3.0)
|
||||||
|
pipeline._sample_solver = "unipc"
|
||||||
|
pipeline._flow_shift = 3.0
|
||||||
|
pipeline.boundary_ratio = None
|
||||||
|
pipeline._guidance_scale = None
|
||||||
|
pipeline._num_timesteps = None
|
||||||
|
pipeline._current_timestep = None
|
||||||
|
pipeline.check_inputs = lambda **kwargs: None
|
||||||
|
pipeline.encode_prompt = lambda **kwargs: (torch.zeros(2, 2, 3), None) # type: ignore[method-assign]
|
||||||
|
|
||||||
|
def _fake_preprocess(video, mask, reference_images, **kwargs):
|
||||||
|
del kwargs
|
||||||
|
return video, mask, [reference_images]
|
||||||
|
|
||||||
|
pipeline.preprocess_conditions = _fake_preprocess # type: ignore[method-assign]
|
||||||
|
pipeline.prepare_video_latents = ( # type: ignore[method-assign]
|
||||||
|
lambda video, mask, refs, generator, device: torch.zeros(1, 8, 5, 2, 2)
|
||||||
|
)
|
||||||
|
pipeline.prepare_masks = lambda mask, refs: torch.zeros(1, 4, 5, 2, 2) # type: ignore[method-assign]
|
||||||
|
video = torch.zeros(1, 3, 5, 16, 16)
|
||||||
|
mask = torch.ones_like(video)
|
||||||
|
reference = torch.zeros(3, 16, 16)
|
||||||
|
batch = DiffusionRequestBatch(
|
||||||
|
requests=[
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="a",
|
||||||
|
prompt={
|
||||||
|
"prompt": "first",
|
||||||
|
"additional_information": {
|
||||||
|
"source_video": video,
|
||||||
|
"mask": mask,
|
||||||
|
"reference_images": [reference],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sampling_params=_make_vace_sampling(),
|
||||||
|
),
|
||||||
|
SimpleNamespace(
|
||||||
|
request_id="b",
|
||||||
|
prompt={
|
||||||
|
"prompt": "second",
|
||||||
|
"additional_information": {
|
||||||
|
"source_video": video,
|
||||||
|
"mask": mask,
|
||||||
|
"reference_images": [reference, reference],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sampling_params=_make_vace_sampling(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="same number of reference images"):
|
||||||
|
pipeline.forward(batch)
|
||||||
@@ -123,6 +123,7 @@ class _ConcurrencyTrackingExecutor:
|
|||||||
def _make_request(tag: str):
|
def _make_request(tag: str):
|
||||||
sampling_params = dict(_SAMPLING_KEY_DEFAULTS)
|
sampling_params = dict(_SAMPLING_KEY_DEFAULTS)
|
||||||
sampling_params["num_inference_steps"] = 1
|
sampling_params["num_inference_steps"] = 1
|
||||||
|
sampling_params["extra_args"] = {}
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
request_id=tag,
|
request_id=tag,
|
||||||
prompt=f"prompt_{tag}",
|
prompt=f"prompt_{tag}",
|
||||||
|
|||||||
@@ -217,13 +217,21 @@ class TestGetRequestBatchSamplingParamsKey:
|
|||||||
num_inference_steps: int = 2,
|
num_inference_steps: int = 2,
|
||||||
seed: int | None = 123,
|
seed: int | None = 123,
|
||||||
generator: torch.Generator | None = None,
|
generator: torch.Generator | None = None,
|
||||||
|
extra_args: dict | None = None,
|
||||||
|
condition_key: tuple | None = None,
|
||||||
) -> OmniDiffusionRequest:
|
) -> OmniDiffusionRequest:
|
||||||
sp = OmniDiffusionSamplingParams(
|
sp = OmniDiffusionSamplingParams(
|
||||||
num_inference_steps=num_inference_steps,
|
num_inference_steps=num_inference_steps,
|
||||||
seed=seed,
|
seed=seed,
|
||||||
generator=generator,
|
generator=generator,
|
||||||
|
extra_args=extra_args or {},
|
||||||
|
)
|
||||||
|
return OmniDiffusionRequest(
|
||||||
|
prompt="prompt",
|
||||||
|
sampling_params=sp,
|
||||||
|
request_id=f"req-{num_inference_steps}",
|
||||||
|
batch_compatibility_key=condition_key,
|
||||||
)
|
)
|
||||||
return OmniDiffusionRequest(prompt="prompt", sampling_params=sp, request_id=f"req-{num_inference_steps}")
|
|
||||||
|
|
||||||
def test_distinguishes_num_inference_steps(self) -> None:
|
def test_distinguishes_num_inference_steps(self) -> None:
|
||||||
scheduler = RequestScheduler()
|
scheduler = RequestScheduler()
|
||||||
@@ -240,6 +248,57 @@ class TestGetRequestBatchSamplingParamsKey:
|
|||||||
self._make(seed=1, generator=gen_a)
|
self._make(seed=1, generator=gen_a)
|
||||||
) == scheduler._build_sampling_params_key(self._make(seed=2, generator=gen_b))
|
) == scheduler._build_sampling_params_key(self._make(seed=2, generator=gen_b))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("first_extra_args", "second_extra_args"),
|
||||||
|
[
|
||||||
|
({"sample_solver": "unipc"}, {"sample_solver": "euler"}),
|
||||||
|
({"flow_shift": 3.0}, {"flow_shift": 5.0}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_distinguishes_wan_scheduler_structure(
|
||||||
|
self,
|
||||||
|
first_extra_args: dict,
|
||||||
|
second_extra_args: dict,
|
||||||
|
) -> None:
|
||||||
|
scheduler = RequestScheduler()
|
||||||
|
|
||||||
|
assert scheduler._build_sampling_params_key(
|
||||||
|
self._make(extra_args=first_extra_args)
|
||||||
|
) != scheduler._build_sampling_params_key(self._make(extra_args=second_extra_args))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("first_extra_args", "second_extra_args"),
|
||||||
|
[
|
||||||
|
({"sample_solver": " Euler "}, {"sample_solver": "euler"}),
|
||||||
|
({"flow_shift": "5.0"}, {"flow_shift": 5.0}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_normalizes_equivalent_wan_scheduler_structure(
|
||||||
|
self,
|
||||||
|
first_extra_args: dict,
|
||||||
|
second_extra_args: dict,
|
||||||
|
) -> None:
|
||||||
|
scheduler = RequestScheduler()
|
||||||
|
|
||||||
|
assert scheduler._build_sampling_params_key(
|
||||||
|
self._make(extra_args=first_extra_args)
|
||||||
|
) == scheduler._build_sampling_params_key(self._make(extra_args=second_extra_args))
|
||||||
|
|
||||||
|
def test_uses_none_for_unspecified_wan_scheduler_structure(self) -> None:
|
||||||
|
scheduler = RequestScheduler()
|
||||||
|
|
||||||
|
key = scheduler._build_sampling_params_key(self._make())
|
||||||
|
|
||||||
|
assert key.sample_solver is None
|
||||||
|
assert key.flow_shift is None
|
||||||
|
|
||||||
|
def test_distinguishes_pipeline_condition_structure(self) -> None:
|
||||||
|
scheduler = RequestScheduler()
|
||||||
|
|
||||||
|
assert scheduler._build_sampling_params_key(
|
||||||
|
self._make(condition_key=("wan22_s2v_condition", True))
|
||||||
|
) != scheduler._build_sampling_params_key(self._make(condition_key=("wan22_s2v_condition", False)))
|
||||||
|
|
||||||
|
|
||||||
class TestRequestScheduler:
|
class TestRequestScheduler:
|
||||||
def setup_method(self) -> None:
|
def setup_method(self) -> None:
|
||||||
@@ -602,6 +661,38 @@ class TestRequestScheduler:
|
|||||||
assert first.num_running_reqs == 1
|
assert first.num_running_reqs == 1
|
||||||
assert first.num_waiting_reqs == 1
|
assert first.num_waiting_reqs == 1
|
||||||
|
|
||||||
|
def test_batches_incompatible_pipeline_conditions_separately(self) -> None:
|
||||||
|
scheduler = RequestScheduler()
|
||||||
|
scheduler.initialize(SimpleNamespace(max_num_seqs=2))
|
||||||
|
|
||||||
|
request_a = OmniDiffusionRequest(
|
||||||
|
prompt="a",
|
||||||
|
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=2, seed=123),
|
||||||
|
request_id="a",
|
||||||
|
batch_compatibility_key=("wan22_s2v_condition", True),
|
||||||
|
)
|
||||||
|
request_b = OmniDiffusionRequest(
|
||||||
|
prompt="b",
|
||||||
|
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=2, seed=456),
|
||||||
|
request_id="b",
|
||||||
|
batch_compatibility_key=("wan22_s2v_condition", False),
|
||||||
|
)
|
||||||
|
scheduler.add_request(request_a)
|
||||||
|
scheduler.add_request(request_b)
|
||||||
|
|
||||||
|
first = scheduler.schedule()
|
||||||
|
|
||||||
|
assert _new_ids(first) == ["a"]
|
||||||
|
assert first.num_running_reqs == 1
|
||||||
|
assert first.num_waiting_reqs == 1
|
||||||
|
|
||||||
|
scheduler.update_from_output(first, _make_request_output("a"))
|
||||||
|
second = scheduler.schedule()
|
||||||
|
|
||||||
|
assert _new_ids(second) == ["b"]
|
||||||
|
assert second.num_running_reqs == 1
|
||||||
|
assert second.num_waiting_reqs == 0
|
||||||
|
|
||||||
def test_batches_different_request_local_seed_together(self) -> None:
|
def test_batches_different_request_local_seed_together(self) -> None:
|
||||||
scheduler = RequestScheduler()
|
scheduler = RequestScheduler()
|
||||||
scheduler.initialize(SimpleNamespace(max_num_seqs=2))
|
scheduler.initialize(SimpleNamespace(max_num_seqs=2))
|
||||||
@@ -625,6 +716,47 @@ class TestRequestScheduler:
|
|||||||
assert first.num_running_reqs == 2
|
assert first.num_running_reqs == 2
|
||||||
assert first.num_waiting_reqs == 0
|
assert first.num_waiting_reqs == 0
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("first_extra_args", "second_extra_args"),
|
||||||
|
[
|
||||||
|
({"sample_solver": "unipc"}, {"sample_solver": "euler"}),
|
||||||
|
({"flow_shift": 3.0}, {"flow_shift": 5.0}),
|
||||||
|
({"sample_solver": "unipc"}, {}),
|
||||||
|
({"flow_shift": 3.0}, {}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_batches_incompatible_wan_scheduler_structure_separately(
|
||||||
|
self,
|
||||||
|
first_extra_args: dict,
|
||||||
|
second_extra_args: dict,
|
||||||
|
) -> None:
|
||||||
|
scheduler = RequestScheduler()
|
||||||
|
scheduler.initialize(SimpleNamespace(max_num_seqs=2))
|
||||||
|
first_id = scheduler.add_request(
|
||||||
|
_make_step_request(
|
||||||
|
"a",
|
||||||
|
sampling_params=OmniDiffusionSamplingParams(
|
||||||
|
num_inference_steps=2,
|
||||||
|
extra_args=first_extra_args,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
scheduler.add_request(
|
||||||
|
_make_step_request(
|
||||||
|
"b",
|
||||||
|
sampling_params=OmniDiffusionSamplingParams(
|
||||||
|
num_inference_steps=2,
|
||||||
|
extra_args=second_extra_args,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
first = scheduler.schedule()
|
||||||
|
|
||||||
|
assert _new_ids(first) == [first_id]
|
||||||
|
assert first.num_running_reqs == 1
|
||||||
|
assert first.num_waiting_reqs == 1
|
||||||
|
|
||||||
def test_incompatible_waiting_head_blocks_later_compatible_request(self) -> None:
|
def test_incompatible_waiting_head_blocks_later_compatible_request(self) -> None:
|
||||||
scheduler = RequestScheduler()
|
scheduler = RequestScheduler()
|
||||||
scheduler.initialize(SimpleNamespace(max_num_seqs=3))
|
scheduler.initialize(SimpleNamespace(max_num_seqs=3))
|
||||||
|
|||||||
@@ -399,6 +399,101 @@ def test_initialize_local_llm_replica_scopes_runtime_env(monkeypatch):
|
|||||||
assert os.environ[runtime_env_var] == "parent-value"
|
assert os.environ[runtime_env_var] == "parent-value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_diffusion_stage_applies_client_batch_size_to_engine(monkeypatch):
|
||||||
|
import vllm_omni.diffusion.stage_diffusion_client as client_mod
|
||||||
|
import vllm_omni.engine.stage_init_utils as init_mod
|
||||||
|
|
||||||
|
od_config = types.SimpleNamespace(max_num_seqs=1)
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
monkeypatch.setattr(init_mod, "build_diffusion_config", lambda *args: od_config)
|
||||||
|
|
||||||
|
def _capture_client(model, config, metadata, stage_init_timeout, batch_size, use_inline):
|
||||||
|
captured.update(
|
||||||
|
model=model,
|
||||||
|
config=config,
|
||||||
|
metadata=metadata,
|
||||||
|
stage_init_timeout=stage_init_timeout,
|
||||||
|
batch_size=batch_size,
|
||||||
|
use_inline=use_inline,
|
||||||
|
)
|
||||||
|
return object()
|
||||||
|
|
||||||
|
monkeypatch.setattr(client_mod, "create_diffusion_client", _capture_client)
|
||||||
|
metadata = _make_diffusion_metadata(0)
|
||||||
|
|
||||||
|
init_mod.initialize_diffusion_stage(
|
||||||
|
0,
|
||||||
|
"dummy-model",
|
||||||
|
types.SimpleNamespace(),
|
||||||
|
metadata,
|
||||||
|
stage_init_timeout=12,
|
||||||
|
batch_size=4,
|
||||||
|
use_inline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert od_config.max_num_seqs == 4
|
||||||
|
assert captured == {
|
||||||
|
"model": "dummy-model",
|
||||||
|
"config": od_config,
|
||||||
|
"metadata": metadata,
|
||||||
|
"stage_init_timeout": 12,
|
||||||
|
"batch_size": 4,
|
||||||
|
"use_inline": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_launch_diffusion_stage_replica_applies_batch_size_to_config(monkeypatch):
|
||||||
|
import vllm_omni.diffusion.stage_diffusion_client as client_mod
|
||||||
|
import vllm_omni.diffusion.stage_diffusion_proc as proc_mod
|
||||||
|
import vllm_omni.engine.stage_engine_startup as startup_mod
|
||||||
|
|
||||||
|
od_config = types.SimpleNamespace(max_num_seqs=1, parallel_config=types.SimpleNamespace(world_size=1))
|
||||||
|
monkeypatch.setattr(startup_mod, "build_diffusion_config", lambda *args: od_config)
|
||||||
|
monkeypatch.setattr(startup_mod, "acquire_device_locks", lambda *args: [])
|
||||||
|
monkeypatch.setattr(
|
||||||
|
startup_mod,
|
||||||
|
"register_stage_with_omni_master",
|
||||||
|
lambda **kwargs: types.SimpleNamespace(
|
||||||
|
handshake_address="tcp://127.0.0.1:26001",
|
||||||
|
input_address="tcp://127.0.0.1:26002",
|
||||||
|
output_address="tcp://127.0.0.1:26003",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
omni_master_server = types.SimpleNamespace(
|
||||||
|
address="127.0.0.1",
|
||||||
|
port=25000,
|
||||||
|
release_route_port_reservations=lambda *args, **kwargs: None,
|
||||||
|
)
|
||||||
|
proc_manager = types.SimpleNamespace(
|
||||||
|
addresses=types.SimpleNamespace(
|
||||||
|
inputs=["tcp://127.0.0.1:26002"],
|
||||||
|
outputs=["tcp://127.0.0.1:26003"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(proc_mod, "StageDiffusionProcManager", lambda **kwargs: proc_manager)
|
||||||
|
sentinel_client = object()
|
||||||
|
monkeypatch.setattr(
|
||||||
|
client_mod.StageDiffusionClient,
|
||||||
|
"from_addresses",
|
||||||
|
lambda metadata, **kwargs: sentinel_client,
|
||||||
|
)
|
||||||
|
|
||||||
|
result, resources = startup_mod.launch_diffusion_stage_replica(
|
||||||
|
model="dummy-model",
|
||||||
|
stage_config=types.SimpleNamespace(),
|
||||||
|
metadata=types.SimpleNamespace(stage_id=0),
|
||||||
|
stage_init_timeout=12,
|
||||||
|
batch_size=4,
|
||||||
|
use_inline=False,
|
||||||
|
omni_master_server=omni_master_server,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is sentinel_client
|
||||||
|
assert od_config.max_num_seqs == 4
|
||||||
|
assert resources.manager is proc_manager
|
||||||
|
|
||||||
|
|
||||||
def test_stage_runtime_initializes_stage_pools(monkeypatch):
|
def test_stage_runtime_initializes_stage_pools(monkeypatch):
|
||||||
import vllm_omni.engine.stage_runtime as runtime_mod
|
import vllm_omni.engine.stage_runtime as runtime_mod
|
||||||
|
|
||||||
|
|||||||
@@ -1138,7 +1138,8 @@ class TestSingleStageReplicaInitialization:
|
|||||||
runtime._init_visible_devices_baseline = "0"
|
runtime._init_visible_devices_baseline = "0"
|
||||||
|
|
||||||
mocker.patch.object(runtime_mod, "inject_kv_stage_info")
|
mocker.patch.object(runtime_mod, "inject_kv_stage_info")
|
||||||
mocker.patch("vllm_omni.engine.stage_engine_startup.build_diffusion_config", return_value="diffusion-config")
|
od_config = SimpleNamespace(max_num_seqs=None, parallel_config=SimpleNamespace(world_size=1))
|
||||||
|
mocker.patch("vllm_omni.engine.stage_engine_startup.build_diffusion_config", return_value=od_config)
|
||||||
mock_register = mocker.patch(
|
mock_register = mocker.patch(
|
||||||
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
|
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
|
||||||
return_value=StageRegistrationResponse(
|
return_value=StageRegistrationResponse(
|
||||||
@@ -1174,6 +1175,7 @@ class TestSingleStageReplicaInitialization:
|
|||||||
os.environ[device_env_var] = prev_device_env
|
os.environ[device_env_var] = prev_device_env
|
||||||
|
|
||||||
assert result is sentinel_client
|
assert result is sentinel_client
|
||||||
|
assert od_config.max_num_seqs == 4
|
||||||
mock_register.assert_called_once_with(
|
mock_register.assert_called_once_with(
|
||||||
omni_master_address="127.0.0.1",
|
omni_master_address="127.0.0.1",
|
||||||
omni_master_port=25000,
|
omni_master_port=25000,
|
||||||
@@ -1183,7 +1185,7 @@ class TestSingleStageReplicaInitialization:
|
|||||||
)
|
)
|
||||||
mock_manager.assert_called_once_with(
|
mock_manager.assert_called_once_with(
|
||||||
model="fake-model",
|
model="fake-model",
|
||||||
od_config="diffusion-config",
|
od_config=od_config,
|
||||||
stage_init_timeout=60,
|
stage_init_timeout=60,
|
||||||
handshake_address="tcp://127.0.0.1:26001",
|
handshake_address="tcp://127.0.0.1:26001",
|
||||||
addresses=mocker.ANY,
|
addresses=mocker.ANY,
|
||||||
@@ -1230,7 +1232,8 @@ class TestSingleStageReplicaInitialization:
|
|||||||
runtime._init_visible_devices_baseline = "0"
|
runtime._init_visible_devices_baseline = "0"
|
||||||
|
|
||||||
mocker.patch.object(runtime_mod, "inject_kv_stage_info")
|
mocker.patch.object(runtime_mod, "inject_kv_stage_info")
|
||||||
mocker.patch("vllm_omni.engine.stage_engine_startup.build_diffusion_config", return_value="diffusion-config")
|
od_config = SimpleNamespace(max_num_seqs=None, parallel_config=SimpleNamespace(world_size=1))
|
||||||
|
mocker.patch("vllm_omni.engine.stage_engine_startup.build_diffusion_config", return_value=od_config)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
|
"vllm_omni.engine.stage_engine_startup.register_stage_with_omni_master",
|
||||||
return_value=StageRegistrationResponse(
|
return_value=StageRegistrationResponse(
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanTransformer3
|
|||||||
from vllm_omni.diffusion.postprocess import interpolate_video_tensor
|
from vllm_omni.diffusion.postprocess import interpolate_video_tensor
|
||||||
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
||||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch, split_diffusion_output_by_request
|
||||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
@@ -204,6 +204,8 @@ def get_wan22_post_process_func(
|
|||||||
output_type: str = "np",
|
output_type: str = "np",
|
||||||
sampling_params=None,
|
sampling_params=None,
|
||||||
):
|
):
|
||||||
|
if sampling_params is not None and sampling_params.output_type is not None:
|
||||||
|
output_type = sampling_params.output_type
|
||||||
if output_type == "latent":
|
if output_type == "latent":
|
||||||
return video
|
return video
|
||||||
video_metadata = {}
|
video_metadata = {}
|
||||||
@@ -233,6 +235,8 @@ def get_wan22_pre_process_func(
|
|||||||
prompt = request.prompt
|
prompt = request.prompt
|
||||||
multi_modal_data = prompt.get("multi_modal_data", {}) if not isinstance(prompt, str) else None
|
multi_modal_data = prompt.get("multi_modal_data", {}) if not isinstance(prompt, str) else None
|
||||||
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
|
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
|
||||||
|
has_image = raw_image is not None and (not isinstance(raw_image, list) or bool(raw_image))
|
||||||
|
request.batch_compatibility_key = ("wan22_image_condition", has_image)
|
||||||
if isinstance(prompt, str):
|
if isinstance(prompt, str):
|
||||||
prompt = OmniTextPrompt(prompt=prompt)
|
prompt = OmniTextPrompt(prompt=prompt)
|
||||||
if "additional_information" not in prompt:
|
if "additional_information" not in prompt:
|
||||||
@@ -288,6 +292,7 @@ class Wan22Pipeline(
|
|||||||
SupportsComponentDiscovery,
|
SupportsComponentDiscovery,
|
||||||
WanLoraLoaderMixin,
|
WanLoraLoaderMixin,
|
||||||
):
|
):
|
||||||
|
supports_request_batch = True
|
||||||
_dit_modules: ClassVar[list[str]] = ["transformer", "transformer_2"]
|
_dit_modules: ClassVar[list[str]] = ["transformer", "transformer_2"]
|
||||||
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
|
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
|
||||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||||
@@ -561,27 +566,33 @@ class Wan22Pipeline(
|
|||||||
|
|
||||||
return latents
|
return latents
|
||||||
|
|
||||||
def forward(self, req: DiffusionRequestBatch) -> DiffusionOutput:
|
def forward(self, req: DiffusionRequestBatch) -> list[DiffusionOutput]:
|
||||||
prompt: str | None = None
|
sampling_params_list = req.sampling_params_list
|
||||||
negative_prompt: str | None = None
|
common = sampling_params_list[0]
|
||||||
prompt_embeds: torch.Tensor | None = None
|
prompt_texts = [prompt if isinstance(prompt, str) else (prompt.get("prompt") or "") for prompt in req.prompts]
|
||||||
negative_prompt_embeds: torch.Tensor | None = None
|
negative_prompts = [
|
||||||
if len(req.prompts) > 1:
|
None if isinstance(prompt, str) else prompt.get("negative_prompt") for prompt in req.prompts
|
||||||
raise ValueError(
|
]
|
||||||
"""This model only supports a single prompt, not a batched request.""",
|
prompt_fields = DiffusionRequestBatch.collate_prompt_field_map(
|
||||||
"""Please pass in a single prompt object or string, or a single-item list.""",
|
req.prompts,
|
||||||
)
|
{
|
||||||
if len(req.prompts) == 1:
|
"prompt_embeds": None,
|
||||||
first_prompt = req.prompts[0]
|
"negative_prompt_embeds": None,
|
||||||
prompt = first_prompt if isinstance(first_prompt, str) else (first_prompt.get("prompt") or "")
|
},
|
||||||
negative_prompt = None if isinstance(first_prompt, str) else first_prompt.get("negative_prompt")
|
)
|
||||||
|
prompt_embeds = prompt_fields["prompt_embeds"]
|
||||||
|
negative_prompt_embeds = prompt_fields["negative_prompt_embeds"]
|
||||||
|
prompt: list[str] | None = prompt_texts if prompt_embeds is None else None
|
||||||
|
negative_prompt: list[str] | None = None
|
||||||
|
if negative_prompt_embeds is None and any(value is not None for value in negative_prompts):
|
||||||
|
negative_prompt = [value or "" for value in negative_prompts]
|
||||||
|
|
||||||
if not prompt:
|
if prompt is not None and not all(prompt):
|
||||||
raise ValueError("Prompt is required for Wan2.2 generation.")
|
raise ValueError("Prompt is required for Wan2.2 generation when prompt_embeds are not provided.")
|
||||||
|
|
||||||
height = req.sampling_params.height or 480
|
height = common.height or 480
|
||||||
width = req.sampling_params.width or 832
|
width = common.width or 832
|
||||||
num_frames = req.sampling_params.num_frames or 81
|
num_frames = common.num_frames or 81
|
||||||
|
|
||||||
# Ensure dimensions are compatible with VAE and patch size
|
# Ensure dimensions are compatible with VAE and patch size
|
||||||
# For expand_timesteps mode, we need latent dims to be even (divisible by patch_size)
|
# For expand_timesteps mode, we need latent dims to be even (divisible by patch_size)
|
||||||
@@ -589,19 +600,20 @@ class Wan22Pipeline(
|
|||||||
mod_value = self.vae_scale_factor_spatial * patch_size[1] # 16*2=32 for TI2V, 8*2=16 for I2V
|
mod_value = self.vae_scale_factor_spatial * patch_size[1] # 16*2=32 for TI2V, 8*2=16 for I2V
|
||||||
height = (height // mod_value) * mod_value
|
height = (height // mod_value) * mod_value
|
||||||
width = (width // mod_value) * mod_value
|
width = (width // mod_value) * mod_value
|
||||||
num_steps = 40 if req.sampling_params.num_inference_steps is None else req.sampling_params.num_inference_steps
|
num_steps = 40 if common.num_inference_steps is None else common.num_inference_steps
|
||||||
|
|
||||||
output_type = req.sampling_params.output_type or "np"
|
output_type = common.output_type or "np"
|
||||||
|
num_outputs_per_prompt = common.num_outputs_per_prompt or 1
|
||||||
attention_kwargs: dict | None = None
|
attention_kwargs: dict | None = None
|
||||||
|
|
||||||
guidance_low, guidance_high = resolve_wan_guidance_scales(req.sampling_params, default_guidance_scale=4.0)
|
guidance_low, guidance_high = resolve_wan_guidance_scales(common, default_guidance_scale=4.0)
|
||||||
|
|
||||||
# record guidance for properties
|
# record guidance for properties
|
||||||
self._guidance_scale = guidance_low
|
self._guidance_scale = guidance_low
|
||||||
self._guidance_scale_2 = guidance_high
|
self._guidance_scale_2 = guidance_high
|
||||||
|
|
||||||
# Prefer engine-configured boundary_ratio, but allow per-request fallback.
|
# Prefer engine-configured boundary_ratio, but allow per-request fallback.
|
||||||
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else req.sampling_params.boundary_ratio
|
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else common.boundary_ratio
|
||||||
|
|
||||||
if boundary_ratio is None:
|
if boundary_ratio is None:
|
||||||
boundary_ratio = 0.875
|
boundary_ratio = 0.875
|
||||||
@@ -633,32 +645,49 @@ class Wan22Pipeline(
|
|||||||
# Fallback to text_encoder dtype if no transformer loaded
|
# Fallback to text_encoder dtype if no transformer loaded
|
||||||
dtype = self.text_encoder.dtype
|
dtype = self.text_encoder.dtype
|
||||||
|
|
||||||
# Seed / generator
|
generator = req.collate_request_generators(num_outputs_per_prompt, None)
|
||||||
generator = req.sampling_params.generator
|
request_latents = req.collate_request_tensors("latents", None)
|
||||||
if generator is None and req.sampling_params.seed is not None:
|
|
||||||
generator = torch.Generator(device=device).manual_seed(req.sampling_params.seed)
|
|
||||||
|
|
||||||
if DEBUG_PERF:
|
if DEBUG_PERF:
|
||||||
# Sync GPU before timing to ensure accurate measurements
|
# Sync GPU before timing to ensure accurate measurements
|
||||||
current_omni_platform.synchronize()
|
current_omni_platform.synchronize()
|
||||||
_t_pipeline_start = time.perf_counter()
|
_t_pipeline_start = time.perf_counter()
|
||||||
_t_text_enc_start = _t_pipeline_start
|
_t_text_enc_start = _t_pipeline_start
|
||||||
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
do_classifier_free_guidance = guidance_low > 1.0 or guidance_high > 1.0
|
||||||
prompt=prompt,
|
if prompt_embeds is None:
|
||||||
negative_prompt=negative_prompt,
|
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
||||||
do_classifier_free_guidance=guidance_low > 1.0 or guidance_high > 1.0,
|
prompt=prompt,
|
||||||
num_videos_per_prompt=req.sampling_params.num_outputs_per_prompt or 1,
|
negative_prompt=negative_prompt,
|
||||||
max_sequence_length=req.sampling_params.max_sequence_length or 512,
|
do_classifier_free_guidance=do_classifier_free_guidance,
|
||||||
device=device,
|
num_videos_per_prompt=num_outputs_per_prompt,
|
||||||
dtype=dtype,
|
max_sequence_length=common.max_sequence_length or 512,
|
||||||
)
|
device=device,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
prompt_embeds = prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
if negative_prompt_embeds is not None:
|
||||||
|
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
negative_prompt_embeds = negative_prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
elif do_classifier_free_guidance:
|
||||||
|
_, negative_prompt_embeds = self.encode_prompt(
|
||||||
|
prompt=[""] * req.num_reqs,
|
||||||
|
negative_prompt=negative_prompt,
|
||||||
|
do_classifier_free_guidance=True,
|
||||||
|
num_videos_per_prompt=num_outputs_per_prompt,
|
||||||
|
max_sequence_length=common.max_sequence_length or 512,
|
||||||
|
device=device,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
|
||||||
if DEBUG_PERF:
|
if DEBUG_PERF:
|
||||||
current_omni_platform.synchronize()
|
current_omni_platform.synchronize()
|
||||||
_t_text_enc_ms = (time.perf_counter() - _t_text_enc_start) * 1000
|
_t_text_enc_ms = (time.perf_counter() - _t_text_enc_start) * 1000
|
||||||
|
|
||||||
sample_solver = resolve_wan_sample_solver(req, default=self._sample_solver)
|
first_request = req.requests[0]
|
||||||
flow_shift = resolve_wan_flow_shift(req, self.od_config)
|
sample_solver = resolve_wan_sample_solver(first_request, default=self._sample_solver)
|
||||||
|
flow_shift = resolve_wan_flow_shift(first_request, self.od_config)
|
||||||
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
|
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
|
||||||
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
|
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
|
||||||
self._sample_solver = sample_solver
|
self._sample_solver = sample_solver
|
||||||
@@ -674,37 +703,41 @@ class Wan22Pipeline(
|
|||||||
|
|
||||||
if DEBUG_PERF:
|
if DEBUG_PERF:
|
||||||
_t_latent_prep_start = time.perf_counter()
|
_t_latent_prep_start = time.perf_counter()
|
||||||
multi_modal_data = req.prompts[0].get("multi_modal_data", {}) if not isinstance(req.prompts[0], str) else None
|
images: list[PIL.Image.Image | torch.Tensor | None] = []
|
||||||
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
|
for request_prompt in req.prompts:
|
||||||
if isinstance(raw_image, list):
|
multi_modal_data = request_prompt.get("multi_modal_data", {}) if not isinstance(request_prompt, str) else {}
|
||||||
if len(raw_image) > 1:
|
raw_image = multi_modal_data.get("image")
|
||||||
logger.warning(
|
if isinstance(raw_image, list):
|
||||||
"""Received a list of image. Only a single image is supported by this model."""
|
if len(raw_image) > 1:
|
||||||
"""Taking only the first image for now."""
|
logger.warning("Received multiple images for one Wan request; using only the first image.")
|
||||||
)
|
raw_image = raw_image[0] if raw_image else None
|
||||||
raw_image = raw_image[0]
|
if isinstance(raw_image, str):
|
||||||
if raw_image is None:
|
raw_image = PIL.Image.open(raw_image)
|
||||||
image = None
|
images.append(cast(PIL.Image.Image | torch.Tensor | None, raw_image))
|
||||||
elif isinstance(raw_image, str):
|
|
||||||
image = PIL.Image.open(raw_image)
|
|
||||||
else:
|
|
||||||
image = cast(PIL.Image.Image | torch.Tensor, raw_image)
|
|
||||||
|
|
||||||
latent_condition = None
|
latent_condition = None
|
||||||
first_frame_mask = None
|
first_frame_mask = None
|
||||||
|
|
||||||
if self.expand_timesteps and image is not None:
|
if self.expand_timesteps and any(image is not None for image in images):
|
||||||
|
if not all(image is not None for image in images):
|
||||||
|
raise ValueError("Cannot batch Wan requests with a mix of provided and missing image conditions.")
|
||||||
# I2V mode: encode image and prepare condition
|
# I2V mode: encode image and prepare condition
|
||||||
from diffusers.video_processor import VideoProcessor
|
from diffusers.video_processor import VideoProcessor
|
||||||
|
|
||||||
video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
|
video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
|
||||||
|
|
||||||
# Preprocess image
|
image_tensors = []
|
||||||
if isinstance(image, PIL.Image.Image):
|
for image in images:
|
||||||
image = image.resize((width, height), PIL.Image.Resampling.LANCZOS)
|
assert image is not None
|
||||||
image_tensor = video_processor.preprocess(image, height=height, width=width)
|
if isinstance(image, PIL.Image.Image):
|
||||||
else:
|
image = image.resize((width, height), PIL.Image.Resampling.LANCZOS)
|
||||||
image_tensor = image
|
image_tensor = video_processor.preprocess(image, height=height, width=width)
|
||||||
|
else:
|
||||||
|
image_tensor = image.unsqueeze(0) if image.ndim == 3 else image
|
||||||
|
image_tensors.append(image_tensor)
|
||||||
|
image_tensor = DiffusionRequestBatch.collate_tensors(image_tensors, "image condition", None)
|
||||||
|
assert image_tensor is not None
|
||||||
|
image_tensor = image_tensor.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
|
||||||
# Use out_channels for noise latents (not in_channels which includes condition)
|
# Use out_channels for noise latents (not in_channels which includes condition)
|
||||||
num_channels_latents = self.transformer_config.out_channels
|
num_channels_latents = self.transformer_config.out_channels
|
||||||
@@ -720,7 +753,7 @@ class Wan22Pipeline(
|
|||||||
dtype=torch.float32,
|
dtype=torch.float32,
|
||||||
device=device,
|
device=device,
|
||||||
generator=generator,
|
generator=generator,
|
||||||
latents=req.sampling_params.latents,
|
latents=request_latents,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Encode image condition
|
# Encode image condition
|
||||||
@@ -731,7 +764,6 @@ class Wan22Pipeline(
|
|||||||
image_tensor = image_tensor.unsqueeze(2) # [B, C, 1, H, W]
|
image_tensor = image_tensor.unsqueeze(2) # [B, C, 1, H, W]
|
||||||
image_tensor = image_tensor.to(device=device, dtype=self.vae.dtype)
|
image_tensor = image_tensor.to(device=device, dtype=self.vae.dtype)
|
||||||
latent_condition = retrieve_latents(self.vae.encode(image_tensor), sample_mode="argmax")
|
latent_condition = retrieve_latents(self.vae.encode(image_tensor), sample_mode="argmax")
|
||||||
latent_condition = latent_condition.repeat(batch_size, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
# Normalize condition latents
|
# Normalize condition latents
|
||||||
latents_mean = (
|
latents_mean = (
|
||||||
@@ -747,7 +779,7 @@ class Wan22Pipeline(
|
|||||||
|
|
||||||
# Create mask: 0 for first frame (condition), 1 for rest (to denoise)
|
# Create mask: 0 for first frame (condition), 1 for rest (to denoise)
|
||||||
first_frame_mask = torch.ones(
|
first_frame_mask = torch.ones(
|
||||||
1, 1, num_latent_frames, latent_height, latent_width, dtype=torch.float32, device=device
|
batch_size, 1, num_latent_frames, latent_height, latent_width, dtype=torch.float32, device=device
|
||||||
)
|
)
|
||||||
first_frame_mask[:, :, 0] = 0
|
first_frame_mask[:, :, 0] = 0
|
||||||
else:
|
else:
|
||||||
@@ -762,7 +794,7 @@ class Wan22Pipeline(
|
|||||||
dtype=torch.float32,
|
dtype=torch.float32,
|
||||||
device=device,
|
device=device,
|
||||||
generator=generator,
|
generator=generator,
|
||||||
latents=req.sampling_params.latents,
|
latents=request_latents,
|
||||||
)
|
)
|
||||||
if DEBUG_PERF:
|
if DEBUG_PERF:
|
||||||
current_omni_platform.synchronize()
|
current_omni_platform.synchronize()
|
||||||
@@ -839,8 +871,13 @@ class Wan22Pipeline(
|
|||||||
_t_pipeline_wall_ms - _t_stages_sum,
|
_t_pipeline_wall_ms - _t_stages_sum,
|
||||||
)
|
)
|
||||||
|
|
||||||
return DiffusionOutput(
|
return split_diffusion_output_by_request(
|
||||||
output=output, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
|
DiffusionOutput(
|
||||||
|
output=output,
|
||||||
|
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
|
||||||
|
),
|
||||||
|
req,
|
||||||
|
num_outputs_per_prompt=num_outputs_per_prompt,
|
||||||
)
|
)
|
||||||
|
|
||||||
def predict_noise(
|
def predict_noise(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanTransformer3
|
|||||||
from vllm_omni.diffusion.postprocess import interpolate_video_tensor
|
from vllm_omni.diffusion.postprocess import interpolate_video_tensor
|
||||||
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
||||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch, split_diffusion_output_by_request
|
||||||
from vllm_omni.inputs.data import OmniTextPrompt
|
from vllm_omni.inputs.data import OmniTextPrompt
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
@@ -65,6 +65,8 @@ def get_wan22_i2v_post_process_func(
|
|||||||
output_type: str = "np",
|
output_type: str = "np",
|
||||||
sampling_params=None,
|
sampling_params=None,
|
||||||
):
|
):
|
||||||
|
if sampling_params is not None and sampling_params.output_type is not None:
|
||||||
|
output_type = sampling_params.output_type
|
||||||
if output_type == "latent":
|
if output_type == "latent":
|
||||||
return video
|
return video
|
||||||
video_metadata = {}
|
video_metadata = {}
|
||||||
@@ -84,6 +86,32 @@ def get_wan22_i2v_post_process_func(
|
|||||||
return post_process_func
|
return post_process_func
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_i2v_last_image(
|
||||||
|
value: str | PIL.Image.Image | torch.Tensor | list[object] | None,
|
||||||
|
) -> PIL.Image.Image | torch.Tensor | None:
|
||||||
|
"""Normalize the optional I2V last-frame condition for batching."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
if len(value) != 1:
|
||||||
|
raise ValueError("I2V accepts at most one last_image.")
|
||||||
|
value = value[0]
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = PIL.Image.open(value).convert("RGB")
|
||||||
|
|
||||||
|
if not isinstance(value, (PIL.Image.Image, torch.Tensor)):
|
||||||
|
raise TypeError(
|
||||||
|
f"Unsupported last_image format {value.__class__}. "
|
||||||
|
"Expected a file path, PIL.Image.Image, torch.Tensor, or None."
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def get_wan22_i2v_pre_process_func(
|
def get_wan22_i2v_pre_process_func(
|
||||||
od_config: OmniDiffusionConfig,
|
od_config: OmniDiffusionConfig,
|
||||||
):
|
):
|
||||||
@@ -109,6 +137,9 @@ def get_wan22_i2v_pre_process_func(
|
|||||||
"""Please correctly set `"multi_modal_data": {"image": <an image object or file path>, …}`""",
|
"""Please correctly set `"multi_modal_data": {"image": <an image object or file path>, …}`""",
|
||||||
)
|
)
|
||||||
image = PIL.Image.open(raw_image).convert("RGB") if isinstance(raw_image, str) else raw_image
|
image = PIL.Image.open(raw_image).convert("RGB") if isinstance(raw_image, str) else raw_image
|
||||||
|
last_image = _normalize_i2v_last_image(multi_modal_data.get("last_image")) # type: ignore[union-attr]
|
||||||
|
prompt["multi_modal_data"]["last_image"] = last_image # type: ignore[index]
|
||||||
|
request.batch_compatibility_key = ("wan22_i2v_last_image", last_image is not None)
|
||||||
|
|
||||||
# Calculate dimensions based on aspect ratio if not provided
|
# Calculate dimensions based on aspect ratio if not provided
|
||||||
if request.sampling_params.height is None or request.sampling_params.width is None:
|
if request.sampling_params.height is None or request.sampling_params.width is None:
|
||||||
@@ -157,6 +188,7 @@ class Wan22I2VPipeline(
|
|||||||
Wan2.2-style I2V (with expand_timesteps for TI2V-5B).
|
Wan2.2-style I2V (with expand_timesteps for TI2V-5B).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
supports_request_batch = True
|
||||||
_dit_modules: ClassVar[list[str]] = ["transformer", "transformer_2"]
|
_dit_modules: ClassVar[list[str]] = ["transformer", "transformer_2"]
|
||||||
_encoder_modules: ClassVar[list[str]] = ["text_encoder", "image_encoder"]
|
_encoder_modules: ClassVar[list[str]] = ["text_encoder", "image_encoder"]
|
||||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||||
@@ -433,60 +465,67 @@ class Wan22I2VPipeline(
|
|||||||
quant_config = getattr(self.od_config, "quantization_config", None)
|
quant_config = getattr(self.od_config, "quantization_config", None)
|
||||||
return create_transformer_from_config(config, quant_config=quant_config)
|
return create_transformer_from_config(config, quant_config=quant_config)
|
||||||
|
|
||||||
def forward(self, req: DiffusionRequestBatch) -> DiffusionOutput:
|
def forward(self, req: DiffusionRequestBatch) -> list[DiffusionOutput]:
|
||||||
prompt: str | None = None
|
sampling_params_list = req.sampling_params_list
|
||||||
negative_prompt: str | None = None
|
common = sampling_params_list[0]
|
||||||
prompt_embeds: torch.Tensor | None = None
|
prompt_texts = [prompt if isinstance(prompt, str) else (prompt.get("prompt") or "") for prompt in req.prompts]
|
||||||
negative_prompt_embeds: torch.Tensor | None = None
|
negative_prompts = [
|
||||||
|
None if isinstance(prompt, str) else prompt.get("negative_prompt") for prompt in req.prompts
|
||||||
|
]
|
||||||
|
prompt_fields = DiffusionRequestBatch.collate_prompt_field_map(
|
||||||
|
req.prompts,
|
||||||
|
{
|
||||||
|
"prompt_embeds": None,
|
||||||
|
"negative_prompt_embeds": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
prompt_embeds = prompt_fields["prompt_embeds"]
|
||||||
|
negative_prompt_embeds = prompt_fields["negative_prompt_embeds"]
|
||||||
image_embeds: torch.Tensor | None = None
|
image_embeds: torch.Tensor | None = None
|
||||||
if len(req.prompts) > 1:
|
prompt: list[str] | None = prompt_texts if prompt_embeds is None else None
|
||||||
raise ValueError(
|
negative_prompt: list[str] | None = None
|
||||||
"""This model only supports a single prompt, not a batched request.""",
|
if negative_prompt_embeds is None and any(value is not None for value in negative_prompts):
|
||||||
"""Please pass in a single prompt object or string, or a single-item list.""",
|
negative_prompt = [value or "" for value in negative_prompts]
|
||||||
)
|
if prompt is not None and not all(prompt):
|
||||||
if len(req.prompts) == 1:
|
raise ValueError("Prompt is required for Wan2.2 I2V generation when prompt_embeds are not provided.")
|
||||||
first_prompt = req.prompts[0]
|
|
||||||
prompt = first_prompt if isinstance(first_prompt, str) else (first_prompt.get("prompt") or "")
|
|
||||||
negative_prompt = None if isinstance(first_prompt, str) else first_prompt.get("negative_prompt")
|
|
||||||
|
|
||||||
if not prompt:
|
images: list[PIL.Image.Image | torch.Tensor] = []
|
||||||
raise ValueError("Prompt is required for Wan2.2 generation.")
|
last_images: list[PIL.Image.Image | torch.Tensor | None] = []
|
||||||
|
for request_prompt in req.prompts:
|
||||||
|
multi_modal_data = request_prompt.get("multi_modal_data", {}) if not isinstance(request_prompt, str) else {}
|
||||||
|
raw_image = multi_modal_data.get("image")
|
||||||
|
if raw_image is None:
|
||||||
|
raise ValueError("Image is required for I2V generation.")
|
||||||
|
if isinstance(raw_image, list):
|
||||||
|
if len(raw_image) > 1:
|
||||||
|
logger.warning("Received multiple images for one I2V request; using only the first image.")
|
||||||
|
raw_image = raw_image[0]
|
||||||
|
if isinstance(raw_image, str):
|
||||||
|
raw_image = PIL.Image.open(raw_image).convert("RGB")
|
||||||
|
images.append(cast(PIL.Image.Image | torch.Tensor, raw_image))
|
||||||
|
|
||||||
# Get image from request
|
last_image = multi_modal_data.get("last_image")
|
||||||
multi_modal_data = req.prompts[0].get("multi_modal_data", {}) if not isinstance(req.prompts[0], str) else None
|
if isinstance(last_image, str):
|
||||||
raw_image = multi_modal_data.get("image", None) if multi_modal_data is not None else None
|
last_image = PIL.Image.open(last_image).convert("RGB")
|
||||||
if raw_image is None:
|
last_images.append(cast(PIL.Image.Image | torch.Tensor | None, last_image))
|
||||||
raise ValueError("Image is required for I2V generation.")
|
if any(image is not None for image in last_images) and not all(image is not None for image in last_images):
|
||||||
if isinstance(raw_image, list):
|
raise ValueError("Cannot batch I2V requests with a mix of provided and missing last_image conditions.")
|
||||||
if len(raw_image) > 1:
|
|
||||||
logger.warning(
|
|
||||||
"""Received a list of image. Only a single image is supported by this model."""
|
|
||||||
"""Taking only the first image for now."""
|
|
||||||
)
|
|
||||||
raw_image = raw_image[0]
|
|
||||||
if isinstance(raw_image, str):
|
|
||||||
image = PIL.Image.open(raw_image)
|
|
||||||
else:
|
|
||||||
image = cast(PIL.Image.Image | torch.Tensor, raw_image)
|
|
||||||
|
|
||||||
last_image: PIL.Image.Image | torch.Tensor | None = None
|
height = common.height or 480
|
||||||
if multi_modal_data is not None:
|
width = common.width or 832
|
||||||
last_image = multi_modal_data.get("last_image", None)
|
num_frames = common.num_frames or 81
|
||||||
|
num_steps = 40 if common.num_inference_steps is None else common.num_inference_steps
|
||||||
|
|
||||||
height = req.sampling_params.height or 480
|
output_type = common.output_type or "np"
|
||||||
width = req.sampling_params.width or 832
|
num_outputs_per_prompt = common.num_outputs_per_prompt or 1
|
||||||
num_frames = req.sampling_params.num_frames or 81
|
|
||||||
num_steps = 40 if req.sampling_params.num_inference_steps is None else req.sampling_params.num_inference_steps
|
|
||||||
|
|
||||||
output_type = req.sampling_params.output_type or "np"
|
|
||||||
attention_kwargs: dict | None = None
|
attention_kwargs: dict | None = None
|
||||||
|
|
||||||
guidance_low, guidance_high = resolve_wan_guidance_scales(req.sampling_params, default_guidance_scale=5.0)
|
guidance_low, guidance_high = resolve_wan_guidance_scales(common, default_guidance_scale=5.0)
|
||||||
|
|
||||||
self._guidance_scale = guidance_low
|
self._guidance_scale = guidance_low
|
||||||
self._guidance_scale_2 = guidance_high
|
self._guidance_scale_2 = guidance_high
|
||||||
|
|
||||||
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else req.sampling_params.boundary_ratio
|
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else common.boundary_ratio
|
||||||
if boundary_ratio is None:
|
if boundary_ratio is None:
|
||||||
boundary_ratio = 0.875
|
boundary_ratio = 0.875
|
||||||
logger.warning("boundary_ratio is required for I2V generation. using default value 0.875")
|
logger.warning("boundary_ratio is required for I2V generation. using default value 0.875")
|
||||||
@@ -495,7 +534,7 @@ class Wan22I2VPipeline(
|
|||||||
self.check_inputs(
|
self.check_inputs(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
image=image,
|
image=images,
|
||||||
height=height,
|
height=height,
|
||||||
width=width,
|
width=width,
|
||||||
prompt_embeds=prompt_embeds,
|
prompt_embeds=prompt_embeds,
|
||||||
@@ -513,10 +552,8 @@ class Wan22I2VPipeline(
|
|||||||
device = self.device
|
device = self.device
|
||||||
dtype = self.transformer.dtype
|
dtype = self.transformer.dtype
|
||||||
|
|
||||||
# Generator setup
|
generator = req.collate_request_generators(num_outputs_per_prompt, None)
|
||||||
generator = req.sampling_params.generator
|
request_latents = req.collate_request_tensors("latents", None)
|
||||||
if generator is None and req.sampling_params.seed is not None:
|
|
||||||
generator = torch.Generator(device=device).manual_seed(req.sampling_params.seed)
|
|
||||||
|
|
||||||
if DEBUG_PERF:
|
if DEBUG_PERF:
|
||||||
# Sync GPU before timing to ensure accurate measurements
|
# Sync GPU before timing to ensure accurate measurements
|
||||||
@@ -524,15 +561,33 @@ class Wan22I2VPipeline(
|
|||||||
_t_pipeline_start = time.perf_counter()
|
_t_pipeline_start = time.perf_counter()
|
||||||
_t_text_enc_start = _t_pipeline_start
|
_t_text_enc_start = _t_pipeline_start
|
||||||
|
|
||||||
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
do_classifier_free_guidance = guidance_low > 1.0 or guidance_high > 1.0
|
||||||
prompt=prompt,
|
if prompt_embeds is None:
|
||||||
negative_prompt=negative_prompt,
|
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
||||||
do_classifier_free_guidance=guidance_low > 1.0 or guidance_high > 1.0,
|
prompt=prompt,
|
||||||
num_videos_per_prompt=req.sampling_params.num_outputs_per_prompt or 1,
|
negative_prompt=negative_prompt,
|
||||||
max_sequence_length=req.sampling_params.max_sequence_length or 512,
|
do_classifier_free_guidance=do_classifier_free_guidance,
|
||||||
device=device,
|
num_videos_per_prompt=num_outputs_per_prompt,
|
||||||
dtype=dtype,
|
max_sequence_length=common.max_sequence_length or 512,
|
||||||
)
|
device=device,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
prompt_embeds = prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
if negative_prompt_embeds is not None:
|
||||||
|
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
negative_prompt_embeds = negative_prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
elif do_classifier_free_guidance:
|
||||||
|
_, negative_prompt_embeds = self.encode_prompt(
|
||||||
|
prompt=[""] * req.num_reqs,
|
||||||
|
negative_prompt=negative_prompt,
|
||||||
|
do_classifier_free_guidance=True,
|
||||||
|
num_videos_per_prompt=num_outputs_per_prompt,
|
||||||
|
max_sequence_length=common.max_sequence_length or 512,
|
||||||
|
device=device,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
|
||||||
if DEBUG_PERF:
|
if DEBUG_PERF:
|
||||||
current_omni_platform.synchronize()
|
current_omni_platform.synchronize()
|
||||||
@@ -543,12 +598,18 @@ class Wan22I2VPipeline(
|
|||||||
if DEBUG_PERF:
|
if DEBUG_PERF:
|
||||||
_t_img_enc_start = time.perf_counter()
|
_t_img_enc_start = time.perf_counter()
|
||||||
if self.has_image_encoder and self.transformer.config.image_dim is not None:
|
if self.has_image_encoder and self.transformer.config.image_dim is not None:
|
||||||
if image_embeds is None:
|
if all(last_image is None for last_image in last_images):
|
||||||
if last_image is None:
|
image_embeds = self.encode_image(images, device)
|
||||||
image_embeds = self.encode_image(image, device)
|
image_embeds = image_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
else:
|
else:
|
||||||
image_embeds = self.encode_image([image, last_image], device)
|
image_pairs = []
|
||||||
image_embeds = image_embeds.repeat(batch_size, 1, 1)
|
for first_image, last_image in zip(images, last_images):
|
||||||
|
assert last_image is not None
|
||||||
|
image_pairs.extend([first_image, last_image])
|
||||||
|
image_embeds = self.encode_image(image_pairs, device)
|
||||||
|
image_embeds = image_embeds.view(req.num_reqs, 2, *image_embeds.shape[1:])
|
||||||
|
image_embeds = image_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
image_embeds = image_embeds.flatten(0, 1)
|
||||||
image_embeds = image_embeds.to(dtype)
|
image_embeds = image_embeds.to(dtype)
|
||||||
else:
|
else:
|
||||||
image_embeds = None
|
image_embeds = None
|
||||||
@@ -557,8 +618,9 @@ class Wan22I2VPipeline(
|
|||||||
current_omni_platform.synchronize()
|
current_omni_platform.synchronize()
|
||||||
_t_img_enc_ms = (time.perf_counter() - _t_img_enc_start) * 1000
|
_t_img_enc_ms = (time.perf_counter() - _t_img_enc_start) * 1000
|
||||||
|
|
||||||
sample_solver = resolve_wan_sample_solver(req, default=self._sample_solver)
|
first_request = req.requests[0]
|
||||||
flow_shift = resolve_wan_flow_shift(req, self.od_config)
|
sample_solver = resolve_wan_sample_solver(first_request, default=self._sample_solver)
|
||||||
|
flow_shift = resolve_wan_flow_shift(first_request, self.od_config)
|
||||||
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
|
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
|
||||||
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
|
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
|
||||||
self._sample_solver = sample_solver
|
self._sample_solver = sample_solver
|
||||||
@@ -582,23 +644,36 @@ class Wan22I2VPipeline(
|
|||||||
|
|
||||||
video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
|
video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
|
||||||
|
|
||||||
if isinstance(image, PIL.Image.Image):
|
image_tensors = []
|
||||||
image = TF.to_tensor(image).to(device)
|
for image in images:
|
||||||
image_tensor = video_processor.preprocess(image, height=height, width=width)
|
if isinstance(image, PIL.Image.Image):
|
||||||
else:
|
image = TF.to_tensor(image).to(device)
|
||||||
image_tensor = image
|
image_tensor = video_processor.preprocess(image, height=height, width=width)
|
||||||
image_tensor = image_tensor.to(device=device, dtype=torch.float32)
|
|
||||||
|
|
||||||
# Handle last_image if provided
|
|
||||||
if last_image is not None:
|
|
||||||
if isinstance(last_image, PIL.Image.Image):
|
|
||||||
image = TF.to_tensor(last_image).to(device)
|
|
||||||
last_image_tensor = video_processor.preprocess(last_image, height=height, width=width)
|
|
||||||
else:
|
else:
|
||||||
last_image_tensor = last_image
|
image_tensor = image.unsqueeze(0) if image.ndim == 3 else image
|
||||||
last_image_tensor = last_image_tensor.to(device=device, dtype=torch.float32)
|
image_tensors.append(image_tensor.to(device=device, dtype=torch.float32))
|
||||||
else:
|
image_tensor = DiffusionRequestBatch.collate_tensors(image_tensors, "image condition", None)
|
||||||
last_image_tensor = None
|
assert image_tensor is not None
|
||||||
|
image_tensor = image_tensor.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
|
||||||
|
last_image_tensor = None
|
||||||
|
if all(last_image is not None for last_image in last_images):
|
||||||
|
last_image_tensors = []
|
||||||
|
for last_image in last_images:
|
||||||
|
assert last_image is not None
|
||||||
|
if isinstance(last_image, PIL.Image.Image):
|
||||||
|
last_image = TF.to_tensor(last_image).to(device)
|
||||||
|
current_last_image = video_processor.preprocess(last_image, height=height, width=width)
|
||||||
|
else:
|
||||||
|
current_last_image = last_image.unsqueeze(0) if last_image.ndim == 3 else last_image
|
||||||
|
last_image_tensors.append(current_last_image.to(device=device, dtype=torch.float32))
|
||||||
|
last_image_tensor = DiffusionRequestBatch.collate_tensors(
|
||||||
|
last_image_tensors,
|
||||||
|
"last image condition",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
assert last_image_tensor is not None
|
||||||
|
last_image_tensor = last_image_tensor.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
|
||||||
latents, condition, first_frame_mask = self.prepare_latents(
|
latents, condition, first_frame_mask = self.prepare_latents(
|
||||||
image=image_tensor,
|
image=image_tensor,
|
||||||
@@ -610,7 +685,7 @@ class Wan22I2VPipeline(
|
|||||||
dtype=torch.float32,
|
dtype=torch.float32,
|
||||||
device=device,
|
device=device,
|
||||||
generator=generator,
|
generator=generator,
|
||||||
latents=req.sampling_params.latents,
|
latents=request_latents,
|
||||||
last_image=last_image_tensor,
|
last_image=last_image_tensor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -694,8 +769,13 @@ class Wan22I2VPipeline(
|
|||||||
_t_pipeline_wall_ms - _t_stages_sum,
|
_t_pipeline_wall_ms - _t_stages_sum,
|
||||||
)
|
)
|
||||||
|
|
||||||
return DiffusionOutput(
|
return split_diffusion_output_by_request(
|
||||||
output=output, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None
|
DiffusionOutput(
|
||||||
|
output=output,
|
||||||
|
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
|
||||||
|
),
|
||||||
|
req,
|
||||||
|
num_outputs_per_prompt=num_outputs_per_prompt,
|
||||||
)
|
)
|
||||||
|
|
||||||
def predict_noise(
|
def predict_noise(
|
||||||
@@ -849,7 +929,6 @@ class Wan22I2VPipeline(
|
|||||||
|
|
||||||
# Encode through VAE
|
# Encode through VAE
|
||||||
latent_condition = retrieve_latents(self.vae.encode(video_condition), sample_mode="argmax")
|
latent_condition = retrieve_latents(self.vae.encode(video_condition), sample_mode="argmax")
|
||||||
latent_condition = latent_condition.repeat(batch_size, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
# Normalize latents
|
# Normalize latents
|
||||||
latents_mean = (
|
latents_mean = (
|
||||||
@@ -866,7 +945,7 @@ class Wan22I2VPipeline(
|
|||||||
if self.expand_timesteps:
|
if self.expand_timesteps:
|
||||||
# TI2V-5B style: create mask where first frame is 0 (condition), rest is 1 (to denoise)
|
# TI2V-5B style: create mask where first frame is 0 (condition), rest is 1 (to denoise)
|
||||||
first_frame_mask = torch.ones(
|
first_frame_mask = torch.ones(
|
||||||
1, 1, num_latent_frames, latent_height, latent_width, dtype=dtype, device=device
|
batch_size, 1, num_latent_frames, latent_height, latent_width, dtype=dtype, device=device
|
||||||
)
|
)
|
||||||
first_frame_mask[:, :, 0] = 0
|
first_frame_mask[:, :, 0] = 0
|
||||||
return latents, latent_condition, first_frame_mask
|
return latents, latent_condition, first_frame_mask
|
||||||
@@ -892,7 +971,9 @@ class Wan22I2VPipeline(
|
|||||||
condition = torch.concat([mask_lat_size, latent_condition], dim=1)
|
condition = torch.concat([mask_lat_size, latent_condition], dim=1)
|
||||||
|
|
||||||
# For non-expand mode, first_frame_mask is not used in the same way
|
# For non-expand mode, first_frame_mask is not used in the same way
|
||||||
first_frame_mask = torch.ones(1, 1, num_latent_frames, latent_height, latent_width, dtype=dtype, device=device)
|
first_frame_mask = torch.ones(
|
||||||
|
batch_size, 1, num_latent_frames, latent_height, latent_width, dtype=dtype, device=device
|
||||||
|
)
|
||||||
|
|
||||||
return latents, condition, first_frame_mask
|
return latents, condition, first_frame_mask
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from vllm_omni.diffusion.models.wan2_2.wan2_2_s2v_transformer import (
|
|||||||
)
|
)
|
||||||
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
||||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch, split_diffusion_output_by_request
|
||||||
from vllm_omni.inputs.data import OmniTextPrompt
|
from vllm_omni.inputs.data import OmniTextPrompt
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
@@ -63,6 +63,30 @@ _S2V_DEFAULT_NEG_PROMPT = (
|
|||||||
_AUDIO_VIDEO_RATE = 30
|
_AUDIO_VIDEO_RATE = 30
|
||||||
|
|
||||||
|
|
||||||
|
def _s2v_audio_condition_key(audio: str | np.ndarray | None) -> tuple[Any, ...]:
|
||||||
|
if isinstance(audio, str):
|
||||||
|
# File paths are deliberately conservative: two different files may
|
||||||
|
# have different durations even when their request metadata matches.
|
||||||
|
return ("path", audio)
|
||||||
|
if isinstance(audio, np.ndarray):
|
||||||
|
return ("array", tuple(audio.shape), str(audio.dtype))
|
||||||
|
return (type(audio).__name__,)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_clip_generators(
|
||||||
|
seeds: list[int | None],
|
||||||
|
request_generators: list[torch.Generator | None],
|
||||||
|
clip_index: int,
|
||||||
|
device: torch.device,
|
||||||
|
) -> list[torch.Generator | None]:
|
||||||
|
return [
|
||||||
|
torch.Generator(device=device).manual_seed((seed if seed is not None else 0) + clip_index)
|
||||||
|
if seed is not None or request_generator is None
|
||||||
|
else request_generator
|
||||||
|
for seed, request_generator in zip(seeds, request_generators)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _linear_interpolation(
|
def _linear_interpolation(
|
||||||
features: torch.Tensor,
|
features: torch.Tensor,
|
||||||
input_fps: float,
|
input_fps: float,
|
||||||
@@ -337,6 +361,12 @@ def get_wan22_s2v_pre_process_func(
|
|||||||
prompt["additional_information"]["init_first_frame"] = (
|
prompt["additional_information"]["init_first_frame"] = (
|
||||||
multi_modal_data.get("init_first_frame", False) if multi_modal_data is not None else False
|
multi_modal_data.get("init_first_frame", False) if multi_modal_data is not None else False
|
||||||
)
|
)
|
||||||
|
request.batch_compatibility_key = (
|
||||||
|
"wan22_s2v_condition",
|
||||||
|
bool(prompt["additional_information"]["init_first_frame"]),
|
||||||
|
prompt["additional_information"].get("num_repeat"),
|
||||||
|
_s2v_audio_condition_key(raw_audio),
|
||||||
|
)
|
||||||
request.prompt = prompt
|
request.prompt = prompt
|
||||||
|
|
||||||
return request
|
return request
|
||||||
@@ -486,6 +516,7 @@ class Wan22S2VPipeline(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Default config values from Wan2.2/wan/configs/wan_s2v_14B.py
|
# Default config values from Wan2.2/wan/configs/wan_s2v_14B.py
|
||||||
|
supports_request_batch = True
|
||||||
_DEFAULT_MOTION_FRAMES = 73
|
_DEFAULT_MOTION_FRAMES = 73
|
||||||
_DEFAULT_INFER_FRAMES = 80
|
_DEFAULT_INFER_FRAMES = 80
|
||||||
_DEFAULT_FPS = 16
|
_DEFAULT_FPS = 16
|
||||||
@@ -942,7 +973,6 @@ class Wan22S2VPipeline(
|
|||||||
"""Forward pass through the S2V transformer to predict noise.
|
"""Forward pass through the S2V transformer to predict noise.
|
||||||
|
|
||||||
With return_dict=False, the model returns (output,) where output is [B, C, T, H, W].
|
With return_dict=False, the model returns (output,) where output is [B, C, T, H, W].
|
||||||
We squeeze batch dim since S2V processes one sample at a time.
|
|
||||||
"""
|
"""
|
||||||
if current_model is None:
|
if current_model is None:
|
||||||
current_model = self.transformer
|
current_model = self.transformer
|
||||||
@@ -952,8 +982,8 @@ class Wan22S2VPipeline(
|
|||||||
if isinstance(result, list):
|
if isinstance(result, list):
|
||||||
return result[0]
|
return result[0]
|
||||||
if isinstance(result, tuple):
|
if isinstance(result, tuple):
|
||||||
return result[0].squeeze(0)
|
return result[0]
|
||||||
return result.sample.squeeze(0)
|
return result.sample
|
||||||
|
|
||||||
def diffuse(
|
def diffuse(
|
||||||
self,
|
self,
|
||||||
@@ -962,7 +992,7 @@ class Wan22S2VPipeline(
|
|||||||
prompt_embeds: torch.Tensor,
|
prompt_embeds: torch.Tensor,
|
||||||
negative_prompt_embeds: torch.Tensor | None,
|
negative_prompt_embeds: torch.Tensor | None,
|
||||||
guidance_scale: float,
|
guidance_scale: float,
|
||||||
clip_generator: torch.Generator,
|
clip_generator: torch.Generator | list[torch.Generator] | None,
|
||||||
dtype: torch.dtype,
|
dtype: torch.dtype,
|
||||||
device: torch.device,
|
device: torch.device,
|
||||||
max_seq_len: int,
|
max_seq_len: int,
|
||||||
@@ -971,13 +1001,13 @@ class Wan22S2VPipeline(
|
|||||||
ref_latents: torch.Tensor,
|
ref_latents: torch.Tensor,
|
||||||
motion_frames: list[int],
|
motion_frames: list[int],
|
||||||
drop_first_motion: bool,
|
drop_first_motion: bool,
|
||||||
positive_audio_emb: torch.Tensor,
|
positive_audio_emb: dict[str, torch.Tensor],
|
||||||
negative_audio_emb: torch.Tensor | None,
|
negative_audio_emb: dict[str, torch.Tensor] | None,
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""Denoising diffusion loop for one S2V clip.
|
"""Denoising diffusion loop for a batch of compatible S2V clips.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
latents: Initial noise tensor [C, T, H, W]
|
latents: Initial noise tensor [B, C, T, H, W]
|
||||||
timesteps: Denoising timesteps
|
timesteps: Denoising timesteps
|
||||||
prompt_embeds: Text embeddings [1, seq_len, dim]
|
prompt_embeds: Text embeddings [1, seq_len, dim]
|
||||||
negative_prompt_embeds: Negative text embeddings (optional)
|
negative_prompt_embeds: Negative text embeddings (optional)
|
||||||
@@ -995,7 +1025,7 @@ class Wan22S2VPipeline(
|
|||||||
negative_audio_emb: Precomputed audio embeddings for negative prompt (optional)
|
negative_audio_emb: Precomputed audio embeddings for negative prompt (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Denoised latents [C, T, H, W]
|
Denoised latents [B, C, T, H, W]
|
||||||
"""
|
"""
|
||||||
do_true_cfg = self.do_classifier_free_guidance and negative_prompt_embeds is not None
|
do_true_cfg = self.do_classifier_free_guidance and negative_prompt_embeds is not None
|
||||||
|
|
||||||
@@ -1004,13 +1034,13 @@ class Wan22S2VPipeline(
|
|||||||
self._current_timestep = t
|
self._current_timestep = t
|
||||||
self.record_denoise_step(step_idx, t)
|
self.record_denoise_step(step_idx, t)
|
||||||
|
|
||||||
latent_model_input = [latents.to(device)]
|
latent_model_input = latents.to(device)
|
||||||
timestep = t.unsqueeze(0).to(device) if t.dim() == 0 else t.to(device)
|
timestep = t.expand(latents.shape[0]).to(device)
|
||||||
|
|
||||||
positive_kwargs = {
|
positive_kwargs = {
|
||||||
"hidden_states": latent_model_input,
|
"hidden_states": latent_model_input,
|
||||||
"timestep": timestep,
|
"timestep": timestep,
|
||||||
"encoder_hidden_states": prompt_embeds[0:1],
|
"encoder_hidden_states": prompt_embeds,
|
||||||
"cond_states": cond_latents,
|
"cond_states": cond_latents,
|
||||||
"motion_latents": input_motion_latents,
|
"motion_latents": input_motion_latents,
|
||||||
"ref_latents": ref_latents,
|
"ref_latents": ref_latents,
|
||||||
@@ -1023,7 +1053,7 @@ class Wan22S2VPipeline(
|
|||||||
negative_kwargs = {
|
negative_kwargs = {
|
||||||
"hidden_states": latent_model_input,
|
"hidden_states": latent_model_input,
|
||||||
"timestep": timestep,
|
"timestep": timestep,
|
||||||
"encoder_hidden_states": negative_prompt_embeds[0:1],
|
"encoder_hidden_states": negative_prompt_embeds,
|
||||||
"cond_states": cond_latents,
|
"cond_states": cond_latents,
|
||||||
"motion_latents": input_motion_latents,
|
"motion_latents": input_motion_latents,
|
||||||
"ref_latents": ref_latents,
|
"ref_latents": ref_latents,
|
||||||
@@ -1044,12 +1074,12 @@ class Wan22S2VPipeline(
|
|||||||
|
|
||||||
# Scheduler step
|
# Scheduler step
|
||||||
latents = self.scheduler.step(
|
latents = self.scheduler.step(
|
||||||
noise_pred.unsqueeze(0),
|
noise_pred,
|
||||||
t,
|
t,
|
||||||
latents.unsqueeze(0),
|
latents,
|
||||||
return_dict=False,
|
return_dict=False,
|
||||||
generator=clip_generator,
|
generator=clip_generator if isinstance(clip_generator, torch.Generator) else None,
|
||||||
)[0].squeeze(0)
|
)[0]
|
||||||
|
|
||||||
pbar.update()
|
pbar.update()
|
||||||
|
|
||||||
@@ -1080,111 +1110,178 @@ class Wan22S2VPipeline(
|
|||||||
prompt_embeds: torch.Tensor | None = None,
|
prompt_embeds: torch.Tensor | None = None,
|
||||||
negative_prompt_embeds: torch.Tensor | None = None,
|
negative_prompt_embeds: torch.Tensor | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> DiffusionOutput:
|
) -> list[DiffusionOutput]:
|
||||||
"""Run S2V generation — may produce multiple autoregressive clips.
|
"""Run S2V generation — may produce multiple autoregressive clips.
|
||||||
|
|
||||||
This method mirrors ``WanS2V.generate()``, reorganized into the
|
This method mirrors ``WanS2V.generate()``, reorganized into the
|
||||||
vLLM-Omni pipeline structure.
|
vLLM-Omni pipeline structure.
|
||||||
"""
|
"""
|
||||||
# ---- Extract params from request ----
|
sampling_params_list = req.sampling_params_list
|
||||||
if len(req.prompts) > 1:
|
common = sampling_params_list[0]
|
||||||
raise ValueError("S2V only supports a single prompt per request.")
|
prompt_fields = DiffusionRequestBatch.collate_prompt_field_map(
|
||||||
|
req.prompts,
|
||||||
if len(req.prompts) == 1:
|
{
|
||||||
prompt_data = req.prompts[0]
|
"prompt_embeds": prompt_embeds,
|
||||||
prompt = prompt_data if isinstance(prompt_data, str) else prompt_data.get("prompt")
|
"negative_prompt_embeds": negative_prompt_embeds,
|
||||||
negative_prompt = None if isinstance(prompt_data, str) else prompt_data.get("negative_prompt")
|
},
|
||||||
|
)
|
||||||
|
prompt_embeds = prompt_fields["prompt_embeds"]
|
||||||
|
negative_prompt_embeds = prompt_fields["negative_prompt_embeds"]
|
||||||
|
|
||||||
|
prompt_texts: list[str] = []
|
||||||
|
negative_prompts: list[str] = []
|
||||||
|
images: list[PIL.Image.Image | None] = []
|
||||||
|
audio_paths: list[str | np.ndarray | None] = []
|
||||||
|
requested_repeats: list[int | None] = []
|
||||||
|
init_first_frames: list[bool] = []
|
||||||
|
for prompt_data in req.prompts:
|
||||||
multi_modal_data = prompt_data.get("multi_modal_data", {}) if not isinstance(prompt_data, str) else {}
|
multi_modal_data = prompt_data.get("multi_modal_data", {}) if not isinstance(prompt_data, str) else {}
|
||||||
additional_info = prompt_data.get("additional_information", {}) if not isinstance(prompt_data, str) else {}
|
additional_info = prompt_data.get("additional_information", {}) if not isinstance(prompt_data, str) else {}
|
||||||
|
|
||||||
if image is None:
|
prompt_texts.append(
|
||||||
raw_image = multi_modal_data.get("image", None)
|
prompt_data if isinstance(prompt_data, str) else (prompt_data.get("prompt") or prompt or "")
|
||||||
if isinstance(raw_image, str):
|
)
|
||||||
image = PIL.Image.open(raw_image).convert("RGB")
|
request_negative_prompt = (
|
||||||
elif isinstance(raw_image, PIL.Image.Image):
|
negative_prompt if isinstance(prompt_data, str) else prompt_data.get("negative_prompt", negative_prompt)
|
||||||
image = raw_image
|
)
|
||||||
|
negative_prompts.append(request_negative_prompt or _S2V_DEFAULT_NEG_PROMPT)
|
||||||
|
|
||||||
if audio_path is None:
|
raw_image = multi_modal_data.get("image", image)
|
||||||
audio_path = additional_info.get("audio_path")
|
if isinstance(raw_image, list):
|
||||||
if audio_path is None:
|
raw_image = raw_image[0] if raw_image else None
|
||||||
audio_path = multi_modal_data.get("audio")
|
if isinstance(raw_image, str):
|
||||||
|
raw_image = PIL.Image.open(raw_image).convert("RGB")
|
||||||
|
images.append(raw_image if isinstance(raw_image, PIL.Image.Image) else None)
|
||||||
|
|
||||||
if pose_video is None:
|
request_audio_path = additional_info.get("audio_path", audio_path)
|
||||||
pose_video = additional_info.get("pose_video")
|
if request_audio_path is None:
|
||||||
|
request_audio_path = multi_modal_data.get("audio")
|
||||||
|
audio_paths.append(request_audio_path)
|
||||||
|
requested_repeats.append(additional_info.get("num_repeat", num_repeat))
|
||||||
|
init_first_frames.append(additional_info.get("init_first_frame", init_first_frame))
|
||||||
|
|
||||||
init_first_frame = additional_info.get("init_first_frame", init_first_frame)
|
height = common.height or height
|
||||||
|
width = common.width or width
|
||||||
if negative_prompt is None:
|
|
||||||
negative_prompt = _S2V_DEFAULT_NEG_PROMPT
|
|
||||||
|
|
||||||
height = req.sampling_params.height or height
|
|
||||||
width = req.sampling_params.width or width
|
|
||||||
# num_frames defaults to 1 (image-model sentinel); S2V needs many frames.
|
# num_frames defaults to 1 (image-model sentinel); S2V needs many frames.
|
||||||
req_num_frames = req.sampling_params.num_frames
|
req_num_frames = common.num_frames
|
||||||
infer_frames = (
|
infer_frames = (
|
||||||
infer_frames
|
infer_frames
|
||||||
or (req_num_frames if req_num_frames and req_num_frames > 1 else None)
|
or (req_num_frames if req_num_frames and req_num_frames > 1 else None)
|
||||||
or self._DEFAULT_INFER_FRAMES
|
or self._DEFAULT_INFER_FRAMES
|
||||||
)
|
)
|
||||||
num_steps = req.sampling_params.num_inference_steps or num_inference_steps
|
num_steps = common.num_inference_steps or num_inference_steps
|
||||||
|
num_outputs_per_prompt = common.num_outputs_per_prompt or 1
|
||||||
|
|
||||||
if req.sampling_params.guidance_scale_provided:
|
if common.guidance_scale_provided:
|
||||||
guidance_scale = req.sampling_params.guidance_scale
|
guidance_scale = common.guidance_scale
|
||||||
|
|
||||||
self._guidance_scale = guidance_scale
|
self._guidance_scale = guidance_scale
|
||||||
|
|
||||||
# ---- Validate ----
|
# ---- Validate ----
|
||||||
self.check_inputs(prompt, image, audio_path, height, width, prompt_embeds)
|
if len(set(init_first_frames)) != 1:
|
||||||
|
raise ValueError("Batched S2V requests must have matching init_first_frame values.")
|
||||||
|
init_first_frame = init_first_frames[0]
|
||||||
|
for request_prompt, request_image, request_audio_path in zip(prompt_texts, images, audio_paths):
|
||||||
|
self.check_inputs(
|
||||||
|
request_prompt if prompt_embeds is None else None,
|
||||||
|
request_image,
|
||||||
|
request_audio_path,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
prompt_embeds,
|
||||||
|
)
|
||||||
|
|
||||||
device = self.device
|
device = self.device
|
||||||
dtype = self.transformer.dtype if hasattr(self.transformer, "dtype") else torch.bfloat16
|
dtype = self.transformer.dtype if hasattr(self.transformer, "dtype") else torch.bfloat16
|
||||||
|
request_generators = req.collate_request_generators(num_outputs_per_prompt, None)
|
||||||
if generator is None:
|
batch_size = req.num_reqs * num_outputs_per_prompt
|
||||||
generator = req.sampling_params.generator
|
if isinstance(request_generators, list):
|
||||||
seed = req.sampling_params.seed
|
generators = request_generators
|
||||||
if generator is None and seed is not None:
|
else:
|
||||||
generator = torch.Generator(device=device).manual_seed(seed)
|
generators = [request_generators] * batch_size
|
||||||
if seed is None:
|
seeds = [sampling.seed for sampling in sampling_params_list for _ in range(num_outputs_per_prompt)]
|
||||||
seed = 0
|
request_latents = req.collate_request_tensors("latents", None)
|
||||||
|
|
||||||
# ---- 1. Text encoding ----
|
# ---- 1. Text encoding ----
|
||||||
if prompt_embeds is None:
|
if prompt_embeds is None:
|
||||||
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
||||||
prompt=prompt,
|
prompt=prompt_texts,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompts,
|
||||||
do_classifier_free_guidance=self.do_classifier_free_guidance,
|
do_classifier_free_guidance=self.do_classifier_free_guidance,
|
||||||
num_videos_per_prompt=1,
|
num_videos_per_prompt=num_outputs_per_prompt,
|
||||||
max_sequence_length=512,
|
max_sequence_length=common.max_sequence_length or 512,
|
||||||
device=device,
|
device=device,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
|
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
prompt_embeds = prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
if negative_prompt_embeds is not None:
|
if negative_prompt_embeds is not None:
|
||||||
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
|
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
negative_prompt_embeds = negative_prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
|
||||||
# ---- 2. Audio encoding ----
|
# ---- 2. Audio encoding ----
|
||||||
audio_emb, audio_num_repeat, target_video_frames = self.encode_audio(
|
audio_embeddings: list[torch.Tensor] = []
|
||||||
audio_path, infer_frames=infer_frames, device=device, dtype=dtype
|
effective_repeats: list[int] = []
|
||||||
)
|
target_video_frames: list[int] = []
|
||||||
if num_repeat is None or num_repeat > audio_num_repeat:
|
for request_audio_path, requested_repeat in zip(audio_paths, requested_repeats):
|
||||||
num_repeat = audio_num_repeat
|
assert request_audio_path is not None
|
||||||
|
audio_emb, audio_num_repeat, request_target_video_frames = self.encode_audio(
|
||||||
|
request_audio_path, infer_frames=infer_frames, device=device, dtype=dtype
|
||||||
|
)
|
||||||
|
audio_embeddings.append(audio_emb.repeat_interleave(num_outputs_per_prompt, dim=0))
|
||||||
|
effective_repeats.append(
|
||||||
|
audio_num_repeat
|
||||||
|
if requested_repeat is None or requested_repeat > audio_num_repeat
|
||||||
|
else requested_repeat
|
||||||
|
)
|
||||||
|
target_video_frames.append(request_target_video_frames)
|
||||||
|
|
||||||
|
if len(set(effective_repeats)) != 1:
|
||||||
|
raise ValueError(f"Batched S2V requests must have matching num_repeat values, got {effective_repeats}.")
|
||||||
|
if len(set(target_video_frames)) != 1:
|
||||||
|
raise ValueError(
|
||||||
|
"Batched S2V requests must have matching audio embedding time lengths; "
|
||||||
|
f"got target video frames {target_video_frames}."
|
||||||
|
)
|
||||||
|
num_repeat = effective_repeats[0]
|
||||||
|
audio_emb = DiffusionRequestBatch.collate_tensors(audio_embeddings, "S2V audio embeddings", None)
|
||||||
|
assert audio_emb is not None
|
||||||
|
|
||||||
# Load raw audio waveform for muxing into output video
|
# Load raw audio waveform for muxing into output video
|
||||||
if isinstance(audio_path, np.ndarray):
|
raw_audio_waveforms: list[np.ndarray] = []
|
||||||
raw_audio_waveform = audio_path.astype(np.float32)
|
raw_audio_sample_rates: list[int] = []
|
||||||
raw_audio_sr = 16000
|
for request_audio_path in audio_paths:
|
||||||
else:
|
assert request_audio_path is not None
|
||||||
raw_audio_waveform, raw_audio_sr = load_audio(audio_path, sr=None, mono=True)
|
if isinstance(request_audio_path, np.ndarray):
|
||||||
|
raw_audio_waveforms.append(request_audio_path.astype(np.float32, copy=False))
|
||||||
|
raw_audio_sample_rates.append(16000)
|
||||||
|
else:
|
||||||
|
raw_audio_waveform, raw_audio_sr = load_audio(request_audio_path, sr=None, mono=True)
|
||||||
|
raw_audio_waveforms.append(raw_audio_waveform)
|
||||||
|
raw_audio_sample_rates.append(raw_audio_sr)
|
||||||
|
if len(set(raw_audio_sample_rates)) != 1 or len({waveform.shape for waveform in raw_audio_waveforms}) != 1:
|
||||||
|
raise ValueError("Batched S2V requests must have matching raw audio shapes and sample rates.")
|
||||||
|
raw_audio_sr = raw_audio_sample_rates[0]
|
||||||
|
|
||||||
# ---- 3. Reference image encoding ----
|
# ---- 3. Reference image encoding ----
|
||||||
ref_latents = self.encode_ref_image(image, height, width, device=device).to(dtype=dtype)
|
ref_latents = DiffusionRequestBatch.collate_tensors(
|
||||||
|
[
|
||||||
|
self.encode_ref_image(request_image, height, width, device=device)
|
||||||
|
.to(dtype=dtype)
|
||||||
|
.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
|
for request_image in images
|
||||||
|
if request_image is not None
|
||||||
|
],
|
||||||
|
"S2V reference image latents",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
assert ref_latents is not None
|
||||||
|
|
||||||
# ---- 4. Initial motion latents (zeros) ----
|
# ---- 4. Initial motion latents (zeros) ----
|
||||||
motion_frames = min(self.motion_frames, infer_frames - 1)
|
motion_frames = min(self.motion_frames, infer_frames - 1)
|
||||||
motion_pixels = torch.zeros(
|
motion_pixels = torch.zeros(
|
||||||
[1, 3, motion_frames, height, width],
|
[batch_size, 3, motion_frames, height, width],
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
device=device,
|
device=device,
|
||||||
)
|
)
|
||||||
@@ -1193,8 +1290,11 @@ class Wan22S2VPipeline(
|
|||||||
drop_first_motion = False
|
drop_first_motion = False
|
||||||
# Place reference image in the last 6 frames of motion
|
# Place reference image in the last 6 frames of motion
|
||||||
tensor_trans = transforms.ToTensor()
|
tensor_trans = transforms.ToTensor()
|
||||||
ref_tensor = tensor_trans(image).to(device=device, dtype=dtype) * 2 - 1.0 # [C, H, W]
|
ref_tensors = torch.stack(
|
||||||
motion_pixels[:, :, -6:] = ref_tensor.unsqueeze(0).unsqueeze(2).expand(-1, -1, 6, -1, -1)
|
[tensor_trans(request_image) for request_image in images if request_image is not None]
|
||||||
|
).to(device=device, dtype=dtype)
|
||||||
|
ref_tensors = ref_tensors.repeat_interleave(num_outputs_per_prompt, dim=0) * 2 - 1.0
|
||||||
|
motion_pixels[:, :, -6:] = ref_tensors.unsqueeze(2).expand(-1, -1, 6, -1, -1)
|
||||||
|
|
||||||
motion_latents = self.prepare_motion_latents(motion_pixels, device=device).to(dtype=dtype)
|
motion_latents = self.prepare_motion_latents(motion_pixels, device=device).to(dtype=dtype)
|
||||||
|
|
||||||
@@ -1211,24 +1311,29 @@ class Wan22S2VPipeline(
|
|||||||
clips: list[torch.Tensor] = []
|
clips: list[torch.Tensor] = []
|
||||||
# Keep a pixel-space buffer of the trailing motion_frames for the
|
# Keep a pixel-space buffer of the trailing motion_frames for the
|
||||||
# autoregressive connection between clips.
|
# autoregressive connection between clips.
|
||||||
videos_last_frames = torch.zeros([1, 3, motion_frames, height, width], dtype=dtype, device=device)
|
videos_last_frames = torch.zeros([batch_size, 3, motion_frames, height, width], dtype=dtype, device=device)
|
||||||
|
|
||||||
for r in range(num_repeat):
|
for r in range(num_repeat):
|
||||||
# Per-clip seed
|
clip_generators = _make_clip_generators(seeds, generators, r, device)
|
||||||
clip_seed = seed + r
|
|
||||||
clip_generator = torch.Generator(device=device).manual_seed(clip_seed)
|
|
||||||
|
|
||||||
# -- Noise --
|
# -- Noise --
|
||||||
latents = self.prepare_latents(
|
if r == 0 and request_latents is not None:
|
||||||
infer_frames=infer_frames,
|
latents = request_latents.to(device=device, dtype=dtype)
|
||||||
height=height,
|
else:
|
||||||
width=width,
|
latents = torch.stack(
|
||||||
motion_frames=motion_frames,
|
[
|
||||||
lat_motion_frames=lat_motion_frames,
|
self.prepare_latents(
|
||||||
dtype=dtype,
|
infer_frames=infer_frames,
|
||||||
device=device,
|
height=height,
|
||||||
generator=clip_generator,
|
width=width,
|
||||||
)
|
motion_frames=motion_frames,
|
||||||
|
lat_motion_frames=lat_motion_frames,
|
||||||
|
dtype=dtype,
|
||||||
|
device=device,
|
||||||
|
generator=clip_generator,
|
||||||
|
)
|
||||||
|
for clip_generator in clip_generators
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# -- Scheduler --
|
# -- Scheduler --
|
||||||
self.scheduler.set_timesteps(num_steps, device=device)
|
self.scheduler.set_timesteps(num_steps, device=device)
|
||||||
@@ -1242,11 +1347,11 @@ class Wan22S2VPipeline(
|
|||||||
|
|
||||||
# -- Pose condition for this clip --
|
# -- Pose condition for this clip --
|
||||||
# Default: zero condition (no pose driving)
|
# Default: zero condition (no pose driving)
|
||||||
# Shape: [B=1, C=16, T, H, W] — passed to transformer which iterates over batch dim
|
# Shape: [B, C=16, T, H, W]
|
||||||
lat_h = height // self.vae_scale_factor_spatial
|
lat_h = height // self.vae_scale_factor_spatial
|
||||||
lat_w = width // self.vae_scale_factor_spatial
|
lat_w = width // self.vae_scale_factor_spatial
|
||||||
lat_target_frames = (infer_frames + 3 + motion_frames) // 4 - lat_motion_frames
|
lat_target_frames = (infer_frames + 3 + motion_frames) // 4 - lat_motion_frames
|
||||||
cond_latents = torch.zeros([1, 16, lat_target_frames, lat_h, lat_w], dtype=dtype, device=device)
|
cond_latents = torch.zeros([batch_size, 16, lat_target_frames, lat_h, lat_w], dtype=dtype, device=device)
|
||||||
|
|
||||||
# -- Clone motion latents for this clip --
|
# -- Clone motion latents for this clip --
|
||||||
input_motion_latents = motion_latents.clone()
|
input_motion_latents = motion_latents.clone()
|
||||||
@@ -1282,7 +1387,7 @@ class Wan22S2VPipeline(
|
|||||||
prompt_embeds=prompt_embeds,
|
prompt_embeds=prompt_embeds,
|
||||||
negative_prompt_embeds=negative_prompt_embeds,
|
negative_prompt_embeds=negative_prompt_embeds,
|
||||||
guidance_scale=guidance_scale,
|
guidance_scale=guidance_scale,
|
||||||
clip_generator=clip_generator,
|
clip_generator=clip_generators,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
device=device,
|
device=device,
|
||||||
max_seq_len=max_seq_len,
|
max_seq_len=max_seq_len,
|
||||||
@@ -1300,11 +1405,10 @@ class Wan22S2VPipeline(
|
|||||||
self.transformer.to("cpu")
|
self.transformer.to("cpu")
|
||||||
current_omni_platform.empty_cache()
|
current_omni_platform.empty_cache()
|
||||||
|
|
||||||
latents_for_decode = latents.unsqueeze(0) # [1, C, T, H, W]
|
|
||||||
if not (drop_first_motion and r == 0):
|
if not (drop_first_motion and r == 0):
|
||||||
decode_latents = torch.cat([motion_latents, latents_for_decode], dim=2)
|
decode_latents = torch.cat([motion_latents, latents], dim=2)
|
||||||
else:
|
else:
|
||||||
decode_latents = torch.cat([ref_latents, latents_for_decode], dim=2)
|
decode_latents = torch.cat([ref_latents, latents], dim=2)
|
||||||
|
|
||||||
decode_latents = self._denormalize_latents(decode_latents)
|
decode_latents = self._denormalize_latents(decode_latents)
|
||||||
decode_latents = decode_latents.to(self.vae.dtype)
|
decode_latents = decode_latents.to(self.vae.dtype)
|
||||||
@@ -1321,7 +1425,7 @@ class Wan22S2VPipeline(
|
|||||||
|
|
||||||
# Create buffer for broadcast
|
# Create buffer for broadcast
|
||||||
clip_video = torch.empty(
|
clip_video = torch.empty(
|
||||||
(1, 3, total_frames, height, width),
|
(batch_size, 3, total_frames, height, width),
|
||||||
device=decode_latents.device,
|
device=decode_latents.device,
|
||||||
dtype=decode_latents.dtype,
|
dtype=decode_latents.dtype,
|
||||||
)
|
)
|
||||||
@@ -1356,12 +1460,19 @@ class Wan22S2VPipeline(
|
|||||||
current_omni_platform.empty_cache()
|
current_omni_platform.empty_cache()
|
||||||
|
|
||||||
# ---- Concatenate all clips ----
|
# ---- Concatenate all clips ----
|
||||||
output = torch.cat(clips, dim=2) # [1, C, T_total, H, W]
|
output = torch.cat(clips, dim=2) # [B, C, T_total, H, W]
|
||||||
|
|
||||||
return DiffusionOutput(
|
outputs = split_diffusion_output_by_request(
|
||||||
output=(output, raw_audio_waveform, raw_audio_sr),
|
DiffusionOutput(
|
||||||
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
|
output=output,
|
||||||
|
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
|
||||||
|
),
|
||||||
|
req,
|
||||||
|
num_outputs_per_prompt=num_outputs_per_prompt,
|
||||||
)
|
)
|
||||||
|
for request_output, raw_audio_waveform in zip(outputs, raw_audio_waveforms):
|
||||||
|
request_output.output = (request_output.output, raw_audio_waveform, raw_audio_sr)
|
||||||
|
return outputs
|
||||||
|
|
||||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||||
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
||||||
|
|||||||
@@ -35,13 +35,27 @@ from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
|
|||||||
)
|
)
|
||||||
from vllm_omni.diffusion.models.wan2_2.wan2_2_vace_transformer import WanVACETransformer3DModel
|
from vllm_omni.diffusion.models.wan2_2.wan2_2_vace_transformer import WanVACETransformer3DModel
|
||||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch, split_diffusion_output_by_request
|
||||||
from vllm_omni.inputs.data import OmniTextPrompt
|
from vllm_omni.inputs.data import OmniTextPrompt
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _vace_condition_structure(value: object) -> tuple:
|
||||||
|
if value is None:
|
||||||
|
return ("none",)
|
||||||
|
if isinstance(value, str):
|
||||||
|
return ("path", value)
|
||||||
|
if isinstance(value, PIL.Image.Image):
|
||||||
|
return ("image", value.size)
|
||||||
|
if isinstance(value, torch.Tensor):
|
||||||
|
return ("tensor", tuple(value.shape), str(value.dtype))
|
||||||
|
if isinstance(value, list):
|
||||||
|
return ("list", tuple(_vace_condition_structure(item) for item in value))
|
||||||
|
return (type(value).__name__,)
|
||||||
|
|
||||||
|
|
||||||
def create_vace_transformer_from_config(
|
def create_vace_transformer_from_config(
|
||||||
config: dict,
|
config: dict,
|
||||||
quant_config: QuantizationConfig | None = None,
|
quant_config: QuantizationConfig | None = None,
|
||||||
@@ -104,6 +118,7 @@ def get_wan22_vace_pre_process_func(od_config: OmniDiffusionConfig):
|
|||||||
prompt["additional_information"] = {}
|
prompt["additional_information"] = {}
|
||||||
|
|
||||||
if not multi_modal_data:
|
if not multi_modal_data:
|
||||||
|
request.batch_compatibility_key = ("wan22_vace_condition", ("none",), ("none",), ("none",))
|
||||||
request.prompt = prompt
|
request.prompt = prompt
|
||||||
return request
|
return request
|
||||||
|
|
||||||
@@ -155,6 +170,13 @@ def get_wan22_vace_pre_process_func(od_config: OmniDiffusionConfig):
|
|||||||
mask = [mask]
|
mask = [mask]
|
||||||
prompt["additional_information"]["mask"] = mask
|
prompt["additional_information"]["mask"] = mask
|
||||||
|
|
||||||
|
request.batch_compatibility_key = (
|
||||||
|
"wan22_vace_condition",
|
||||||
|
_vace_condition_structure(ref_images),
|
||||||
|
_vace_condition_structure(source_video),
|
||||||
|
_vace_condition_structure(mask),
|
||||||
|
)
|
||||||
|
|
||||||
request.prompt = prompt
|
request.prompt = prompt
|
||||||
return request
|
return request
|
||||||
|
|
||||||
@@ -482,7 +504,7 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
|||||||
attention_kwargs: dict | None = None,
|
attention_kwargs: dict | None = None,
|
||||||
vace_context_scale: float | list[float] = 1.0,
|
vace_context_scale: float | list[float] = 1.0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> DiffusionOutput:
|
) -> list[DiffusionOutput]:
|
||||||
"""Generate or edit video using VACE.
|
"""Generate or edit video using VACE.
|
||||||
|
|
||||||
The mode is determined by which inputs are provided in the request:
|
The mode is determined by which inputs are provided in the request:
|
||||||
@@ -507,47 +529,48 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
|||||||
attention_kwargs: Additional kwargs for attention layers.
|
attention_kwargs: Additional kwargs for attention layers.
|
||||||
vace_context_scale: VACE conditioning strength.
|
vace_context_scale: VACE conditioning strength.
|
||||||
"""
|
"""
|
||||||
# Get parameters from request or arguments
|
sampling_params_list = req.sampling_params_list
|
||||||
if len(req.prompts) > 1:
|
common = sampling_params_list[0]
|
||||||
raise ValueError(
|
prompt_texts = [item if isinstance(item, str) else (item.get("prompt") or "") for item in req.prompts]
|
||||||
"This model only supports a single prompt, not a batched request. "
|
negative_prompts = [None if isinstance(item, str) else item.get("negative_prompt") for item in req.prompts]
|
||||||
"Please pass in a single prompt object or string, or a single-item list."
|
prompt_fields = DiffusionRequestBatch.collate_prompt_field_map(
|
||||||
)
|
req.prompts,
|
||||||
|
{
|
||||||
reference_images = None
|
"prompt_embeds": prompt_embeds,
|
||||||
source_video = None
|
"negative_prompt_embeds": negative_prompt_embeds,
|
||||||
source_mask = None
|
},
|
||||||
|
)
|
||||||
if len(req.prompts) == 1:
|
prompt_embeds = prompt_fields["prompt_embeds"]
|
||||||
first_prompt = req.prompts[0]
|
negative_prompt_embeds = prompt_fields["negative_prompt_embeds"]
|
||||||
if isinstance(first_prompt, str):
|
prompt = prompt_texts if prompt_embeds is None else None
|
||||||
prompt = first_prompt
|
negative_prompt = None
|
||||||
else:
|
if negative_prompt_embeds is None and any(value is not None for value in negative_prompts):
|
||||||
prompt = first_prompt.get("prompt")
|
negative_prompt = [value or "" for value in negative_prompts]
|
||||||
negative_prompt = negative_prompt or first_prompt.get("negative_prompt")
|
if prompt is not None and not all(prompt):
|
||||||
prompt_embeds = prompt_embeds if prompt_embeds is not None else first_prompt.get("prompt_embeds")
|
|
||||||
negative_prompt_embeds = (
|
|
||||||
negative_prompt_embeds
|
|
||||||
if negative_prompt_embeds is not None
|
|
||||||
else first_prompt.get("negative_prompt_embeds")
|
|
||||||
)
|
|
||||||
|
|
||||||
additional_info = first_prompt.get("additional_information", {})
|
|
||||||
reference_images = additional_info.get("reference_images")
|
|
||||||
source_video = additional_info.get("source_video")
|
|
||||||
source_mask = additional_info.get("mask")
|
|
||||||
|
|
||||||
if prompt is None and prompt_embeds is None:
|
|
||||||
raise ValueError("Prompt or prompt_embeds is required for VACE generation.")
|
raise ValueError("Prompt or prompt_embeds is required for VACE generation.")
|
||||||
|
|
||||||
height = req.sampling_params.height or height
|
reference_images_list = []
|
||||||
width = req.sampling_params.width or width
|
source_videos = []
|
||||||
num_frames = req.sampling_params.num_frames or frame_num
|
source_masks = []
|
||||||
num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps
|
for request_prompt in req.prompts:
|
||||||
generator = req.sampling_params.generator or generator
|
additional_info = (
|
||||||
|
request_prompt.get("additional_information", {}) if not isinstance(request_prompt, str) else {}
|
||||||
|
)
|
||||||
|
reference_images_list.append(additional_info.get("reference_images"))
|
||||||
|
source_videos.append(additional_info.get("source_video"))
|
||||||
|
source_masks.append(additional_info.get("mask"))
|
||||||
|
|
||||||
if req.sampling_params.guidance_scale_provided:
|
height = common.height or height
|
||||||
guidance_scale = req.sampling_params.guidance_scale
|
width = common.width or width
|
||||||
|
num_frames = common.num_frames or frame_num
|
||||||
|
num_inference_steps = common.num_inference_steps or num_inference_steps
|
||||||
|
num_outputs_per_prompt = common.num_outputs_per_prompt or 1
|
||||||
|
generator = req.collate_request_generators(num_outputs_per_prompt, None)
|
||||||
|
request_latents = req.collate_request_tensors("latents", None)
|
||||||
|
output_type = common.output_type or output_type
|
||||||
|
|
||||||
|
if common.guidance_scale_provided:
|
||||||
|
guidance_scale = common.guidance_scale
|
||||||
|
|
||||||
# Ensure dimensions are compatible with VAE and patch size
|
# Ensure dimensions are compatible with VAE and patch size
|
||||||
mod_value = self.vae_scale_factor_spatial * 2 # 8 * 2 = 16
|
mod_value = self.vae_scale_factor_spatial * 2 # 8 * 2 = 16
|
||||||
@@ -558,47 +581,47 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
|||||||
num_frames = num_frames // self.vae_scale_factor_temporal * self.vae_scale_factor_temporal + 1
|
num_frames = num_frames // self.vae_scale_factor_temporal * self.vae_scale_factor_temporal + 1
|
||||||
num_frames = max(num_frames, 1)
|
num_frames = max(num_frames, 1)
|
||||||
|
|
||||||
self.check_inputs(
|
for index in range(req.num_reqs):
|
||||||
prompt=prompt,
|
self.check_inputs(
|
||||||
negative_prompt=negative_prompt,
|
prompt=None if prompt is None else prompt[index],
|
||||||
height=height,
|
negative_prompt=None if negative_prompt is None else negative_prompt[index],
|
||||||
width=width,
|
height=height,
|
||||||
prompt_embeds=prompt_embeds,
|
width=width,
|
||||||
negative_prompt_embeds=negative_prompt_embeds,
|
prompt_embeds=None if prompt_embeds is None else prompt_embeds[index],
|
||||||
video=source_video,
|
negative_prompt_embeds=None if negative_prompt_embeds is None else negative_prompt_embeds[index],
|
||||||
mask=source_mask,
|
video=source_videos[index],
|
||||||
reference_images=reference_images,
|
mask=source_masks[index],
|
||||||
)
|
reference_images=reference_images_list[index],
|
||||||
|
)
|
||||||
|
|
||||||
device = self.device
|
device = self.device
|
||||||
self._guidance_scale = guidance_scale
|
self._guidance_scale = guidance_scale
|
||||||
dtype = self.transformer.dtype if self.transformer is not None else torch.bfloat16
|
dtype = self.transformer.dtype if self.transformer is not None else torch.bfloat16
|
||||||
|
|
||||||
if generator is None and req.sampling_params.seed is not None:
|
|
||||||
generator = torch.Generator(device=device).manual_seed(req.sampling_params.seed)
|
|
||||||
|
|
||||||
# Encode prompts
|
# Encode prompts
|
||||||
if prompt_embeds is None:
|
if prompt_embeds is None:
|
||||||
if prompt is None:
|
|
||||||
raise ValueError("Either prompt or prompt_embeds must be provided.")
|
|
||||||
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
do_classifier_free_guidance=guidance_scale > 1.0,
|
do_classifier_free_guidance=guidance_scale > 1.0,
|
||||||
num_videos_per_prompt=req.sampling_params.num_outputs_per_prompt or 1,
|
num_videos_per_prompt=num_outputs_per_prompt,
|
||||||
max_sequence_length=req.sampling_params.max_sequence_length or 512,
|
max_sequence_length=common.max_sequence_length or 512,
|
||||||
device=device,
|
device=device,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
|
prompt_embeds = prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
prompt_embeds = prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
if negative_prompt_embeds is not None:
|
if negative_prompt_embeds is not None:
|
||||||
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
|
negative_prompt_embeds = negative_prompt_embeds.to(device=device, dtype=dtype)
|
||||||
|
negative_prompt_embeds = negative_prompt_embeds.repeat_interleave(num_outputs_per_prompt, dim=0)
|
||||||
elif guidance_scale > 1.0:
|
elif guidance_scale > 1.0:
|
||||||
_, negative_prompt_embeds = self.encode_prompt(
|
_, negative_prompt_embeds = self.encode_prompt(
|
||||||
prompt="",
|
prompt=[""] * req.num_reqs,
|
||||||
negative_prompt=None,
|
negative_prompt=negative_prompt,
|
||||||
do_classifier_free_guidance=True,
|
do_classifier_free_guidance=True,
|
||||||
|
num_videos_per_prompt=num_outputs_per_prompt,
|
||||||
|
max_sequence_length=common.max_sequence_length or 512,
|
||||||
device=device,
|
device=device,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
)
|
)
|
||||||
@@ -608,24 +631,43 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
|||||||
# so read it from whichever transformer exists.
|
# so read it from whichever transformer exists.
|
||||||
active_transformer = self.transformer if self.transformer is not None else self.transformer_2
|
active_transformer = self.transformer if self.transformer is not None else self.transformer_2
|
||||||
if active_transformer.vace_patch_embedding is not None:
|
if active_transformer.vace_patch_embedding is not None:
|
||||||
video, mask, ref_images_processed = self.preprocess_conditions(
|
contexts = []
|
||||||
video=source_video,
|
reference_counts = []
|
||||||
mask=source_mask,
|
for index, (source_video, source_mask, reference_images) in enumerate(
|
||||||
reference_images=reference_images,
|
zip(source_videos, source_masks, reference_images_list)
|
||||||
height=height,
|
):
|
||||||
width=width,
|
video, mask, ref_images_processed = self.preprocess_conditions(
|
||||||
num_frames=num_frames,
|
video=source_video,
|
||||||
dtype=dtype,
|
mask=source_mask,
|
||||||
device=device,
|
reference_images=reference_images,
|
||||||
)
|
height=height,
|
||||||
|
width=width,
|
||||||
|
num_frames=num_frames,
|
||||||
|
dtype=dtype,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
request_generator = sampling_params_list[index].generator
|
||||||
|
if isinstance(request_generator, list):
|
||||||
|
request_generator = request_generator[0]
|
||||||
|
conditioning_latents = self.prepare_video_latents(
|
||||||
|
video,
|
||||||
|
mask,
|
||||||
|
ref_images_processed,
|
||||||
|
request_generator,
|
||||||
|
device,
|
||||||
|
)
|
||||||
|
mask_encoded = self.prepare_masks(mask, ref_images_processed)
|
||||||
|
context = torch.cat([conditioning_latents, mask_encoded], dim=1)
|
||||||
|
contexts.append(context.repeat_interleave(num_outputs_per_prompt, dim=0))
|
||||||
|
reference_counts.append(len(ref_images_processed[0]) if ref_images_processed else 0)
|
||||||
|
|
||||||
conditioning_latents = self.prepare_video_latents(video, mask, ref_images_processed, generator, device)
|
if len(set(reference_counts)) != 1:
|
||||||
mask_encoded = self.prepare_masks(mask, ref_images_processed)
|
raise ValueError(
|
||||||
|
f"Batched VACE conditions must have the same number of reference images; got {reference_counts}."
|
||||||
# Unified VACE context: [video_latents, mask] along channels -> [B, C, T, H, W]
|
)
|
||||||
vace_context = torch.cat([conditioning_latents, mask_encoded], dim=1)
|
vace_context = DiffusionRequestBatch.collate_tensors(contexts, "VACE condition", None)
|
||||||
|
assert vace_context is not None
|
||||||
num_reference_images = len(ref_images_processed[0]) if ref_images_processed else 0
|
num_reference_images = reference_counts[0]
|
||||||
else:
|
else:
|
||||||
vace_context = None
|
vace_context = None
|
||||||
|
|
||||||
@@ -641,7 +683,7 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
|||||||
dtype=torch.float32,
|
dtype=torch.float32,
|
||||||
device=device,
|
device=device,
|
||||||
generator=generator,
|
generator=generator,
|
||||||
latents=req.sampling_params.latents,
|
latents=request_latents,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set up scheduler
|
# Set up scheduler
|
||||||
@@ -654,9 +696,7 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
|||||||
# stays None and diffuse() uses the same transformer for every step.
|
# stays None and diffuse() uses the same transformer for every step.
|
||||||
boundary_timestep = None
|
boundary_timestep = None
|
||||||
if self.transformer_2 is not None:
|
if self.transformer_2 is not None:
|
||||||
boundary_ratio = (
|
boundary_ratio = self.boundary_ratio if self.boundary_ratio is not None else common.boundary_ratio
|
||||||
self.boundary_ratio if self.boundary_ratio is not None else req.sampling_params.boundary_ratio
|
|
||||||
)
|
|
||||||
if boundary_ratio is None:
|
if boundary_ratio is None:
|
||||||
boundary_ratio = 0.875
|
boundary_ratio = 0.875
|
||||||
logger.warning_once("boundary_ratio is required for Wan2.2 VACE generation. using default value 0.875")
|
logger.warning_once("boundary_ratio is required for Wan2.2 VACE generation. using default value 0.875")
|
||||||
@@ -700,7 +740,11 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
|||||||
latents = latents / latents_std + latents_mean
|
latents = latents / latents_std + latents_mean
|
||||||
output = self.vae.decode(latents, return_dict=False)[0]
|
output = self.vae.decode(latents, return_dict=False)[0]
|
||||||
|
|
||||||
return DiffusionOutput(output=output)
|
return split_diffusion_output_by_request(
|
||||||
|
DiffusionOutput(output=output),
|
||||||
|
req,
|
||||||
|
num_outputs_per_prompt=num_outputs_per_prompt,
|
||||||
|
)
|
||||||
|
|
||||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||||
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ class OmniDiffusionRequest:
|
|||||||
# BaseScheduler takes ownership and clears this field before the request is
|
# BaseScheduler takes ownership and clears this field before the request is
|
||||||
# sent to a Worker.
|
# sent to a Worker.
|
||||||
diffusion_kv_requests: tuple[DiffusionKVRequest, ...] | None = None
|
diffusion_kv_requests: tuple[DiffusionKVRequest, ...] | None = None
|
||||||
|
# Optional model-specific condition structure used by request-batch admission.
|
||||||
|
# This is populated by a pipeline preprocessor before the request reaches
|
||||||
|
# the scheduler; ``None`` keeps the default behavior for other pipelines.
|
||||||
|
batch_compatibility_key: tuple[Any, ...] | None = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""Initialize dependent fields after dataclass initialization."""
|
"""Initialize dependent fields after dataclass initialization."""
|
||||||
|
|||||||
@@ -88,9 +88,10 @@ class RequestBatchSamplingParamsKey:
|
|||||||
"""Request level Batch-compatibility key derived from ``OmniDiffusionSamplingParams``.
|
"""Request level Batch-compatibility key derived from ``OmniDiffusionSamplingParams``.
|
||||||
|
|
||||||
Only request-batch-wide fields belong here. Request-local values such as
|
Only request-batch-wide fields belong here. Request-local values such as
|
||||||
seeds, generators, latent tensors, timesteps, and pipeline-specific
|
seeds, generators, latent tensors, timesteps, and request-local
|
||||||
``extra_args`` are read per request from
|
``extra_args`` are read per request from
|
||||||
``DiffusionRequestBatch.sampling_params_list``.
|
``DiffusionRequestBatch.sampling_params_list``. Structural ``extra_args``
|
||||||
|
that affect batching are listed explicitly below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Spatial / temporal shape.
|
# Spatial / temporal shape.
|
||||||
@@ -131,6 +132,16 @@ class RequestBatchSamplingParamsKey:
|
|||||||
# the model and must remain distinct from explicit ``lossless``.
|
# the model and must remain distinct from explicit ``lossless``.
|
||||||
quality: str | None = None
|
quality: str | None = None
|
||||||
|
|
||||||
|
# Wan scheduler structure is carried through extra_args. Requests using
|
||||||
|
# different solvers or flow shifts must not share a request batch.
|
||||||
|
sample_solver: str | None = None
|
||||||
|
flow_shift: float | None = None
|
||||||
|
|
||||||
|
# Pipeline-specific condition structure populated during preprocessing.
|
||||||
|
# It prevents independently valid requests with incompatible conditions
|
||||||
|
# from being admitted to the same request batch.
|
||||||
|
condition_key: tuple[Any, ...] | None = None
|
||||||
|
|
||||||
# LoRA identity.
|
# LoRA identity.
|
||||||
lora_int_id: int | None = None
|
lora_int_id: int | None = None
|
||||||
lora_scale: float = 1.0
|
lora_scale: float = 1.0
|
||||||
|
|||||||
@@ -22,7 +22,21 @@ if TYPE_CHECKING:
|
|||||||
# on sampling params, so it must be resolved separately from the bulk lookup.
|
# on sampling params, so it must be resolved separately from the bulk lookup.
|
||||||
_REQUEST_BATCH_SAMPLING_PARAMS_KEY_FIELD_NAMES = frozenset(
|
_REQUEST_BATCH_SAMPLING_PARAMS_KEY_FIELD_NAMES = frozenset(
|
||||||
field.name for field in fields(RequestBatchSamplingParamsKey)
|
field.name for field in fields(RequestBatchSamplingParamsKey)
|
||||||
) - {"lora_int_id"}
|
) - {"condition_key", "flow_shift", "lora_int_id", "sample_solver"}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_explicit_sample_solver(value: object | None) -> str | None:
|
||||||
|
"""Normalize an explicitly provided solver without selecting a default."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return str(value).strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_explicit_flow_shift(value: object | None) -> float | None:
|
||||||
|
"""Normalize an explicitly provided flow shift without selecting a default."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
def build_request_batch_sampling_params_key(request: OmniDiffusionRequest) -> RequestBatchSamplingParamsKey:
|
def build_request_batch_sampling_params_key(request: OmniDiffusionRequest) -> RequestBatchSamplingParamsKey:
|
||||||
@@ -31,6 +45,13 @@ def build_request_batch_sampling_params_key(request: OmniDiffusionRequest) -> Re
|
|||||||
# LoRA identity is optional on sampling params (and on test stubs).
|
# LoRA identity is optional on sampling params (and on test stubs).
|
||||||
lora_request = getattr(sampling, "lora_request", None)
|
lora_request = getattr(sampling, "lora_request", None)
|
||||||
key_kwargs = {name: getattr(sampling, name) for name in _REQUEST_BATCH_SAMPLING_PARAMS_KEY_FIELD_NAMES}
|
key_kwargs = {name: getattr(sampling, name) for name in _REQUEST_BATCH_SAMPLING_PARAMS_KEY_FIELD_NAMES}
|
||||||
|
extra_args = sampling.extra_args or {}
|
||||||
|
# Match pipeline resolution for explicit overrides, but preserve None:
|
||||||
|
# pipeline/engine defaults are configuration-dependent and must not be
|
||||||
|
# inferred while building the request-batch key.
|
||||||
|
key_kwargs["sample_solver"] = _normalize_explicit_sample_solver(extra_args.get("sample_solver"))
|
||||||
|
key_kwargs["flow_shift"] = _normalize_explicit_flow_shift(extra_args.get("flow_shift"))
|
||||||
|
key_kwargs["condition_key"] = getattr(request, "batch_compatibility_key", None)
|
||||||
key_kwargs["lora_int_id"] = lora_request.lora_int_id if lora_request is not None else None
|
key_kwargs["lora_int_id"] = lora_request.lora_int_id if lora_request is not None else None
|
||||||
return RequestBatchSamplingParamsKey(**key_kwargs)
|
return RequestBatchSamplingParamsKey(**key_kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -1558,6 +1558,7 @@ def launch_diffusion_stage_replica(
|
|||||||
from vllm_omni.diffusion.stage_diffusion_client import StageDiffusionClient
|
from vllm_omni.diffusion.stage_diffusion_client import StageDiffusionClient
|
||||||
|
|
||||||
od_config = build_diffusion_config(model, stage_config, metadata)
|
od_config = build_diffusion_config(model, stage_config, metadata)
|
||||||
|
od_config.max_num_seqs = batch_size
|
||||||
parallel_config = getattr(od_config, "parallel_config", None)
|
parallel_config = getattr(od_config, "parallel_config", None)
|
||||||
world_size = getattr(parallel_config, "world_size", 1)
|
world_size = getattr(parallel_config, "world_size", 1)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1441,6 +1441,7 @@ def initialize_diffusion_stage(
|
|||||||
from vllm_omni.diffusion.stage_diffusion_client import create_diffusion_client
|
from vllm_omni.diffusion.stage_diffusion_client import create_diffusion_client
|
||||||
|
|
||||||
od_config = build_diffusion_config(model, stage_cfg, metadata)
|
od_config = build_diffusion_config(model, stage_cfg, metadata)
|
||||||
|
od_config.max_num_seqs = batch_size
|
||||||
return create_diffusion_client(model, od_config, metadata, stage_init_timeout, batch_size, use_inline)
|
return create_diffusion_client(model, od_config, metadata, stage_init_timeout, batch_size, use_inline)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user