2144 lines
88 KiB
Python
2144 lines
88 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""
|
|
Unit tests for StageConfigFactory and related classes.
|
|
"""
|
|
|
|
import importlib
|
|
import inspect
|
|
import warnings
|
|
from dataclasses import dataclass, fields
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from transformers import PretrainedConfig, Qwen3OmniMoeConfig
|
|
|
|
from tests.helpers.stage_config import get_deploy_config_path, get_deploy_config_stage
|
|
from vllm_omni.config.config_factory import StageConfigFactory
|
|
from vllm_omni.config.endpoint_policy import EndpointRestriction, OmniServingCapability
|
|
from vllm_omni.config.pipeline_registry import OMNI_PIPELINES, register_pipeline
|
|
from vllm_omni.config.stage_config import (
|
|
DeployConfig,
|
|
PipelineConfig,
|
|
StageConfig,
|
|
StageDeployConfig,
|
|
StageExecutionType,
|
|
StagePipelineConfig,
|
|
StageType,
|
|
_apply_platform_overrides,
|
|
_deep_merge_stage,
|
|
_resolve_scheduler,
|
|
build_stage_runtime_overrides,
|
|
load_deploy_config,
|
|
merge_pipeline_deploy,
|
|
pipeline_cfg_resolver,
|
|
strip_parent_engine_args,
|
|
)
|
|
from vllm_omni.engine.arg_utils import SHARED_FIELDS, EngineArgs, internal_blacklist_keys
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_config_factory_caches():
|
|
"""Clear cached classmethods from the StageConfigFactory to prevent test pollution."""
|
|
yield
|
|
StageConfigFactory.get_hf_config.cache_clear()
|
|
StageConfigFactory.try_infer_model_type.cache_clear()
|
|
StageConfigFactory.get_pipeline_config.cache_clear()
|
|
|
|
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG = Qwen3OmniMoeConfig(enable_audio_output=True)
|
|
Q3_OMNI_THINKER_HF_CONFIG = Qwen3OmniMoeConfig(enable_audio_output=False)
|
|
|
|
|
|
class TestStageType:
|
|
"""Tests for StageType enum."""
|
|
|
|
def test_stage_type_values(self):
|
|
"""Test StageType enum values."""
|
|
assert StageType.LLM.value == "llm"
|
|
assert StageType.DIFFUSION.value == "diffusion"
|
|
|
|
def test_stage_type_from_string(self):
|
|
"""Test creating StageType from string."""
|
|
assert StageType("llm") == StageType.LLM
|
|
assert StageType("diffusion") == StageType.DIFFUSION
|
|
|
|
|
|
class TestStageConfig:
|
|
"""Tests for StageConfig dataclass."""
|
|
|
|
def test_minimal_config(self):
|
|
"""Test creating StageConfig with minimal required fields."""
|
|
config = StageConfig(stage_id=0, model_stage="thinker")
|
|
assert config.stage_id == 0
|
|
assert config.model_stage == "thinker"
|
|
assert config.stage_type == StageType.LLM
|
|
assert config.input_sources == []
|
|
assert config.final_output is False
|
|
assert config.worker_type is None
|
|
|
|
def test_full_config(self):
|
|
"""Test creating StageConfig with all fields."""
|
|
config = StageConfig(
|
|
stage_id=1,
|
|
model_stage="talker",
|
|
stage_type=StageType.LLM,
|
|
input_sources=[0],
|
|
custom_process_input_func="module.path.func",
|
|
final_output=True,
|
|
final_output_type="audio",
|
|
worker_type="ar",
|
|
scheduler_cls="path.to.Scheduler",
|
|
hf_config_name="talker_config",
|
|
is_comprehension=False,
|
|
)
|
|
assert config.stage_id == 1
|
|
assert config.model_stage == "talker"
|
|
assert config.input_sources == [0]
|
|
assert config.final_output_type == "audio"
|
|
assert config.worker_type == "ar"
|
|
|
|
def test_to_omegaconf_basic(self):
|
|
"""Test converting StageConfig to OmegaConf format."""
|
|
config = StageConfig(
|
|
stage_id=0,
|
|
model_stage="thinker",
|
|
stage_type=StageType.LLM,
|
|
worker_type="ar",
|
|
final_output=True,
|
|
final_output_type="text",
|
|
)
|
|
omega_config = config.to_omegaconf()
|
|
|
|
assert omega_config.stage_id == 0
|
|
assert omega_config.stage_type == "llm"
|
|
assert omega_config.engine_args.model_stage == "thinker"
|
|
assert omega_config.engine_args.worker_type == "ar"
|
|
assert omega_config.final_output is True
|
|
assert omega_config.final_output_type == "text"
|
|
assert "max_num_seqs" not in omega_config.engine_args
|
|
# Legacy field name for backward compatibility
|
|
assert omega_config.engine_input_source == []
|
|
|
|
def test_to_omegaconf_with_runtime_overrides(self):
|
|
"""Test that runtime overrides are applied to OmegaConf output."""
|
|
config = StageConfig(
|
|
stage_id=0,
|
|
model_stage="thinker",
|
|
runtime_overrides={
|
|
"gpu_memory_utilization": 0.9,
|
|
"tensor_parallel_size": 2,
|
|
"devices": "0,1",
|
|
"max_batch_size": 64,
|
|
},
|
|
)
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", FutureWarning)
|
|
omega_config = config.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.gpu_memory_utilization == 0.9
|
|
assert omega_config.engine_args.tensor_parallel_size == 2
|
|
assert omega_config.runtime.devices == "0,1"
|
|
# max_batch_size is migrated to engine_args.max_num_seqs
|
|
assert omega_config.engine_args.max_num_seqs == 64
|
|
|
|
def test_to_omegaconf_max_batch_size_deprecation(self):
|
|
"""Test that runtime.max_batch_size emits a FutureWarning."""
|
|
config = StageConfig(
|
|
stage_id=0,
|
|
model_stage="thinker",
|
|
runtime_overrides={"max_batch_size": 8},
|
|
)
|
|
with warnings.catch_warnings(record=True) as w:
|
|
warnings.simplefilter("always")
|
|
config.to_omegaconf()
|
|
deprecation_warnings = [x for x in w if issubclass(x.category, FutureWarning)]
|
|
assert len(deprecation_warnings) == 1
|
|
assert "max_batch_size" in str(deprecation_warnings[0].message)
|
|
|
|
def test_to_omegaconf_max_num_seqs_in_engine_args(self):
|
|
"""Test that max_num_seqs in yaml_engine_args takes precedence."""
|
|
config = StageConfig(
|
|
stage_id=0,
|
|
model_stage="thinker",
|
|
yaml_engine_args={"max_num_seqs": 32},
|
|
)
|
|
omega_config = config.to_omegaconf()
|
|
assert omega_config.engine_args.max_num_seqs == 32
|
|
|
|
def test_to_omegaconf_diffusion_parallel_overrides_replace_nested_values(self):
|
|
config = StageConfig(
|
|
stage_id=1,
|
|
model_stage="diffusion",
|
|
stage_type=StageType.DIFFUSION,
|
|
yaml_engine_args={
|
|
"parallel_config": {
|
|
"pipeline_parallel_size": 1,
|
|
"data_parallel_size": 1,
|
|
"tensor_parallel_size": 4,
|
|
"enable_expert_parallel": False,
|
|
"ulysses_degree": 1,
|
|
"ring_degree": 1,
|
|
"ulysses_mode": "strict",
|
|
"sequence_parallel_size": 1,
|
|
"cfg_parallel_size": 1,
|
|
"vae_patch_parallel_size": 1,
|
|
"use_hsdp": False,
|
|
"hsdp_shard_size": -1,
|
|
"hsdp_replicate_size": 1,
|
|
}
|
|
},
|
|
runtime_overrides={
|
|
"pipeline_parallel_size": 2,
|
|
"data_parallel_size": 3,
|
|
"tensor_parallel_size": 8,
|
|
"enable_expert_parallel": True,
|
|
"ulysses_degree": 2,
|
|
"ring_degree": 4,
|
|
"ulysses_mode": "advanced_uaa",
|
|
"sequence_parallel_size": 8,
|
|
"cfg_parallel_size": 2,
|
|
"vae_patch_parallel_size": 2,
|
|
"use_hsdp": True,
|
|
"hsdp_shard_size": 8,
|
|
"hsdp_replicate_size": 2,
|
|
},
|
|
)
|
|
|
|
omega_config = config.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.parallel_config.pipeline_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.data_parallel_size == 3
|
|
assert omega_config.engine_args.parallel_config.tensor_parallel_size == 8
|
|
assert omega_config.engine_args.parallel_config.enable_expert_parallel is True
|
|
assert omega_config.engine_args.parallel_config.ulysses_degree == 2
|
|
assert omega_config.engine_args.parallel_config.ring_degree == 4
|
|
assert omega_config.engine_args.parallel_config.ulysses_mode == "advanced_uaa"
|
|
assert omega_config.engine_args.parallel_config.sequence_parallel_size == 8
|
|
assert omega_config.engine_args.parallel_config.cfg_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.vae_patch_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.use_hsdp is True
|
|
assert omega_config.engine_args.parallel_config.hsdp_shard_size == 8
|
|
assert omega_config.engine_args.parallel_config.hsdp_replicate_size == 2
|
|
assert "pipeline_parallel_size" not in omega_config.engine_args
|
|
assert "data_parallel_size" not in omega_config.engine_args
|
|
assert "tensor_parallel_size" not in omega_config.engine_args
|
|
assert "enable_expert_parallel" not in omega_config.engine_args
|
|
assert "ulysses_degree" not in omega_config.engine_args
|
|
assert "ring_degree" not in omega_config.engine_args
|
|
assert "ulysses_mode" not in omega_config.engine_args
|
|
assert "sequence_parallel_size" not in omega_config.engine_args
|
|
assert "cfg_parallel_size" not in omega_config.engine_args
|
|
assert "vae_patch_parallel_size" not in omega_config.engine_args
|
|
assert "use_hsdp" not in omega_config.engine_args
|
|
assert "hsdp_shard_size" not in omega_config.engine_args
|
|
assert "hsdp_replicate_size" not in omega_config.engine_args
|
|
|
|
def test_to_omegaconf_diffusion_parallel_overrides_create_parallel_config(self):
|
|
config = StageConfig(
|
|
stage_id=1,
|
|
model_stage="diffusion",
|
|
stage_type=StageType.DIFFUSION,
|
|
runtime_overrides={
|
|
"pipeline_parallel_size": 2,
|
|
"data_parallel_size": 3,
|
|
"tensor_parallel_size": 8,
|
|
"enable_expert_parallel": True,
|
|
"ulysses_degree": 2,
|
|
"ring_degree": 4,
|
|
"ulysses_mode": "advanced_uaa",
|
|
"sequence_parallel_size": 8,
|
|
"cfg_parallel_size": 2,
|
|
"vae_patch_parallel_size": 2,
|
|
"use_hsdp": True,
|
|
"hsdp_shard_size": 8,
|
|
"hsdp_replicate_size": 2,
|
|
},
|
|
)
|
|
|
|
omega_config = config.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.parallel_config.pipeline_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.data_parallel_size == 3
|
|
assert omega_config.engine_args.parallel_config.tensor_parallel_size == 8
|
|
assert omega_config.engine_args.parallel_config.enable_expert_parallel is True
|
|
assert omega_config.engine_args.parallel_config.ulysses_degree == 2
|
|
assert omega_config.engine_args.parallel_config.ring_degree == 4
|
|
assert omega_config.engine_args.parallel_config.ulysses_mode == "advanced_uaa"
|
|
assert omega_config.engine_args.parallel_config.sequence_parallel_size == 8
|
|
assert omega_config.engine_args.parallel_config.cfg_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.vae_patch_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.use_hsdp is True
|
|
assert omega_config.engine_args.parallel_config.hsdp_shard_size == 8
|
|
assert omega_config.engine_args.parallel_config.hsdp_replicate_size == 2
|
|
assert "pipeline_parallel_size" not in omega_config.engine_args
|
|
assert "data_parallel_size" not in omega_config.engine_args
|
|
assert "tensor_parallel_size" not in omega_config.engine_args
|
|
assert "enable_expert_parallel" not in omega_config.engine_args
|
|
assert "ulysses_degree" not in omega_config.engine_args
|
|
assert "ring_degree" not in omega_config.engine_args
|
|
assert "ulysses_mode" not in omega_config.engine_args
|
|
assert "sequence_parallel_size" not in omega_config.engine_args
|
|
assert "cfg_parallel_size" not in omega_config.engine_args
|
|
assert "vae_patch_parallel_size" not in omega_config.engine_args
|
|
assert "use_hsdp" not in omega_config.engine_args
|
|
assert "hsdp_shard_size" not in omega_config.engine_args
|
|
assert "hsdp_replicate_size" not in omega_config.engine_args
|
|
|
|
def test_to_omegaconf_diffusion_parallel_degree_overrides_recompute_sequence_parallel_size(self):
|
|
config = StageConfig(
|
|
stage_id=1,
|
|
model_stage="diffusion",
|
|
stage_type=StageType.DIFFUSION,
|
|
yaml_engine_args={
|
|
"parallel_config": {
|
|
"sequence_parallel_size": 1,
|
|
"ulysses_degree": 1,
|
|
"ring_degree": 1,
|
|
}
|
|
},
|
|
runtime_overrides={
|
|
"ulysses_degree": 2,
|
|
"ring_degree": 4,
|
|
},
|
|
)
|
|
|
|
omega_config = config.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.parallel_config.ulysses_degree == 2
|
|
assert omega_config.engine_args.parallel_config.ring_degree == 4
|
|
assert omega_config.engine_args.parallel_config.sequence_parallel_size == 8
|
|
assert "ulysses_degree" not in omega_config.engine_args
|
|
assert "ring_degree" not in omega_config.engine_args
|
|
assert "sequence_parallel_size" not in omega_config.engine_args
|
|
|
|
def test_to_omegaconf_diffusion_parallel_explicit_sequence_parallel_size_is_preserved(self):
|
|
config = StageConfig(
|
|
stage_id=1,
|
|
model_stage="diffusion",
|
|
stage_type=StageType.DIFFUSION,
|
|
yaml_engine_args={
|
|
"parallel_config": {
|
|
"sequence_parallel_size": 1,
|
|
"ulysses_degree": 1,
|
|
"ring_degree": 1,
|
|
}
|
|
},
|
|
runtime_overrides={
|
|
"ulysses_degree": 2,
|
|
"ring_degree": 4,
|
|
"sequence_parallel_size": 16,
|
|
},
|
|
)
|
|
|
|
omega_config = config.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.parallel_config.ulysses_degree == 2
|
|
assert omega_config.engine_args.parallel_config.ring_degree == 4
|
|
assert omega_config.engine_args.parallel_config.sequence_parallel_size == 16
|
|
|
|
def test_to_omegaconf_llm_parallel_overrides_remain_top_level(self):
|
|
config = StageConfig(
|
|
stage_id=0,
|
|
model_stage="thinker",
|
|
stage_type=StageType.LLM,
|
|
runtime_overrides={
|
|
"pipeline_parallel_size": 2,
|
|
"data_parallel_size": 3,
|
|
"tensor_parallel_size": 8,
|
|
},
|
|
)
|
|
|
|
omega_config = config.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.pipeline_parallel_size == 2
|
|
assert omega_config.engine_args.data_parallel_size == 3
|
|
assert omega_config.engine_args.tensor_parallel_size == 8
|
|
assert "pipeline_parallel_size" in omega_config.engine_args
|
|
assert "data_parallel_size" in omega_config.engine_args
|
|
assert "tensor_parallel_size" in omega_config.engine_args
|
|
assert "parallel_config" not in omega_config.engine_args
|
|
|
|
|
|
class TestStageConfigFactory:
|
|
"""Tests for StageConfigFactory class."""
|
|
|
|
def test_default_diffusion_no_yaml(self):
|
|
"""Test single-stage diffusion works without YAML config (@ZJY0516)."""
|
|
kwargs = {
|
|
"cache_backend": "none",
|
|
"cache_config": None,
|
|
"dtype": "bfloat16",
|
|
}
|
|
configs = StageConfigFactory.create_default_diffusion(kwargs)
|
|
|
|
assert len(configs) == 1
|
|
cfg = configs[0]
|
|
assert cfg["stage_id"] == 0
|
|
assert cfg["stage_type"] == "diffusion"
|
|
assert cfg["final_output"] is True
|
|
assert cfg["final_output_type"] == "image"
|
|
|
|
def test_default_diffusion_with_parallel_config(self):
|
|
"""Test diffusion config calculates devices from parallel_config."""
|
|
|
|
@dataclass
|
|
class MockParallelConfig:
|
|
world_size: int = 4
|
|
|
|
kwargs = {
|
|
"parallel_config": MockParallelConfig(),
|
|
"cache_backend": "tea_cache",
|
|
}
|
|
configs = StageConfigFactory.create_default_diffusion(kwargs)
|
|
|
|
assert configs[0]["runtime"]["devices"] == "0,1,2,3"
|
|
|
|
def test_per_stage_override_precedence(self):
|
|
"""Test that --stage-0-gpu-memory-utilization overrides global."""
|
|
stage = StageConfig(stage_id=0, model_stage="thinker", input_sources=[])
|
|
cli_overrides = {
|
|
"gpu_memory_utilization": 0.5, # Global
|
|
"stage_0_gpu_memory_utilization": 0.9, # Per-stage override
|
|
}
|
|
|
|
overrides = StageConfigFactory._merge_cli_overrides(stage, cli_overrides)
|
|
|
|
# Per-stage should override global
|
|
assert overrides["gpu_memory_utilization"] == 0.9
|
|
|
|
def test_cli_override_forwards_engine_registered_args(self):
|
|
"""Test that any engine-registered CLI arg is forwarded (@wuhang2014)."""
|
|
stage = StageConfig(stage_id=0, model_stage="thinker", input_sources=[])
|
|
cli_overrides = {
|
|
"gpu_memory_utilization": 0.9, # Well-known param
|
|
"custom_engine_flag": True, # Not orchestrator-owned, so forwarded
|
|
}
|
|
|
|
overrides = StageConfigFactory._merge_cli_overrides(stage, cli_overrides)
|
|
|
|
assert overrides["gpu_memory_utilization"] == 0.9
|
|
assert overrides["custom_engine_flag"] is True
|
|
|
|
def test_cli_override_excludes_internal_keys(self):
|
|
"""Test that internal/orchestrator keys are not forwarded."""
|
|
stage = StageConfig(stage_id=0, model_stage="thinker", input_sources=[])
|
|
cli_overrides = {
|
|
"gpu_memory_utilization": 0.9,
|
|
"model": "some_model", # Internal
|
|
"stage_configs_path": "/path", # Internal
|
|
"batch_timeout": 10, # Internal
|
|
}
|
|
|
|
overrides = StageConfigFactory._merge_cli_overrides(stage, cli_overrides)
|
|
|
|
assert overrides["gpu_memory_utilization"] == 0.9
|
|
assert "model" not in overrides
|
|
assert "stage_configs_path" not in overrides
|
|
assert "batch_timeout" not in overrides
|
|
|
|
def test_per_stage_override_excludes_internal_keys(self):
|
|
"""Test that per-stage overrides also skip internal keys."""
|
|
stage = StageConfig(stage_id=0, model_stage="thinker", input_sources=[])
|
|
cli_overrides = {
|
|
"stage_0_gpu_memory_utilization": 0.9,
|
|
"stage_0_model": "override_model", # Internal, should be skipped
|
|
"stage_0_batch_timeout": 5, # Internal, should be skipped
|
|
}
|
|
|
|
overrides = StageConfigFactory._merge_cli_overrides(stage, cli_overrides)
|
|
|
|
assert overrides["gpu_memory_utilization"] == 0.9
|
|
assert "model" not in overrides
|
|
assert "batch_timeout" not in overrides
|
|
|
|
|
|
class TestStageResolutionHelpers:
|
|
"""Tests for shared stage override / filtering helpers."""
|
|
|
|
def test_build_stage_runtime_overrides_ignores_other_stage_and_internal_keys(self):
|
|
# Pass the same filter set the function uses by default
|
|
# (orchestrator-only fields plus SHARED_FIELDS so ``model`` is
|
|
# treated as not-per-stage-overridable).
|
|
overrides = build_stage_runtime_overrides(
|
|
0,
|
|
{
|
|
"gpu_memory_utilization": 0.5,
|
|
"stage_0_gpu_memory_utilization": 0.9,
|
|
"stage_1_gpu_memory_utilization": 0.1,
|
|
"stage_0_model": "should_be_ignored",
|
|
"parallel_config": {"world_size": 2},
|
|
},
|
|
internal_keys=internal_blacklist_keys() | SHARED_FIELDS,
|
|
)
|
|
|
|
assert overrides["gpu_memory_utilization"] == 0.9
|
|
assert "model" not in overrides
|
|
assert "parallel_config" not in overrides
|
|
|
|
def test_strip_parent_engine_args_reports_only_surprising_parent_overrides(self):
|
|
parent_fields = {f.name: f for f in fields(EngineArgs)}
|
|
filtered, overridden = strip_parent_engine_args(
|
|
{
|
|
"model": "some/model",
|
|
"stage_configs_path": "/tmp/stages.yaml",
|
|
"tensor_parallel_size": 4,
|
|
"worker_extension_cls": "some.Extension",
|
|
"custom_pipeline_args": {"pipeline_class": "demo.Pipeline"},
|
|
},
|
|
parent_fields=parent_fields,
|
|
keep_keys={"worker_extension_cls"},
|
|
strip_keys={"stage_configs_path"},
|
|
no_warn_keys={"model"},
|
|
)
|
|
|
|
assert filtered == {
|
|
"worker_extension_cls": "some.Extension",
|
|
"custom_pipeline_args": {"pipeline_class": "demo.Pipeline"},
|
|
}
|
|
assert overridden == ["tensor_parallel_size"]
|
|
|
|
def test_strip_parent_engine_args_keeps_allowed_media_access_controls(self):
|
|
parent_fields = {f.name: f for f in fields(EngineArgs)}
|
|
filtered, overridden = strip_parent_engine_args(
|
|
{
|
|
"model": "some/model",
|
|
"stage_configs_path": "/tmp/stages.yaml",
|
|
"allowed_local_media_path": "/data/qwentts",
|
|
"allowed_media_domains": ["example.com"],
|
|
},
|
|
parent_fields=parent_fields,
|
|
keep_keys={"allowed_local_media_path", "allowed_media_domains"},
|
|
strip_keys={"stage_configs_path"},
|
|
no_warn_keys={"model"},
|
|
)
|
|
|
|
assert filtered == {
|
|
"allowed_local_media_path": "/data/qwentts",
|
|
"allowed_media_domains": ["example.com"],
|
|
}
|
|
assert overridden == []
|
|
|
|
|
|
class TestPipelineDiscovery:
|
|
"""Tests for the central pipeline registry (``OMNI_PIPELINES``)."""
|
|
|
|
def test_registry_has_known_models(self):
|
|
"""Check that specific models are in OMNI_PIPELINES."""
|
|
assert "qwen2_5_omni" in OMNI_PIPELINES
|
|
assert "qwen3_omni_moe" in OMNI_PIPELINES
|
|
assert "qwen3_tts" in OMNI_PIPELINES
|
|
|
|
def test_registry_resolver_qwen3_omni_all_stages(self):
|
|
"""Test that providing the HF config for qwen3 omni with audio enabled uses all stages."""
|
|
pipeline = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
assert pipeline.model_type == "qwen3_omni_moe"
|
|
assert len(pipeline.stages) == 3 # thinker + talker + code2wav
|
|
|
|
def test_registry_resolver_qwen3_omni_thinker_only(self):
|
|
"""Test that providing the HF config for qwen3 omni without audio is thinker only."""
|
|
pipeline = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_THINKER_HF_CONFIG,
|
|
)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
assert pipeline.model_type == "qwen3_omni_moe_thinker_only"
|
|
assert len(pipeline.stages) == 1 # thinker only
|
|
|
|
def test_registry_returns_none_for_unknown(self):
|
|
"""Unknown model_types aren't found and resolve to `None`."""
|
|
assert "definitely_not_a_real_model" not in OMNI_PIPELINES
|
|
assert OMNI_PIPELINES.get("definitely_not_a_real_model") is None
|
|
assert StageConfigFactory.resolve_pipeline_config("definitely_not_a_real_model") is None
|
|
|
|
def test_pipeline_config_supports_hf_architectures(self):
|
|
"""PipelineConfig accepts hf_architectures for HF-arch fallback
|
|
(replaces the old _ARCHITECTURE_MODELS dict)."""
|
|
p = PipelineConfig(
|
|
model_type="custom_collide",
|
|
hf_architectures=("SomeCollidingArch",),
|
|
)
|
|
assert p.hf_architectures == ("SomeCollidingArch",)
|
|
|
|
|
|
class TestStagePipelineConfig:
|
|
def test_frozen(self):
|
|
s = StagePipelineConfig(stage_id=0, model_stage="a")
|
|
with pytest.raises(AttributeError):
|
|
s.model_stage = "changed"
|
|
|
|
def test_defaults(self):
|
|
s = StagePipelineConfig(stage_id=0, model_stage="a")
|
|
assert s.execution_type == StageExecutionType.LLM_AR
|
|
assert s.input_sources == ()
|
|
assert s.final_output is False
|
|
assert s.sampling_constraints == {}
|
|
assert s.engine_output_type is None
|
|
assert s.scheduler_cls is None
|
|
|
|
|
|
class TestPipelineConfigNew:
|
|
def test_frozen(self):
|
|
p = PipelineConfig(model_type="t", model_arch="A")
|
|
with pytest.raises(AttributeError):
|
|
p.model_type = "changed"
|
|
|
|
def test_validate_valid(self):
|
|
p = PipelineConfig(
|
|
model_type="t",
|
|
model_arch="A",
|
|
stages=(
|
|
StagePipelineConfig(stage_id=0, model_stage="a"),
|
|
StagePipelineConfig(stage_id=1, model_stage="b", input_sources=(0,)),
|
|
),
|
|
)
|
|
assert p.validate() == []
|
|
|
|
def test_validate_no_stages(self):
|
|
p = PipelineConfig(model_type="t", model_arch="A")
|
|
assert any("no stages" in e.lower() for e in p.validate())
|
|
|
|
|
|
class TestPipelineRegistration:
|
|
def test_pipeline_registration(self, clean_pipeline_registry):
|
|
"""Ensure that we can register and create a custom pipeline config."""
|
|
new_model_type = "new_model_type"
|
|
pipe_cfg = PipelineConfig(model_type=new_model_type)
|
|
|
|
# Register the new PipelineConfig
|
|
assert new_model_type not in OMNI_PIPELINES
|
|
register_pipeline(pipe_cfg)
|
|
assert new_model_type in OMNI_PIPELINES
|
|
assert OMNI_PIPELINES[new_model_type] is pipe_cfg
|
|
|
|
class FakeConfig(PretrainedConfig):
|
|
model_type = new_model_type
|
|
|
|
# Create the model
|
|
with (
|
|
patch("vllm_omni.config.config_factory.get_config", return_value=FakeConfig()),
|
|
patch.object(StageConfigFactory, "_create_from_registry") as mock_create,
|
|
):
|
|
StageConfigFactory.create_from_model("fake/model")
|
|
mock_create.assert_called_once()
|
|
# Ensure that the passed pipeline_config is the right type
|
|
pipeline_cfg = mock_create.call_args[0][1]
|
|
assert isinstance(pipeline_cfg, PipelineConfig)
|
|
assert pipe_cfg.model_type == new_model_type
|
|
|
|
def test_resolver_registration(self, clean_pipeline_registry):
|
|
"""Ensure that we can register and create a custom resolver."""
|
|
new_model_type = "new_model_type"
|
|
resolved_type = "resolved_type"
|
|
|
|
class FakeConfig(PretrainedConfig):
|
|
model_type = new_model_type
|
|
|
|
@pipeline_cfg_resolver(config_type=FakeConfig)
|
|
def custom_resolver(
|
|
hf_config: FakeConfig,
|
|
) -> PipelineConfig:
|
|
return PipelineConfig(model_type=resolved_type)
|
|
|
|
# Register the new PipelineConfig
|
|
assert new_model_type not in OMNI_PIPELINES
|
|
|
|
register_pipeline(custom_resolver, model_type=new_model_type)
|
|
assert new_model_type in OMNI_PIPELINES
|
|
assert OMNI_PIPELINES[new_model_type] is custom_resolver
|
|
|
|
# Create the model
|
|
with (
|
|
patch("vllm_omni.config.config_factory.get_config", return_value=FakeConfig()),
|
|
patch.object(StageConfigFactory, "_create_from_registry") as mock_create,
|
|
):
|
|
StageConfigFactory.create_from_model("fake/model")
|
|
mock_create.assert_called_once()
|
|
# Ensure that the passed pipeline_config is the right type
|
|
pipeline_cfg = mock_create.call_args[0][1]
|
|
assert isinstance(pipeline_cfg, PipelineConfig)
|
|
assert pipeline_cfg.model_type == resolved_type
|
|
|
|
def test_resolve_when_autodetect_resolves_none(self):
|
|
"""Regression test for: https://github.com/vllm-project/vllm-omni/issues/4726"""
|
|
deploy_path = get_deploy_config_path("ming_tts.yaml")
|
|
resolved_config = StageConfigFactory.create_from_model(
|
|
model="inclusionAI/Ming-omni-tts-0.5B",
|
|
deploy_config_path=deploy_path,
|
|
)
|
|
assert resolved_config is not None
|
|
assert len(resolved_config) > 0
|
|
|
|
def test_deploy_override_uses_correct_endpoint_restrictions(self, clean_pipeline_registry, tmp_path):
|
|
"""Ensure endpoint restrictions must come from the final pipeline
|
|
after deploy config overrides, not the auto-detected pipeline.
|
|
"""
|
|
# Register two pipeline configs, where one has an endpoint restriction, and one doesn't
|
|
restriction = EndpointRestriction(
|
|
OmniServingCapability.COMPLETIONS,
|
|
"pipeline_a blocks completions",
|
|
)
|
|
pipe_a = PipelineConfig(model_type="detect_type", endpoint_restrictions=(restriction,))
|
|
pipe_b = PipelineConfig(model_type="override_type", endpoint_restrictions=())
|
|
register_pipeline(pipe_a)
|
|
register_pipeline(pipe_b)
|
|
|
|
# Create a config with the autodetected type, and write the
|
|
# deploy config specifying the override type to a temp path
|
|
class FakeConfig(PretrainedConfig):
|
|
model_type = "detect_type"
|
|
|
|
deploy_yaml = tmp_path / "override.yaml"
|
|
deploy_yaml.write_text("pipeline: override_type\n")
|
|
|
|
# Get the endpoint restrictions, passing the deploy config with the override
|
|
# type + patching the config for the detected type. Ensure that the endpoint
|
|
# restrictions correspond to the type in the deploy config.
|
|
with patch(
|
|
"vllm_omni.config.config_factory.get_config",
|
|
return_value=FakeConfig(),
|
|
):
|
|
restrictions = StageConfigFactory.get_pipeline_endpoint_restrictions(
|
|
model="fake/model",
|
|
trust_remote_code=False,
|
|
deploy_config_path=str(deploy_yaml),
|
|
)
|
|
|
|
assert restrictions == ()
|
|
|
|
|
|
class TestResolveScheduler:
|
|
def test_all_execution_types_handled(self):
|
|
for et in StageExecutionType:
|
|
_resolve_scheduler(et)
|
|
|
|
def test_ar_sync_when_false(self):
|
|
cls = _resolve_scheduler(StageExecutionType.LLM_AR, async_scheduling=False)
|
|
assert cls is not None
|
|
assert "Async" not in cls.__name__
|
|
|
|
def test_ar_async_when_true(self):
|
|
cls = _resolve_scheduler(StageExecutionType.LLM_AR, async_scheduling=True)
|
|
assert cls is not None
|
|
assert "Async" in cls.__name__
|
|
|
|
def test_generation(self):
|
|
cls = _resolve_scheduler(StageExecutionType.LLM_GENERATION)
|
|
assert cls is not None
|
|
assert "Generation" in cls.__name__
|
|
|
|
def test_diffusion_returns_none(self):
|
|
assert _resolve_scheduler(StageExecutionType.DIFFUSION) is None
|
|
|
|
|
|
class TestDeployConfigLoading:
|
|
def test_custom_voice_dir_is_pipeline_wide_engine_arg(self, tmp_path):
|
|
deploy_path = tmp_path / "qwen3_tts_custom_voice.yaml"
|
|
custom_voice_dir = tmp_path / "voices"
|
|
deploy_path.write_text(
|
|
f"""
|
|
async_chunk: true
|
|
custom_voice_dir: {custom_voice_dir}
|
|
stages:
|
|
- stage_id: 0
|
|
devices: "0"
|
|
- stage_id: 1
|
|
devices: "0"
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
pipeline = StageConfigFactory.resolve_pipeline_config("qwen3_tts")
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
|
|
assert deploy.custom_voice_dir == str(custom_voice_dir)
|
|
assert {s.yaml_engine_args.get("custom_voice_dir") for s in stages} == {str(custom_voice_dir)}
|
|
|
|
def test_load_qwen3_omni_moe_deploy_config(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert len(deploy.stages) == 3
|
|
assert deploy.async_chunk is True
|
|
assert deploy.connectors is not None
|
|
assert deploy.platforms is not None
|
|
|
|
def test_load_voxtral_tts_deploy_config_schema_fields(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "voxtral_tts.yaml"
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert deploy.stages[0].config_format == "mistral"
|
|
assert deploy.stages[0].load_format == "mistral"
|
|
assert deploy.stages[0].tokenizer_mode == "mistral"
|
|
assert not any(
|
|
name in deploy.stages[0].engine_extras for name in ("config_format", "load_format", "tokenizer_mode")
|
|
)
|
|
|
|
def test_load_ming_flash_omni_deploy_config_schema_fields(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "ming_flash_omni.yaml"
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert deploy.stages[0].compilation_config == {"pass_config": {"fuse_allreduce_rms": False}}
|
|
assert "compilation_config" not in deploy.stages[0].engine_extras
|
|
|
|
def test_load_voxcpm2_deploy_config_preserves_engine_extras(self):
|
|
deploy_path = get_deploy_config_path("voxcpm2.yaml")
|
|
raw_stage = get_deploy_config_stage("voxcpm2.yaml", 0)
|
|
expected_runtime_config = raw_stage["engine_extras"]["hf_overrides"]["voxcpm2_runtime_config"]
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
runtime_config = deploy.stages[0].engine_extras["hf_overrides"]["voxcpm2_runtime_config"]
|
|
assert runtime_config == expected_runtime_config
|
|
|
|
@pytest.mark.parametrize(
|
|
("deploy_name", "pipeline_name", "stage_count", "final_output_type"),
|
|
[
|
|
("mammoth_moda2.yaml", "mammoth_moda2", 2, "image"),
|
|
("mammoth_moda2_ar.yaml", "mammoth_moda2_ar", 1, "text"),
|
|
("omnivoice.yaml", "omnivoice", 1, "audio"),
|
|
],
|
|
)
|
|
def test_load_new_registry_backed_deploy_configs(
|
|
self,
|
|
deploy_name: str,
|
|
pipeline_name: str,
|
|
stage_count: int,
|
|
final_output_type: str,
|
|
):
|
|
deploy_path = Path(get_deploy_config_path(deploy_name))
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert deploy.pipeline == pipeline_name
|
|
|
|
with patch("vllm_omni.platforms.current_omni_platform") as platform:
|
|
platform.device_name = "cuda"
|
|
stages = merge_pipeline_deploy(OMNI_PIPELINES[pipeline_name], deploy)
|
|
assert len(stages) == stage_count
|
|
assert stages[-1].final_output is True
|
|
assert stages[-1].final_output_type == final_output_type
|
|
|
|
def test_merge_pipeline_deploy(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
pipeline = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
|
|
assert len(stages) == 3
|
|
s0 = stages[0]
|
|
assert s0.model_stage == "thinker"
|
|
assert s0.yaml_engine_args["model_arch"] == "Qwen3OmniMoeForConditionalGeneration"
|
|
assert s0.yaml_engine_args["engine_output_type"] == "latent"
|
|
assert s0.yaml_extras["default_sampling_params"]["detokenize"] is True
|
|
|
|
def test_merge_pipeline_deploy_preserves_num_replicas(self, tmp_path):
|
|
pipeline = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
|
|
base = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not base.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
overlay = tmp_path / "multi_replicas.yaml"
|
|
overlay.write_text(f'base_config: {base}\nstages:\n - stage_id: 1\n devices: "1,2"\n num_replicas: 2\n')
|
|
|
|
deploy = load_deploy_config(overlay)
|
|
assert deploy.stages[1].num_replicas == 2
|
|
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
assert stages[1].yaml_runtime["devices"] == "1,2"
|
|
assert stages[1].yaml_runtime["num_replicas"] == 2
|
|
|
|
def test_merge_pipeline_deploy_preserves_requires_multimodal_data(self):
|
|
pipeline = PipelineConfig(
|
|
model_type="test_mm",
|
|
model_arch="TestModel",
|
|
stages=(
|
|
StagePipelineConfig(
|
|
stage_id=0,
|
|
model_stage="ar",
|
|
execution_type=StageExecutionType.LLM_AR,
|
|
requires_multimodal_data=True,
|
|
),
|
|
),
|
|
)
|
|
deploy = DeployConfig(async_chunk=False, stages=[StageDeployConfig(stage_id=0)])
|
|
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
|
|
assert stages[0].yaml_runtime["requires_multimodal_data"] is True
|
|
|
|
def test_merge_pipeline_deploy_preserves_pipeline_scheduler_cls(self):
|
|
scheduler_cls = "tests.fake.CustomScheduler"
|
|
pipeline = PipelineConfig(
|
|
model_type="test_scheduler",
|
|
model_arch="TestModel",
|
|
stages=(
|
|
StagePipelineConfig(
|
|
stage_id=0,
|
|
model_stage="ar",
|
|
execution_type=StageExecutionType.LLM_AR,
|
|
scheduler_cls=scheduler_cls,
|
|
),
|
|
),
|
|
)
|
|
deploy = DeployConfig(async_chunk=False, stages=[StageDeployConfig(stage_id=0)])
|
|
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
|
|
assert stages[0].scheduler_cls == scheduler_cls
|
|
|
|
def test_mixed_schema_preserves_flat_fields(self):
|
|
"""Ensure flat fields are not dropped when engine_args are present."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"gpu_memory_utilization": 0.7,
|
|
"max_num_seqs": 8,
|
|
"engine_args": {
|
|
"tensor_parallel_size": 4,
|
|
"enforce_eager": True,
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
assert len(deploy.stages) == 1
|
|
stage = deploy.stages[0]
|
|
# Check that the engine args are set
|
|
assert stage.tensor_parallel_size == 4
|
|
assert stage.enforce_eager is True
|
|
# Check that the other settings are also preserved
|
|
assert stage.gpu_memory_utilization == 0.7
|
|
assert stage.max_num_seqs == 8
|
|
|
|
def test_engine_parse_engine_fields(self):
|
|
"""Test that we correctly parse & recursively merge stage deploy fields."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"compilation_config": {
|
|
"encoder_cudagraph_token_budgets": [1024, 2048],
|
|
"pass_config": {"fuse_norm_quant": True},
|
|
},
|
|
"engine_args": {
|
|
"compilation_config": {
|
|
"cudagraph_mm_encoder": True,
|
|
"pass_config": {"fuse_allreduce_rms": False},
|
|
},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
assert len(deploy.stages) == 1
|
|
stage = deploy.stages[0]
|
|
assert stage.compilation_config == {
|
|
"encoder_cudagraph_token_budgets": [1024, 2048],
|
|
"pass_config": {
|
|
"fuse_norm_quant": True,
|
|
"fuse_allreduce_rms": False,
|
|
},
|
|
"cudagraph_mm_encoder": True,
|
|
}
|
|
|
|
def test_engine_extras_deep_merges_dicts_simple(self):
|
|
"""Ensure dictionary valued keys merge properly for top level dicts."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"foo": {"a": 111, "b": 1},
|
|
"engine_args": {
|
|
"foo": {"b": 2, "c": 3},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
assert len(deploy.stages) == 1
|
|
stage = deploy.stages[0]
|
|
assert "foo" in stage.engine_extras
|
|
assert stage.engine_extras["foo"] == {"a": 111, "b": 2, "c": 3}
|
|
|
|
def test_engine_extras_dict_type_mismatch(self):
|
|
"""Ensure that we handle type mismatches with nested dicts correctly."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"foo": {"b": {1: 1}},
|
|
"engine_args": {
|
|
"foo": {"b": 2},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
assert len(deploy.stages) == 1
|
|
stage = deploy.stages[0]
|
|
assert "foo" in stage.engine_extras
|
|
assert stage.engine_extras["foo"] == {"b": 2}
|
|
|
|
def test_mixed_engine_extras_deep_merges_dicts(self):
|
|
"""Ensure dictionary valued keys merge properly for nested dicts."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"foo": {"a": 111, "b": {"e": 199}},
|
|
"engine_args": {
|
|
"foo": {"b": {"d": 9}, "c": 3},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
assert len(deploy.stages) == 1
|
|
stage = deploy.stages[0]
|
|
assert "foo" in stage.engine_extras
|
|
assert stage.engine_extras["foo"] == {"a": 111, "b": {"d": 9, "e": 199}, "c": 3}
|
|
|
|
def test_explicit_engine_extras_merge_with_flat_and_engine_args(self):
|
|
"""Explicit engine_extras should be preserved with existing pass-through styles."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"engine_extras": {
|
|
"hf_overrides": {
|
|
"runtime_config": {
|
|
"explicit_only": True,
|
|
"shared": "explicit",
|
|
"nested": {"a": 1},
|
|
}
|
|
}
|
|
},
|
|
"hf_overrides": {
|
|
"runtime_config": {
|
|
"flat_only": True,
|
|
"shared": "flat",
|
|
"nested": {"b": 2},
|
|
}
|
|
},
|
|
"engine_args": {
|
|
"hf_overrides": {
|
|
"runtime_config": {
|
|
"engine_args_only": True,
|
|
"shared": "engine_args",
|
|
"nested": {"c": 3},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
assert deploy.stages[0].engine_extras["hf_overrides"] == {
|
|
"runtime_config": {
|
|
"engine_args_only": True,
|
|
"explicit_only": True,
|
|
"flat_only": True,
|
|
"nested": {"a": 1, "b": 2, "c": 3},
|
|
"shared": "engine_args",
|
|
}
|
|
}
|
|
|
|
def test_deep_merge_does_not_mutate_inputs(self):
|
|
"""Merging engine_args must not mutate the base stage dict."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"foo": {"a": 1, "b": {"x": 10, "y": 20}},
|
|
"engine_args": {
|
|
"foo": {"b": {"y": 99, "z": 30}, "c": 3},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
original_foo = fake_config["stages"][0]["foo"]
|
|
original_b = original_foo["b"]
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
# Values should merge into the new dict correctly
|
|
assert deploy.stages[0].engine_extras["foo"] == {
|
|
"a": 1,
|
|
"b": {"x": 10, "y": 99, "z": 30},
|
|
"c": 3,
|
|
}
|
|
# But the original nested dicts foo / b should be unchanged, since
|
|
# we recursively shallow copy to avoid mutating in place.
|
|
assert original_foo == {"a": 1, "b": {"x": 10, "y": 20}}
|
|
assert original_b == {"x": 10, "y": 20}
|
|
|
|
def test_mixed_schema_engine_args_wins_scalars(self):
|
|
"""engine_args takes precedence over flat fields for scalar conflicts."""
|
|
fake_config = {
|
|
"stages": [
|
|
{
|
|
"stage_id": 0,
|
|
"gpu_memory_utilization": 0.7,
|
|
"engine_args": {
|
|
"gpu_memory_utilization": 0.5,
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
with patch("vllm_omni.config.stage_config.resolve_deploy_yaml", return_value=fake_config):
|
|
deploy = load_deploy_config("dummy.yaml")
|
|
|
|
assert deploy.stages[0].gpu_memory_utilization == 0.5
|
|
|
|
|
|
class TestQwen3OmniPipeline:
|
|
def test_registered(self):
|
|
p = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(p, PipelineConfig)
|
|
assert p.model_arch == "Qwen3OmniMoeForConditionalGeneration"
|
|
assert len(p.stages) == 3
|
|
assert p.validate() == []
|
|
|
|
def test_thinker(self):
|
|
p = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(0)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.model_stage == "thinker"
|
|
assert s.execution_type == StageExecutionType.LLM_AR
|
|
assert s.owns_tokenizer is True
|
|
assert s.engine_output_type == "latent"
|
|
assert s.sampling_constraints["detokenize"] is True
|
|
|
|
def test_talker(self):
|
|
p = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(1)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.input_sources == (0,)
|
|
assert s.sampling_constraints["stop_token_ids"] == [2150]
|
|
# thinker2talker was removed: sync_process_input_func always wins.
|
|
assert s.custom_process_input_func is None
|
|
assert s.sync_process_input_func is not None
|
|
assert s.sync_process_input_func.endswith("thinker2talker_token_only")
|
|
assert s.custom_process_next_stage_input_func is not None
|
|
|
|
def test_code2wav(self):
|
|
p = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(2)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.execution_type == StageExecutionType.LLM_GENERATION
|
|
assert s.final_output_type == "audio"
|
|
assert s.engine_output_type == "audio"
|
|
|
|
|
|
class TestQwen2_5OmniPipeline:
|
|
def test_registered(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("qwen2_5_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
assert p.model_arch == "Qwen2_5OmniForConditionalGeneration"
|
|
assert len(p.stages) == 3
|
|
assert p.validate() == []
|
|
|
|
def test_thinker(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("qwen2_5_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(0)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.model_stage == "thinker"
|
|
assert s.execution_type == StageExecutionType.LLM_AR
|
|
assert s.owns_tokenizer is True
|
|
assert s.engine_output_type == "latent"
|
|
assert s.requires_multimodal_data is True
|
|
|
|
def test_talker(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("qwen2_5_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(1)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.input_sources == (0,)
|
|
assert s.sampling_constraints["stop_token_ids"] == [8294]
|
|
# thinker2talker was removed: qwen2_5_omni has no async_chunk support,
|
|
# so sync_process_input_func always wins and custom_process_input_func
|
|
# was dead code.
|
|
assert s.custom_process_input_func is None
|
|
assert s.sync_process_input_func is not None
|
|
|
|
def test_code2wav(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("qwen2_5_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(2)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.execution_type == StageExecutionType.LLM_GENERATION
|
|
assert s.final_output_type == "audio"
|
|
assert s.engine_output_type == "audio"
|
|
|
|
|
|
class TestQwen3TTSPipeline:
|
|
def test_registered(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("qwen3_tts")
|
|
assert isinstance(p, PipelineConfig)
|
|
assert p is not None
|
|
assert p.model_arch == "Qwen3TTSTalkerForConditionalGeneration"
|
|
assert len(p.stages) == 2
|
|
assert p.validate() == []
|
|
|
|
def test_talker_stage(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("qwen3_tts")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(0)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.model_stage == "qwen3_tts"
|
|
assert s.execution_type == StageExecutionType.LLM_AR
|
|
assert s.owns_tokenizer is True
|
|
assert s.engine_output_type == "latent"
|
|
assert s.sampling_constraints["stop_token_ids"] == [2150]
|
|
# Stage 0 inherits the pipeline-level model_arch
|
|
assert s.model_arch is None
|
|
|
|
def test_code2wav_stage_has_per_stage_model_arch(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("qwen3_tts")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(1)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.execution_type == StageExecutionType.LLM_GENERATION
|
|
assert s.final_output_type == "audio"
|
|
assert s.engine_output_type == "audio"
|
|
# Per-stage model_arch override (different from pipeline-level talker)
|
|
assert s.model_arch == "Qwen3TTSCode2Wav"
|
|
# tts_args is passed through via extras
|
|
assert s.extras["tts_args"]["max_instructions_length"] == 500
|
|
|
|
def test_per_stage_model_arch_flows_through_merge(self, tmp_path):
|
|
"""Verify the new ps.model_arch override survives merge_pipeline_deploy."""
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_tts.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("qwen3_tts deploy yaml not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
pipeline = OMNI_PIPELINES["qwen3_tts"]
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
|
|
# Stage 0 inherits pipeline-level model_arch
|
|
assert stages[0].yaml_engine_args["model_arch"] == "Qwen3TTSTalkerForConditionalGeneration"
|
|
# Stage 1 uses its per-stage override
|
|
assert stages[1].yaml_engine_args["model_arch"] == "Qwen3TTSCode2Wav"
|
|
|
|
def test_subtalker_sampling_params_deep_merge_preserves_base_keys(self):
|
|
"""Verify subtalker sampling params participate in stage deep-merge."""
|
|
base = {
|
|
"stage_id": 0,
|
|
"subtalker_sampling_params": {
|
|
"do_sample": True,
|
|
"temperature": 0.9,
|
|
"top_k": 50,
|
|
"top_p": 1.0,
|
|
},
|
|
}
|
|
overlay = {
|
|
"stage_id": 0,
|
|
"subtalker_sampling_params": {
|
|
"temperature": 0.7,
|
|
"top_k": 32,
|
|
},
|
|
}
|
|
|
|
merged = _deep_merge_stage(base, overlay)
|
|
|
|
assert merged["subtalker_sampling_params"] == {
|
|
"do_sample": True,
|
|
"temperature": 0.7,
|
|
"top_k": 32,
|
|
"top_p": 1.0,
|
|
}
|
|
|
|
|
|
class TestMingFlashOmniPipeline:
|
|
def test_registered(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
assert p.model_arch == "MingFlashOmniForConditionalGeneration"
|
|
assert len(p.stages) == 2
|
|
assert p.validate() == []
|
|
|
|
def test_thinker_stage(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(0)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.model_stage == "thinker"
|
|
assert s.execution_type == StageExecutionType.LLM_AR
|
|
assert s.owns_tokenizer is True
|
|
assert s.requires_multimodal_data is True
|
|
assert s.engine_output_type == "text"
|
|
assert s.hf_config_name == "llm_config"
|
|
assert s.sampling_constraints["detokenize"] is True
|
|
|
|
def test_talker_stage(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(1)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.model_stage == "ming_tts"
|
|
assert s.execution_type == StageExecutionType.LLM_GENERATION
|
|
assert s.input_sources == (0,)
|
|
assert s.final_output_type == "audio"
|
|
assert s.engine_output_type == "audio"
|
|
assert s.hf_config_name == "talker_config"
|
|
# Per-stage model_arch override (Ming talker has its own self-contained LLM)
|
|
assert s.model_arch == "MingFlashOmniTalkerForConditionalGeneration"
|
|
assert s.tokenizer_subdir == "talker/llm"
|
|
# thinker2talker was removed: ming_flash_omni has no async_chunk support
|
|
# and both thinker2talker / thinker2talker_token_only called _build_talker_inputs
|
|
# identically, so custom_process_input_func was dead code.
|
|
assert s.custom_process_input_func is None
|
|
assert s.sync_process_input_func is not None
|
|
|
|
def test_talker_stage_processor_wiring_resolves(self):
|
|
"""The sync_process_input_func string must point to a real callable.
|
|
|
|
Lazy string references only fail at first inference otherwise — this
|
|
catches typos in the pipeline declaration at import / registration time.
|
|
"""
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(1)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
module_path, _, attr = s.sync_process_input_func.rpartition(".")
|
|
module = importlib.import_module(module_path)
|
|
assert callable(getattr(module, attr))
|
|
|
|
def test_tts_pipeline_registered(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_tts")
|
|
assert isinstance(p, PipelineConfig)
|
|
assert p.model_arch == "MingFlashOmniTalkerForConditionalGeneration"
|
|
assert len(p.stages) == 1
|
|
assert p.validate() == []
|
|
|
|
def test_tts_stage(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_tts")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(0)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.model_stage == "ming_tts"
|
|
assert s.execution_type == StageExecutionType.LLM_GENERATION
|
|
assert s.input_sources == ()
|
|
assert s.owns_tokenizer is True
|
|
assert s.final_output_type == "audio"
|
|
assert s.engine_output_type == "audio"
|
|
assert s.hf_config_name == "talker_config"
|
|
assert s.tokenizer_subdir == "talker/llm"
|
|
|
|
def test_full_yaml_loads_and_merges(self):
|
|
"""deploy/ming_flash_omni.yaml parses and merges with the registered pipeline."""
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "ming_flash_omni.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("ming_flash_omni deploy yaml not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert len(deploy.stages) == 2
|
|
assert deploy.async_chunk is False
|
|
assert deploy.pipeline == "ming_flash_omni"
|
|
# We won't test stage 0/1 colocation contract here,
|
|
# as there could exist more variant of custom device setup
|
|
|
|
pipeline = StageConfigFactory.resolve_pipeline_config("ming_flash_omni")
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
assert len(stages) == 2
|
|
assert stages[0].yaml_engine_args["model_arch"] == "MingFlashOmniForConditionalGeneration"
|
|
assert stages[1].yaml_engine_args["model_arch"] == "MingFlashOmniTalkerForConditionalGeneration"
|
|
|
|
def test_tts_yaml_loads_and_merges(self):
|
|
"""deploy/ming_flash_omni_tts.yaml parses and routes to the TTS-only pipeline."""
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "ming_flash_omni_tts.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("ming_flash_omni_tts deploy yaml not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert len(deploy.stages) == 1
|
|
assert deploy.pipeline == "ming_flash_omni_tts"
|
|
|
|
pipeline = OMNI_PIPELINES["ming_flash_omni_tts"]
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
assert len(stages) == 1
|
|
assert stages[0].yaml_engine_args["model_arch"] == "MingFlashOmniTalkerForConditionalGeneration"
|
|
|
|
def test_thinker_only_pipeline_registered(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_thinker_only")
|
|
assert isinstance(p, PipelineConfig)
|
|
assert p.model_arch == "MingFlashOmniForConditionalGeneration"
|
|
assert len(p.stages) == 1
|
|
assert p.validate() == []
|
|
|
|
def test_thinker_only_stage(self):
|
|
p = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_thinker_only")
|
|
assert isinstance(p, PipelineConfig)
|
|
|
|
s = p.get_stage(0)
|
|
assert isinstance(s, StagePipelineConfig)
|
|
assert s.model_stage == "thinker"
|
|
assert s.execution_type == StageExecutionType.LLM_AR
|
|
assert s.input_sources == ()
|
|
assert s.owns_tokenizer is True
|
|
assert s.requires_multimodal_data is True
|
|
assert s.final_output_type == "text"
|
|
assert s.engine_output_type == "text"
|
|
assert s.hf_config_name == "llm_config"
|
|
assert s.sampling_constraints["detokenize"] is True
|
|
|
|
def test_thinker_only_yaml_loads_and_merges(self):
|
|
"""deploy/ming_flash_omni_thinker_only.yaml parses and routes to the thinker-only pipeline."""
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "ming_flash_omni_thinker_only.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("ming_flash_omni_thinker_only deploy yaml not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert len(deploy.stages) == 1
|
|
assert deploy.pipeline == "ming_flash_omni_thinker_only"
|
|
|
|
pipeline = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_thinker_only")
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
assert len(stages) == 1
|
|
assert stages[0].yaml_engine_args["model_arch"] == "MingFlashOmniForConditionalGeneration"
|
|
|
|
def test_image_pipeline_registered(self):
|
|
p = OMNI_PIPELINES.get("ming_flash_omni_image")
|
|
assert p is not None
|
|
assert p.model_arch == "MingFlashOmniForConditionalGeneration"
|
|
assert len(p.stages) == 2
|
|
assert p.validate() == []
|
|
|
|
def test_image_thinker_stage(self):
|
|
s = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_image").get_stage(0)
|
|
assert s.model_stage == "thinker"
|
|
assert s.execution_type == StageExecutionType.LLM_AR
|
|
assert s.input_sources == ()
|
|
assert s.final_output is False
|
|
assert s.owns_tokenizer is True
|
|
assert s.requires_multimodal_data is True
|
|
# Image variant exports hidden states for the diffusion stage.
|
|
assert s.engine_output_type == "latent"
|
|
assert s.hf_config_name == "thinker_config"
|
|
assert s.sampling_constraints["detokenize"] is False
|
|
assert s.prompt_expand_func is not None
|
|
|
|
def test_image_dit_stage(self):
|
|
s = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_image").get_stage(1)
|
|
assert s.model_stage == "dit"
|
|
assert s.execution_type == StageExecutionType.DIFFUSION
|
|
assert s.input_sources == (0,)
|
|
assert s.final_output is True
|
|
assert s.final_output_type == "image"
|
|
assert s.hf_config_name == "image_gen_config"
|
|
assert s.model_arch == "MingImagePipeline"
|
|
assert s.custom_process_input_func is not None
|
|
|
|
def test_image_processor_wiring_resolves(self):
|
|
"""The prompt_expand_func and custom_process_input_func strings must point to real callables."""
|
|
pipeline = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_image")
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
|
|
thinker = pipeline.get_stage(0)
|
|
dit = pipeline.get_stage(1)
|
|
for ref in (thinker.prompt_expand_func, dit.custom_process_input_func):
|
|
module_path, _, attr = ref.rpartition(".")
|
|
module = importlib.import_module(module_path)
|
|
assert callable(getattr(module, attr))
|
|
|
|
def test_image_yaml_loads_and_merges(self):
|
|
"""deploy/ming_flash_omni_image.yaml parses and routes to the image pipeline."""
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "ming_flash_omni_image.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("ming_flash_omni_image deploy yaml not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
assert len(deploy.stages) == 2
|
|
assert deploy.async_chunk is False
|
|
assert deploy.pipeline == "ming_flash_omni_image"
|
|
assert deploy.connectors is not None
|
|
assert "shared_memory_connector" in deploy.connectors
|
|
|
|
pipeline = StageConfigFactory.resolve_pipeline_config("ming_flash_omni_image")
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
assert len(stages) == 2
|
|
# Stage 0 thinker: AR worker that emits latents.
|
|
assert stages[0].yaml_engine_args["model_arch"] == "MingFlashOmniForConditionalGeneration"
|
|
assert stages[0].yaml_engine_args["engine_output_type"] == "latent"
|
|
assert stages[0].yaml_extras["default_sampling_params"]["detokenize"] is False
|
|
assert stages[0].yaml_extras["prompt_expand_func"] is not None
|
|
# Stage 1 dit: diffusion stage with MingImagePipeline.
|
|
assert stages[1].yaml_engine_args["model_arch"] == "MingImagePipeline"
|
|
assert stages[1].custom_process_input_func is not None
|
|
assert stages[1].final_output is True
|
|
assert stages[1].final_output_type == "image"
|
|
|
|
|
|
class TestBaseConfigInheritance:
|
|
"""Test deploy YAML base_config inheritance."""
|
|
|
|
def test_ci_inherits_from_main(self):
|
|
ci_path = Path(get_deploy_config_path("ci/qwen3_omni_moe.yaml"))
|
|
if not ci_path.exists():
|
|
pytest.skip("CI deploy config not found")
|
|
|
|
deploy = load_deploy_config(ci_path)
|
|
assert len(deploy.stages) == 3
|
|
# CI overrides
|
|
assert deploy.stages[0].load_format is None
|
|
assert "load_format" not in deploy.stages[0].engine_extras
|
|
assert deploy.stages[0].max_num_seqs == 5
|
|
# Inherited from base
|
|
assert deploy.stages[0].gpu_memory_utilization == 0.9
|
|
assert deploy.connectors is not None
|
|
assert "connector_of_shared_memory" in deploy.connectors
|
|
# CI overlay explicitly sets async_chunk: False (see
|
|
# tests.helpers.stage_config._CI_OVERLAYS and PR #2383 discussion). Overlay
|
|
# bool overrides base even when the base yaml has async_chunk: true.
|
|
assert deploy.async_chunk is False
|
|
|
|
def test_ci_sampling_merge(self):
|
|
ci_path = Path(get_deploy_config_path("ci/qwen3_omni_moe.yaml"))
|
|
if not ci_path.exists():
|
|
pytest.skip("CI deploy config not found")
|
|
|
|
deploy = load_deploy_config(ci_path)
|
|
s0 = deploy.stages[0].default_sampling_params
|
|
# CI overrides max_tokens
|
|
assert s0["max_tokens"] == 150
|
|
|
|
def test_pure_inheritance_overlay(self, tmp_path):
|
|
"""An overlay with only ``base_config`` inherits everything."""
|
|
base = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not base.exists():
|
|
pytest.skip("Base deploy config not found")
|
|
|
|
overlay = tmp_path / "overlay.yaml"
|
|
overlay.write_text(f"base_config: {base}\n")
|
|
|
|
deploy = load_deploy_config(overlay)
|
|
assert len(deploy.stages) == 3
|
|
assert deploy.stages[0].gpu_memory_utilization == 0.9
|
|
|
|
def test_single_field_overlay(self, tmp_path):
|
|
"""An overlay overriding one stage field merges with the base."""
|
|
base = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not base.exists():
|
|
pytest.skip("Base deploy config not found")
|
|
|
|
overlay = tmp_path / "overlay.yaml"
|
|
overlay.write_text(f"base_config: {base}\nstages:\n - stage_id: 2\n max_num_batched_tokens: 1000000\n")
|
|
|
|
deploy = load_deploy_config(overlay)
|
|
assert deploy.stages[2].max_num_batched_tokens == 1000000
|
|
# Rest inherited
|
|
assert deploy.stages[0].gpu_memory_utilization == 0.9
|
|
|
|
|
|
class TestPlatformOverrides:
|
|
"""Test platform-specific deploy config overrides."""
|
|
|
|
def test_npu_overrides(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
deploy = _apply_platform_overrides(deploy, platform="npu")
|
|
|
|
assert deploy.stages[0].gpu_memory_utilization == 0.6
|
|
assert deploy.stages[0].tensor_parallel_size == 2
|
|
assert deploy.stages[0].devices == "0,1"
|
|
# Stage 2 unaffected fields stay at base
|
|
assert deploy.stages[2].enforce_eager is False
|
|
|
|
def test_xpu_overrides(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
deploy = _apply_platform_overrides(deploy, platform="xpu")
|
|
|
|
assert deploy.stages[0].tensor_parallel_size == 4
|
|
assert deploy.stages[0].devices == "0,1,2,3"
|
|
assert deploy.stages[0].engine_extras.get("max_cudagraph_capture_size") == 0
|
|
|
|
def test_unknown_platform_noop(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
original_mem = deploy.stages[0].gpu_memory_utilization
|
|
deploy = _apply_platform_overrides(deploy, platform="unknown_hw")
|
|
assert deploy.stages[0].gpu_memory_utilization == original_mem
|
|
|
|
def test_platforms_deep_merge_inheritance(self, tmp_path):
|
|
"""Overlay's platforms: block layers onto base's, per-stage."""
|
|
base = tmp_path / "base.yaml"
|
|
base.write_text(
|
|
"stages:\n"
|
|
" - stage_id: 0\n"
|
|
" gpu_memory_utilization: 0.9\n"
|
|
"platforms:\n"
|
|
" rocm:\n"
|
|
" stages:\n"
|
|
" - stage_id: 0\n"
|
|
" enforce_eager: true\n"
|
|
)
|
|
overlay = tmp_path / "overlay.yaml"
|
|
overlay.write_text(
|
|
f"base_config: {base.name}\n"
|
|
"platforms:\n"
|
|
" rocm:\n"
|
|
" stages:\n"
|
|
" - stage_id: 0\n"
|
|
" max_num_seqs: 1\n"
|
|
)
|
|
|
|
deploy = load_deploy_config(overlay)
|
|
deploy = _apply_platform_overrides(deploy, platform="rocm")
|
|
# Both base's enforce_eager and overlay's max_num_seqs should apply.
|
|
assert deploy.stages[0].enforce_eager is True
|
|
assert deploy.stages[0].max_num_seqs == 1
|
|
# Inherited stage default not touched by overlay platforms section.
|
|
assert deploy.stages[0].gpu_memory_utilization == 0.9
|
|
|
|
|
|
class TestCLIOverrideFlow:
|
|
"""Test --stage-overrides JSON merge into StageConfig."""
|
|
|
|
def test_stage_overrides_merge(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
pipeline = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
|
|
# Simulate --stage-overrides '{"0": {"gpu_memory_utilization": 0.5}}'
|
|
overrides = {"stage_0_gpu_memory_utilization": 0.5}
|
|
stages[0].runtime_overrides = StageConfigFactory._merge_cli_overrides(stages[0], overrides)
|
|
assert stages[0].runtime_overrides["gpu_memory_utilization"] == 0.5
|
|
|
|
def test_global_override_applies_to_all(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
pipeline = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
|
|
overrides = {"enforce_eager": True}
|
|
for s in stages:
|
|
s.runtime_overrides = StageConfigFactory._merge_cli_overrides(s, overrides)
|
|
assert s.runtime_overrides["enforce_eager"] is True
|
|
|
|
|
|
class TestAuraOmniDeploy:
|
|
def test_aura_omni_deploy_forces_pipeline_override(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "aura_omni.yaml"
|
|
deploy = load_deploy_config(deploy_path)
|
|
|
|
assert deploy.pipeline == "aura_omni"
|
|
|
|
def test_aura_omni_deploy_resolves_four_native_stages(self):
|
|
pipeline_cfg = StageConfigFactory.resolve_pipeline_config("aura_omni")
|
|
|
|
stages = StageConfigFactory._create_from_registry(
|
|
"qwen3_tts",
|
|
pipeline_cfg,
|
|
cli_overrides={},
|
|
deploy_config_path=str(Path(__file__).parent.parent / "vllm_omni" / "deploy" / "aura_omni.yaml"),
|
|
)
|
|
|
|
assert [stage.model_stage for stage in stages] == [
|
|
"asr",
|
|
"aura",
|
|
"qwen3_tts",
|
|
"code2wav",
|
|
]
|
|
assert [stage.final_output for stage in stages] == [False, True, False, True]
|
|
assert [stage.final_output_type for stage in stages] == [None, "text", None, "audio"]
|
|
assert stages[0].yaml_engine_args["model_arch"] == "Qwen3ASRForConditionalGeneration"
|
|
assert stages[1].yaml_engine_args["model_arch"] == "AuraQwen3VLForConditionalGeneration"
|
|
assert stages[2].yaml_engine_args["model_arch"] == "Qwen3TTSTalkerForConditionalGeneration"
|
|
assert stages[3].yaml_engine_args["model_arch"] == "Qwen3TTSCode2Wav"
|
|
|
|
|
|
class TestDeployCliOverrideFlow:
|
|
"""Test deploy-YAML baselines overridden by CLI runtime overrides."""
|
|
|
|
def test_diffusion_deploy_fields_can_be_overridden_by_cli(self, tmp_path):
|
|
deploy_path = tmp_path / "diffusion_stage.yaml"
|
|
deploy_path.write_text(
|
|
"""
|
|
async_chunk: false
|
|
stages:
|
|
- stage_id: 0
|
|
devices: "4,5"
|
|
parallel_config:
|
|
pipeline_parallel_size: 1
|
|
data_parallel_size: 1
|
|
tensor_parallel_size: 1
|
|
enable_expert_parallel: false
|
|
sequence_parallel_size: 1
|
|
ulysses_degree: 1
|
|
ring_degree: 1
|
|
cfg_parallel_size: 1
|
|
vae_patch_parallel_size: 1
|
|
use_hsdp: false
|
|
hsdp_shard_size: -1
|
|
hsdp_replicate_size: 1
|
|
engine_args:
|
|
cache_backend: cache_dit
|
|
diffusion_attention_backend: FLASH_ATTN
|
|
diffusion_kv_cache_dtype: auto
|
|
step_execution: false
|
|
vae_use_tiling: false
|
|
enable_cpu_offload: false
|
|
max_generated_image_size: 1048576
|
|
tts_max_instructions_length: 1000
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
pipeline = PipelineConfig(
|
|
model_type="test_diffusion",
|
|
stages=(
|
|
StagePipelineConfig(
|
|
stage_id=0,
|
|
model_stage="dit",
|
|
execution_type=StageExecutionType.DIFFUSION,
|
|
final_output=True,
|
|
final_output_type="image",
|
|
),
|
|
),
|
|
)
|
|
deploy = load_deploy_config(deploy_path)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
stage = stages[0]
|
|
|
|
assert stage.yaml_engine_args["parallel_config"]["pipeline_parallel_size"] == 1
|
|
assert stage.yaml_engine_args["parallel_config"]["data_parallel_size"] == 1
|
|
assert stage.yaml_engine_args["parallel_config"]["tensor_parallel_size"] == 1
|
|
assert stage.yaml_engine_args["parallel_config"]["enable_expert_parallel"] is False
|
|
assert stage.yaml_engine_args["parallel_config"]["sequence_parallel_size"] == 1
|
|
assert stage.yaml_engine_args["parallel_config"]["ulysses_degree"] == 1
|
|
assert stage.yaml_engine_args["parallel_config"]["ring_degree"] == 1
|
|
assert stage.yaml_engine_args["parallel_config"]["cfg_parallel_size"] == 1
|
|
assert stage.yaml_engine_args["parallel_config"]["vae_patch_parallel_size"] == 1
|
|
assert stage.yaml_engine_args["cache_backend"] == "cache_dit"
|
|
assert stage.yaml_engine_args["parallel_config"]["use_hsdp"] is False
|
|
assert stage.yaml_engine_args["parallel_config"]["hsdp_shard_size"] == -1
|
|
assert stage.yaml_engine_args["parallel_config"]["hsdp_replicate_size"] == 1
|
|
assert stage.yaml_engine_args["diffusion_attention_backend"] == "FLASH_ATTN"
|
|
assert stage.yaml_engine_args["diffusion_kv_cache_dtype"] == "auto"
|
|
assert stage.yaml_engine_args["step_execution"] is False
|
|
assert stage.yaml_engine_args["vae_use_tiling"] is False
|
|
assert stage.yaml_engine_args["enable_cpu_offload"] is False
|
|
assert stage.yaml_engine_args["max_generated_image_size"] == 1048576
|
|
assert stage.yaml_engine_args["tts_max_instructions_length"] == 1000
|
|
|
|
stage.runtime_overrides = StageConfigFactory._merge_cli_overrides(
|
|
stage,
|
|
{
|
|
"pipeline_parallel_size": 2,
|
|
"data_parallel_size": 3,
|
|
"tensor_parallel_size": 4,
|
|
"enable_expert_parallel": True,
|
|
"sequence_parallel_size": 24,
|
|
"ulysses_degree": 2,
|
|
"ring_degree": 4,
|
|
"cfg_parallel_size": 2,
|
|
"vae_patch_parallel_size": 2,
|
|
"cache_backend": "tea_cache",
|
|
"use_hsdp": True,
|
|
"hsdp_shard_size": 8,
|
|
"hsdp_replicate_size": 2,
|
|
"diffusion_attention_backend": "SAGE_ATTN",
|
|
"diffusion_kv_cache_dtype": "fp8",
|
|
"step_execution": True,
|
|
"vae_use_tiling": True,
|
|
"enable_cpu_offload": True,
|
|
"max_generated_image_size": 2097152,
|
|
"tts_max_instructions_length": 2000,
|
|
},
|
|
)
|
|
|
|
omega_config = stage.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.cache_backend == "tea_cache"
|
|
assert omega_config.engine_args.diffusion_attention_backend == "SAGE_ATTN"
|
|
assert omega_config.engine_args.diffusion_kv_cache_dtype == "fp8"
|
|
assert omega_config.engine_args.step_execution is True
|
|
assert omega_config.engine_args.vae_use_tiling is True
|
|
assert omega_config.engine_args.enable_cpu_offload is True
|
|
assert omega_config.engine_args.max_generated_image_size == 2097152
|
|
assert omega_config.engine_args.tts_max_instructions_length == 2000
|
|
assert omega_config.engine_args.parallel_config.pipeline_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.data_parallel_size == 3
|
|
assert omega_config.engine_args.parallel_config.tensor_parallel_size == 4
|
|
assert omega_config.engine_args.parallel_config.enable_expert_parallel is True
|
|
assert omega_config.engine_args.parallel_config.sequence_parallel_size == 24
|
|
assert omega_config.engine_args.parallel_config.ulysses_degree == 2
|
|
assert omega_config.engine_args.parallel_config.ring_degree == 4
|
|
assert omega_config.engine_args.parallel_config.cfg_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.vae_patch_parallel_size == 2
|
|
assert omega_config.engine_args.parallel_config.use_hsdp is True
|
|
assert omega_config.engine_args.parallel_config.hsdp_shard_size == 8
|
|
assert omega_config.engine_args.parallel_config.hsdp_replicate_size == 2
|
|
|
|
def test_llm_deploy_fields_can_be_overridden_by_cli(self, tmp_path):
|
|
deploy_path = tmp_path / "llm_stage.yaml"
|
|
deploy_path.write_text(
|
|
"""
|
|
async_chunk: false
|
|
stages:
|
|
- stage_id: 0
|
|
devices: "0,1"
|
|
engine_args:
|
|
tensor_parallel_size: 1
|
|
enable_expert_parallel: false
|
|
gpu_memory_utilization: 0.5
|
|
max_num_seqs: 16
|
|
max_num_batched_tokens: 1024
|
|
max_model_len: 4096
|
|
enforce_eager: false
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
pipeline = PipelineConfig(
|
|
model_type="test_llm",
|
|
stages=(
|
|
StagePipelineConfig(
|
|
stage_id=0,
|
|
model_stage="thinker",
|
|
execution_type=StageExecutionType.LLM_AR,
|
|
final_output=True,
|
|
final_output_type="text",
|
|
),
|
|
),
|
|
)
|
|
deploy = load_deploy_config(deploy_path)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
stage = stages[0]
|
|
|
|
assert stage.yaml_engine_args["tensor_parallel_size"] == 1
|
|
assert stage.yaml_engine_args["enable_expert_parallel"] is False
|
|
assert stage.yaml_engine_args["gpu_memory_utilization"] == 0.5
|
|
assert stage.yaml_engine_args["max_num_seqs"] == 16
|
|
assert stage.yaml_engine_args["max_num_batched_tokens"] == 1024
|
|
assert stage.yaml_engine_args["max_model_len"] == 4096
|
|
assert stage.yaml_engine_args["enforce_eager"] is False
|
|
|
|
stage.runtime_overrides = StageConfigFactory._merge_cli_overrides(
|
|
stage,
|
|
{
|
|
"tensor_parallel_size": 2,
|
|
"enable_expert_parallel": True,
|
|
"gpu_memory_utilization": 0.9,
|
|
"max_num_seqs": 32,
|
|
"max_num_batched_tokens": 2048,
|
|
"max_model_len": 8192,
|
|
"enforce_eager": True,
|
|
},
|
|
)
|
|
|
|
omega_config = stage.to_omegaconf()
|
|
|
|
assert omega_config.engine_args.tensor_parallel_size == 2
|
|
assert omega_config.engine_args.enable_expert_parallel is True
|
|
assert omega_config.engine_args.gpu_memory_utilization == 0.9
|
|
assert omega_config.engine_args.max_num_seqs == 32
|
|
assert omega_config.engine_args.max_num_batched_tokens == 2048
|
|
assert omega_config.engine_args.max_model_len == 8192
|
|
assert omega_config.engine_args.enforce_eager is True
|
|
|
|
|
|
class TestSentinelDefaultPrecedence:
|
|
"""Caller-typed (non-None) values win over YAML; None values fall through
|
|
to YAML / dataclass defaults (#3035)."""
|
|
|
|
def _stages(self, cli_overrides):
|
|
model_type = "qwen3_omni_moe"
|
|
pipeline_cfg = StageConfigFactory.resolve_pipeline_config(
|
|
model_type,
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
return StageConfigFactory._create_from_registry(
|
|
"qwen3_omni_moe",
|
|
pipeline_cfg,
|
|
cli_overrides=cli_overrides,
|
|
)
|
|
|
|
def test_typed_kwarg_overrides_yaml(self):
|
|
stages = self._stages({"max_num_seqs": 999})
|
|
assert stages[2].runtime_overrides.get("max_num_seqs") == 999
|
|
|
|
def test_none_value_skipped_yaml_wins(self):
|
|
stages = self._stages({"max_num_seqs": None})
|
|
assert stages[2].runtime_overrides.get("max_num_seqs") is None
|
|
assert stages[2].yaml_engine_args.get("max_num_seqs") == 64
|
|
|
|
def test_empty_kwargs_yaml_only(self):
|
|
stages = self._stages({})
|
|
for stage in stages:
|
|
assert stage.runtime_overrides == {}
|
|
|
|
def test_typed_kwarg_equal_to_dataclass_default_still_overrides(self):
|
|
# Caller intent honored regardless of value coincidence (no heuristic).
|
|
stages = self._stages({"gpu_memory_utilization": 0.9})
|
|
assert stages[2].runtime_overrides.get("gpu_memory_utilization") == 0.9
|
|
|
|
def test_per_stage_kwarg_routed_to_correct_stage(self):
|
|
stages = self._stages({"stage_0_gpu_memory_utilization": 0.42})
|
|
assert stages[0].runtime_overrides.get("gpu_memory_utilization") == 0.42
|
|
assert stages[2].runtime_overrides.get("gpu_memory_utilization") is None
|
|
|
|
def test_async_chunk_false_overrides_yaml_true(self):
|
|
stages = self._stages({"async_chunk": False})
|
|
for stage in stages:
|
|
assert stage.yaml_engine_args.get("async_chunk") is not True
|
|
|
|
def test_async_chunk_none_keeps_yaml_true(self):
|
|
stages = self._stages({"async_chunk": None})
|
|
for stage in stages:
|
|
assert stage.yaml_engine_args.get("async_chunk") is True
|
|
|
|
def test_enable_prefix_caching_typed_overrides_yaml(self):
|
|
stages = self._stages({"enable_prefix_caching": True})
|
|
for stage in stages:
|
|
assert stage.runtime_overrides.get("enable_prefix_caching") is True
|
|
|
|
def test_omni_with_vars_args_anti_pattern_is_safe(self):
|
|
# Omni(**vars(args)) with mostly-None namespace must not clobber YAML.
|
|
simulated_vars_args = {
|
|
"gpu_memory_utilization": None,
|
|
"max_num_seqs": None,
|
|
"async_chunk": None,
|
|
"enable_prefix_caching": None,
|
|
"dtype": None,
|
|
}
|
|
stages = self._stages(simulated_vars_args)
|
|
for stage in stages:
|
|
assert stage.runtime_overrides == {}
|
|
|
|
def test_create_from_registry_no_cli_explicit_keys_param(self):
|
|
sig = inspect.signature(StageConfigFactory._create_from_registry)
|
|
named = [p for p in sig.parameters.values() if p.kind != p.VAR_KEYWORD]
|
|
assert "cli_explicit_keys" not in {p.name for p in named}
|
|
|
|
def test_async_chunk_dispatches_processors(self):
|
|
"""A single ``qwen3_tts`` pipeline picks per-chunk vs end-to-end
|
|
processors based on ``deploy.async_chunk``, without needing a
|
|
separate variant pipeline registration."""
|
|
pipeline = StageConfigFactory.resolve_pipeline_config("qwen3_tts")
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
|
|
# async_chunk=True → stage 0's per-chunk processor wires up, stage 1
|
|
# has no sync input processor.
|
|
async_stages = merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=True))
|
|
assert (
|
|
async_stages[0]
|
|
.yaml_engine_args.get("custom_process_next_stage_input_func", "")
|
|
.endswith("talker2code2wav_async_chunk")
|
|
)
|
|
assert async_stages[1].custom_process_input_func is None
|
|
|
|
# async_chunk=False → stage 0 ships the bulk codec via the
|
|
# worker-connector full-payload producer; stage 1 wires the
|
|
# ``_token_only`` placeholder so the orchestrator emits no
|
|
# legacy ``additional_information``-shaped input (PR3 sync-
|
|
# via-connector data plane).
|
|
sync_stages = merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=False))
|
|
assert (
|
|
sync_stages[0]
|
|
.yaml_engine_args["custom_process_next_stage_input_func"]
|
|
.endswith("talker2code2wav_full_payload")
|
|
)
|
|
assert sync_stages[1].custom_process_input_func is not None
|
|
assert sync_stages[1].custom_process_input_func.endswith("talker2code2wav_token_only")
|
|
|
|
def test_async_chunk_dispatches_qwen3_omni_processors(self):
|
|
import runpy
|
|
from pathlib import Path
|
|
|
|
from vllm_omni.config.stage_config import DeployConfig, merge_pipeline_deploy
|
|
|
|
pipeline_path = (
|
|
Path(__file__).parent.parent / "vllm_omni" / "model_executor" / "models" / "qwen3_omni" / "pipeline.py"
|
|
)
|
|
pipeline = runpy.run_path(str(pipeline_path))["QWEN3_OMNI_PIPELINE"]
|
|
|
|
async_stages = merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=True))
|
|
assert (
|
|
async_stages[0]
|
|
.yaml_engine_args["custom_process_next_stage_input_func"]
|
|
.endswith("thinker2talker_async_chunk")
|
|
)
|
|
assert (
|
|
async_stages[1]
|
|
.yaml_engine_args["custom_process_next_stage_input_func"]
|
|
.endswith("talker2code2wav_async_chunk")
|
|
)
|
|
|
|
sync_stages = merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=False))
|
|
assert (
|
|
sync_stages[0]
|
|
.yaml_engine_args["custom_process_next_stage_input_func"]
|
|
.endswith("thinker2talker_full_payload")
|
|
)
|
|
assert (
|
|
sync_stages[1]
|
|
.yaml_engine_args["custom_process_next_stage_input_func"]
|
|
.endswith("talker2code2wav_full_payload")
|
|
)
|
|
|
|
def test_ming_flash_omni_topology(self):
|
|
"""Guard ming_flash_omni's SIP cleanup: stage 0 has no full-payload
|
|
producer hook (arch is not in ``_FULL_PAYLOAD_INPUT_STAGES``), and
|
|
stage 1 uses only ``thinker2talker_token_only`` (sync_process_input_func).
|
|
The dead ``thinker2talker`` (custom_process_input_func) was removed
|
|
because ming_flash_omni has no async_chunk support and both functions
|
|
called ``_build_talker_inputs`` identically.
|
|
Merge under either async_chunk mode must not re-introduce a
|
|
stage-0 full-payload hook."""
|
|
pipeline = StageConfigFactory.resolve_pipeline_config("ming_flash_omni")
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
|
|
stage0, stage1 = pipeline.stages
|
|
assert stage0.custom_process_next_stage_input_func is None, (
|
|
"ming_flash_omni stage 0 must not declare a full-payload producer "
|
|
"(connector path is not active for this arch)."
|
|
)
|
|
assert stage1.custom_process_input_func is None
|
|
assert stage1.sync_process_input_func is not None
|
|
assert stage1.sync_process_input_func.endswith("thinker2talker_token_only")
|
|
|
|
# async_chunk=True must now be rejected: removing the fake hook means
|
|
# there is no next-stage input processor for the validator to accept.
|
|
# (Positive consequence -- users can't accidentally enable async_chunk
|
|
# on an arch that doesn't actually support it.)
|
|
import pytest as _pytest
|
|
|
|
with _pytest.raises(ValueError, match="async_chunk=True"):
|
|
merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=True))
|
|
|
|
# async_chunk=False merges cleanly and stage-0 yaml_engine_args carries
|
|
# no spurious full-payload hook.
|
|
merged = merge_pipeline_deploy(pipeline, DeployConfig(async_chunk=False))
|
|
assert "custom_process_next_stage_input_func" not in merged[0].yaml_engine_args, (
|
|
"stage-0 full-payload hook unexpectedly re-appeared in yaml_engine_args"
|
|
)
|
|
|
|
|
|
class TestSamplingConstraintsPrecedence:
|
|
"""Test that pipeline sampling_constraints override deploy defaults."""
|
|
|
|
def test_constraints_win(self):
|
|
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
|
|
if not deploy_path.exists():
|
|
pytest.skip("Deploy config not found")
|
|
|
|
deploy = load_deploy_config(deploy_path)
|
|
pipeline = StageConfigFactory.resolve_pipeline_config(
|
|
"qwen3_omni_moe",
|
|
Q3_OMNI_ALL_STAGES_HF_CONFIG,
|
|
)
|
|
assert isinstance(pipeline, PipelineConfig)
|
|
stages = merge_pipeline_deploy(pipeline, deploy)
|
|
|
|
# Pipeline says detokenize=True for thinker, deploy can't override
|
|
assert stages[0].yaml_extras["default_sampling_params"]["detokenize"] is True
|
|
# Pipeline says stop_token_ids=[2150] for talker
|
|
assert stages[1].yaml_extras["default_sampling_params"]["stop_token_ids"] == [2150]
|
|
|
|
|
|
class TestPipelineConfigResolvers:
|
|
@pytest.mark.parametrize("resolver", [obj for obj in OMNI_PIPELINES.values() if callable(obj)])
|
|
def test_all_resolvers_reject_bad_types(self, resolver):
|
|
"""Ensure that all resolvers registered reject incorrect config types."""
|
|
|
|
class NotTheRightHfConfig(PretrainedConfig):
|
|
pass
|
|
|
|
assert resolver(NotTheRightHfConfig()) is None
|