Compare commits

...
23 changed files with 1957 additions and 264 deletions

No files matched your search

@@ -0,0 +1,114 @@
from __future__ import annotations
from types import SimpleNamespace
import pytest
from vllm.v1.request import RequestStatus
import vllm_omni.core.sched.omni_ar_scheduler as scheduler_mod
from vllm_omni.model_executor.models.voxcpm2.scheduler import VoxCPM2OmniARAsyncScheduler
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _MockQueue:
def __init__(self, items: list | None = None) -> None:
self._items = list(items or [])
def __bool__(self) -> bool:
return bool(self._items)
def __iter__(self):
return iter(self._items)
def __len__(self) -> int:
return len(self._items)
def add_request(self, request) -> None:
self._items.append(request)
def prepend_requests(self, requests) -> None:
self._items = list(requests) + self._items
class _MockRequest:
def __init__(
self,
request_id: str,
*,
status: RequestStatus = RequestStatus.RUNNING,
num_prompt_tokens: int = 4,
num_computed_tokens: int = 4,
num_output_placeholders: int = 0,
) -> None:
self.request_id = request_id
self.status = status
self.num_prompt_tokens = num_prompt_tokens
self.num_computed_tokens = num_computed_tokens
self.num_output_placeholders = num_output_placeholders
def is_finished(self) -> bool:
return RequestStatus.is_finished(self.status)
def _make_scheduler(*, enable_unified_decode_graph: bool = True) -> VoxCPM2OmniARAsyncScheduler:
sched = VoxCPM2OmniARAsyncScheduler.__new__(VoxCPM2OmniARAsyncScheduler)
runtime_config = SimpleNamespace(enable_unified_decode_graph=enable_unified_decode_graph)
sched.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(
hf_config=SimpleNamespace(voxcpm2_runtime_config=runtime_config),
)
)
return sched
def test_voxcpm2_unified_decode_graph_defers_waiting_when_decode_ready() -> None:
scheduler = _make_scheduler()
scheduler.running = [_MockRequest("decode")]
scheduler.waiting = _MockQueue([_MockRequest("prefill", status=RequestStatus.WAITING)])
assert scheduler._should_defer_waiting_for_unified_decode_graph()
def test_voxcpm2_unified_decode_graph_does_not_defer_without_decode_ready() -> None:
scheduler = _make_scheduler()
scheduler.running = [_MockRequest("prefill-running", num_prompt_tokens=8, num_computed_tokens=4)]
scheduler.waiting = _MockQueue([_MockRequest("waiting", status=RequestStatus.WAITING)])
assert not scheduler._should_defer_waiting_for_unified_decode_graph()
def test_voxcpm2_unified_decode_graph_does_not_defer_when_disabled() -> None:
scheduler = _make_scheduler(enable_unified_decode_graph=False)
scheduler.running = [_MockRequest("decode")]
scheduler.waiting = _MockQueue([_MockRequest("prefill", status=RequestStatus.WAITING)])
assert not scheduler._should_defer_waiting_for_unified_decode_graph()
def test_unified_decode_graph_deferral_restores_waiting_queue(monkeypatch) -> None:
scheduler = _make_scheduler()
scheduler.running = [_MockRequest("decode")]
original_waiting_req = _MockRequest("waiting", status=RequestStatus.WAITING)
deferred_by_upstream = _MockRequest("deferred-by-upstream", status=RequestStatus.WAITING)
original_waiting = _MockQueue([original_waiting_req])
scheduler.waiting = original_waiting
scheduler.policy = "fcfs"
scheduler.chunk_transfer_adapter = None
scheduler.input_coordinator = None
scheduler._consume_pending_connector_output = lambda model_mode: None
monkeypatch.setattr(scheduler_mod, "create_request_queue", lambda _policy: _MockQueue())
def fake_upstream_schedule(self, throttle_prefills: bool = False):
assert self.waiting is not original_waiting
assert not self.waiting
self.waiting.add_request(deferred_by_upstream)
raise RuntimeError("stop before output wrapping")
monkeypatch.setattr(scheduler_mod.VLLMScheduler, "schedule", fake_upstream_schedule)
with pytest.raises(RuntimeError, match="stop before output wrapping"):
scheduler.schedule()
assert scheduler.waiting._items == [deferred_by_upstream, original_waiting_req]
@@ -7,7 +7,7 @@ and are supported by Qwen-Image-Layered model.
Kept cases (maximum feature coverage, no cpu-offload): Kept cases (maximum feature coverage, no cpu-offload):
sp_001 : cache_dit + Ulysses-SP 2 (2×H100) sp_001 : cache_dit + Ulysses-SP 2 (2×H100)
cfg_parallel_002 : cache_dit + CFG-Parallel 2 (2×H100) cfg_parallel_002 : cache_dit + CFG-Parallel 2 (2×H100)
layers_guard_001 : layerwise offload + layers=3 (1×H100, issue #1969 guard) layers_guard_001 : layerwise offload + layers=2 (1×H100, issue #1969 guard)
Total distinct features covered: cache_dit, Ulysses-SP, CFG-Parallel, layerwise-offload. Total distinct features covered: cache_dit, Ulysses-SP, CFG-Parallel, layerwise-offload.
""" """
@@ -116,8 +116,8 @@ LAYERS_GUARD_CASES = [
model=MODEL, model=MODEL,
server_args=["--enable-layerwise-offload"], server_args=["--enable-layerwise-offload"],
), ),
3, 2,
id="layers_guard_001_layers3", id="layers_guard_001_layers2",
marks=SINGLE_CARD_FEATURE_MARKS, marks=SINGLE_CARD_FEATURE_MARKS,
), ),
] ]
@@ -1244,7 +1244,7 @@ def test_parameter_validation():
# Invalid layers for layered models (must stay within the backend-supported range) # Invalid layers for layered models (must stay within the backend-supported range)
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageGenerationRequest(prompt="test", layers=2) ImageGenerationRequest(prompt="test", layers=1)
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageGenerationRequest(prompt="test", layers=11) ImageGenerationRequest(prompt="test", layers=11)
@@ -1663,13 +1663,13 @@ def test_image_edit_invalid_layers(async_omni_test_client):
files=[("image", img_bytes)], files=[("image", img_bytes)],
data={ data={
"prompt": "test", "prompt": "test",
"layers": 2, "layers": 1,
}, },
) )
assert response.status_code == 400 assert response.status_code == 400
detail = response.json()["detail"] detail = response.json()["detail"]
assert "Invalid layers" in detail assert "Invalid layers" in detail
assert "layers must be between 3 and 10 inclusive" in detail assert "layers must be between 2 and 10 inclusive" in detail
# Test layers above the supported range # Test layers above the supported range
response = async_omni_test_client.post( response = async_omni_test_client.post(
@@ -1683,7 +1683,7 @@ def test_image_edit_invalid_layers(async_omni_test_client):
assert response.status_code == 400 assert response.status_code == 400
detail = response.json()["detail"] detail = response.json()["detail"]
assert "Invalid layers" in detail assert "Invalid layers" in detail
assert "layers must be between 3 and 10 inclusive" in detail assert "layers must be between 2 and 10 inclusive" in detail
def test_image_edit_resolution_and_size_conflict(async_omni_test_client): def test_image_edit_resolution_and_size_conflict(async_omni_test_client):
@@ -39,6 +39,7 @@ from vllm_omni.entrypoints.openai.serving_speech import (
OmniOpenAIServingSpeech, OmniOpenAIServingSpeech,
_create_wav_header, _create_wav_header,
) )
from vllm_omni.entrypoints.openai.tts_adapters.base import PreparedRequest
from vllm_omni.model_executor.models.fish_speech.prompt_utils import ( from vllm_omni.model_executor.models.fish_speech.prompt_utils import (
FISH_TEXT_ONLY_SYSTEM_PROMPT, FISH_TEXT_ONLY_SYSTEM_PROMPT,
build_fish_voice_clone_prompt_ids, build_fish_voice_clone_prompt_ids,
@@ -3343,6 +3344,40 @@ class TestTTSAsyncOffloading:
qwen3_tts_server._build_tts_params.assert_called_once() qwen3_tts_server._build_tts_params.assert_called_once()
qwen3_tts_server._estimate_prompt_len_async.assert_awaited_once() qwen3_tts_server._estimate_prompt_len_async.assert_awaited_once()
def test_prepare_speech_generation_uses_adapter_model_type_label(
self,
voxtral_server,
mocker: MockerFixture,
):
"""Adapter model_type should replace the legacy _tts_model_type label ladder."""
legacy_tts_model_type = "dummy_tts"
adapter_model_type = "adapter_dummy_tts"
class FakeAdapter:
def validate(self, request):
return None
async def build(self, request, sampling_params_list, has_inline_ref_audio):
return PreparedRequest(
prompt={"prompt": request.input},
tts_params={},
model_type=adapter_model_type,
)
voxtral_server._tts_model_type = legacy_tts_model_type
mocker.patch.object(voxtral_server, "_get_tts_adapter", return_value=FakeAdapter())
log_info = mocker.patch("vllm_omni.entrypoints.openai.serving_speech.logger.info")
asyncio.run(voxtral_server._prepare_speech_generation(OpenAICreateSpeechRequest(input="hello")))
assert adapter_model_type != legacy_tts_model_type
assert any(
call.args
and call.args[0] == "TTS speech request %s: text=%r, model=%s"
and call.args[3] == adapter_model_type
for call in log_info.call_args_list
)
def test_prepare_speech_generation_treats_sse_as_streaming(self, qwen3_tts_server, mocker: MockerFixture): def test_prepare_speech_generation_treats_sse_as_streaming(self, qwen3_tts_server, mocker: MockerFixture):
"""stream_format=sse should request delta-style multimodal outputs.""" """stream_format=sse should request delta-style multimodal outputs."""
qwen3_tts_server._validate_tts_request = mocker.MagicMock(return_value=None) qwen3_tts_server._validate_tts_request = mocker.MagicMock(return_value=None)
+13
View File
@@ -633,6 +633,18 @@ def get_deploy_config_path(rel_path: str) -> str:
return str(_DEPLOY_DIR / rel_path) return str(_DEPLOY_DIR / rel_path)
def get_deploy_config_stage(rel_path: str, stage_id: int) -> dict[str, Any]:
"""Return one stage entry from a deploy yaml by ``stage_id``."""
with open(get_deploy_config_path(rel_path), encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
stage_key = "stages" if "stages" in cfg else "stage_args"
for stage in cfg.get(stage_key, []):
if stage.get("stage_id") == stage_id:
return stage
raise KeyError(f"No stage_id={stage_id} in deploy config {rel_path!r}")
def _get_config_value_by_path(config_dict: dict, path: str) -> Any: def _get_config_value_by_path(config_dict: dict, path: str) -> Any:
"""Read a dot-separated path from a nested dict (e.g. ``engine_args.load_format``).""" """Read a dot-separated path from a nested dict (e.g. ``engine_args.load_format``)."""
current: Any = config_dict current: Any = config_dict
@@ -707,6 +719,7 @@ def stage_config_path_for_run_level(stage_config_path: str | None, run_level: st
__all__ = [ __all__ = [
"get_deploy_config_path", "get_deploy_config_path",
"get_deploy_config_stage",
"modify_stage_config", "modify_stage_config",
"stage_config_path_for_run_level", "stage_config_path_for_run_level",
] ]
@@ -0,0 +1,204 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Static regression checks for VoxCPM2 runner-assisted unified graph hooks.
These tests intentionally avoid importing torch/vLLM so they can run in a
lightweight local checkout. Runtime audio quality still requires a CUDA test.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[4]
RUNNER = REPO_ROOT / "vllm_omni" / "worker" / "gpu_ar_model_runner.py"
RUNNER_ASSISTED_METADATA = REPO_ROOT / "vllm_omni" / "worker" / "runner_assisted_metadata.py"
TALKER = REPO_ROOT / "vllm_omni" / "model_executor" / "models" / "voxcpm2" / "voxcpm2_talker.py"
VOXCPM2_PIPELINE = REPO_ROOT / "vllm_omni" / "model_executor" / "models" / "voxcpm2" / "pipeline.py"
VOXCPM2_SCHEDULER = REPO_ROOT / "vllm_omni" / "model_executor" / "models" / "voxcpm2" / "scheduler.py"
OMNI_AR_SCHEDULER = REPO_ROOT / "vllm_omni" / "core" / "sched" / "omni_ar_scheduler.py"
DEPLOY = REPO_ROOT / "vllm_omni" / "deploy" / "voxcpm2.yaml"
def test_ar_runner_exposes_runner_assisted_full_metadata_hook():
source = RUNNER.read_text()
assert "Models without this hook keep the normal runner path" in source
assert "get_runner_assisted_full_attention_metadata_request" in source
assert "set_runner_assisted_full_attention_metadata_context" in source
assert "runner_assisted_full_attn" in source
assert "runner_assisted_full_attn_capture" in source
assert "pad_attn = cudagraph_mode == CUDAGraphMode.FULL or runner_assisted_full_attn" in source
assert "for_cudagraph_capture=runner_assisted_full_attn_capture" in source
assert "cudagraph_runtime_mode=(" in source
assert "CUDAGraphMode.FULL if runner_assisted_full_attn else cudagraph_mode" in source
refresh_source = source[source.index("def _refresh_runner_assisted_full_attention_metadata_buffers") :]
refresh_source = refresh_source[: refresh_source.index("def _set_runner_assisted_full_attention_metadata_context")]
assert "num_computed_tokens_cpu" in refresh_source
assert "num_scheduled_tokens_np" in refresh_source
assert "optimistic_seq_lens_cpu[:num_reqs]=1" not in "".join(refresh_source.split())
padding_source = source[
source.index("runner_assisted_full_attn_request = self._get_runner_assisted_full_attention_metadata_request") :
]
padding_source = padding_source[: padding_source.index("ubatch_slices, ubatch_slices_padded")]
assert "runner_assisted_full_attn_request.num_reqs_padded" in padding_source
assert "runner_assisted_full_attn_request.for_cudagraph_capture" in padding_source
assert "num_reqs_padded * max_num_scheduled_tokens" in padding_source
def test_ar_runner_without_model_hook_stays_on_normal_path():
source = RUNNER.read_text()
contract_source = RUNNER_ASSISTED_METADATA.read_text()
request_source = source[source.index("def _get_runner_assisted_full_attention_metadata_request") :]
request_source = request_source[
: request_source.index("def _refresh_runner_assisted_full_attention_metadata_buffers")
]
compact_request_source = "".join(request_source.split())
assert "Models without this hook keep the normal runner path" in request_source
assert "class RunnerAssistedAttentionMetadataProvider(Protocol)" in contract_source
assert "class RunnerAssistedFullAttentionMetadataRequest(NamedTuple)" in contract_source
assert "tuple[int, bool]" not in contract_source
assert 'hook=getattr(self.model,"get_runner_assisted_full_attention_metadata_request",None)' in (
compact_request_source
)
assert "ifnotcallable(hook):returnNone" in compact_request_source
assert ".tolist()" not in compact_request_source
assert "isinstance(request,RunnerAssistedFullAttentionMetadataRequest)" in compact_request_source
assert "request.num_reqs_padded" in request_source
assert "request.for_cudagraph_capture" in request_source
assert "exceptException" not in compact_request_source
set_context_source = source[source.index("def _set_runner_assisted_full_attention_metadata_context") :]
set_context_source = set_context_source[: set_context_source.index("def _deferred_prefix_cache_mm_keys")]
assert "except Exception" not in set_context_source
def test_voxcpm2_graph_paths_fail_closed_and_preserve_deterministic_noise():
source = TALKER.read_text()
compact_source = "".join(source.split())
assert "_voxcpm2_compile_without_inductor_cudagraphs" in source
assert "def _compile_without_inductor_cudagraphs" not in source
assert "_voxcpm2_unwrap_torch_compile" in source
assert "_orig_mod" in source
compile_source = source[source.index("def _voxcpm2_compile_without_inductor_cudagraphs") :]
compile_source = compile_source[: compile_source.index("def _setup_torch_compile")]
assert "torch._inductor.config" not in compile_source
assert '"triton.cudagraphs": False' in compile_source
assert '"triton.cudagraph_trees": False' in compile_source
assert "self._enable_unified_decode_graph=(use_cuda_graph" in compact_source
assert "andnotself._deterministic_cfm_noise" in compact_source
assert "decode_tail_graph" not in source
unified_source = source[source.index("def _forward_unified_decode") :]
unified_source = unified_source[: unified_source.index("# -------------------- vllm hooks")]
assert "except Exception:" not in unified_source
assert "_failed_unified_graph_sizes" not in source
assert "self._enable_unified_decode_graph = False" not in unified_source
assert "_forward_unified_decode_fallback" not in source
assert "capture_failed" not in compact_source
assert "record_fallback" not in source
assert "fallbacks" not in source
assert "**info_dict: Unpack[VoxCPM2PreprocessInput]" in source
assert "**info: Unpack[VoxCPM2PostprocessInput]" in source
assert "reference_audio: Any" not in source
assert "prompt_audio: Any" not in source
assert "def _nullify_volatile_metadata(ctx: _ForwardContextLike) -> _ForwardContextLike" in source
assert "class _PrefillInputs(NamedTuple)" in source
assert "-> _PrefillInputs" in source
assert 'inputs["audio_feat"]' not in source
assert 'inputs["text_token"]' not in source
capture_source = source[source.index("def _capture_unified_decode_graph") :]
capture_source = capture_source[: capture_source.index("def _unified_decode_graph_skip_reason")]
assert "_voxcpm2_compile_unified_capture_estimator" in capture_source
assert "_voxcpm2_compile_unified_capture_feat_encoder" in capture_source
graph_body = capture_source[capture_source.index("with torch.cuda.graph") :]
assert "g.cfm_noise.normal_()" not in graph_body
assert "g.cfm_noise.normal_()" in unified_source
assert "_pre_capture_unified_graphs" not in source
assert "_unified_graph_pre_capture_sizes" not in source
assert "unified_decode_graph_pre_capture_sizes" not in source
def test_voxcpm2_batch_unified_graph_requires_runner_metadata_marker():
source = TALKER.read_text()
compact_source = "".join(source.split())
assert "enable_runner_assisted_unified_decode_graph" not in source
assert "allow_unified_decode_graph_batch_attention" not in source
assert "get_runner_assisted_full_attention_metadata_request" in source
assert "set_runner_assisted_full_attention_metadata_context" in source
assert "RunnerAssistedFullAttentionMetadataRequest" in source
assert "_runner_assisted_unified_decode_graph_active" in source
assert "runner_full_metadata_missing" in source
assert "_build_unified_graph_bucket_sizes" in source
assert "_select_unified_graph_bucket_size" in source
capture_source = source[source.index("def _capture_unified_decode_graph") :]
capture_source = capture_source[: capture_source.index("def _unified_decode_graph_skip_reason")]
assert "override_forward_context" in capture_source
assert "_nullify_volatile_metadata" in capture_source
assert "capture_context = override_forward_context(self._nullify_volatile_metadata(ctx))" in capture_source
assert "ifsize>1:continue" not in compact_source
forward_source = source[source.index("def _forward_unified_decode") :]
forward_source = forward_source[: forward_source.index("# -------------------- vllm hooks")]
assert "graph_size = self._select_unified_graph_bucket_size(num_reqs)" in forward_source
assert "self._unified_graphs[graph_size]" in forward_source
assert "g.input_embeds[num_reqs:graph_size].zero_()" not in forward_source
assert "padded rows as valid duplicates" in forward_source
assert "g.input_embeds[last : last + 1].expand" in forward_source
assert "ifnum_reqs>1andnotself._runner_assisted_unified_decode_graph_active" in compact_source
assert "andnotcfg.enable_runner_assisted_unified_decode_graph" not in compact_source
needs_source = source[source.index("def get_runner_assisted_full_attention_metadata_request") :]
needs_source = needs_source[: needs_source.index("def set_runner_assisted_full_attention_metadata_context")]
assert "_select_unified_graph_bucket_size(num_reqs)" in needs_source
assert "_should_use_decode_graph(num_reqs)" not in needs_source
assert "RunnerAssistedFullAttentionMetadataRequest(" in needs_source
def test_voxcpm2_unified_skip_preserves_segmented_decode_graphs():
source = TALKER.read_text()
compact_source = "".join(source.split())
forward_source = source[source.index("def forward(") :]
forward_source = forward_source[: forward_source.index("# -------------------- prefill / decode helpers")]
assert "use_segmented_decode_graph" in forward_source
assert "unified_skip_reasonisnotNone" in compact_source
assert "notself._enable_unified_decode_graphorunified_skip_reasonisnotNone" in compact_source
assert "ifuse_segmented_decode_graph:" in compact_source
assert "anduse_segmented_decode_graph" in compact_source
def test_voxcpm2_scheduler_policy_stays_model_local():
common_source = OMNI_AR_SCHEDULER.read_text()
voxcpm2_scheduler_source = VOXCPM2_SCHEDULER.read_text()
pipeline_source = VOXCPM2_PIPELINE.read_text()
assert "VoxCPM2TalkerForConditionalGeneration" not in common_source
assert "voxcpm2_runtime_config" not in common_source
assert "pure_decode_graph" not in common_source
assert "_schedule_with_optional_waiting_deferral" not in common_source
assert "def _should_defer_waiting_admission(self) -> bool:" in common_source
assert "class VoxCPM2OmniARAsyncScheduler(OmniARAsyncScheduler)" in voxcpm2_scheduler_source
assert "voxcpm2_runtime_config" in voxcpm2_scheduler_source
assert "_should_defer_waiting_for_unified_decode_graph" in voxcpm2_scheduler_source
assert "def schedule(" not in voxcpm2_scheduler_source
assert "create_request_queue" not in voxcpm2_scheduler_source
assert "original_waiting.prepend_requests(deferred_waiting)" not in voxcpm2_scheduler_source
assert "return self._should_defer_waiting_for_unified_decode_graph()" in voxcpm2_scheduler_source
assert "vllm_omni.model_executor.models.voxcpm2.scheduler.VoxCPM2OmniARAsyncScheduler" in pipeline_source
def test_voxcpm2_deploy_defaults_to_full_unified_graph_only():
source = DEPLOY.read_text()
assert "max_num_seqs: 8" in source
assert "enable_unified_decode_graph: true" in source
assert "unified_decode_graph_max_batch_size: 8" in source
assert "unified_decode_graph_pre_capture_sizes" not in source
assert "enable_runner_assisted_unified_decode_graph" not in source
assert "allow_unified_decode_graph_batch_attention" not in source
assert "decode_graph_capture_policy" not in source
+270 -1
View File
@@ -14,7 +14,7 @@ from unittest.mock import patch
import pytest import pytest
from transformers import PretrainedConfig, Qwen3OmniMoeConfig from transformers import PretrainedConfig, Qwen3OmniMoeConfig
from tests.helpers.stage_config import get_deploy_config_path 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.config_factory import StageConfigFactory
from vllm_omni.config.pipeline_registry import OMNI_PIPELINES, register_pipeline from vllm_omni.config.pipeline_registry import OMNI_PIPELINES, register_pipeline
from vllm_omni.config.stage_config import ( from vllm_omni.config.stage_config import (
@@ -571,6 +571,7 @@ class TestStagePipelineConfig:
assert s.final_output is False assert s.final_output is False
assert s.sampling_constraints == {} assert s.sampling_constraints == {}
assert s.engine_output_type is None assert s.engine_output_type is None
assert s.scheduler_cls is None
class TestPipelineConfigNew: class TestPipelineConfigNew:
@@ -739,6 +740,15 @@ stages:
assert deploy.stages[0].compilation_config == {"pass_config": {"fuse_allreduce_rms": False}} assert deploy.stages[0].compilation_config == {"pass_config": {"fuse_allreduce_rms": False}}
assert "compilation_config" not in deploy.stages[0].engine_extras 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
def test_merge_pipeline_deploy(self): def test_merge_pipeline_deploy(self):
deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml" deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml"
if not deploy_path.exists(): if not deploy_path.exists():
@@ -800,6 +810,26 @@ stages:
assert stages[0].yaml_runtime["requires_multimodal_data"] is True 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): def test_mixed_schema_preserves_flat_fields(self):
"""Ensure flat fields are not dropped when engine_args are present.""" """Ensure flat fields are not dropped when engine_args are present."""
fake_config = { fake_config = {
@@ -928,6 +958,54 @@ stages:
assert "foo" in stage.engine_extras assert "foo" in stage.engine_extras
assert stage.engine_extras["foo"] == {"a": 111, "b": {"d": 9, "e": 199}, "c": 3} 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): def test_deep_merge_does_not_mutate_inputs(self):
"""Merging engine_args must not mutate the base stage dict.""" """Merging engine_args must not mutate the base stage dict."""
fake_config = { fake_config = {
@@ -1588,6 +1666,197 @@ class TestAuraOmniDeploy:
assert stages[3].yaml_engine_args["model_arch"] == "Qwen3TTSCode2Wav" 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: class TestSentinelDefaultPrecedence:
"""Caller-typed (non-None) values win over YAML; None values fall through """Caller-typed (non-None) values win over YAML; None values fall through
to YAML / dataclass defaults (#3035).""" to YAML / dataclass defaults (#3035)."""
+138
View File
@@ -1,3 +1,4 @@
from collections.abc import Sequence
from types import SimpleNamespace from types import SimpleNamespace
import numpy as np import numpy as np
@@ -10,6 +11,7 @@ from vllm_omni.worker.gpu_ar_model_runner import (
GPUARModelRunner, GPUARModelRunner,
OmniAsyncGPUModelRunnerOutput, OmniAsyncGPUModelRunnerOutput,
) )
from vllm_omni.worker.runner_assisted_metadata import RunnerAssistedFullAttentionMetadataRequest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -58,6 +60,101 @@ def test_sparse_mm_req_ids_requires_sparse_audio_marker():
assert GPUARModelRunner._sparse_mm_req_ids({"meta.req_id": ["r1"], "meta.sparse_audio": ["1"]}) == ["r1"] assert GPUARModelRunner._sparse_mm_req_ids({"meta.req_id": ["r1"], "meta.sparse_audio": ["1"]}) == ["r1"]
def test_runner_assisted_full_attention_metadata_request_is_opt_in():
runner = object.__new__(GPUARModelRunner)
runner.model = object()
runner.scheduler_config = SimpleNamespace(max_num_seqs=16)
request = runner._get_runner_assisted_full_attention_metadata_request(
req_ids=["r1", "r2"],
num_reqs=2,
num_reqs_padded=4,
num_scheduled_tokens_np=np.array([1, 1], dtype=np.int32),
num_computed_tokens_cpu=np.array([5, 6], dtype=np.int32),
max_num_scheduled_tokens=1,
)
assert request is None
def test_runner_assisted_full_attention_metadata_request_and_context_hooks():
calls = []
class Model:
def get_runner_assisted_full_attention_metadata_request(
self,
*,
req_ids: Sequence[str],
num_reqs: int,
num_scheduled_tokens: Sequence[int],
num_computed_tokens: Sequence[int],
max_num_scheduled_tokens: int,
) -> RunnerAssistedFullAttentionMetadataRequest:
calls.append(
(
"request",
{
"req_ids": list(req_ids),
"num_reqs": num_reqs,
"num_scheduled_tokens": [int(n) for n in num_scheduled_tokens],
"num_computed_tokens": [int(n) for n in num_computed_tokens],
"max_num_scheduled_tokens": max_num_scheduled_tokens,
},
)
)
return RunnerAssistedFullAttentionMetadataRequest(
num_reqs_padded=12,
for_cudagraph_capture=True,
)
def set_runner_assisted_full_attention_metadata_context(
self,
*,
enabled: bool,
num_reqs: int = 0,
) -> None:
calls.append(("context", {"enabled": enabled, "num_reqs": num_reqs}))
runner = object.__new__(GPUARModelRunner)
runner.model = Model()
runner.scheduler_config = SimpleNamespace(max_num_seqs=8)
request = runner._get_runner_assisted_full_attention_metadata_request(
req_ids=["r1", "r2"],
num_reqs=2,
num_reqs_padded=4,
num_scheduled_tokens_np=np.array([1, 1], dtype=np.int32),
num_computed_tokens_cpu=np.array([5, 6], dtype=np.int32),
max_num_scheduled_tokens=1,
)
context_enabled = runner._set_runner_assisted_full_attention_metadata_context(
enabled=True,
num_reqs=2,
)
context_disabled = runner._set_runner_assisted_full_attention_metadata_context(enabled=False)
assert request == RunnerAssistedFullAttentionMetadataRequest(
num_reqs_padded=8,
for_cudagraph_capture=True,
)
assert context_enabled
assert context_disabled
assert calls == [
(
"request",
{
"req_ids": ["r1", "r2"],
"num_reqs": 2,
"num_scheduled_tokens": [1, 1],
"num_computed_tokens": [5, 6],
"max_num_scheduled_tokens": 1,
},
),
("context", {"enabled": True, "num_reqs": 2}),
("context", {"enabled": False, "num_reqs": 0}),
]
def test_omni_async_gpu_model_runner_output_builds_lazily_once(): def test_omni_async_gpu_model_runner_output_builds_lazily_once():
async_output = object.__new__(OmniAsyncGPUModelRunnerOutput) async_output = object.__new__(OmniAsyncGPUModelRunnerOutput)
calls = [] calls = []
@@ -357,6 +454,47 @@ def test_async_snapshot_payload_omits_hidden_when_model_opts_out():
assert payload["multimodal_outputs"]["codes"]["audio"].tolist() == [[1]] assert payload["multimodal_outputs"]["codes"]["audio"].tolist() == [[1]]
def test_runner_assisted_full_attention_metadata_refresh_pads_buffers():
class QueryStartLoc:
def __init__(self):
self.np = np.full(5, -1, dtype=np.int32)
self.copied = False
def copy_to_gpu(self):
self.copied = True
class BlockTable:
def __init__(self):
self.commits = []
def commit_block_table(self, num_reqs_padded):
self.commits.append(num_reqs_padded)
runner = object.__new__(GPUARModelRunner)
block_table = BlockTable()
runner.input_batch = SimpleNamespace(
num_computed_tokens_cpu=torch.tensor([10, 20, 99, 99], dtype=torch.int32),
block_table=block_table,
)
runner.optimistic_seq_lens_cpu = torch.zeros(4, dtype=torch.int32)
runner.seq_lens = torch.empty(4, dtype=torch.int32)
runner.query_pos = SimpleNamespace(np=np.empty(4, dtype=np.int32))
runner.query_start_loc = QueryStartLoc()
runner._get_cumsum_and_arange = lambda scheduled, _query_pos: np.cumsum(scheduled, dtype=np.int32)
runner._refresh_runner_assisted_full_attention_metadata_buffers(
num_reqs=2,
num_reqs_padded=4,
num_scheduled_tokens_np=np.array([1, 2], dtype=np.int32),
)
assert runner.optimistic_seq_lens_cpu.tolist() == [11, 22, 0, 0]
assert runner.seq_lens.tolist() == [11, 22, 0, 0]
assert runner.query_start_loc.np.tolist() == [0, 1, 3, 3, 3]
assert runner.query_start_loc.copied
assert block_table.commits == [4]
@pytest.mark.parametrize("query_start_loc_attr", ["method", "tensor_attr"]) @pytest.mark.parametrize("query_start_loc_attr", ["method", "tensor_attr"])
def test_sample_tokens_tail_only_prefix_cache_uses_staged_cpu_hidden_states(monkeypatch, query_start_loc_attr): def test_sample_tokens_tail_only_prefix_cache_uses_staged_cpu_hidden_states(monkeypatch, query_start_loc_attr):
runner = object.__new__(GPUARModelRunner) runner = object.__new__(GPUARModelRunner)
+14 -1
View File
@@ -1264,7 +1264,7 @@ if "daily-omni" not in OPENAI_COMPATIBLE_BACKENDS:
# Prevent import order from causing patch failures # Prevent import order from causing patch failures
from vllm.benchmarks import serve from vllm.benchmarks import serve
from vllm.benchmarks.lib.ready_checker import wait_for_endpoint from vllm.benchmarks.lib.ready_checker import wait_for_endpoint
from vllm.benchmarks.serve import TaskType, _merge_overrides, calculate_metrics_for_embeddings, get_request from vllm.benchmarks.serve import TaskType, calculate_metrics_for_embeddings, get_request
from vllm_omni.benchmarks.metrics.metrics import ( from vllm_omni.benchmarks.metrics.metrics import (
MultiModalsBenchmarkMetrics, MultiModalsBenchmarkMetrics,
@@ -1276,6 +1276,19 @@ from vllm_omni.benchmarks.metrics.metrics import (
benchmark_old = serve.benchmark benchmark_old = serve.benchmark
def _merge_overrides(base: dict | None, overrides: dict | None) -> dict | None:
"""Merge benchmark extra_body with per-request overrides.
vLLM 0.24 removed the private helper from ``vllm.benchmarks.serve``.
Keep the same shallow-merge behavior here, with request overrides winning.
"""
if not base and not overrides:
return None
merged = dict(base or {})
merged.update(overrides or {})
return merged
async def benchmark( async def benchmark(
task_type: TaskType, task_type: TaskType,
endpoint_type: str, endpoint_type: str,
+75 -18
View File
@@ -63,7 +63,12 @@ def build_stage_runtime_overrides(
if internal_keys is None: if internal_keys is None:
from vllm_omni.engine.arg_utils import SHARED_FIELDS, internal_blacklist_keys from vllm_omni.engine.arg_utils import SHARED_FIELDS, internal_blacklist_keys
internal_keys = internal_blacklist_keys() | SHARED_FIELDS # Some fields are modeled as orchestrator-owned for top-level CLI
# parsing, but are also legitimate deploy-time stage overrides. Keep
# the default blacklist for true orchestrator/shared fields while
# allowing any field explicitly represented by the deploy schema to
# continue flowing into per-stage overrides.
internal_keys = (internal_blacklist_keys() | SHARED_FIELDS) - deploy_runtime_override_keys()
result: dict[str, Any] = {} result: dict[str, Any] = {}
@@ -223,6 +228,7 @@ class StagePipelineConfig:
prompt_expand_func: str | None = None prompt_expand_func: str | None = None
cfg_kv_collect_func: str | None = None cfg_kv_collect_func: str | None = None
omni_kv_config: dict[str, Any] | None = None omni_kv_config: dict[str, Any] | None = None
scheduler_cls: str | None = None
# Model subdirectory indirections: for multi-component HF repos where the # Model subdirectory indirections: for multi-component HF repos where the
# stage's config/tokenizer lives in a subdirectory (e.g. GLM-Image's AR # stage's config/tokenizer lives in a subdirectory (e.g. GLM-Image's AR
# config is in ``vision_language_encoder/``). Consumed at stage-init time # config is in ``vision_language_encoder/``). Consumed at stage-init time
@@ -303,7 +309,7 @@ class StageDeployConfig:
the top level of ``DeployConfig`` and propagated to every stage. the top level of ``DeployConfig`` and propagated to every stage.
""" """
# === Omni fields === # === Omni stage wrapper fields ===
# Stage identity and Omni runtime placement. # Stage identity and Omni runtime placement.
stage_id: int stage_id: int
devices: str | None = None devices: str | None = None
@@ -316,22 +322,33 @@ class StageDeployConfig:
default_sampling_params: dict[str, Any] | None = None default_sampling_params: dict[str, Any] | None = None
subtalker_sampling_params: dict[str, Any] | None = None subtalker_sampling_params: dict[str, Any] | None = None
# === vLLM EngineArgs fields === # === Generic stage engine fields ===
# Parallelism and scheduler/memory capacity. # Parallelism, scheduler, and memory-capacity controls.
tensor_parallel_size: int | None = None tensor_parallel_size: int | None = None
enable_expert_parallel: bool | None = None
gpu_memory_utilization: float | None = None gpu_memory_utilization: float | None = None
max_num_seqs: int | None = None max_num_seqs: int | None = None
max_num_batched_tokens: int | None = None max_num_batched_tokens: int | None = None
max_model_len: int | None = None max_model_len: int | None = None
# Execution, scheduling, and KV/cache behavior. # Generic execution, scheduling, and KV/cache behavior.
enforce_eager: bool | None = None enforce_eager: bool | None = None
async_scheduling: bool | None = None async_scheduling: bool | None = None
disable_hybrid_kv_cache_manager: bool | None = None disable_hybrid_kv_cache_manager: bool | None = None
mm_processor_cache_gb: float | None = None mm_processor_cache_gb: float | None = None
# Diffusion parallel_config deploy override fields. # Generic compilation, profiling, tokenizer/config parsing, and model
enable_expert_parallel: bool | None = None # loading controls.
compilation_config: dict[str, Any] | None = None
profiler_config: dict[str, Any] | None = None
skip_mm_profiling: bool | None = None
enable_flashinfer_autotune: bool | None = None
config_format: str | None = None
load_format: str | None = None
tokenizer_mode: str | None = None
# === Diffusion stage runtime fields ===
# Diffusion parallel_config deploy/runtime override fields.
ulysses_degree: int | None = None ulysses_degree: int | None = None
ulysses_mode: str | None = None ulysses_mode: str | None = None
ring_degree: int | None = None ring_degree: int | None = None
@@ -343,16 +360,44 @@ class StageDeployConfig:
hsdp_shard_size: int | None = None hsdp_shard_size: int | None = None
hsdp_replicate_size: int | None = None hsdp_replicate_size: int | None = None
# Compilation, profiling, tokenizer/config parsing, and model loading. # Diffusion model loading and adapter construction.
compilation_config: dict[str, Any] | None = None model_class_name: str | None = None
profiler_config: dict[str, Any] | None = None diffusion_load_format: str | None = None
skip_mm_profiling: bool | None = None diffusers_load_kwargs: dict[str, Any] | None = None
enable_flashinfer_autotune: bool | None = None diffusers_call_kwargs: dict[str, Any] | None = None
config_format: str | None = None diffusion_quantization_config: str | None = None
load_format: str | None = None diffusion_attention_backend: str | None = None
tokenizer_mode: str | None = None diffusion_attention_config: dict[str, Any] | None = None
# Pass-through vLLM EngineArgs fields that are not represented above. # Diffusion execution, cache, and VAE behavior.
cache_backend: str | None = None
cache_config: dict[str, Any] | None = None
enable_cache_dit_summary: bool | None = None
step_execution: bool | None = None
vae_use_slicing: bool | None = None
vae_use_tiling: bool | None = None
boundary_ratio: float | None = None
flow_shift: float | None = None
diffusion_kv_cache_dtype: str | None = None
diffusion_kv_cache_skip_steps: str | None = None
diffusion_kv_cache_skip_layers: str | None = None
auxiliary_text_encoder: str | None = None
# Runtime optimizations used by diffusion loading/execution.
enable_multithread_weight_load: bool | None = None
num_weight_load_threads: int | None = None
enable_cpu_offload: bool | None = None
enable_layerwise_offload: bool | None = None
# Diffusion-specific debug and observability knobs.
enable_diffusion_pipeline_profiler: bool | None = None
# Modality/service constraints consumed outside the core engine config.
max_generated_image_size: int | None = None
tts_max_instructions_length: int | None = None
# === Pass-through stage engine fields ===
# Pass-through stage engine args that are not represented above.
engine_extras: dict[str, Any] = field(default_factory=dict) engine_extras: dict[str, Any] = field(default_factory=dict)
@@ -409,10 +454,22 @@ _STAGE_RESERVED_KEYS = frozenset(
_STAGE_DEPLOY_FIELDS = {f.name: f for f in fields(StageDeployConfig) if f.name not in _STAGE_RESERVED_KEYS} _STAGE_DEPLOY_FIELDS = {f.name: f for f in fields(StageDeployConfig) if f.name not in _STAGE_RESERVED_KEYS}
def deploy_runtime_override_keys() -> frozenset[str]:
"""Return deploy-schema fields that are valid CLI/runtime overrides.
These keys form the positive contract for stage override propagation:
stage-scoped deploy knobs plus top-level pipeline-wide engine settings.
They must remain overridable even if they are also modeled on
``OrchestratorArgs`` for top-level CLI parsing.
"""
return frozenset(_STAGE_DEPLOY_FIELDS) | frozenset(_PIPELINE_WIDE_ENGINE_FIELDS)
def _parse_stage_deploy(stage_data: dict[str, Any]) -> StageDeployConfig: def _parse_stage_deploy(stage_data: dict[str, Any]) -> StageDeployConfig:
"""Parse a single stage entry from deploy YAML into StageDeployConfig.""" """Parse a single stage entry from deploy YAML into StageDeployConfig."""
# Get the non-reserved keys for this stage # Get the non-reserved keys for this stage
flat_args = {k: v for k, v in stage_data.items() if k not in _STAGE_RESERVED_KEYS} flat_args = {k: v for k, v in stage_data.items() if k not in _STAGE_RESERVED_KEYS}
explicit_engine_extras = dict(stage_data.get("engine_extras") or {})
runtime_cfg = dict(stage_data.get("runtime", {})) runtime_cfg = dict(stage_data.get("runtime", {}))
devices = runtime_cfg.get("devices", stage_data.get("devices")) devices = runtime_cfg.get("devices", stage_data.get("devices"))
num_replicas = runtime_cfg.get("num_replicas", stage_data.get("num_replicas", 1)) num_replicas = runtime_cfg.get("num_replicas", stage_data.get("num_replicas", 1))
@@ -440,7 +497,7 @@ def _parse_stage_deploy(stage_data: dict[str, Any]) -> StageDeployConfig:
kwargs["output_connectors"] = stage_data.get("output_connectors") kwargs["output_connectors"] = stage_data.get("output_connectors")
kwargs["input_connectors"] = stage_data.get("input_connectors") kwargs["input_connectors"] = stage_data.get("input_connectors")
kwargs["default_sampling_params"] = stage_data.get("default_sampling_params") kwargs["default_sampling_params"] = stage_data.get("default_sampling_params")
kwargs["engine_extras"] = flat_args kwargs["engine_extras"] = _get_recursively_merged_dict(explicit_engine_extras, flat_args)
return StageDeployConfig(**kwargs) return StageDeployConfig(**kwargs)
@@ -840,7 +897,7 @@ def merge_pipeline_deploy(
final_output=ps.final_output, final_output=ps.final_output,
final_output_type=ps.final_output_type, final_output_type=ps.final_output_type,
worker_type=worker_type, worker_type=worker_type,
scheduler_cls=_scheduler_path(sched_cls), scheduler_cls=ps.scheduler_cls or _scheduler_path(sched_cls),
hf_config_name=ps.hf_config_name, hf_config_name=ps.hf_config_name,
is_comprehension=ps.owns_tokenizer, is_comprehension=ps.owns_tokenizer,
yaml_engine_args=engine_args, yaml_engine_args=engine_args,
+14
View File
@@ -11,6 +11,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1.metrics import KVConnectorStat
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.v1.core.sched.async_scheduler import AsyncScheduler as AsyncVLLMScheduler from vllm.v1.core.sched.async_scheduler import AsyncScheduler as AsyncVLLMScheduler
from vllm.v1.core.sched.output import SchedulerOutput from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.core.sched.request_queue import create_request_queue
from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler
from vllm.v1.core.sched.utils import remove_all from vllm.v1.core.sched.utils import remove_all
from vllm.v1.engine import EngineCoreEventType, EngineCoreOutput, EngineCoreOutputs, FinishReason from vllm.v1.engine import EngineCoreEventType, EngineCoreOutput, EngineCoreOutputs, FinishReason
@@ -133,6 +134,9 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
self._omits_kv_transfer_cache[rid] = result self._omits_kv_transfer_cache[rid] = result
return result return result
def _should_defer_waiting_admission(self) -> bool:
return False
def _process_kv_transfer_trigger(self, request: Request, new_token_ids: list[int]) -> bool: def _process_kv_transfer_trigger(self, request: Request, new_token_ids: list[int]) -> bool:
""" """
Check triggers and process side effects (marking transfer). Check triggers and process side effects (marking transfer).
@@ -222,9 +226,19 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler):
self.waiting, self.running, scheduler_requests=self.requests self.waiting, self.running, scheduler_requests=self.requests
) )
original_waiting = None
if self._should_defer_waiting_admission():
original_waiting = self.waiting
self.waiting = create_request_queue(self.policy)
try: try:
scheduler_output = super().schedule(throttle_prefills) scheduler_output = super().schedule(throttle_prefills)
finally: finally:
if original_waiting is not None:
deferred_waiting = list(self.waiting)
if deferred_waiting:
original_waiting.prepend_requests(deferred_waiting)
self.waiting = original_waiting
if self.chunk_transfer_adapter: if self.chunk_transfer_adapter:
# Add request waiting for chunk to the waiting and running queue # Add request waiting for chunk to the waiting and running queue
self.chunk_transfer_adapter.restore_queues( self.chunk_transfer_adapter.restore_queues(
+8 -4
View File
@@ -8,16 +8,17 @@ dtype: bfloat16
stages: stages:
- stage_id: 0 - stage_id: 0
max_num_seqs: 32 max_num_seqs: 8
gpu_memory_utilization: 0.9 gpu_memory_utilization: 0.9
# Right-size the KV cache instead of letting it fill gpu_memory_utilization. # Right-size the KV cache instead of letting it fill gpu_memory_utilization.
# max_num_seqs(32) * max_model_len(4096) ~= 131k tokens; 6 GiB holds ~175k # max_num_seqs(8) * max_model_len(4096) ~= 32k tokens; 6 GiB leaves headroom
# (42x concurrency, above the 32-seq scheduler cap, so no throughput loss). # for longer live requests while keeping VoxCPM2 TTS decode batches small
# enough to avoid the high ITL observed with large admission caps.
# The remaining VRAM stays free for the VoxCPM2 diffusion side-path # The remaining VRAM stays free for the VoxCPM2 diffusion side-path
# (LocDiT / AudioVAE / code2wav + batch CUDA-graph pools), which allocates # (LocDiT / AudioVAE / code2wav + batch CUDA-graph pools), which allocates
# outside vLLM's KV accounting during inference. Keeps peak ~13 GiB on any # outside vLLM's KV accounting during inference. Keeps peak ~13 GiB on any
# card and avoids per-rebase OOM tuning on small CI GPUs. # card and avoids per-rebase OOM tuning on small CI GPUs.
# Assumes prefix caching off and max_num_seqs*max_model_len <= ~175k tokens; # Assumes prefix caching off and typical live decode concurrency <= ~8;
# raise this cap if either grows or enable_prefix_caching is turned on. # raise this cap if either grows or enable_prefix_caching is turned on.
kv_cache_memory_bytes: 6442450944 # 6 GiB kv_cache_memory_bytes: 6442450944 # 6 GiB
enforce_eager: true enforce_eager: true
@@ -39,6 +40,9 @@ stages:
enable_loc_dit_fused_mlp: true enable_loc_dit_fused_mlp: true
enable_loc_dit_zero_dt_cache: true enable_loc_dit_zero_dt_cache: true
enable_loc_dit_skip_qkv_contig: true enable_loc_dit_skip_qkv_contig: true
enable_loc_dit_reduce_overhead_no_cg: true
enable_unified_decode_graph: true
unified_decode_graph_max_batch_size: 8
default_sampling_params: default_sampling_params:
temperature: 0.0 temperature: 0.0
top_p: 1.0 top_p: 1.0
@@ -24,7 +24,6 @@ from __future__ import annotations
import logging import logging
import os import os
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -49,7 +48,9 @@ from vllm_omni.diffusion.models.ming_flash_omni.ming_zimage_transformer import (
MingZImageTransformer2DModel, MingZImageTransformer2DModel,
) )
from vllm_omni.diffusion.models.z_image.pipeline_z_image import ZImagePipeline from vllm_omni.diffusion.models.z_image.pipeline_z_image import ZImagePipeline
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.model_executor.model_loader.weight_utils import ( from vllm_omni.model_executor.model_loader.weight_utils import (
download_weights_from_hf_specific, download_weights_from_hf_specific,
) )
@@ -58,38 +59,6 @@ from vllm_omni.transformers_utils.configs.ming_flash_omni import MingImageGenCon
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class _ZPipelineSamplingParams:
"""Typed shim satisfying the attributes ZImagePipeline.forward reads
from ``req.sampling_params``. Unlike SimpleNamespace this fails loudly
at construction if a required field is missing."""
height: int
width: int
num_inference_steps: int
guidance_scale: float
generator: torch.Generator | None = None
strength: float | None = None
sigmas: list[float] | None = None
max_sequence_length: int = 512
guidance_rescale: float | None = None
num_outputs_per_prompt: int = 1
@dataclass
class _ZPipelineRequest:
"""Typed shim satisfying the attributes ZImagePipeline.forward reads
from ``req``."""
request_id: str
sampling_params: _ZPipelineSamplingParams
prompts: list[dict[str, Any]] = field(default_factory=lambda: [{"prompt": "", "negative_prompt": ""}])
@property
def num_reqs(self) -> int:
return 1
class MingImagePipeline(ZImagePipeline): class MingImagePipeline(ZImagePipeline):
"""Ming-flash-omni-2.0 text-to-image diffusion pipeline. """Ming-flash-omni-2.0 text-to-image diffusion pipeline.
@@ -123,6 +92,10 @@ class MingImagePipeline(ZImagePipeline):
self.device = self._execution_device # Ming convention alias self.device = self._execution_device # Ming convention alias
self._dtype = dtype self._dtype = dtype
# Request-scoped conditioning handed to the inherited encode_prompt override
self._pending_prompt_embeds: list[torch.Tensor] | None = None
self._pending_negative_prompt_embeds: list[torch.Tensor] | None = None
# Ming's per-checkpoint image-gen configuration. We cannot rely on # Ming's per-checkpoint image-gen configuration. We cannot rely on
# ``od_config.hf_config.image_gen_config`` because the diffusion # ``od_config.hf_config.image_gen_config`` because the diffusion
# stage is started with ``hf_config_name: thinker_config`` (the # stage is started with ``hf_config_name: thinker_config`` (the
@@ -273,6 +246,21 @@ class MingImagePipeline(ZImagePipeline):
latent = self.vae.encode(ref).latent_dist.mode() latent = self.vae.encode(ref).latent_dist.mode()
return (latent - self.vae.config.shift_factor) * self.vae.config.scaling_factor return (latent - self.vae.config.shift_factor) * self.vae.config.scaling_factor
def encode_prompt(self, *args, **kwargs): # noqa: ARG002
"""Return Ming's precomputed conditioning instead of encoding text.
NOTE: Ming has no Z-Image text_encoder; its conditioning (cap_feats, optionally ByT5-augmented)
is computed in forward and stashed on `self._pending_*` immediately before
the `super().forward` call, so we simply hand it back here.
"""
if self._pending_prompt_embeds is None:
raise RuntimeError(
"MingImagePipeline.encode_prompt called without pending "
"conditioning; it must run within MingImagePipeline.forward."
)
return self._pending_prompt_embeds, self._pending_negative_prompt_embeds
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Forward # Forward
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -416,17 +404,27 @@ class MingImagePipeline(ZImagePipeline):
negative_prompt_embeds = [negative_cap_feats[i] for i in range(negative_cap_feats.shape[0])] negative_prompt_embeds = [negative_cap_feats[i] for i in range(negative_cap_feats.shape[0])]
else: else:
negative_prompt_embeds = [self.condition_encoder.zero_negative(e) for e in prompt_embeds] negative_prompt_embeds = [self.condition_encoder.zero_negative(e) for e in prompt_embeds]
self._pending_prompt_embeds = prompt_embeds
self._pending_negative_prompt_embeds = negative_prompt_embeds
z_sp = _ZPipelineSamplingParams( # Build a real single-request DiffusionRequestBatch;
# the prompt text itself is a neutral placeholder here.
z_sp = OmniDiffusionSamplingParams(
height=height, height=height,
width=width, width=width,
num_inference_steps=num_inference_steps, num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale, guidance_scale=guidance_scale,
generator=generator, generator=generator,
output_type="pt",
) )
z_req = _ZPipelineRequest( z_req = DiffusionRequestBatch(
request_id=req.request_id or "ming-imagegen", requests=[
sampling_params=z_sp, OmniDiffusionRequest(
prompt={"prompt": ""},
sampling_params=z_sp,
request_id=req.request_id or "ming-imagegen",
)
]
) )
# Reference image (img2img) → VAE-encoded latent published on the # Reference image (img2img) → VAE-encoded latent published on the
@@ -446,21 +444,12 @@ class MingImagePipeline(ZImagePipeline):
None if ref_latent is None else tuple(ref_latent.shape), None if ref_latent is None else tuple(ref_latent.shape),
) )
try: try:
outputs: DiffusionOutput = super().forward( outputs: DiffusionOutput = super().forward(z_req)
z_req,
prompt=None,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
generator=generator,
output_type="pt",
return_dict=True,
)
finally: finally:
set_forward_context_ref_latent(None) set_forward_context_ref_latent(None)
# Drop request-scoped conditioning so we don't retain GPU tensors.
self._pending_prompt_embeds = None
self._pending_negative_prompt_embeds = None
raw = outputs.output raw = outputs.output
if not isinstance(raw, torch.Tensor): if not isinstance(raw, torch.Tensor):
@@ -14,7 +14,7 @@ import io
import PIL.Image import PIL.Image
SUPPORTED_LAYERED_RESOLUTIONS = (640, 1024) SUPPORTED_LAYERED_RESOLUTIONS = (640, 1024)
SUPPORTED_LAYERED_LAYERS_RANGE = range(3, 11) SUPPORTED_LAYERED_LAYERS_RANGE = range(2, 11)
def parse_size(size_str: str) -> tuple[int, int]: def parse_size(size_str: str) -> tuple[int, int]:
@@ -60,7 +60,7 @@ class ImageGenerationRequest(BaseModel):
user: str | None = Field(default=None, description="User identifier for tracking") user: str | None = Field(default=None, description="User identifier for tracking")
layers: int | None = Field( layers: int | None = Field(
default=None, default=None,
description="Number of output layers for layered image models. Supported range: 3-10.", description="Number of output layers for layered image models. Supported range: 2-10.",
) )
@field_validator("size") @field_validator("size")
+8 -30
View File
@@ -3513,11 +3513,13 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
# apply uploaded speakers inside validate(), which sets request.ref_audio # apply uploaded speakers inside validate(), which sets request.ref_audio
# in place. The builders need to know whether the caller supplied audio # in place. The builders need to know whether the caller supplied audio
# inline vs. via an uploaded voice. # inline vs. via an uploaded voice.
model_type: str | None = None
has_inline_ref_audio = request.ref_audio is not None has_inline_ref_audio = request.ref_audio is not None
if self._tts_model_type == "ming_flash_omni_tts": if self._tts_model_type == "ming_flash_omni_tts":
# ming_flash_omni is intentionally NOT migrated onto the adapter # ming_flash_omni is intentionally NOT migrated onto the adapter
# framework in this PR (it has no registered adapter); keep it on the # framework in this PR (it has no registered adapter); keep it on the
# legacy inline dispatch so serving still works. # legacy inline dispatch so serving still works.
model_type = "ming_flash_omni_tts"
validation_error = self._validate_ming_flash_omni_tts_request(request) validation_error = self._validate_ming_flash_omni_tts_request(request)
if validation_error: if validation_error:
raise ValueError(validation_error) raise ValueError(validation_error)
@@ -3531,6 +3533,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
prepared = await adapter.build(request, sampling_params_list, has_inline_ref_audio) prepared = await adapter.build(request, sampling_params_list, has_inline_ref_audio)
prompt = prepared.prompt prompt = prepared.prompt
tts_params = prepared.tts_params tts_params = prepared.tts_params
model_type = prepared.model_type
qwen3_ref_audio_warmup_artifact_key = prepared.warmup_artifact_key qwen3_ref_audio_warmup_artifact_key = prepared.warmup_artifact_key
else: else:
# Qwen omni models (Qwen3-Omni, Qwen2.5-Omni) use a "talker" # Qwen omni models (Qwen3-Omni, Qwen2.5-Omni) use a "talker"
@@ -3554,36 +3557,11 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
tts_params = {} tts_params = {}
prompt = {"prompt": request.input} prompt = {"prompt": request.input}
if self._tts_model_type == "step_audio2": if model_type is None:
model_type = "step_audio2" if self._is_tts:
elif self._is_fish_speech: model_type = tts_params.get("task_type", ["unknown"])[0]
model_type = "fish_speech" else:
elif self._tts_model_type == "covo_audio": model_type = "generic"
model_type = "covo_audio"
elif self._tts_model_type == "voxtral_tts":
model_type = "voxtral_tts"
elif self._tts_model_type == "cosyvoice3":
model_type = "cosyvoice3"
elif self._tts_model_type == "voxcpm2":
model_type = "voxcpm2"
elif self._tts_model_type == "ming_flash_omni_tts":
model_type = "ming_flash_omni_tts"
elif self._tts_model_type == "ming_tts":
model_type = "ming_tts"
elif self._tts_model_type == "moss_tts_nano":
model_type = "moss_tts_nano"
elif self._tts_model_type == "moss_tts":
model_type = "moss_tts"
elif self._tts_model_type == "higgs_audio_v2":
model_type = "higgs_audio_v2"
elif self._tts_model_type == "glm_tts":
model_type = "glm_tts"
elif self._tts_model_type == "indextts2":
model_type = "indextts2"
elif self._is_tts:
model_type = tts_params.get("task_type", ["unknown"])[0]
else:
model_type = "generic"
logger.info( logger.info(
"TTS speech request %s: text=%r, model=%s", "TTS speech request %s: text=%r, model=%s",
request_id, request_id,
@@ -587,9 +587,9 @@ class OmniBagelForConditionalGeneration(BagelForConditionalGeneration):
input_ids: torch.Tensor | None, input_ids: torch.Tensor | None,
positions: torch.Tensor | None, positions: torch.Tensor | None,
inputs_embeds: torch.Tensor | None, inputs_embeds: torch.Tensor | None,
req_ids: list[str], req_ids: Sequence[str],
num_computed_tokens: list[int], num_computed_tokens: Sequence[int],
num_scheduled_tokens: list[int], num_scheduled_tokens: Sequence[int],
input_ids_buffer: torch.Tensor | None = None, input_ids_buffer: torch.Tensor | None = None,
) -> tuple[torch.Tensor | None, torch.Tensor | None]: ) -> tuple[torch.Tensor | None, torch.Tensor | None]:
"""Restore input_ids so _adjust_positions_for_img2img can locate """Restore input_ids so _adjust_positions_for_img2img can locate
@@ -599,7 +599,7 @@ class OmniBagelForConditionalGeneration(BagelForConditionalGeneration):
input_ids = input_ids_buffer input_ids = input_ids_buffer
return input_ids, positions return input_ids, positions
def flush_pending_metadata(self, req_ids: list[str]) -> None: def flush_pending_metadata(self, req_ids: Sequence[str]) -> None:
"""Map pending metadata (batch order) to req_ids after forward(). """Map pending metadata (batch order) to req_ids after forward().
Guard: if a request already has metadata with ``image_shape`` Guard: if a request already has metadata with ``image_shape``
@@ -26,6 +26,7 @@ VOXCPM2_PIPELINE = PipelineConfig(
final_output_type="audio", final_output_type="audio",
owns_tokenizer=True, owns_tokenizer=True,
engine_output_type="audio", engine_output_type="audio",
scheduler_cls="vllm_omni.model_executor.models.voxcpm2.scheduler.VoxCPM2OmniARAsyncScheduler",
sampling_constraints={ sampling_constraints={
"detokenize": False, "detokenize": False,
"stop_token_ids": [1], "stop_token_ids": [1],
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
from vllm.v1.request import RequestStatus
from vllm_omni.core.sched.omni_ar_scheduler import OmniARAsyncScheduler
class VoxCPM2OmniARAsyncScheduler(OmniARAsyncScheduler):
"""VoxCPM2 scheduler variant for full unified decode graph serving.
VoxCPM2's full unified decode graph only applies to pure decode batches.
When a decode-ready request is already running, this scheduler defers new
waiting admissions for the current tick so the decode batch can stay on the
VoxCPM2 graph path. This is a model-local serving policy, not a generic AR
scheduler rule.
"""
def _unified_decode_graph_enabled(self) -> bool:
hf_config = getattr(self.vllm_config.model_config, "hf_config", None)
runtime_config = getattr(hf_config, "voxcpm2_runtime_config", None)
if isinstance(runtime_config, dict):
return bool(runtime_config.get("enable_unified_decode_graph", False))
return bool(getattr(runtime_config, "enable_unified_decode_graph", False))
def _should_defer_waiting_for_unified_decode_graph(self) -> bool:
if not self._unified_decode_graph_enabled():
return False
if not self.waiting or not self.running:
return False
for request in self.running:
if getattr(request, "status", None) != RequestStatus.RUNNING or request.is_finished():
continue
if self._get_confirmed_num_computed_tokens(request) >= request.num_prompt_tokens:
return True
return False
def _should_defer_waiting_admission(self) -> bool:
return self._should_defer_waiting_for_unified_decode_graph()
File diff suppressed because it is too large. Load diff
@@ -621,8 +621,8 @@ class NPUARModelRunner(OmniNPUModelRunner):
positions=positions, positions=positions,
inputs_embeds=inputs_embeds, inputs_embeds=inputs_embeds,
req_ids=req_ids[:num_reqs], req_ids=req_ids[:num_reqs],
num_computed_tokens=[int(self.input_batch.num_computed_tokens_cpu[i]) for i in range(num_reqs)], num_computed_tokens=self.input_batch.num_computed_tokens_cpu[:num_reqs],
num_scheduled_tokens=[int(num_scheduled_tokens_np[i]) for i in range(num_reqs)], num_scheduled_tokens=num_scheduled_tokens_np[:num_reqs],
input_ids_buffer=self.input_ids.gpu[:num_tokens_padded], input_ids_buffer=self.input_ids.gpu[:num_tokens_padded],
) )
# -------------------------------------- Omni-new ------------------------------------------------- # -------------------------------------- Omni-new -------------------------------------------------
@@ -690,8 +690,9 @@ class NPUARModelRunner(OmniNPUModelRunner):
with record_function_or_nullcontext("post process"): with record_function_or_nullcontext("post process"):
# -------------------------------------- Omni-new ------------------------------------------------- # -------------------------------------- Omni-new -------------------------------------------------
# [Omni] Map pending ropes metadata to req_ids. # [Omni] Map pending ropes metadata to req_ids.
if hasattr(self.model, "flush_pending_metadata"): flush_pending_metadata = getattr(self.model, "flush_pending_metadata", None)
self.model.flush_pending_metadata(list(req_ids)) if callable(flush_pending_metadata):
flush_pending_metadata(req_ids[:num_reqs])
hidden_states, multimodal_outputs = self.extract_multimodal_outputs(hidden_states) hidden_states, multimodal_outputs = self.extract_multimodal_outputs(hidden_states)
+170 -39
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
import gc import gc
import threading import threading
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping, Sequence
from contextlib import nullcontext from contextlib import nullcontext
from copy import copy from copy import copy
from dataclasses import replace from dataclasses import replace
@@ -46,6 +46,7 @@ from vllm_omni.outputs import OmniModelRunnerOutput
from vllm_omni.utils.mm_outputs import build_mm_cpu, partition_payload_list, to_payload_element from vllm_omni.utils.mm_outputs import build_mm_cpu, partition_payload_list, to_payload_element
from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner from vllm_omni.worker.gpu_model_runner import OmniGPUModelRunner
from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorModelRunnerMixin from vllm_omni.worker.omni_connector_model_runner_mixin import OmniConnectorModelRunnerMixin
from vllm_omni.worker.runner_assisted_metadata import RunnerAssistedFullAttentionMetadataRequest
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -588,6 +589,93 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
model = getattr(self, "model", None) model = getattr(self, "model", None)
return bool(getattr(model, "requires_full_prefix_cached_hidden_states", True)) return bool(getattr(model, "requires_full_prefix_cached_hidden_states", True))
def _get_runner_assisted_full_attention_metadata_request(
self,
*,
req_ids: Sequence[str],
num_reqs: int,
num_reqs_padded: int,
num_scheduled_tokens_np: np.ndarray,
num_computed_tokens_cpu: torch.Tensor | np.ndarray,
max_num_scheduled_tokens: int,
) -> RunnerAssistedFullAttentionMetadataRequest | None:
# Models without this hook keep the normal runner path. VoxCPM2 uses
# it to ask the runner for padded FULL attention metadata while keeping
# graph policy and lifecycle in the model layer.
hook = getattr(self.model, "get_runner_assisted_full_attention_metadata_request", None)
if not callable(hook):
return None
num_computed_tokens = num_computed_tokens_cpu
if hasattr(num_computed_tokens, "numpy"):
num_computed_tokens = num_computed_tokens.numpy()
request = hook(
req_ids=req_ids,
num_reqs=num_reqs,
num_scheduled_tokens=num_scheduled_tokens_np[:num_reqs],
num_computed_tokens=num_computed_tokens,
max_num_scheduled_tokens=max_num_scheduled_tokens,
)
if request is None:
return None
if not isinstance(request, RunnerAssistedFullAttentionMetadataRequest):
raise TypeError(
"runner-assisted full attention metadata hook must return "
"RunnerAssistedFullAttentionMetadataRequest or None, got "
f"{type(request).__name__}"
)
padded_num_reqs = max(
num_reqs_padded,
min(int(request.num_reqs_padded), self.scheduler_config.max_num_seqs),
)
return RunnerAssistedFullAttentionMetadataRequest(
num_reqs_padded=padded_num_reqs,
for_cudagraph_capture=bool(request.for_cudagraph_capture),
)
def _refresh_runner_assisted_full_attention_metadata_buffers(
self,
*,
num_reqs: int,
num_reqs_padded: int,
num_scheduled_tokens_np: np.ndarray,
) -> None:
num_computed_tokens = self.input_batch.num_computed_tokens_cpu[:num_reqs]
if hasattr(num_computed_tokens, "numpy"):
num_computed_tokens = num_computed_tokens.numpy()
np.add(
num_computed_tokens,
num_scheduled_tokens_np,
out=self.optimistic_seq_lens_cpu[:num_reqs].numpy(),
)
self.optimistic_seq_lens_cpu[num_reqs:num_reqs_padded].fill_(0)
self.seq_lens.copy_(self.optimistic_seq_lens_cpu, non_blocking=True)
cum_num_tokens = self._get_cumsum_and_arange(
num_scheduled_tokens_np,
self.query_pos.np,
)
self.query_start_loc.np[0] = 0
self.query_start_loc.np[1 : num_reqs + 1] = cum_num_tokens
self.query_start_loc.np[num_reqs + 1 : num_reqs_padded + 1].fill(cum_num_tokens[-1])
self.query_start_loc.copy_to_gpu()
self.input_batch.block_table.commit_block_table(num_reqs_padded)
def _set_runner_assisted_full_attention_metadata_context(
self,
*,
enabled: bool,
num_reqs: int = 0,
) -> bool:
hook = getattr(self.model, "set_runner_assisted_full_attention_metadata_context", None)
if not callable(hook):
return False
hook(
enabled=enabled,
num_reqs=num_reqs,
)
return True
def _deferred_prefix_cache_mm_keys(self) -> set[str]: def _deferred_prefix_cache_mm_keys(self) -> set[str]:
"""Model-declared multimodal keys whose prefix-cache writes are deferred.""" """Model-declared multimodal keys whose prefix-cache writes are deferred."""
model = getattr(self, "model", None) model = getattr(self, "model", None)
@@ -1028,6 +1116,26 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
) )
num_tokens_padded = batch_desc.num_tokens num_tokens_padded = batch_desc.num_tokens
num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs
num_computed_tokens_cpu = self.input_batch.num_computed_tokens_cpu[:num_reqs]
runner_assisted_full_attn_request = self._get_runner_assisted_full_attention_metadata_request(
req_ids=req_ids[:num_reqs],
num_reqs=num_reqs,
num_reqs_padded=num_reqs_padded,
num_scheduled_tokens_np=num_scheduled_tokens_np,
num_computed_tokens_cpu=num_computed_tokens_cpu,
max_num_scheduled_tokens=max_num_scheduled_tokens,
)
runner_assisted_full_attn_capture = False
if runner_assisted_full_attn_request is not None:
num_reqs_padded = runner_assisted_full_attn_request.num_reqs_padded
runner_assisted_full_attn_capture = runner_assisted_full_attn_request.for_cudagraph_capture
num_tokens_padded = max(
num_tokens_padded,
num_reqs_padded * max_num_scheduled_tokens,
)
runner_assisted_full_attn = runner_assisted_full_attn_request is not None
ubatch_slices, ubatch_slices_padded = maybe_create_ubatch_slices( ubatch_slices, ubatch_slices_padded = maybe_create_ubatch_slices(
should_ubatch, should_ubatch,
num_scheduled_tokens_np, num_scheduled_tokens_np,
@@ -1035,8 +1143,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
num_reqs_padded, num_reqs_padded,
self.parallel_config.num_ubatches, self.parallel_config.num_ubatches,
) )
pad_attn = cudagraph_mode == CUDAGraphMode.FULL or runner_assisted_full_attn
pad_attn = cudagraph_mode == CUDAGraphMode.FULL
use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0 use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0
ubatch_slices_attn = ubatch_slices_padded if pad_attn else ubatch_slices ubatch_slices_attn = ubatch_slices_padded if pad_attn else ubatch_slices
@@ -1058,11 +1165,18 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
num_tokens_unpadded=num_tokens_unpadded, num_tokens_unpadded=num_tokens_unpadded,
ubatch_slices=ubatch_slices_padded, ubatch_slices=ubatch_slices_padded,
) )
if runner_assisted_full_attn:
self._refresh_runner_assisted_full_attention_metadata_buffers(
num_reqs=num_reqs,
num_reqs_padded=num_reqs_padded,
num_scheduled_tokens_np=num_scheduled_tokens_np,
)
attn_num_reqs = num_reqs_padded if runner_assisted_full_attn else num_reqs
attn_metadata, spec_decode_common_attn_metadata = self._build_attention_metadata( attn_metadata, spec_decode_common_attn_metadata = self._build_attention_metadata(
num_tokens=num_tokens_unpadded, num_tokens=num_tokens_unpadded,
num_tokens_padded=num_tokens_padded if pad_attn else None, num_tokens_padded=num_tokens_padded if pad_attn else None,
num_reqs=num_reqs, num_reqs=attn_num_reqs,
num_reqs_padded=num_reqs_padded if pad_attn else None, num_reqs_padded=num_reqs_padded if pad_attn else None,
max_query_len=max_num_scheduled_tokens, max_query_len=max_num_scheduled_tokens,
ubatch_slices=ubatch_slices_attn, ubatch_slices=ubatch_slices_attn,
@@ -1071,6 +1185,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
num_scheduled_tokens=scheduler_output.num_scheduled_tokens, num_scheduled_tokens=scheduler_output.num_scheduled_tokens,
cascade_attn_prefix_lens=cascade_attn_prefix_lens, cascade_attn_prefix_lens=cascade_attn_prefix_lens,
slot_mappings=slot_mappings_by_group, slot_mappings=slot_mappings_by_group,
for_cudagraph_capture=runner_assisted_full_attn_capture,
) )
self._maybe_attach_attention_metadata_extensions( self._maybe_attach_attention_metadata_extensions(
attn_metadata=attn_metadata, attn_metadata=attn_metadata,
@@ -1079,6 +1194,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
max_query_len=max_num_scheduled_tokens, max_query_len=max_num_scheduled_tokens,
pad_attn=pad_attn, pad_attn=pad_attn,
num_scheduled_tokens_np=num_scheduled_tokens_np, num_scheduled_tokens_np=num_scheduled_tokens_np,
for_cudagraph_capture=runner_assisted_full_attn_capture,
) )
( (
@@ -1092,14 +1208,15 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
# Let the model adjust inputs before forward (e.g. restore input_ids # Let the model adjust inputs before forward (e.g. restore input_ids
# for multimodal position detection, fix decode position offsets). # for multimodal position detection, fix decode position offsets).
if hasattr(self.model, "prepare_runner_inputs"): prepare_runner_inputs = getattr(self.model, "prepare_runner_inputs", None)
input_ids, positions = self.model.prepare_runner_inputs( if callable(prepare_runner_inputs):
input_ids, positions = prepare_runner_inputs(
input_ids=input_ids, input_ids=input_ids,
positions=positions, positions=positions,
inputs_embeds=inputs_embeds, inputs_embeds=inputs_embeds,
req_ids=req_ids[:num_reqs], req_ids=req_ids[:num_reqs],
num_computed_tokens=[int(self.input_batch.num_computed_tokens_cpu[i]) for i in range(num_reqs)], num_computed_tokens=self.input_batch.num_computed_tokens_cpu[:num_reqs],
num_scheduled_tokens=[int(num_scheduled_tokens_np[i]) for i in range(num_reqs)], num_scheduled_tokens=num_scheduled_tokens_np[:num_reqs],
input_ids_buffer=self.input_ids.gpu[:num_tokens_padded], input_ids_buffer=self.input_ids.gpu[:num_tokens_padded],
) )
@@ -1108,46 +1225,60 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
# with CUDA graph capture. # with CUDA graph capture.
if self.calculate_kv_scales: if self.calculate_kv_scales:
cudagraph_mode = CUDAGraphMode.NONE cudagraph_mode = CUDAGraphMode.NONE
runner_assisted_full_attn = False
# Mark KV scales as calculated after the first forward pass # Mark KV scales as calculated after the first forward pass
self.calculate_kv_scales = False self.calculate_kv_scales = False
runner_assisted_context_enabled = False
if runner_assisted_full_attn:
runner_assisted_context_enabled = self._set_runner_assisted_full_attention_metadata_context(
enabled=True,
num_reqs=num_reqs,
)
runner_assisted_full_attn = runner_assisted_context_enabled
# Run the model. # Run the model.
# Use persistent buffers for CUDA graphs. # Use persistent buffers for CUDA graphs.
# When spec decode is enabled, defer connector finalization # When spec decode is enabled, defer connector finalization
# (wait_for_save + clear metadata) until after draft model runs. # (wait_for_save + clear metadata) until after draft model runs.
defer_kv_connector_finalize = self.speculative_config is not None defer_kv_connector_finalize = self.speculative_config is not None
with ( try:
nullcontext(), with (
set_forward_context( nullcontext(),
attn_metadata, set_forward_context(
self.vllm_config, attn_metadata,
num_tokens=num_tokens_padded, self.vllm_config,
num_tokens_across_dp=num_tokens_across_dp, num_tokens=num_tokens_padded,
cudagraph_runtime_mode=cudagraph_mode, num_tokens_across_dp=num_tokens_across_dp,
batch_descriptor=batch_desc, cudagraph_runtime_mode=(CUDAGraphMode.FULL if runner_assisted_full_attn else cudagraph_mode),
ubatch_slices=ubatch_slices_padded, batch_descriptor=batch_desc,
slot_mapping=slot_mappings, # OMNI: required for KV cache operations ubatch_slices=ubatch_slices_padded,
), slot_mapping=slot_mappings, # OMNI: required for KV cache operations
record_function_or_nullcontext("gpu_model_runner: forward"), ),
self.maybe_get_kv_connector_output( record_function_or_nullcontext("gpu_model_runner: forward"),
scheduler_output, self.maybe_get_kv_connector_output(
defer_finalize=defer_kv_connector_finalize, scheduler_output,
) as kv_connector_output, defer_finalize=defer_kv_connector_finalize,
): ) as kv_connector_output,
model_output = self._model_forward( ):
input_ids=input_ids, model_output = self._model_forward(
positions=positions, input_ids=input_ids,
intermediate_tensors=intermediate_tensors, positions=positions,
inputs_embeds=inputs_embeds, intermediate_tensors=intermediate_tensors,
**model_kwargs, inputs_embeds=inputs_embeds,
sampling_metadata=self.input_batch.sampling_metadata, **model_kwargs,
logits_index=logits_indices, sampling_metadata=self.input_batch.sampling_metadata,
sampler=self.sampler, logits_index=logits_indices,
) sampler=self.sampler,
)
# [Omni] Map pending ropes metadata to req_ids. # [Omni] Map pending ropes metadata to req_ids.
if hasattr(self.model, "flush_pending_metadata"): flush_pending_metadata = getattr(self.model, "flush_pending_metadata", None)
self.model.flush_pending_metadata(list(req_ids)) if callable(flush_pending_metadata):
flush_pending_metadata(req_ids[:num_reqs])
finally:
if runner_assisted_context_enabled:
self._set_runner_assisted_full_attention_metadata_context(enabled=False)
with record_function_or_nullcontext("gpu_model_runner: postprocess"): with record_function_or_nullcontext("gpu_model_runner: postprocess"):
if self.use_aux_hidden_state_outputs: if self.use_aux_hidden_state_outputs:
@@ -0,0 +1,33 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
from collections.abc import Sequence
from typing import NamedTuple, Protocol
class RunnerAssistedFullAttentionMetadataRequest(NamedTuple):
num_reqs_padded: int
for_cudagraph_capture: bool
class RunnerAssistedAttentionMetadataProvider(Protocol):
"""Optional model hook for runner-built full attention metadata."""
def get_runner_assisted_full_attention_metadata_request(
self,
*,
req_ids: Sequence[str],
num_reqs: int,
num_scheduled_tokens: Sequence[int],
num_computed_tokens: Sequence[int],
max_num_scheduled_tokens: int,
) -> RunnerAssistedFullAttentionMetadataRequest | None: ...
def set_runner_assisted_full_attention_metadata_context(
self,
*,
enabled: bool,
num_reqs: int = 0,
) -> None: ...