Skip to content

[TRTLLM-17262][refactor] Split the disaggregated server's entry-point wrapper into prepare, run, finish and serialize steps - #21442

Draft
JunyiXu-nv wants to merge 6 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-disagg-entry-split
Draft

JunyiXu-nv wants to merge 6 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-disagg-entry-split

Conversation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator

Stacked on #21406 ([TRTLLM-17261][test] Add route, middleware-order and shim-identity snapshots and a CPU test seam for trtllm-serve): the first commits below belong to that PR and disappear from this diff when it merges; this PR's own change is the last 2 commits.

@coderabbitai summary

Description

Every OpenAI request on the disaggregated frontend, and every Anthropic /v1/messages request, goes through one closure, OpenAIDisaggServer._wrap_entry_point (tensorrt_llm/serve/openai_disagg_server.py). Upcoming disaggregated-serving work (client-disconnect watching, in-stream errors for streamed Responses, request tracing) all lands in that closure; splitting it first means each of those changes lands in a step of its own instead of growing one closure. This PR splits the closure, with no behavior change, into a composition of five methods:

Step Owns Raises or returns
_prepare_request total_requests, stream_requests / nonstream_requests; chat-template permission; conversation identity (mutates the request); RawRequestResponseHooks (queue-latency histogram, perf records) HTTPException(400) for a forbidden template
_run_entry_point the call into OpenAIDisaggregatedService whatever the service raises (aiohttp.ClientResponseError, CppExecutorError, UnusedChatTemplateKwargsError, ...)
_finish_request success: total_responses —
_finish_failed_request failure: _handle_exception, so http_exceptions or internal_errors, the SIGINT on CppExecutorError, the context-length envelope raises HTTPException, or returns JSONResponse / None
_serialize_response StreamingResponse (with the Responses stream guard) / JSONResponse(content=...model_dump(by_alias=True)) for the OpenAI routes serialization errors go to the failure path, as today
_wrap_entry_point composition, the disaggregated_request trace span, FastAPI's request model —

Where in-flight changes to this closure land after the split:

Some step parameters are not read yet (raw_req of _run_entry_point, req and hooks of the finish steps): they are the inputs the known upcoming changes to each step need, so those changes will not have to alter the signatures again.

OpenAIDisaggServer is subclassed outside this repository: NVIDIA NeMo-RL's OpenAIDisaggServerAdaptor constructs it as (config, req_timeout_secs=..., coordinator_url=None), adds middleware to self.app right after __init__, and overrides __call__(host, port, sockets=None), _wrap_entry_point and _handle_exception (counting on _perf_metrics_collector.http_exceptions). This PR keeps that whole surface and pins it three ways: a contract test (tests/unittest/disaggregated/test_disagg_server_contract.py, which builds a subclass of the same shape), the new OpenAIDisaggServer class docstring, and a tensorrt_llm/serve/AGENTS.md bullet. Changing any item of that surface needs the NeMo-RL owners' agreement first; the tests' failure messages say so.

Part of the trtllm-serve refactor (epic TRTLLM-17056, task TRTLLM-17262). Builds on the test seam of #21406 (the server factory the new tests build the disaggregated server with).

Behavior

No behavior change. The wrapper keeps the same trace span, the same try block and the same statement order: the counters run before the template check, total_responses before serialization, and a serialization failure still reaches _handle_exception, which is called while the exception is being handled (its traceback logging needs that). The commit structure separates the pin from the change: the first of the two commits adds the characterization tests, the contract test, the class docstring and the AGENTS.md bullet against the unsplit closure; the second performs the split. Both test files pass identically on both commits.

CI bookkeeping

  • Multi-GPU table: none (the file is already listed; openai_disagg_server.py changes, so the multi-GPU stages run and need the ci: full pre-merge approved label)
  • CODEOWNERS: none
  • legacy-files.txt: none (the five new methods live in the already-listed file; the two test files are new code, formatted by the default hooks)
  • Test lists: none to edit — both new test files are CPU-only (pytest.mark.cpu_only) and collected by the existing unittest/disaggregated entry of tests/integration/test_lists/test-db/l0_cpu.yml

Test Coverage

New, both collected by the unittest/disaggregated entry of l0_cpu.yml and run on the tests-only commit and on the final commit with identical results:

  • tests/unittest/disaggregated/test_disagg_entry_point.py (13 tests): per-request counters for non-streaming, streaming, Anthropic and Responses requests; chat-template 400 before any worker; conversation identity from the headers; upstream error status kept; the context-length worker envelope; None after an engine fatal error (SIGINT recorded); unexpected errors as 500; serialization failures reaching the failure path; exactly one disaggregated_request span per request; the streamed Responses relay guarded; text.format.schema serialized under its wire name (by_alias).
  • tests/unittest/disaggregated/test_disagg_server_contract.py (6 tests): the NeMo-RL subclass surface above, exercised through a subclass of the same shape.

Commands run locally (CPU devel container, no GPU):

  • pytest -m cpu_only tests/unittest/disaggregated/test_disagg_entry_point.py tests/unittest/disaggregated/test_disagg_server_contract.py -q — 19 passed on the tests-only commit and on the final commit.
  • pytest -m cpu_only tests/unittest/disaggregated -q — 1415 passed, 7 failed on this branch; the same run on the base commit gives 1402 passed, 9 failed, and every failure on this branch also fails on the base (4 TestEtcdClusterStorage cases that spawn a real etcd on a port shared with other test processes on the box, 3 test_router.py cases that need LLM_MODELS_ROOT, which that run did not set; they pass 4/4 when rerun with it); no new failure. (8 TestHttpClusterStorage errors in one run were port-18000 collisions with another test process on the shared box; the class passes 8/8 when rerun alone.)
  • LLM_MODELS_ROOT=... PYTHONPATH=tests python3 -m test_common.serve.cpu_tests (every serve CPU test, one pytest process per test-db entry, 66 entries) — all entries green except the 4 pre-existing TestEtcdClusterStorage cases above and unittest/grpc/openengine/test_frontend_lifecycle.py::test_frontend_group_cleanup[launcher-death], a process-lifecycle test unrelated to this change (no gRPC file is touched) that passes 8/8 when rerun alone. The route, middleware-order, shim-identity, layering and object.__new__ ratchet checks are among the green entries.
  • pytest -m cpu_only tests/unittest/llmapi/apps/test_anthropic_routes.py tests/unittest/llmapi/apps/test_context_length_error.py -q — 58 passed.
  • pytest tests/unittest/api_stability -q — 98 passed.
  • pytest -m cpu_only tests/unittest/serve/test_agents_md.py -q — 6 passed (the new AGENTS.md bullet names only real paths).
  • python scripts/legacy_utils.py lint-precommit tensorrt_llm/serve/openai_disagg_server.py — no new violations.
  • pre-commit run --files <changed files> — clean on the second run.
  • Negative check (local only, reverted): making the failure step call _handle_exception through the class instead of self fails test_every_failure_reaches_the_overridden_handle_exception with the NeMo-RL message.

GPU coverage is left to CI: the single-GPU disaggregated block of l0_a10.yml (in particular test_disaggregated_single_gpu, test_disaggregated_perf_metrics, test_disaggregated_chat_completion_tool_calls) and the multi-GPU disaggregated stages; openai_disagg_server.py is on the multi-GPU trigger list, so those stages need the ci: full pre-merge approved label.

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

🤖 Generated with Claude Code

…apshots and a CPU test seam for trtllm-serve

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… baselines at the merge base

The route reference is generated from the live apps; the object.__new__
baseline and the gRPC import allowance record what exists on main today
and only shrink.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…n the snapshot matrix

An attached frontend of a multi-frontend server registers ten launcher
forwarding routes before the local table; seven share a method and path
with local handlers and three use a path parameter that matches every
batch id, so registration order decides which handler serves them. The
new configuration records all of it, a live invariant pins the
forwarding-before-local order independently of the reference, and the
route-class failure message now says why the msgpack route class matters.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
- AGENTS.md: the CPU test command sets LLM_MODELS_ROOT, which the runner
  requires, and the stage-mapping note says the mapper matches list-entry
  text, so a file listed through its directory maps by the directory.
- check_contracts matches each from-import as <module>.<name>, so
  'from pkg import sub' and 'from .. import sub' count as imports of the
  submodule; new test for both forms and for the allowance entry.
- The VisualGen /v1/models test asserts the 200 status.
- The AGENTS.md command check also recognizes commands that start with
  LLM_MODELS_ROOT=.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…s outside-subclass contract

Every OpenAI request on the disaggregated frontend, and every Anthropic
/v1/messages request, goes through OpenAIDisaggServer._wrap_entry_point.
Before reorganizing that closure, pin what it does today:

- tests/unittest/disaggregated/test_disagg_entry_point.py pins the
  per-request counters, the chat-template 400, the conversation identity
  from the headers, the serialization of streaming and non-streaming
  responses (including the guarded streamed Responses relay and
  model_dump(by_alias=True)), the error mapping through
  _handle_exception, and the single disaggregated_request trace span.
- tests/unittest/disaggregated/test_disagg_server_contract.py pins the
  surface NeMo-RL's OpenAIDisaggServerAdaptor subclass relies on: the
  constructor (config, req_timeout_secs=..., coordinator_url=None),
  self.app with every route registered when __init__ returns,
  __call__(host, port, sockets=None), _wrap_entry_point,
  _handle_exception and _perf_metrics_collector.http_exceptions.
- The OpenAIDisaggServer class docstring and tensorrt_llm/serve/AGENTS.md
  name that surface, so a change to it is agreed with the NeMo-RL owners
  first.

Both files are CPU-only and collected by the unittest/disaggregated
entry of tests/integration/test_lists/test-db/l0_cpu.yml.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…into prepare, run, finish and serialize steps

No behavior change. The body of the closure OpenAIDisaggServer builds in
_wrap_entry_point becomes a composition of five methods, so that later
changes (disconnect watching, in-stream errors, request trace) land in
the step they belong to instead of growing one closure:

- _prepare_request: the request counters, the chat-template permission
  (HTTPException 400), the conversation identity, the per-request hooks
- _run_entry_point: the call into the disaggregated service
- _finish_request: success accounting (total_responses)
- _finish_failed_request: every failure, through _handle_exception while
  the exception is being handled (its traceback logging needs that)
- _serialize_response: StreamingResponse with the Responses stream guard,
  or JSONResponse(content=...model_dump(by_alias=True)), as before

The trace span stays on the wrapper that FastAPI calls; the steps open no
spans of their own. The statements keep their order inside the same try
block: the counters run before the template check, total_responses before
serialization, and a serialization failure still reaches
_handle_exception. Some step parameters (raw_req of _run_entry_point, req
and hooks of the finish steps) are not read yet; they are the inputs the
known upcoming changes to each step need, so those changes will not have
to alter the signatures again.

The characterization tests and the subclass contract tests of the
previous commit pass unchanged on both sides of the split.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant