Skip to content

[Core] Flag pipelines with no terminal output stage in PipelineConfig.validate() - #6291

Merged
alex-jw-brooks merged 1 commit into
vllm-project:mainfrom
m0g3r:fix/4560-pipeline-terminal-stage-validation
Aug 30, 2026
Merged

alex-jw-brooks merged 1 commit into
vllm-project:mainfrom
m0g3r:fix/4560-pipeline-terminal-stage-validation

Conversation

@m0g3r

@m0g3r m0g3r commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Purpose

Partially addresses #4560 — the first of the two gaps listed there.

PipelineConfig.validate() (vllm_omni/config/stage_config.py) already reports an empty pipeline, duplicate stage IDs, dangling and self-referential input_sources, and a missing entry point. It accepts a pipeline in which no stage declares final_output=True.

That topology cannot work. Both request completion and client-visible output are driven by stages that declare final_output, in Orchestrator._route_output:

if (
    finished
    and self.stage_pools[stage_id].final_output
    and not (req_state.streaming.enabled and req_state.streaming.segment_finished)
):
    req_state.finished_final_output_stage_ids.add(stage_id)
    ...
    request_finished = final_output_stage_ids.issubset(req_state.finished_final_output_stage_ids)
...
if self.stage_pools[stage_id].final_output and not is_duplex_stage0_segment:
    await self.output_async_queue.put(OutputMessage(...))

With no final_output stage anywhere, neither branch is ever taken: no OutputMessage is emitted and request_finished never becomes true, so every request hangs. validate() returns [] for it and register_pipeline logs nothing.

Repro (the one from the issue, unchanged, against current main):

from vllm_omni.config.stage_config import (
    PipelineConfig, StagePipelineConfig, StageExecutionType as E,
)

mk = lambda sid, srcs, final=False: StagePipelineConfig(
    stage_id=sid, model_stage=f"s{sid}", execution_type=E.LLM_AR,
    input_sources=tuple(srcs), final_output=final,
)

nof = PipelineConfig(model_type="nofinal_demo", stages=(mk(0, []), mk(1, [0])))
print(nof.validate())   # main: []   this branch: ['No terminal stage (stage with final_output=True)']

What this changes

  1. PipelineConfig.validate() gains the missing check (5 lines). It is cycle-agnostic, per the issue's explicit non-goal: it asserts no acyclicity, does not detect cycles, and does not require the terminal stage to be last — so it stays compatible with the circular-stage work in [RFC]: Support circular dependencies between stages #831. Any stage may carry final_output.

  2. An L1 CPU guard that every statically registered in-tree pipeline passes validate(). validate() runs only from register_pipeline, which in-tree pipelines never go through, so today a shipped topology error stays invisible until serving hangs. Resolver entries are skipped — they need an hf_config to produce a PipelineConfig. Happy to drop this test if you'd rather keep the diff to the validator alone.

  3. TestPipelineConfigNew.test_validate_valid is corrected. Its fixture was a two-stage pipeline with no terminal stage, asserting validate() == []. Its last stage is now final_output=True, so the "valid pipeline" fixture describes an actually-servable topology. That the repo's own valid-pipeline fixture was not servable is the gap this PR closes.

Not included, deliberately: the read-only topology-dump helper (entry stages / terminal stages / adjacency) that #4560 also proposes. That is new public API and a separate design decision, especially against the config refactors in #2072 / #4021 / #6200. Say the word and I'll add it here or in a follow-up.

Blast radius: the only production behaviour change is one extra string in the list register_pipeline logs for an out-of-tree pipeline that cannot emit output. No in-tree pipeline is affected — see the audit below.

Test Plan

L1 unit / CPU, per the vllm-omni-test skill. Both target modules already carry pytestmark = [pytest.mark.core_model, pytest.mark.cpu], so they are already collected by the Simple Unit Test step in test-ready.yml / test-merge.yml. No Buildkite YAML change is needed.

  • tests/config/test_config_factory.py::TestPipelineConfigNew
    • test_validate_no_terminal_stage — new; the regression test, fails without the fix
    • test_validate_terminal_stage_need_not_be_last — new; pins the cycle-agnostic contract
    • test_validate_valid — fixture corrected
  • tests/config/test_pipeline_registry.py::test_in_tree_pipelines_pass_topology_validation — new guard
cd tests
# whole modules
pytest -s -v config/test_config_factory.py config/test_pipeline_registry.py -m "core_model and cpu"
# just the new regression test
pytest -s -v config/test_config_factory.py::TestPipelineConfigNew::test_validate_no_terminal_stage
# CI-equivalent L1 sweep
pytest -s -v -m "core_model and cpu"

vLLM Version: 0.27.0 (as pinned by this branch's base; not installed on the machine used here — see below)

vLLM-Omni Commit: 2fdbf2234aeb76715618a5b236f0016f115e3e64

Test Result

Checks that were run

Check Result
Red/green of the committed test bodies 3 passed / 1 failed before the fix → 4 passed / 0 failed after
Real validate() against the issue's repro [] before → ['No terminal stage (stage with final_output=True)'] after
Every in-tree PipelineConfig re-validated with the patched checker 57 definitions, 0 failures, 0 skipped
pre-commit run --files <the 3 changed files> all hooks Passed (ruff-check, ruff-format, typos, end-of-file-fixer, mixed-line-ending, trailing-whitespace, debug-statements, check-test-ci-coverage, check-pickle-imports)
python -m py_compile on the 3 changed files (3.12.13) OK
precheck-pr code-quality sweep (5 diff-scoped patterns) 0 findings
precheck-pr examples-policy sweep no examples/** paths touched
L1 mocking rule (mocker/monkeypatch only) not applicable — the new tests use no mocks

Red/green was measured by lifting the committed test methods out of tests/config/test_config_factory.py with ast and running them against two sources: git show HEAD:vllm_omni/config/stage_config.py (pre-fix) and the patched file. So the failure is on the code that ships, not on a paraphrase of it.

BEFORE FIX (pre-fix stage_config.py from git):
  PASS  test_validate_valid
  PASS  test_validate_no_stages
  FAIL  test_validate_no_terminal_stage
  PASS  test_validate_terminal_stage_need_not_be_last
  -> 3 passed, 1 failed

AFTER FIX (this branch):
  PASS  test_validate_valid
  PASS  test_validate_no_stages
  PASS  test_validate_no_terminal_stage
  PASS  test_validate_terminal_stage_need_not_be_last
  -> 4 passed, 0 failed

For the in-tree guard, every literal PipelineConfig(...) in vllm_omni/ was rebuilt and re-validated with the patched checker (including ming_tts_moe, which reuses MING_TTS_PIPELINE.stages):

validated 57 in-tree PipelineConfig definitions with the PATCHED validate()
skipped (non-literal stages): 0
failures: 0

That is a superset of what test_in_tree_pipelines_pass_topology_validation asserts, which covers only the PipelineConfig instances actually in OMNI_PIPELINES. So the new guard should be green in CI, and no shipped pipeline regresses.

Checks that could NOT be run locally, and why

  • pytest was not run on this machine. vLLM 0.27 has no usable macOS arm64 wheel, so tests/config/test_config_factory.py and tests/config/test_pipeline_registry.py cannot even be imported here (vllm_omni pulls in vllm). The red/green above comes from a dependency-isolated harness that stubs only this module's external imports (transformers, vllm.logger, vllm.v1.core.sched.scheduler, vllm_omni.config.endpoint_policy, vllm_omni.config.yaml_util, the two omni schedulers) and then executes the real vllm_omni/config/stage_config.py source, so PipelineConfig.validate() under test is the genuine implementation. It is not a substitute for a CI pytest run, and py_compile is not a test pass.
  • No GPU / L2–L4 runs. Not applicable to this change: it is pure config-topology logic with no model, device, or serving path.
  • The in-tree pipeline audit reconstructs pipelines from source with ast rather than importing OMNI_PIPELINES, for the same dependency reason. Resolver-produced pipelines are outside both that audit and the new test.

Please re-run the L1 CPU suite in CI; if the guard test surfaces anything I could not see from static reconstruction, I'll fix it here.


Disclosure: this change was implemented with AI coding-agent assistance, using the repository's own vllm-omni-test and precheck-pr skills as instructed in docs/contributing/README.md. Every command and result reported above was actually executed; the one check I could not run locally (pytest) is named explicitly rather than implied to have passed.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@vllm-omni-review-bot

Copy link
Copy Markdown

This PR appears to belong to: docs/design/module/vllm_omni_config.md, docs/design/module/stage_runtime.md, docs/design/module/entrypoints.md.

Module owners: @lishunyang12 @alex-jw-brooks @tzhouam @fake0fan @linyueqian @NickCao

@m0g3r, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer.

Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment.

….validate()

`PipelineConfig.validate()` already rejects an empty pipeline, duplicate stage
IDs, dangling and self-referential `input_sources`, and a missing entry point,
but it accepts a pipeline in which no stage declares `final_output=True`.

Request completion and client-visible output are both driven by stages that
declare `final_output` (`Orchestrator._route_output`), so such a pipeline can
never emit a result and every request hangs. Today `validate()` returns `[]`
for it, and `register_pipeline` logs nothing.

Add the missing check. It is cycle-agnostic: it makes no acyclicity assumption
and does not detect cycles, so it stays compatible with the circular-stage work
in vllm-project#831. Any stage may carry `final_output`, not only the last one.

Also add an L1 CPU guard that every statically registered in-tree pipeline
passes `validate()`. `validate()` runs only on `register_pipeline`, which
in-tree pipelines never go through, so a shipped topology error would otherwise
stay invisible until serving hangs.

`TestPipelineConfigNew.test_validate_valid` described a two-stage pipeline with
no terminal stage; its last stage is now marked `final_output=True` so the
fixture is an actually-servable topology.

Partially addresses vllm-project#4560 (the validation gap). The read-only topology-dump
helper in that issue is deliberately not included: it is new public API and a
separate design decision.

Signed-off-by: m0g3r <87276771+m0g3r@users.noreply.github.com>
@m0g3r
m0g3r force-pushed the fix/4560-pipeline-terminal-stage-validation branch from a894328 to e1dced3 Compare August 30, 2026 19:53

@alex-jw-brooks alex-jw-brooks left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix, looks good to me!

@alex-jw-brooks
alex-jw-brooks enabled auto-merge (squash) August 30, 2026 21:11
@alex-jw-brooks alex-jw-brooks added the ready label to trigger buildkite CI label Aug 30, 2026
@alex-jw-brooks
alex-jw-brooks merged commit 8fe913e into vllm-project:main Aug 30, 2026
7 of 9 checks passed
JoseCarlosGarcia95 pushed a commit to valendra-tech/vllm-omni that referenced this pull request Sep 5, 2026
….validate() (vllm-project#6291)

Signed-off-by: m0g3r <87276771+m0g3r@users.noreply.github.com>
khairulkabir1661 pushed a commit to khairulkabir1661/vllm-omni that referenced this pull request Sep 25, 2026
….validate() (vllm-project#6291)

Signed-off-by: m0g3r <87276771+m0g3r@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core related to core module: cache, scheduler, engine, worker, modelrunner ready label to trigger buildkite CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants