diff --git a/docs/configuration/stage_configs.md b/docs/configuration/stage_configs.md index 66113a5f9..855390739 100644 --- a/docs/configuration/stage_configs.md +++ b/docs/configuration/stage_configs.md @@ -59,7 +59,7 @@ The new deploy schema lives under `vllm_omni/deploy/` and is paired with a froze | `edges` | list | optional | `null` | Explicit edge list for the KV transfer graph. Auto-derived from stage inputs if omitted. | | `stages` | list | optional | `[]` | Per-stage runtime overrides matched by `stage_id`. Pipeline stages are still created from `PipelineConfig` when this list is empty. | | `platforms` | dict | optional | `null` | Keyed by `npu` / `rocm` / `xpu`, each contains a `stages:` list with per-platform overrides applied on top of the CUDA defaults. | -| `pipeline` | str | optional | `null` | Override the auto-detected pipeline registry key (used for structural variants like `qwen2_5_omni_thinker_only`). | +| `pipeline` | str | optional | `null` | Override the auto-detected pipeline registry key (used for structural variants like `qwen2_5_omni_thinker_only` / `qwen3_omni_moe_thinker_only`). | | `trust_remote_code` | bool \| null | optional | `null` | **Pipeline-wide.** Trust HF remote code on model load; applies to every stage when specified. | | `distributed_executor_backend` | str \| null | optional | `null` | **Pipeline-wide.** Distributed executor backend forwarded to vLLM (`"mp"`, `"ray"`, `"external_launcher"`). If omitted, vLLM auto-selects backend from runtime topology. | | `dtype` | str \| null | optional | `null` | **Pipeline-wide.** Model dtype for every stage. | diff --git a/docs/user_guide/examples/online_serving/qwen3_omni.md b/docs/user_guide/examples/online_serving/qwen3_omni.md index 6ef10b5fd..f92463f84 100644 --- a/docs/user_guide/examples/online_serving/qwen3_omni.md +++ b/docs/user_guide/examples/online_serving/qwen3_omni.md @@ -28,6 +28,18 @@ vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \ --deploy-config /path/to/deploy_config_file ``` +To serve **thinker-only** (text output, no talker / code2wav loaded) with Instruct +weights, pass the bundled thinker-only deploy YAML. The `pipeline:` key +`qwen3_omni_moe_thinker_only` overrides the HF `enable_audio_output` resolver: + +```bash +vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \ + --deploy-config vllm_omni/deploy/qwen3_omni_moe_thinking.yaml +``` + +Captioner / Thinking checkpoints (`enable_audio_output=false`) still auto-select +the same single-stage pipeline without `--deploy-config`. + ### Launch individual stages (stage-based CLI) Adopt the stage-based CLI architecture to independently instantiate execution processes per functional stage. diff --git a/examples/online_serving/qwen3_omni/README.md b/examples/online_serving/qwen3_omni/README.md index 0403790cc..926a58682 100644 --- a/examples/online_serving/qwen3_omni/README.md +++ b/examples/online_serving/qwen3_omni/README.md @@ -24,6 +24,18 @@ vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \ --deploy-config /path/to/your_deploy_config.yaml ``` +To serve **thinker-only** (text output, no talker / code2wav loaded) with Instruct +weights, pass the bundled thinker-only deploy YAML. The `pipeline:` key +`qwen3_omni_moe_thinker_only` overrides the HF `enable_audio_output` resolver: + +```bash +vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \ + --deploy-config vllm_omni/deploy/qwen3_omni_moe_thinking.yaml +``` + +Captioner / Thinking checkpoints (`enable_audio_output=false`) still auto-select +the same single-stage pipeline without `--deploy-config`. + For a 3x-GPU multi-replica layout (talker/code2wav scale-out on cuda:1,2), use `--stage-overrides` on top of the default config: diff --git a/tests/config/test_config_factory.py b/tests/config/test_config_factory.py index 5f7359ddc..bc820cbab 100644 --- a/tests/config/test_config_factory.py +++ b/tests/config/test_config_factory.py @@ -534,6 +534,7 @@ class TestPipelineDiscovery: """Check that specific models are in OMNI_PIPELINES.""" assert "qwen2_5_omni" in OMNI_PIPELINES assert "qwen3_omni_moe" in OMNI_PIPELINES + assert "qwen3_omni_moe_thinker_only" in OMNI_PIPELINES assert "qwen3_tts" in OMNI_PIPELINES def test_registry_resolver_qwen3_omni_all_stages(self): @@ -558,6 +559,18 @@ class TestPipelineDiscovery: assert pipeline.default_deploy_config_name is None assert len(pipeline.stages) == 1 # thinker only + def test_registry_qwen3_omni_thinker_only_key_is_static(self): + """Explicit registry key must not re-enter the HF-config resolver.""" + pipeline = resolve_pipeline_config( + "qwen3_omni_moe_thinker_only", + Q3_OMNI_ALL_STAGES_HF_CONFIG, + ) + assert pipeline is OMNI_PIPELINES["qwen3_omni_moe_thinker_only"] + assert pipeline.model_type == "qwen3_omni_moe_thinker_only" + assert len(pipeline.stages) == 1 + assert pipeline.stages[0].engine_output_type == "text" + assert pipeline.stages[0].final_output_type == "text" + @pytest.mark.parametrize( "pipeline", [registered for registered in OMNI_PIPELINES.values() if isinstance(registered, PipelineConfig)], @@ -1225,14 +1238,22 @@ stages: assert deploy.connectors is not None assert deploy.platforms is not None - def test_qwen3_omni_thinker_only_deploy_config_uses_resolved_pipeline(self): + @pytest.mark.parametrize( + ("hf_config", "model"), + [ + (Q3_OMNI_THINKER_HF_CONFIG, "Qwen/Qwen3-Omni-30B-A3B-Thinking"), + (Q3_OMNI_ALL_STAGES_HF_CONFIG, "Qwen/Qwen3-Omni-30B-A3B-Instruct"), + ], + ids=["thinker_checkpoint", "instruct_forced_thinker_only"], + ) + def test_qwen3_omni_thinker_only_deploy_config_uses_resolved_pipeline(self, hf_config, model): deploy_path = Path(get_deploy_config_path("qwen3_omni_moe_thinking.yaml")) with patch( "vllm_omni.config.config_factory.get_config", - return_value=Q3_OMNI_THINKER_HF_CONFIG, + return_value=hf_config, ): pipeline = StageConfigFactory.get_pipeline_config( - "Qwen/Qwen3-Omni-30B-A3B-Thinking", + model, trust_remote_code=False, deploy_config_path=str(deploy_path), ) @@ -1241,6 +1262,7 @@ stages: assert pipeline.model_type == "qwen3_omni_moe_thinker_only" deploy = load_deploy_config(deploy_path) + assert deploy.pipeline == "qwen3_omni_moe_thinker_only" stages = merge_pipeline_deploy(pipeline, deploy) assert len(stages) == 1 diff --git a/vllm_omni/config/pipeline_registry.py b/vllm_omni/config/pipeline_registry.py index 47b649e85..260f84862 100644 --- a/vllm_omni/config/pipeline_registry.py +++ b/vllm_omni/config/pipeline_registry.py @@ -97,7 +97,10 @@ from vllm_omni.model_executor.models.qwen2_5_omni.pipeline import ( QWEN2_5_OMNI_PIPELINE, QWEN2_5_OMNI_THINKER_ONLY_PIPELINE, ) -from vllm_omni.model_executor.models.qwen3_omni.pipeline import resolve_qwen3_omni_pipeline +from vllm_omni.model_executor.models.qwen3_omni.pipeline import ( + QWEN3_OMNI_THINKER_ONLY_PIPELINE, + resolve_qwen3_omni_pipeline, +) from vllm_omni.model_executor.models.qwen3_tts.pipeline import QWEN3_TTS_PIPELINE from vllm_omni.model_executor.models.step_audio2.pipeline import ( STEP_AUDIO2_ASR_PIPELINE, @@ -123,6 +126,7 @@ OMNI_PIPELINES: dict[str, PipelineConfig | PipelineResolverFunc] = { # has no model_type key). "nemotron_labs_voicechat": NEMOTRON_VOICECHAT_PIPELINE, "qwen3_omni_moe": resolve_qwen3_omni_pipeline, + "qwen3_omni_moe_thinker_only": QWEN3_OMNI_THINKER_ONLY_PIPELINE, "qwen3_tts": QWEN3_TTS_PIPELINE, "step_audio_2": STEP_AUDIO2_PIPELINE, "step_audio_2_asr": STEP_AUDIO2_ASR_PIPELINE, diff --git a/vllm_omni/deploy/qwen3_omni_moe_thinking.yaml b/vllm_omni/deploy/qwen3_omni_moe_thinking.yaml index 7ccc64750..d0b3d5ac7 100644 --- a/vllm_omni/deploy/qwen3_omni_moe_thinking.yaml +++ b/vllm_omni/deploy/qwen3_omni_moe_thinking.yaml @@ -1,6 +1,8 @@ -# Deploy config for Qwen3-Omni-MoE thinker-only checkpoints. +# Deploy config for Qwen3-Omni-MoE thinker-only (text output, no talker/code2wav). +# Works for Captioner/Thinking checkpoints and for Instruct weights when this +# YAML is passed via --deploy-config (pipeline key forces the single-stage topology). # Verified on 2x H100-80G GPUs. -pipeline: qwen3_omni_moe +pipeline: qwen3_omni_moe_thinker_only distributed_executor_backend: mp enable_prefix_caching: true