[Feature] Transfer AR-to-diffusion payloads over NIXL (#6264)
Signed-off-by: yuanwu <yuan.wu@intel.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
8953d082c0
commit
cb5f508212
60 files changed
+4397
-641
No files matched your search
@@ -146,3 +146,34 @@ def test_set_seq_parallel_pg_validates_sp_group_ranks(monkeypatch):
|
||||
world_size=4,
|
||||
sp_group_ranks=[[0, 2]],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rank", [1, 3])
|
||||
def test_tensor_dict_broadcast_preserves_subgroup_source(rank, monkeypatch):
|
||||
from vllm_omni.diffusion.distributed.group_coordinator import GroupCoordinator, TensorMetadata
|
||||
|
||||
coordinator = object.__new__(GroupCoordinator)
|
||||
coordinator.rank = rank
|
||||
coordinator.ranks = [1, 3]
|
||||
coordinator.rank_in_group = [1, 3].index(rank)
|
||||
coordinator.world_size = 2
|
||||
coordinator.device_group = object()
|
||||
coordinator.cpu_group = object()
|
||||
metadata = [("payload", TensorMetadata("cpu", torch.float32, (2,)))]
|
||||
monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True)
|
||||
|
||||
def broadcast_object(value, src):
|
||||
assert src == 0
|
||||
return metadata
|
||||
|
||||
def broadcast(tensor, src, group, async_op):
|
||||
assert src == 1
|
||||
assert group is coordinator.cpu_group
|
||||
assert async_op
|
||||
tensor.fill_(7)
|
||||
return SimpleNamespace(wait=lambda: None)
|
||||
|
||||
monkeypatch.setattr(coordinator, "broadcast_object", broadcast_object)
|
||||
monkeypatch.setattr(torch.distributed, "broadcast", broadcast)
|
||||
result = coordinator.broadcast_tensor_dict({"payload": torch.full((2,), 7.0)} if rank == 1 else None)
|
||||
assert torch.equal(result["payload"], torch.full((2,), 7.0))
|
||||
@@ -89,6 +89,23 @@ def test_launch_failure_disables_runtime_key(monkeypatch):
|
||||
assert len(fused_rope._FAILED_KEYS) == 1
|
||||
|
||||
|
||||
def test_non_cuda_platform_does_not_launch_fused_kernel(monkeypatch):
|
||||
from vllm_omni.diffusion.models.ernie_image import fused_rope
|
||||
|
||||
monkeypatch.setattr(fused_rope, "HAS_TRITON", True)
|
||||
monkeypatch.setattr(fused_rope.current_omni_platform, "is_cuda", lambda: False)
|
||||
monkeypatch.setattr(torch.compiler, "is_compiling", lambda: False)
|
||||
|
||||
def unexpected_launch(*args):
|
||||
raise AssertionError("non-CUDA platforms must remain on the native path")
|
||||
|
||||
monkeypatch.setattr(fused_rope, "_launch_fused_qk_rotary_emb", unexpected_launch)
|
||||
query = torch.zeros((1, 1, 1, 128), dtype=torch.bfloat16)
|
||||
freqs = torch.zeros((1, 1, 64), dtype=torch.float32)
|
||||
assert fused_rope.try_fused_qk_rotary_emb(query, query, freqs, freqs) is None
|
||||
assert not fused_rope._FAILED_KEYS
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
|
||||
def test_failed_runtime_key_cache_is_bounded():
|
||||
from vllm_omni.diffusion.models.ernie_image import fused_rope
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -10,6 +10,7 @@ import torch
|
||||
from PIL import Image
|
||||
|
||||
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
|
||||
from vllm_omni.errors import OmniClientError
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
@@ -42,8 +43,10 @@ def pipeline():
|
||||
return model
|
||||
|
||||
|
||||
def _request(task):
|
||||
def _request(task, *, payload=None):
|
||||
prompt: dict[str, Any] = {"prompt": "A person waves."}
|
||||
if payload is not None:
|
||||
prompt["additional_information"] = {"encoder_output": payload}
|
||||
if task != "t2va":
|
||||
prompt["multi_modal_data"] = {"image": Image.new("RGB", (256, 256))}
|
||||
sampling = OmniDiffusionSamplingParams(
|
||||
@@ -64,7 +67,120 @@ def test_single_stage_encodes_without_stage_zero(pipeline, task):
|
||||
torch.testing.assert_close(audio, pipeline.decode.return_value[1])
|
||||
|
||||
|
||||
def test_mixed_reference_conditioning_matches_stage_zero_handoff(pipeline, monkeypatch):
|
||||
def test_single_stage_reuses_legacy_text_but_encodes_media(pipeline):
|
||||
payload = {
|
||||
"hidden_states": torch.full((2, 5120), 3.0, dtype=torch.bfloat16),
|
||||
"token_tags": torch.ones(2, dtype=torch.long),
|
||||
}
|
||||
pipeline.forward(_request("fl2va", payload=payload))
|
||||
pipeline.encode_prompt.assert_not_called()
|
||||
pipeline.video_vae.encode_image.assert_called_once()
|
||||
torch.testing.assert_close(pipeline.diffuse.call_args.kwargs["text_embeddings"], payload["hidden_states"])
|
||||
|
||||
|
||||
def test_text_disaggregation_keeps_local_media_encoding(pipeline):
|
||||
payload = {
|
||||
"hidden_states": torch.full((2, 5120), 3.0, dtype=torch.bfloat16),
|
||||
"token_tags": torch.ones(2, dtype=torch.long),
|
||||
}
|
||||
pipeline.load_text_encoder = False
|
||||
pipeline.load_vae_encoder = True
|
||||
pipeline.forward(_request("fl2va", payload=payload))
|
||||
pipeline.encode_prompt.assert_not_called()
|
||||
pipeline.video_vae.encode_image.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", [None, {}, {"hidden_states": torch.ones(2, 5120), "token_tags": torch.ones(2)}])
|
||||
def test_stage_one_rejects_missing_or_legacy_conditioning(pipeline, payload):
|
||||
pipeline.load_text_encoder = False
|
||||
pipeline.load_vae_encoder = False
|
||||
with pytest.raises(OmniClientError):
|
||||
pipeline.forward(_request("t2va", payload=payload))
|
||||
pipeline.encode_prompt.assert_not_called()
|
||||
pipeline.video_vae.encode_image.assert_not_called()
|
||||
pipeline.diffuse.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("task", ["t2va", "fl2va", "ref2va"])
|
||||
@pytest.mark.parametrize("load_text_encoder", [True, False])
|
||||
def test_prepare_encode_uses_the_same_mode_and_conditioning(pipeline, task, load_text_encoder):
|
||||
from vllm_omni.diffusion.worker.utils import StepRequestState
|
||||
|
||||
request = _request(task)
|
||||
if not load_text_encoder:
|
||||
conditioning, window_text = pipeline._prepare_local_conditioning(request.prompts[0], request.sampling_params)
|
||||
assert window_text is None
|
||||
request.prompts[0] = {"additional_information": {"encoder_output": conditioning.to_omni_payload()}}
|
||||
pipeline.encode_prompt.reset_mock()
|
||||
pipeline.video_vae.encode_image.reset_mock()
|
||||
pipeline.load_text_encoder = load_text_encoder
|
||||
pipeline.load_vae_encoder = load_text_encoder
|
||||
state = StepRequestState(request_id="step", prompt=request.prompts[0], sampling=request.sampling_params)
|
||||
assert pipeline.prepare_encode(state) is state
|
||||
assert state.latents is not None
|
||||
assert state.latents.shape[1] == 96
|
||||
assert state.total_steps == 1
|
||||
assert pipeline.encode_prompt.call_count == int(load_text_encoder)
|
||||
assert pipeline.video_vae.encode_image.call_count == int(load_text_encoder and task != "t2va")
|
||||
|
||||
|
||||
def test_stage_one_step_execution_never_falls_back(pipeline):
|
||||
from vllm_omni.diffusion.worker.utils import StepRequestState
|
||||
|
||||
pipeline.load_text_encoder = False
|
||||
pipeline.load_vae_encoder = False
|
||||
request = _request("t2va")
|
||||
with pytest.raises(OmniClientError, match="requires encoder conditioning"):
|
||||
pipeline.prepare_encode(
|
||||
StepRequestState(request_id="step", prompt=request.prompts[0], sampling=request.sampling_params)
|
||||
)
|
||||
pipeline.encode_prompt.assert_not_called()
|
||||
|
||||
|
||||
def test_multiple_outputs_encode_only_once(pipeline):
|
||||
request = _request("fl2va")
|
||||
request.sampling_params.num_outputs_per_prompt = 3
|
||||
request.sampling_params.seed = 42
|
||||
pipeline.forward(request)
|
||||
pipeline.encode_prompt.assert_called_once()
|
||||
pipeline.video_vae.encode_image.assert_called_once()
|
||||
assert [call.kwargs["seed"] for call in pipeline.diffuse.call_args_list] == [42, 43, 44]
|
||||
|
||||
|
||||
def test_ref2va_partition_preserves_image_only_implicit_task(pipeline):
|
||||
request = _request("fl2va")
|
||||
del request.sampling_params.extra_args["task"]
|
||||
pipeline.partition = "ref2va"
|
||||
pipeline.supported_tasks = frozenset({"ref2va"})
|
||||
pipeline.forward(request)
|
||||
assert pipeline.diffuse.call_args.kwargs["task"] == "ref2va"
|
||||
|
||||
|
||||
def test_invalid_legacy_text_is_not_ignored(pipeline):
|
||||
with pytest.raises(OmniClientError, match="encoder wire requires"):
|
||||
pipeline.forward(_request("fl2va", payload={"hidden_states": "invalid"}))
|
||||
pipeline.encode_prompt.assert_not_called()
|
||||
pipeline.video_vae.encode_image.assert_not_called()
|
||||
|
||||
|
||||
def test_text_weight_loading_preserves_encoder_specific_lifecycle(pipeline):
|
||||
class Encoder(torch.nn.Module):
|
||||
def load_weights(self, weights):
|
||||
return {name for name, _ in weights}
|
||||
|
||||
pipeline.text_encoder = Encoder()
|
||||
pipeline.video_vae = torch.nn.Identity()
|
||||
pipeline.audio_vae = torch.nn.Identity()
|
||||
assert pipeline.load_weights([("text_encoder.layer.weight", torch.ones(1))]) == {"text_encoder.layer.weight"}
|
||||
pipeline.load_text_encoder = False
|
||||
pipeline.load_vae_encoder = False
|
||||
pipeline.text_encoder = None
|
||||
with pytest.raises(ValueError, match="disabled in this deployment"):
|
||||
pipeline.load_weights([("text_encoder.layer.weight", torch.ones(1))])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("connector_handoff", [False, True], ids=["inline", "full_payload"])
|
||||
def test_mixed_reference_conditioning_matches_stage_zero_handoff(pipeline, monkeypatch, connector_handoff):
|
||||
import numpy as np
|
||||
|
||||
from vllm_omni.data_entry_keys import flatten_payload
|
||||
@@ -74,7 +190,11 @@ def test_mixed_reference_conditioning_matches_stage_zero_handoff(pipeline, monke
|
||||
MiniMaxH3TextConditioning,
|
||||
)
|
||||
from vllm_omni.model_executor.models.minimax_h3.encoder import MiniMaxH3Encoder
|
||||
from vllm_omni.model_executor.stage_input_processors.minimax_h3 import encoder2diffusion, prepare_encoder_prompt
|
||||
from vllm_omni.model_executor.stage_input_processors.minimax_h3 import (
|
||||
encoder2diffusion,
|
||||
encoder2diffusion_full_payload,
|
||||
prepare_encoder_prompt,
|
||||
)
|
||||
|
||||
frames = np.zeros((48, 32, 32, 3), dtype=np.uint8)
|
||||
decode_frames = Mock(return_value=frames)
|
||||
@@ -110,11 +230,33 @@ def test_mixed_reference_conditioning_matches_stage_zero_handoff(pipeline, monke
|
||||
media = stage_zero._encode_media(stage_zero._media_input(transformed["additional_information"]))
|
||||
text = MiniMaxH3TextConditioning(*pipeline.encode_prompt.return_value)
|
||||
payload = MiniMaxH3EncoderConditioning.from_components(text, media).to_omni_payload()
|
||||
source = SimpleNamespace(finished=True, outputs=[SimpleNamespace(multimodal_output=flatten_payload(payload))])
|
||||
wire = flatten_payload(payload)
|
||||
source = SimpleNamespace(
|
||||
finished=True, outputs=[SimpleNamespace(multimodal_output=None if connector_handoff else wire)]
|
||||
)
|
||||
bridged = encoder2diffusion([source], request.prompts[0])
|
||||
assert bridged["multi_modal_data"] is None
|
||||
if connector_handoff:
|
||||
assert "encoder_output" not in bridged["additional_information"]
|
||||
transferred = encoder2diffusion_full_payload(pooling_output=wire)
|
||||
assert set(transferred) == {"encoder_output"}
|
||||
received = transferred["encoder_output"]
|
||||
for section, key in (
|
||||
("hidden_states", "output"),
|
||||
("meta", "token_role_ids"),
|
||||
("embed", "embedding"),
|
||||
("embed", "speech_feat"),
|
||||
("kv_metadata", "minimax_h3_encoder_layout"),
|
||||
):
|
||||
torch.testing.assert_close(received[section][key], payload[section][key])
|
||||
# Simulate the generic diffusion runner merging the received payload;
|
||||
# native NIXL transport is covered separately, not by this CPU test.
|
||||
bridged["additional_information"].update(transferred)
|
||||
pipeline.load_text_encoder = False
|
||||
pipeline.load_vae_encoder = False
|
||||
pipeline.encode_prompt.reset_mock()
|
||||
pipeline.video_vae.encode_image.reset_mock()
|
||||
pipeline.video_vae.encode_video.reset_mock()
|
||||
pipeline.forward(SimpleNamespace(prompts=[bridged], sampling_params=request.sampling_params))
|
||||
external_kwargs = pipeline.diffuse.call_args.kwargs
|
||||
for key, value in local_kwargs.items():
|
||||
@@ -123,6 +265,8 @@ def test_mixed_reference_conditioning_matches_stage_zero_handoff(pipeline, monke
|
||||
else:
|
||||
assert value == external_kwargs[key]
|
||||
pipeline.encode_prompt.assert_not_called()
|
||||
pipeline.video_vae.encode_image.assert_not_called()
|
||||
pipeline.video_vae.encode_video.assert_not_called()
|
||||
assert decode_frames.call_count == sampler.call_count == 2
|
||||
assert all(call.kwargs["decoded_frames"] is frames for call in sampler.call_args_list)
|
||||
assert [block["kind"] for block in local_kwargs["ref_blocks"]] == ["image", "video_audio", "audio"]
|
||||
|
||||
@@ -10,9 +10,17 @@ from unittest.mock import Mock
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.models.minimax_h3.conditioning import MiniMaxH3EncoderMediaInput
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
|
||||
def _media(**kwargs):
|
||||
return MiniMaxH3EncoderMediaInput(
|
||||
task="ref2va", height=768, width=1344, num_frames=124, latent_t=37, audio_t=207, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def test_h3_full_pipeline_profiler_includes_local_encoding():
|
||||
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
|
||||
|
||||
@@ -28,6 +36,9 @@ def test_h3_full_pipeline_profiler_includes_local_encoding():
|
||||
"denoise_step",
|
||||
"post_decode",
|
||||
]
|
||||
assert "_encode_video_conditions" not in targets
|
||||
assert "_encode_video_audio_conditions" not in targets
|
||||
assert "_encode_audio_conditions" not in targets
|
||||
|
||||
|
||||
def test_decoded_output_is_unchanged_without_active_model_offload():
|
||||
@@ -316,46 +327,6 @@ def test_h3_model_cpu_offload_registers_direct_vae_stages(monkeypatch, load_text
|
||||
remove_offload.assert_called_once_with([*dits, *stages])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("decode_fails", [False, True])
|
||||
def test_h3_model_cpu_offload_scopes_direct_vae_call(monkeypatch, decode_fails):
|
||||
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
|
||||
from vllm_omni.diffusion.models.minimax_h3 import pipeline_minimax_h3 as module
|
||||
|
||||
pipeline = object.__new__(MiniMaxH3Pipeline)
|
||||
torch.nn.Module.__init__(pipeline)
|
||||
component = torch.nn.Linear(2, 2)
|
||||
events: list[tuple[Any, ...]] = []
|
||||
|
||||
@contextmanager
|
||||
def record_component(value):
|
||||
events.append(("activate", value))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
events.append(("offload", value))
|
||||
|
||||
monkeypatch.setattr(module, "sequential_offload_component", record_component)
|
||||
pipeline._model_cpu_offload_modules = [component]
|
||||
|
||||
def decode():
|
||||
with pipeline._component_on_device(component):
|
||||
events.append(("decode", component))
|
||||
if decode_fails:
|
||||
raise RuntimeError("decode failed")
|
||||
|
||||
if decode_fails:
|
||||
with pytest.raises(RuntimeError, match="decode failed"):
|
||||
decode()
|
||||
else:
|
||||
decode()
|
||||
|
||||
assert events == [
|
||||
("activate", component),
|
||||
("decode", component),
|
||||
("offload", component),
|
||||
]
|
||||
|
||||
|
||||
def test_h3_model_cpu_offload_keeps_unselected_vaes_resident(monkeypatch):
|
||||
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
|
||||
from vllm_omni.diffusion.models.minimax_h3 import pipeline_minimax_h3 as module
|
||||
@@ -416,6 +387,46 @@ def test_h3_model_cpu_offload_rejects_unloaded_selected_encoder(monkeypatch):
|
||||
apply_offload.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("decode_fails", [False, True])
|
||||
def test_h3_model_cpu_offload_scopes_direct_vae_call(monkeypatch, decode_fails):
|
||||
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
|
||||
from vllm_omni.diffusion.models.minimax_h3 import pipeline_minimax_h3 as module
|
||||
|
||||
pipeline = object.__new__(MiniMaxH3Pipeline)
|
||||
torch.nn.Module.__init__(pipeline)
|
||||
component = torch.nn.Linear(2, 2)
|
||||
events: list[tuple[Any, ...]] = []
|
||||
|
||||
@contextmanager
|
||||
def record_component(value):
|
||||
events.append(("activate", value))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
events.append(("offload", value))
|
||||
|
||||
monkeypatch.setattr(module, "sequential_offload_component", record_component)
|
||||
pipeline._model_cpu_offload_modules = [component]
|
||||
|
||||
def decode():
|
||||
with pipeline._component_on_device(component):
|
||||
events.append(("decode", component))
|
||||
if decode_fails:
|
||||
raise RuntimeError("decode failed")
|
||||
|
||||
if decode_fails:
|
||||
with pytest.raises(RuntimeError, match="decode failed"):
|
||||
decode()
|
||||
else:
|
||||
decode()
|
||||
|
||||
assert events == [
|
||||
("activate", component),
|
||||
("decode", component),
|
||||
("offload", component),
|
||||
]
|
||||
|
||||
|
||||
def _encode_media(pipeline, **kwargs):
|
||||
from vllm_omni.model_executor.models.minimax_h3.conditioning import MiniMaxH3EncoderMediaInput
|
||||
from vllm_omni.model_executor.models.minimax_h3.encoder_processing import encode_media
|
||||
|
||||
@@ -416,7 +416,7 @@ def test_prepare_encode_seeds_runner_visible_state(monkeypatch, batch_frames):
|
||||
monkeypatch.setattr(
|
||||
mod.MiniMaxH3Pipeline,
|
||||
"_prepare_encoder_conditioning_inputs",
|
||||
lambda self, value, sampling: context,
|
||||
lambda self, value, sampling: context if value is conditioning else pytest.fail("wrong encoder handoff"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
mod.MiniMaxH3Pipeline,
|
||||
|
||||
@@ -15,6 +15,21 @@ from vllm_omni.diffusion.models.minimax_h3.temporal_chunks import decode_tempora
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("chunked", [False, True])
|
||||
def test_encode_only_video_vae_rejects_full_and_chunked_decode(chunked):
|
||||
from vllm_omni.diffusion.models.minimax_h3.vae import MiniMaxH3VideoVAE
|
||||
|
||||
vae = object.__new__(MiniMaxH3VideoVAE)
|
||||
torch.nn.Module.__init__(vae)
|
||||
vae.encode_only = True
|
||||
latent = torch.empty(1, 1, 1, 1, 1)
|
||||
with pytest.raises(RuntimeError, match="encode-only video VAE cannot decode"):
|
||||
if chunked:
|
||||
vae.decode_with_chunks(latent, on_chunk=lambda frames: None)
|
||||
else:
|
||||
vae.decode_latent(latent)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cpu_process_group():
|
||||
if dist.is_initialized():
|
||||
|
||||
@@ -547,6 +547,9 @@ def test_execute_stepwise_streaming_returns_chunks_at_boundaries(monkeypatch):
|
||||
runner.od_config.step_execution = True
|
||||
req = _make_request()
|
||||
req.request_id = "req"
|
||||
req.external_req_id = "external-req"
|
||||
sent = []
|
||||
runner._maybe_send_stage_payload = lambda requests, outputs: sent.append((requests[0].external_req_id, outputs))
|
||||
|
||||
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
|
||||
monkeypatch.setattr(model_runner_module.current_omni_platform, "reset_peak_memory_stats", lambda: None)
|
||||
@@ -560,6 +563,7 @@ def test_execute_stepwise_streaming_returns_chunks_at_boundaries(monkeypatch):
|
||||
|
||||
first = DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
|
||||
assert first.get_request_output("req").result is None
|
||||
assert sent == []
|
||||
|
||||
scheduler_output = SimpleNamespace(
|
||||
finished_req_ids=set(),
|
||||
@@ -569,11 +573,13 @@ def test_execute_stepwise_streaming_returns_chunks_at_boundaries(monkeypatch):
|
||||
second = DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
|
||||
assert second.get_request_output("req").result == chunks[0]
|
||||
assert second.get_request_output("req").finished is False
|
||||
assert sent == []
|
||||
|
||||
DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
|
||||
fourth = DiffusionModelRunner.execute_stepwise(runner, scheduler_output)
|
||||
assert fourth.get_request_output("req").result == chunks[1]
|
||||
assert fourth.get_request_output("req").finished is True
|
||||
assert sent == [("external-req", [chunks[1]])]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@@ -733,6 +739,7 @@ def test_execute_model_passes_single_request_batch_to_non_admission_pipeline(mon
|
||||
req = _make_request()
|
||||
|
||||
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
|
||||
monkeypatch.setattr(model_runner_module, "current_omni_platform", _fake_platform_for_peak_memory())
|
||||
|
||||
output = DiffusionModelRunner.execute_model(runner, req)
|
||||
|
||||
@@ -749,6 +756,7 @@ def test_execute_model_accepts_bare_diffusion_output_from_single_request_pipelin
|
||||
req = _make_request()
|
||||
|
||||
monkeypatch.setattr(model_runner_module, "set_forward_context", _noop_forward_context)
|
||||
monkeypatch.setattr(model_runner_module, "current_omni_platform", _fake_platform_for_peak_memory())
|
||||
|
||||
output = DiffusionModelRunner.execute_model(runner, req)
|
||||
|
||||
|
||||
@@ -0,0 +1,785 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
HANDLE_KEY = DiffusionModelRunner._STAGE_PAYLOAD_HANDLE_KEY
|
||||
|
||||
|
||||
class _FakeConnector:
|
||||
def abandon_get(self, get_key):
|
||||
pass
|
||||
|
||||
def get_with_deadline(self, from_stage, to_stage, get_key, metadata=None, *, deadline):
|
||||
return self.get(from_stage, to_stage, get_key, metadata)
|
||||
|
||||
def __init__(self, payload=None, *, raises=False, put_result=(True, 128, {"schema_version": 1})):
|
||||
self._payload = payload
|
||||
self._raises = raises
|
||||
self._put_result = put_result
|
||||
self.calls: list[tuple[str, str, str, object]] = []
|
||||
self.put_calls: list[tuple[str, str, str, object]] = []
|
||||
|
||||
def get(self, from_stage, to_stage, get_key, metadata=None):
|
||||
self.calls.append((from_stage, to_stage, get_key, metadata))
|
||||
if self._raises:
|
||||
raise RuntimeError("transfer failed")
|
||||
if self._payload is None:
|
||||
return None
|
||||
return self._payload, 0
|
||||
|
||||
def put(self, from_stage, to_stage, put_key, data):
|
||||
self.put_calls.append((from_stage, to_stage, put_key, data))
|
||||
if self._raises:
|
||||
raise RuntimeError("transfer failed")
|
||||
return self._put_result
|
||||
|
||||
|
||||
class _FakeKVTransferManager:
|
||||
from vllm_omni.distributed.omni_connectors.kv_transfer_manager import OmniKVTransferManager
|
||||
|
||||
_resolve_sender_info = OmniKVTransferManager._resolve_sender_info
|
||||
|
||||
def __init__(self, connector, recv_stages=("0", "1"), send_stages=("0", "1")):
|
||||
self.connector = connector
|
||||
self.recv_stages = recv_stages
|
||||
self.send_stages = send_stages
|
||||
self.sender_info_calls: list[tuple[dict, str | None]] = []
|
||||
|
||||
def update_sender_info(self, sender_info, sender_stage_id=None):
|
||||
self.sender_info_calls.append((sender_info, sender_stage_id))
|
||||
|
||||
|
||||
def _make_runner(connector, *, payload_keys=("text_encoder_output",), recv_stages=("0", "1")):
|
||||
runner = object.__new__(DiffusionModelRunner)
|
||||
runner.od_config = SimpleNamespace(stage_input_payload_keys=payload_keys, stage_id=1)
|
||||
runner.device = torch.device("cpu")
|
||||
runner._local_rank = 0
|
||||
runner.pipeline = None
|
||||
runner.kv_transfer_manager = _FakeKVTransferManager(connector, recv_stages=recv_stages)
|
||||
runner.init_omni_connectors(runner.od_config, runner.kv_transfer_manager, synchronous=True)
|
||||
return runner
|
||||
|
||||
|
||||
def _make_sender(connector, *, payload_keys=("prompt_embeds",), send_stages=("0", "1")):
|
||||
runner = object.__new__(DiffusionModelRunner)
|
||||
runner.od_config = SimpleNamespace(stage_output_payload_keys=payload_keys, stage_id=0)
|
||||
runner.device = torch.device("cpu")
|
||||
runner.pipeline = None
|
||||
runner.kv_transfer_manager = _FakeKVTransferManager(connector, send_stages=send_stages)
|
||||
runner.init_omni_connectors(runner.od_config, runner.kv_transfer_manager, synchronous=True)
|
||||
return runner
|
||||
|
||||
|
||||
def _make_output(**custom):
|
||||
return SimpleNamespace(custom_output=dict(custom))
|
||||
|
||||
|
||||
def _make_request(prompt, *, request_id="req-7", kv_sender_info=None, payload_sender_info=None):
|
||||
return SimpleNamespace(
|
||||
prompt=prompt,
|
||||
request_id=request_id,
|
||||
kv_sender_info=kv_sender_info,
|
||||
payload_sender_info=payload_sender_info,
|
||||
)
|
||||
|
||||
|
||||
def _conditioning():
|
||||
return {"text_encoder_output": {"hidden_states": torch.zeros(4, 8), "token_tags": torch.zeros(4)}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shard_size,replicate_size", [(4, 1), (1, 4), (2, 2)])
|
||||
def test_standalone_hsdp_elects_one_payload_consumer(monkeypatch, shard_size, replicate_size):
|
||||
from vllm_omni.diffusion.distributed import parallel_state
|
||||
|
||||
leaders = []
|
||||
for rank in range(shard_size * replicate_size):
|
||||
shard_group = SimpleNamespace(world_size=shard_size, rank_in_group=rank % shard_size)
|
||||
replicate_group = SimpleNamespace(world_size=replicate_size, rank_in_group=rank // shard_size)
|
||||
monkeypatch.setattr(parallel_state, "get_fs_group", lambda: shard_group)
|
||||
monkeypatch.setattr(parallel_state, "get_hsdp_replicate_group", lambda: replicate_group)
|
||||
monkeypatch.setattr(parallel_state, "get_sp_group", lambda: None)
|
||||
runner = _make_runner(_FakeConnector())
|
||||
monkeypatch.setattr(runner, "_get_local_tp_group", lambda: None)
|
||||
if runner.is_data_transfer_rank():
|
||||
leaders.append(rank)
|
||||
|
||||
assert leaders == [0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shard_size,replicate_size", [(4, 1), (1, 4), (2, 2)])
|
||||
@pytest.mark.parametrize("delivered", [True, False])
|
||||
def test_standalone_hsdp_payload_fanout(monkeypatch, shard_size, replicate_size, delivered):
|
||||
from uuid import uuid4
|
||||
|
||||
from vllm_omni.diffusion.distributed import parallel_state
|
||||
from vllm_omni.distributed.omni_connectors.connectors.shm_connector import SharedMemoryConnector
|
||||
|
||||
packets = {}
|
||||
|
||||
class MeshGroup:
|
||||
def __init__(self, dimension, peer_rank, rank, size):
|
||||
self.key = (dimension, peer_rank)
|
||||
self.rank_in_group = rank
|
||||
self.world_size = size
|
||||
|
||||
def broadcast_object(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
packets[self.key, "delivered"] = value
|
||||
return packets[self.key, "delivered"]
|
||||
|
||||
def broadcast_tensor_dict(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
packets[self.key, "payload"] = value
|
||||
return packets[self.key, "payload"]
|
||||
|
||||
connector = SharedMemoryConnector({})
|
||||
request_id = f"hsdp-{uuid4().hex}"
|
||||
key = f"{request_id}_0_0"
|
||||
expected = torch.arange(8).reshape(2, 4)
|
||||
calls = []
|
||||
original_get = connector.get_with_deadline
|
||||
|
||||
def tracked_get(*args, **kwargs):
|
||||
calls.append(rank)
|
||||
return original_get(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(connector, "get_with_deadline", tracked_get)
|
||||
monkeypatch.setattr(parallel_state, "get_sp_group", lambda: None)
|
||||
try:
|
||||
if delivered:
|
||||
assert connector.put("0", "1", key, {"encoder_output": expected})[0]
|
||||
for rank in range(shard_size * replicate_size):
|
||||
shard_rank = rank % shard_size
|
||||
replicate_rank = rank // shard_size
|
||||
shard_group = MeshGroup("shard", replicate_rank, shard_rank, shard_size)
|
||||
replicate_group = MeshGroup("replicate", shard_rank, replicate_rank, replicate_size)
|
||||
monkeypatch.setattr(parallel_state, "get_fs_group", lambda: shard_group)
|
||||
monkeypatch.setattr(parallel_state, "get_hsdp_replicate_group", lambda: replicate_group)
|
||||
runner = _make_runner(connector, payload_keys=("encoder_output",))
|
||||
monkeypatch.setattr(runner, "_get_local_tp_group", lambda: None)
|
||||
request = _make_request({"prompt": "test"}, request_id=request_id)
|
||||
if delivered:
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
torch.testing.assert_close(request.prompt["additional_information"]["encoder_output"], expected)
|
||||
else:
|
||||
with pytest.raises(RuntimeError, match="Stage payload unavailable"):
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
assert set(calls) == {0}
|
||||
if delivered:
|
||||
assert calls == [0]
|
||||
assert connector.get("0", "1", key) is None
|
||||
finally:
|
||||
connector.close()
|
||||
|
||||
|
||||
def test_key_convention_fetch_merges_into_additional_information():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector)
|
||||
req = _make_request({"prompt": "a cat"})
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert connector.calls == [("0", "1", "req-7_0_0", None)]
|
||||
assert set(req.prompt["additional_information"]) == {"text_encoder_output"}
|
||||
assert req.prompt["additional_information"]["text_encoder_output"]["hidden_states"].shape == (4, 8)
|
||||
|
||||
|
||||
def test_handle_path_uses_its_own_key_and_metadata():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector, payload_keys=())
|
||||
handle = {
|
||||
"key": "custom-key",
|
||||
"from_stage": "3",
|
||||
"to_stage": "4",
|
||||
"metadata": {"schema_version": 1},
|
||||
}
|
||||
req = _make_request({"prompt": "a cat", HANDLE_KEY: handle})
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert connector.calls == [("3", "4", "custom-key", {"schema_version": 1})]
|
||||
# The handle is transport bookkeeping and must not reach the pipeline.
|
||||
assert HANDLE_KEY not in req.prompt
|
||||
assert "text_encoder_output" in req.prompt["additional_information"]
|
||||
|
||||
|
||||
def test_receive_retries_until_producer_publishes(monkeypatch):
|
||||
connector = _FakeConnector(_conditioning())
|
||||
get = connector.get
|
||||
attempts = []
|
||||
|
||||
def delayed_get(*args, **kwargs):
|
||||
attempts.append(args)
|
||||
return get(*args, **kwargs) if len(attempts) == 3 else None
|
||||
|
||||
monkeypatch.setattr(connector, "get", delayed_get)
|
||||
runner = _make_runner(connector)
|
||||
request = _make_request({"prompt": "a cat"})
|
||||
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
|
||||
assert len(attempts) == 3
|
||||
assert "text_encoder_output" in request.prompt["additional_information"]
|
||||
|
||||
|
||||
def test_receive_retry_budget_preserves_inline_payload(monkeypatch):
|
||||
from vllm_omni.distributed.omni_connectors.model_runner import omni_connector_payload_transport
|
||||
|
||||
clock = [0.0]
|
||||
|
||||
def advance(duration):
|
||||
clock[0] += duration
|
||||
|
||||
monkeypatch.setattr(
|
||||
omni_connector_payload_transport, "time", SimpleNamespace(monotonic=lambda: clock[0], sleep=advance)
|
||||
)
|
||||
connector = _FakeConnector(None)
|
||||
runner = _make_runner(connector)
|
||||
inline = _conditioning()
|
||||
request = _make_request({"additional_information": inline})
|
||||
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
|
||||
assert 1 < len(connector.calls) <= 41
|
||||
assert clock[0] == pytest.approx(2.0)
|
||||
assert request.prompt["additional_information"] is inline
|
||||
|
||||
|
||||
def test_stage_without_declared_keys_never_touches_the_connector():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector, payload_keys=())
|
||||
req = _make_request({"prompt": "a cat"})
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert connector.calls == []
|
||||
assert "additional_information" not in req.prompt
|
||||
|
||||
|
||||
def test_undeclared_payload_keys_are_dropped():
|
||||
connector = _FakeConnector({**_conditioning(), "debug_blob": torch.zeros(2)})
|
||||
runner = _make_runner(connector)
|
||||
req = _make_request({"prompt": "a cat"})
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert set(req.prompt["additional_information"]) == {"text_encoder_output"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"connector",
|
||||
[
|
||||
_FakeConnector(None),
|
||||
_FakeConnector(raises=True),
|
||||
_FakeConnector("not-a-dict"),
|
||||
],
|
||||
ids=["missing", "raises", "wrong_type"],
|
||||
)
|
||||
def test_failed_transfer_falls_back_to_the_inline_prompt(connector):
|
||||
runner = _make_runner(connector)
|
||||
inline = {"text_encoder_output": {"hidden_states": torch.ones(2, 8)}}
|
||||
req = _make_request({"prompt": "a cat", "additional_information": dict(inline)})
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert torch.equal(
|
||||
req.prompt["additional_information"]["text_encoder_output"]["hidden_states"],
|
||||
inline["text_encoder_output"]["hidden_states"],
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_sender_info_is_passed_as_request_metadata():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector)
|
||||
req = _make_request({"prompt": "a cat"}, kv_sender_info={0: {"host": "10.0.0.1", "zmq_port": 50171}})
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert runner.kv_transfer_manager.sender_info_calls == []
|
||||
assert connector.calls[0][3] == {"source_host": "10.0.0.1", "source_port": 50171}
|
||||
|
||||
|
||||
def test_payload_sender_info_overrides_kv_sender_info_for_full_payloads():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector)
|
||||
req = _make_request(
|
||||
{"prompt": "a cat"},
|
||||
kv_sender_info={0: {"host": "10.0.0.1", "zmq_port": 50171}},
|
||||
payload_sender_info={"host": "10.0.0.1", "zmq_port": 50071},
|
||||
)
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert runner.kv_transfer_manager.sender_info_calls == []
|
||||
assert connector.calls[0][3] == {"source_host": "10.0.0.1", "source_port": 50071}
|
||||
|
||||
|
||||
def test_synchronous_receive_keeps_endpoints_request_scoped_and_uses_external_ids():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector, recv_stages=("2", "5"))
|
||||
for index in range(2):
|
||||
request = _make_request(
|
||||
{},
|
||||
request_id=f"internal-{index}",
|
||||
payload_sender_info={"host": f"10.0.0.{index + 1}", "zmq_port": 50071 + index},
|
||||
)
|
||||
request.external_req_id = f"external-{index}"
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
assert "text_encoder_output" in request.prompt["additional_information"]
|
||||
assert connector.calls == [
|
||||
("2", "5", "external-0_2_0", {"source_host": "10.0.0.1", "source_port": 50071}),
|
||||
("2", "5", "external-1_2_0", {"source_host": "10.0.0.2", "source_port": 50072}),
|
||||
]
|
||||
assert runner.kv_transfer_manager.sender_info_calls == []
|
||||
assert runner._pending_load_reqs == {}
|
||||
assert runner._get_req_chunk == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stage_key", [2, "2"])
|
||||
@pytest.mark.parametrize("use_handle", [False, True])
|
||||
def test_sender_map_selects_actual_source_stage(stage_key, use_handle):
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector, recv_stages=("0" if use_handle else "2", "5"))
|
||||
prompt = {}
|
||||
if use_handle:
|
||||
prompt[HANDLE_KEY] = {"key": "explicit", "from_stage": "2", "to_stage": "5"}
|
||||
request = _make_request(
|
||||
prompt,
|
||||
payload_sender_info={
|
||||
"0": {"host": "wrong", "zmq_port": 50000},
|
||||
stage_key: {"host": "right", "zmq_port": 50002},
|
||||
},
|
||||
)
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
assert connector.calls[0][0:2] == ("2", "5")
|
||||
assert connector.calls[0][3] == {"source_host": "right", "source_port": 50002}
|
||||
|
||||
|
||||
def test_tp_payload_is_fetched_once_by_leader_and_broadcast_to_followers():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
state = {}
|
||||
|
||||
class _FakeTPGroup:
|
||||
world_size = 4
|
||||
|
||||
def __init__(self, rank):
|
||||
self.rank_in_group = rank
|
||||
|
||||
def broadcast_object(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
state["delivered"] = value
|
||||
return state["delivered"]
|
||||
|
||||
def broadcast_tensor_dict(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
state["payload"] = value
|
||||
return state["payload"]
|
||||
|
||||
leader = _make_runner(connector)
|
||||
leader._local_rank = 0
|
||||
leader._get_local_tp_group = lambda: _FakeTPGroup(0)
|
||||
follower = _make_runner(connector)
|
||||
follower._local_rank = 1
|
||||
follower._get_local_tp_group = lambda: _FakeTPGroup(1)
|
||||
|
||||
leader_req = _make_request({"prompt": "a cat"})
|
||||
follower_req = _make_request({"prompt": "a cat"})
|
||||
leader._maybe_recv_stage_payload(leader_req)
|
||||
follower._maybe_recv_stage_payload(follower_req)
|
||||
|
||||
assert len(connector.calls) == 1
|
||||
leader_output = leader_req.prompt["additional_information"]["text_encoder_output"]["hidden_states"]
|
||||
follower_output = follower_req.prompt["additional_information"]["text_encoder_output"]["hidden_states"]
|
||||
assert torch.equal(leader_output, follower_output)
|
||||
|
||||
|
||||
def test_tp_payload_miss_is_broadcast_without_follower_connector_access():
|
||||
connector = _FakeConnector(None)
|
||||
state = {}
|
||||
|
||||
class _FakeTPGroup:
|
||||
world_size = 2
|
||||
|
||||
def __init__(self, rank):
|
||||
self.rank_in_group = rank
|
||||
|
||||
def broadcast_object(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
state["delivered"] = value
|
||||
return state["delivered"]
|
||||
|
||||
def broadcast_tensor_dict(self, value, src=0):
|
||||
raise AssertionError("missing payload must not be broadcast")
|
||||
|
||||
leader = _make_runner(connector)
|
||||
leader._get_local_tp_group = lambda: _FakeTPGroup(0)
|
||||
follower = _make_runner(connector)
|
||||
follower._local_rank = 1
|
||||
follower._get_local_tp_group = lambda: _FakeTPGroup(1)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Stage payload unavailable"):
|
||||
leader._maybe_recv_stage_payload(_make_request({"prompt": "a cat"}))
|
||||
with pytest.raises(RuntimeError, match="Stage payload unavailable"):
|
||||
follower._maybe_recv_stage_payload(_make_request({"prompt": "a cat"}))
|
||||
|
||||
assert 1 < len(connector.calls) <= 41
|
||||
|
||||
|
||||
def test_sp_payload_is_fetched_once_by_leader_and_broadcast_to_followers():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
state = {}
|
||||
|
||||
class _FakeSPGroup:
|
||||
world_size = 4
|
||||
|
||||
def __init__(self, rank):
|
||||
self.rank_in_group = rank
|
||||
|
||||
def broadcast_object(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
state["delivered"] = value
|
||||
return state["delivered"]
|
||||
|
||||
def broadcast_tensor_dict(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
state["payload"] = value
|
||||
return state["payload"]
|
||||
|
||||
leader = _make_runner(connector)
|
||||
leader._get_local_tp_group = lambda: None
|
||||
leader._stage_payload_broadcast_groups = lambda: (_FakeSPGroup(0),)
|
||||
follower = _make_runner(connector)
|
||||
follower._local_rank = 1
|
||||
follower._get_local_tp_group = lambda: None
|
||||
follower._stage_payload_broadcast_groups = lambda: (_FakeSPGroup(1),)
|
||||
|
||||
leader_req = _make_request({"prompt": "a cat"})
|
||||
follower_req = _make_request({"prompt": "a cat"})
|
||||
leader._maybe_recv_stage_payload(leader_req)
|
||||
follower._maybe_recv_stage_payload(follower_req)
|
||||
|
||||
assert len(connector.calls) == 1
|
||||
leader_output = leader_req.prompt["additional_information"]["text_encoder_output"]["hidden_states"]
|
||||
follower_output = follower_req.prompt["additional_information"]["text_encoder_output"]["hidden_states"]
|
||||
assert torch.equal(leader_output, follower_output)
|
||||
|
||||
|
||||
def test_missing_incoming_edge_is_reported_not_fetched():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector, recv_stages=(None, None))
|
||||
req = _make_request({"prompt": "a cat"})
|
||||
|
||||
with pytest.raises(RuntimeError, match="no incoming edge"):
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert connector.calls == []
|
||||
assert "additional_information" not in req.prompt
|
||||
|
||||
|
||||
@pytest.mark.parametrize("delivered", [True, False])
|
||||
def test_payload_reaches_full_tp_sp_grid_once(monkeypatch, delivered):
|
||||
from vllm_omni.diffusion.distributed import parallel_state
|
||||
|
||||
connector = _FakeConnector(_conditioning() if delivered else None)
|
||||
packets = {}
|
||||
|
||||
class GridGroup:
|
||||
world_size = 2
|
||||
|
||||
def __init__(self, dimension, peer_rank, rank):
|
||||
self.key = (dimension, peer_rank)
|
||||
self.rank_in_group = rank
|
||||
|
||||
def broadcast_object(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
packets[self.key, "delivered"] = value
|
||||
return packets[self.key, "delivered"]
|
||||
|
||||
def broadcast_tensor_dict(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
packets[self.key, "payload"] = value
|
||||
return packets[self.key, "payload"]
|
||||
|
||||
for sp_rank in range(2):
|
||||
for tp_rank in range(2):
|
||||
runner = _make_runner(connector)
|
||||
tp_group = GridGroup("tp", sp_rank, tp_rank)
|
||||
sp_group = GridGroup("sp", tp_rank, sp_rank)
|
||||
monkeypatch.setattr(runner, "_get_local_tp_group", lambda: tp_group)
|
||||
monkeypatch.setattr(parallel_state, "get_sp_group", lambda: sp_group)
|
||||
request = _make_request({"prompt": "a cat"})
|
||||
|
||||
if delivered:
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
output = request.prompt["additional_information"]["text_encoder_output"]
|
||||
assert torch.equal(output["hidden_states"], connector._payload["text_encoder_output"]["hidden_states"])
|
||||
else:
|
||||
with pytest.raises(RuntimeError, match="Stage payload unavailable"):
|
||||
runner._maybe_recv_stage_payload(request)
|
||||
if delivered:
|
||||
assert len(connector.calls) == 1
|
||||
else:
|
||||
assert 1 < len(connector.calls) <= 41
|
||||
|
||||
|
||||
def test_native_kv_runner_keeps_synchronous_payload_transport(monkeypatch):
|
||||
from unittest.mock import Mock
|
||||
|
||||
from vllm_omni.diffusion.worker import diffusion_model_runner as runner_module
|
||||
|
||||
connector = _FakeConnector(_conditioning())
|
||||
manager = _FakeKVTransferManager(connector)
|
||||
config = SimpleNamespace(stage_input_payload_keys=("text_encoder_output",), stage_id=1, kv_transfer_config=object())
|
||||
monkeypatch.setattr(runner_module.OmniKVTransferManager, "from_od_config", lambda config: manager)
|
||||
monkeypatch.setattr(runner_module, "DiffusionKVModelRunnerBackend", Mock())
|
||||
runner = DiffusionModelRunner(SimpleNamespace(), config, torch.device("cpu"))
|
||||
runner._initialize_generator = Mock()
|
||||
request = _make_request({"prompt": "a cat"})
|
||||
request.sampling_params = SimpleNamespace()
|
||||
|
||||
runner._prepare_request_for_forward(request, od_config=config)
|
||||
|
||||
assert runner.kv_transfer_manager is None
|
||||
assert runner._kv_transfer_manager is manager
|
||||
assert runner._kv_connector is None
|
||||
assert not runner._kv_prefetch_enabled
|
||||
assert len(connector.calls) == 1
|
||||
assert "text_encoder_output" in request.prompt["additional_information"]
|
||||
runner._initialize_generator.assert_called_once_with(request.sampling_params)
|
||||
|
||||
|
||||
def test_step_mode_receives_payload_before_kv_and_reuses_cached_state():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector)
|
||||
runner.state_cache = {}
|
||||
request = _make_request({"prompt": "a cat"})
|
||||
request.sampling_params = SimpleNamespace()
|
||||
kv_calls = []
|
||||
|
||||
def receive_kv(state_request, **kwargs):
|
||||
assert "text_encoder_output" in state_request.prompt["additional_information"]
|
||||
kv_calls.append(state_request.request_id)
|
||||
|
||||
runner.kv_transfer_manager.receive_multi_kv_cache_distributed = receive_kv
|
||||
scheduled = SimpleNamespace(
|
||||
scheduled_new_reqs=[SimpleNamespace(request_id=request.request_id, req=request, diffusion_kv_metadata=None)],
|
||||
scheduled_cached_reqs=SimpleNamespace(request_ids=[]),
|
||||
)
|
||||
states, new_ids = runner._update_states(scheduled)
|
||||
assert new_ids == [request.request_id]
|
||||
assert states[0].prompt is request.prompt
|
||||
assert len(connector.calls) == 1
|
||||
assert kv_calls == [request.request_id]
|
||||
|
||||
scheduled.scheduled_new_reqs = []
|
||||
scheduled.scheduled_cached_reqs.request_ids = [request.request_id]
|
||||
cached_states, new_ids = runner._update_states(scheduled)
|
||||
assert cached_states == states
|
||||
assert new_ids == []
|
||||
assert len(connector.calls) == 1
|
||||
assert kv_calls == [request.request_id]
|
||||
|
||||
|
||||
def test_non_dict_prompt_is_left_alone():
|
||||
connector = _FakeConnector(_conditioning())
|
||||
runner = _make_runner(connector)
|
||||
req = _make_request("a cat")
|
||||
|
||||
runner._maybe_recv_stage_payload(req)
|
||||
|
||||
assert connector.calls == []
|
||||
assert req.prompt == "a cat"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("request_id", ["dummy_req_id", "dummy_req_id/profile"])
|
||||
def test_warmup_never_receives_or_publishes_payload(request_id):
|
||||
connector = _FakeConnector(raises=True)
|
||||
request = _make_request({}, request_id=request_id)
|
||||
receiver = _make_runner(connector)
|
||||
receiver._maybe_recv_stage_payload(request)
|
||||
sender = _make_sender(connector)
|
||||
sender._maybe_send_stage_payload([request], [_make_output(prompt_embeds=torch.ones(1))])
|
||||
assert connector.calls == []
|
||||
assert connector.put_calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("inline", [{}, {"text_encoder_output": None}])
|
||||
def test_failed_transfer_requires_complete_inline_payload(inline):
|
||||
runner = _make_runner(_FakeConnector(raises=True))
|
||||
with pytest.raises(RuntimeError, match="missing keys.*text_encoder_output"):
|
||||
runner._maybe_recv_stage_payload(_make_request({"additional_information": inline}))
|
||||
|
||||
|
||||
def test_send_puts_declared_keys_and_attaches_a_handle():
|
||||
connector = _FakeConnector()
|
||||
runner = _make_sender(connector)
|
||||
req = _make_request({"prompt": "a cat"})
|
||||
output = _make_output(prompt_embeds=torch.zeros(2, 8), latents=torch.zeros(1, 4))
|
||||
|
||||
runner._maybe_send_stage_payload([req], [output])
|
||||
|
||||
from_stage, to_stage, put_key, data = connector.put_calls[0]
|
||||
assert (from_stage, to_stage, put_key) == ("0", "1", "req-7_0_0")
|
||||
# Only declared keys travel over the connector.
|
||||
assert isinstance(data, dict)
|
||||
assert set(data) == {"prompt_embeds"}
|
||||
handle = output.custom_output[HANDLE_KEY]
|
||||
assert handle["key"] == "req-7_0_0"
|
||||
assert handle["metadata"] == {"schema_version": 1}
|
||||
assert handle["payload_keys"] == ["prompt_embeds"]
|
||||
assert handle["size_bytes"] == 128
|
||||
# Transferred keys must not also travel inline through the orchestrator.
|
||||
assert "prompt_embeds" not in output.custom_output
|
||||
# Undeclared keys are untouched.
|
||||
assert "latents" in output.custom_output
|
||||
|
||||
|
||||
def test_send_key_matches_the_receive_key_convention():
|
||||
sender_connector = _FakeConnector()
|
||||
sender = _make_sender(sender_connector)
|
||||
req = _make_request({"prompt": "a cat"})
|
||||
sender._maybe_send_stage_payload([req], [_make_output(prompt_embeds=torch.zeros(2, 8))])
|
||||
|
||||
recv_connector = _FakeConnector(_conditioning())
|
||||
receiver = _make_runner(recv_connector)
|
||||
receiver._maybe_recv_stage_payload(_make_request({"prompt": "a cat"}))
|
||||
|
||||
assert sender_connector.put_calls[0][2] == recv_connector.calls[0][2]
|
||||
|
||||
|
||||
def test_stage_without_declared_output_keys_never_puts():
|
||||
connector = _FakeConnector()
|
||||
runner = _make_sender(connector, payload_keys=())
|
||||
output = _make_output(prompt_embeds=torch.zeros(2, 8))
|
||||
|
||||
runner._maybe_send_stage_payload([_make_request({"prompt": "a cat"})], [output])
|
||||
|
||||
assert connector.put_calls == []
|
||||
assert HANDLE_KEY not in output.custom_output
|
||||
|
||||
|
||||
def test_missing_outgoing_edge_is_reported_not_sent():
|
||||
connector = _FakeConnector()
|
||||
runner = _make_sender(connector, send_stages=(None, None))
|
||||
output = _make_output(prompt_embeds=torch.zeros(2, 8))
|
||||
|
||||
runner._maybe_send_stage_payload([_make_request({"prompt": "a cat"})], [output])
|
||||
|
||||
assert connector.put_calls == []
|
||||
assert HANDLE_KEY not in output.custom_output
|
||||
|
||||
|
||||
def test_output_without_the_declared_keys_is_skipped():
|
||||
connector = _FakeConnector()
|
||||
runner = _make_sender(connector)
|
||||
output = _make_output(latents=torch.zeros(1, 4))
|
||||
|
||||
runner._maybe_send_stage_payload([_make_request({"prompt": "a cat"})], [output])
|
||||
|
||||
assert connector.put_calls == []
|
||||
assert HANDLE_KEY not in output.custom_output
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"connector",
|
||||
[
|
||||
_FakeConnector(raises=True),
|
||||
_FakeConnector(put_result=(False, 0, None)),
|
||||
],
|
||||
ids=["raises", "rejected"],
|
||||
)
|
||||
def test_failed_send_leaves_the_inline_payload_unannotated(connector):
|
||||
runner = _make_sender(connector)
|
||||
output = _make_output(prompt_embeds=torch.zeros(2, 8))
|
||||
|
||||
runner._maybe_send_stage_payload([_make_request({"prompt": "a cat"})], [output])
|
||||
|
||||
assert HANDLE_KEY not in output.custom_output
|
||||
assert "prompt_embeds" in output.custom_output
|
||||
|
||||
|
||||
class _FakeTPGroup:
|
||||
def __init__(self, rank_in_group, world_size=2):
|
||||
self.rank_in_group = rank_in_group
|
||||
self.world_size = world_size
|
||||
self.broadcast_calls: list[object] = []
|
||||
self.leader_packet: object = None
|
||||
|
||||
def broadcast_object(self, obj, src=0):
|
||||
self.broadcast_calls.append(obj)
|
||||
return obj if self.rank_in_group == src else self.leader_packet
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rank_in_group", [0, 1], ids=["leader", "follower"])
|
||||
def test_send_puts_only_on_the_tp_leader(monkeypatch, rank_in_group):
|
||||
connector = _FakeConnector()
|
||||
runner = _make_sender(connector)
|
||||
tp_group = _FakeTPGroup(rank_in_group)
|
||||
expected_handle = {
|
||||
"key": "req-7_0_0",
|
||||
"from_stage": "0",
|
||||
"to_stage": "1",
|
||||
"size_bytes": 128,
|
||||
"metadata": {"schema_version": 1},
|
||||
"payload_keys": ["prompt_embeds"],
|
||||
}
|
||||
tp_group.leader_packet = {"req-7": expected_handle}
|
||||
monkeypatch.setattr(DiffusionModelRunner, "_get_local_tp_group", staticmethod(lambda: tp_group))
|
||||
output = _make_output(prompt_embeds=torch.zeros(2, 8))
|
||||
|
||||
runner._maybe_send_stage_payload([_make_request({"prompt": "a cat"})], [output])
|
||||
|
||||
assert len(connector.put_calls) == (1 if rank_in_group == 0 else 0)
|
||||
# Every rank ends up reporting the same handle, leader or not.
|
||||
assert output.custom_output[HANDLE_KEY] == expected_handle
|
||||
# ...and every rank drops the inline copy, not just the sender.
|
||||
assert "prompt_embeds" not in output.custom_output
|
||||
assert len(tp_group.broadcast_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tp_size", [1, 2])
|
||||
@pytest.mark.parametrize("accepted", [True, False])
|
||||
def test_send_elects_one_tp_sp_leader(monkeypatch, tp_size, accepted):
|
||||
from vllm_omni.diffusion.distributed import parallel_state
|
||||
|
||||
connector = _FakeConnector(put_result=(accepted, 128, {"schema_version": 1}))
|
||||
packets = {}
|
||||
outputs = []
|
||||
|
||||
class GridGroup:
|
||||
def __init__(self, dimension, peer_rank, rank, world_size):
|
||||
self.key = (dimension, peer_rank)
|
||||
self.rank_in_group = rank
|
||||
self.world_size = world_size
|
||||
|
||||
def broadcast_object(self, value, src=0):
|
||||
if self.rank_in_group == src:
|
||||
packets[self.key] = value
|
||||
return packets[self.key]
|
||||
|
||||
for sp_rank in range(2):
|
||||
for tp_rank in range(tp_size):
|
||||
runner = _make_sender(connector)
|
||||
tp_group = GridGroup("tp", sp_rank, tp_rank, tp_size)
|
||||
sp_group = GridGroup("sp", tp_rank, sp_rank, 2)
|
||||
monkeypatch.setattr(runner, "_get_local_tp_group", lambda: tp_group)
|
||||
monkeypatch.setattr(parallel_state, "get_sp_group", lambda: sp_group)
|
||||
output = _make_output(prompt_embeds=torch.zeros(2, 8))
|
||||
|
||||
runner._maybe_send_stage_payload([_make_request({})], [output])
|
||||
|
||||
outputs.append(output.custom_output)
|
||||
assert (HANDLE_KEY in output.custom_output) == accepted
|
||||
assert ("prompt_embeds" in output.custom_output) != accepted
|
||||
|
||||
assert len(connector.put_calls) == 1
|
||||
if accepted:
|
||||
assert all(output[HANDLE_KEY] == outputs[0][HANDLE_KEY] for output in outputs)
|
||||
@@ -616,7 +616,8 @@ def test_make_batch_explicit_mapping_still_selects_and_orders_states():
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_step_profiler_reports_denoise_step_as_diffuse():
|
||||
def test_step_profiler_reports_denoise_step_as_diffuse(monkeypatch):
|
||||
monkeypatch.setattr(current_omni_platform, "synchronize", lambda: None)
|
||||
pipeline = _AutoDenoiseProfilerPipeline()
|
||||
|
||||
assert pipeline.denoise_step() == "ok"
|
||||
@@ -629,6 +630,13 @@ def test_step_profiler_reports_denoise_step_as_diffuse():
|
||||
class TestRunner:
|
||||
"""DiffusionModelRunner.execute_stepwise"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_platform_memory(self, monkeypatch):
|
||||
monkeypatch.setattr(model_runner_module.current_omni_platform, "is_available", lambda: True)
|
||||
monkeypatch.setattr(model_runner_module.current_omni_platform, "reset_peak_memory_stats", lambda: None)
|
||||
monkeypatch.setattr(model_runner_module.current_omni_platform, "max_memory_reserved", lambda: 0)
|
||||
monkeypatch.setattr(model_runner_module.current_omni_platform, "max_memory_allocated", lambda: 0)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("diffusion_kv_mode", "should_cleanup"),
|
||||
[
|
||||
|
||||
@@ -87,6 +87,38 @@ async def test_inline_dispatch_request_success(client, mock_engine):
|
||||
assert output.request_id == "req-1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("streaming", [False, True])
|
||||
def test_inline_dispatch_preserves_payload_sender_info(client, mock_engine, streaming):
|
||||
async def _run_test():
|
||||
requests = []
|
||||
|
||||
async def _step_streaming(request):
|
||||
requests.append(request)
|
||||
yield [OmniRequestOutput.from_diffusion(request_id=request.request_id, images=[MagicMock()])]
|
||||
|
||||
mock_engine.step_streaming = _step_streaming
|
||||
client.od_config.streaming_output = streaming
|
||||
payload_sender_info = {"host": "10.0.0.1", "zmq_port": 50071}
|
||||
kv_transfer_params = {"remote_engine_id": "mooncake-producer", "remote_block_ids": [1, 2]}
|
||||
await client.add_request_async(
|
||||
"req-payload",
|
||||
"A test prompt",
|
||||
OmniDiffusionSamplingParams(),
|
||||
kv_transfer_params=kv_transfer_params,
|
||||
payload_sender_info=payload_sender_info,
|
||||
)
|
||||
|
||||
for _ in range(10):
|
||||
if requests:
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
assert requests[0].payload_sender_info == payload_sender_info
|
||||
assert requests[0].kv_transfer_params == kv_transfer_params
|
||||
|
||||
asyncio.run(_run_test())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inline_non_streaming_dispatches_lifecycle_before_final(client, mock_engine):
|
||||
lifecycle = OmniRequestOutput.from_diffusion(
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.diffusion.stage_diffusion_client import StageDiffusionClient
|
||||
from vllm_omni.diffusion.stage_diffusion_proc import StageDiffusionProc
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("streaming", [False, True])
|
||||
def test_subprocess_client_and_worker_preserve_both_transfer_contracts(streaming):
|
||||
"""Exercise the IPC message and both process request builders without GPUs."""
|
||||
|
||||
async def run():
|
||||
client = object.__new__(StageDiffusionClient)
|
||||
client._engine_dead = False
|
||||
client.stage_id = 1
|
||||
client.replica_id = 0
|
||||
client._encoder = MagicMock()
|
||||
client._encoder.encode.side_effect = lambda message: message
|
||||
client._request_socket = MagicMock()
|
||||
kv_params = {"remote_engine_id": "mooncake-producer", "remote_block_ids": [1, 2]}
|
||||
payload_sender = {"host": "10.0.0.1", "zmq_port": 50071}
|
||||
kv_sender = {0: {"host": "10.0.0.2", "zmq_port": 50171}}
|
||||
# Keep main's existing fifth positional argument bound to native KV.
|
||||
await client.add_request_async(
|
||||
"req-both",
|
||||
"prompt",
|
||||
OmniDiffusionSamplingParams(),
|
||||
kv_sender,
|
||||
kv_params,
|
||||
payload_sender_info=payload_sender,
|
||||
)
|
||||
message = client._request_socket.send.call_args.args[0]
|
||||
assert message["kv_transfer_params"] == kv_params
|
||||
assert message["payload_sender_info"] == payload_sender
|
||||
captured = []
|
||||
|
||||
class Engine:
|
||||
async def step_streaming(self, request):
|
||||
captured.append(request)
|
||||
yield [OmniRequestOutput.from_diffusion(request_id=request.request_id, images=[])]
|
||||
|
||||
proc = object.__new__(StageDiffusionProc)
|
||||
proc._engine = Engine()
|
||||
args = (message["request_id"], message["prompt"], message["sampling_params"])
|
||||
transfer_args = {
|
||||
"kv_sender_info": kv_sender,
|
||||
"kv_transfer_params": kv_params,
|
||||
"payload_sender_info": message["payload_sender_info"],
|
||||
}
|
||||
if streaming:
|
||||
outputs = [output async for output in proc._process_streaming_request(*args, **transfer_args)]
|
||||
else:
|
||||
outputs = [await proc._process_request(*args, **transfer_args)]
|
||||
assert outputs[0].request_id == "req-both"
|
||||
assert captured[0].kv_transfer_params == kv_params
|
||||
assert captured[0].kv_sender_info == kv_sender
|
||||
assert captured[0].payload_sender_info == payload_sender
|
||||
|
||||
asyncio.run(run())
|
||||
@@ -12,10 +12,13 @@ This module tests the WorkerWrapperBase implementation:
|
||||
- Dynamic worker class extension
|
||||
"""
|
||||
|
||||
import gc
|
||||
import weakref
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from vllm_omni.diffusion.worker.diffusion_worker import (
|
||||
@@ -227,12 +230,13 @@ class TestWorkerWrapperBaseDelegation:
|
||||
wrapper.worker.shutdown.assert_called_once()
|
||||
assert result is None
|
||||
|
||||
def test_worker_shutdown_disables_offloader_before_distributed_teardown(self, mocker: MockerFixture):
|
||||
@pytest.mark.parametrize("native_kv", [False, True])
|
||||
def test_worker_shutdown_disables_offloader_before_distributed_teardown(self, mocker: MockerFixture, native_kv):
|
||||
events: list[str] = []
|
||||
offload_backend = mocker.Mock()
|
||||
offload_backend.disable.side_effect = lambda: events.append("offload")
|
||||
kv_manager = mocker.Mock()
|
||||
kv_manager.shutdown_prefetch.side_effect = lambda: events.append("kv")
|
||||
kv_manager.close.side_effect = lambda: events.append("kv")
|
||||
destroy = mocker.patch(
|
||||
"vllm_omni.diffusion.worker.diffusion_worker.destroy_distributed_env",
|
||||
side_effect=lambda: events.append("distributed"),
|
||||
@@ -240,14 +244,71 @@ class TestWorkerWrapperBaseDelegation:
|
||||
worker = DiffusionWorker.__new__(DiffusionWorker)
|
||||
worker.model_runner = SimpleNamespace(
|
||||
offload_backend=offload_backend,
|
||||
kv_transfer_manager=kv_manager,
|
||||
kv_transfer_manager=None if native_kv else kv_manager,
|
||||
_kv_transfer_manager=kv_manager,
|
||||
)
|
||||
|
||||
worker.shutdown()
|
||||
|
||||
assert events == ["offload", "kv", "distributed"]
|
||||
kv_manager.close.assert_called_once_with()
|
||||
destroy.assert_called_once_with()
|
||||
|
||||
@pytest.mark.parametrize("cleanup_fails", [False, True])
|
||||
def test_worker_shutdown_releases_model_references(self, mocker: MockerFixture, cleanup_fails):
|
||||
mocker.patch("vllm_omni.diffusion.worker.diffusion_worker.destroy_distributed_env")
|
||||
mocker.patch("vllm_omni.diffusion.worker.diffusion_worker.shutdown_kv_connector")
|
||||
offload_backend = mocker.Mock()
|
||||
if cleanup_fails:
|
||||
offload_backend.disable.side_effect = RuntimeError("offload cleanup failed")
|
||||
worker = DiffusionWorker.__new__(DiffusionWorker)
|
||||
weights = torch.ones(1)
|
||||
weights_ref = weakref.ref(weights)
|
||||
pipeline = SimpleNamespace(weights=weights)
|
||||
worker.model_runner = SimpleNamespace(
|
||||
pipeline=pipeline, offload_backend=offload_backend, kv_transfer_manager=None
|
||||
)
|
||||
worker.lora_manager = SimpleNamespace(pipeline=pipeline)
|
||||
worker._sleep_saved_buffers = {"buffer": weights}
|
||||
del weights, pipeline
|
||||
|
||||
if cleanup_fails:
|
||||
with pytest.raises(RuntimeError, match="offload cleanup failed"):
|
||||
worker.shutdown()
|
||||
else:
|
||||
worker.shutdown()
|
||||
gc.collect()
|
||||
|
||||
assert worker.model_runner is None
|
||||
assert worker.lora_manager is None
|
||||
assert worker._sleep_saved_buffers == {}
|
||||
assert weights_ref() is None
|
||||
|
||||
@pytest.mark.parametrize("owns_pool", [False, True])
|
||||
@pytest.mark.parametrize("cleanup_fails", [False, True])
|
||||
def test_worker_shutdown_releases_only_owned_sleep_pool(self, mocker: MockerFixture, owns_pool, cleanup_fails):
|
||||
destroy = mocker.patch("vllm_omni.diffusion.worker.diffusion_worker.destroy_distributed_env")
|
||||
shutdown_connector = mocker.patch("vllm_omni.diffusion.worker.diffusion_worker.shutdown_kv_connector")
|
||||
allocator_class = mocker.patch("vllm_omni.diffusion.worker.diffusion_worker._get_cumem_allocator_class")
|
||||
release = allocator_class.return_value.get_instance.return_value.release_pools
|
||||
if cleanup_fails:
|
||||
release.side_effect = RuntimeError("pool release failed")
|
||||
worker = DiffusionWorker.__new__(DiffusionWorker)
|
||||
worker.model_runner = None
|
||||
worker._owns_sleep_pool = owns_pool
|
||||
|
||||
if owns_pool and cleanup_fails:
|
||||
with pytest.raises(RuntimeError, match="pool release failed"):
|
||||
worker.shutdown()
|
||||
else:
|
||||
worker.shutdown()
|
||||
|
||||
assert release.call_count == int(owns_pool)
|
||||
shutdown_connector.assert_called_once_with()
|
||||
destroy.assert_called_once_with()
|
||||
if not cleanup_fails:
|
||||
assert not worker._owns_sleep_pool
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Tests: execute_method
|
||||
@@ -507,6 +568,26 @@ class TestCustomPipelineWorkerExtension:
|
||||
# Verify re_init_pipeline was called with custom_pipeline_args
|
||||
mock_worker_instance.re_init_pipeline.assert_called_once_with(custom_args)
|
||||
|
||||
@pytest.mark.parametrize("cleanup_fails", [False, True])
|
||||
def test_custom_pipeline_initialization_failure_cleans_worker(
|
||||
self, mocker: MockerFixture, mock_od_config, cleanup_fails
|
||||
):
|
||||
worker = mocker.Mock()
|
||||
worker.re_init_pipeline.side_effect = RuntimeError("custom pipeline load failed")
|
||||
if cleanup_fails:
|
||||
worker.shutdown.side_effect = RuntimeError("cleanup failed")
|
||||
worker_class = mocker.Mock(return_value=worker)
|
||||
mocker.patch.object(WorkerWrapperBase, "_prepare_worker_class", return_value=worker_class)
|
||||
|
||||
with pytest.raises(RuntimeError, match="custom pipeline load failed"):
|
||||
WorkerWrapperBase(
|
||||
gpu_id=0,
|
||||
od_config=mock_od_config,
|
||||
custom_pipeline_args={"pipeline_class": "unused"},
|
||||
)
|
||||
|
||||
worker.shutdown.assert_called_once_with()
|
||||
|
||||
def test_custom_pipeline_with_explicit_extension(self, mocker: MockerFixture, mock_od_config):
|
||||
"""Test that explicit worker_extension_cls is preserved when custom_pipeline_args is provided."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user