Repository navigation
[TRTLLM-17269][refactor] Extract the shared HTTP app assembly into serve/_base/app_builder.py - #21556
Draft
JunyiXu-nv wants to merge 15 commits into
Draft
JunyiXu-nv wants to merge 15 commits into
JunyiXu-nv wants to merge 15 commits into
Conversation
…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>
… and disaggregated servers Create tensorrt_llm/serve/middleware/ and move ServerArrivalTimeMiddleware (from serve/responses_utils.py), PerfMetricsMiddleware (from serve/perf_metrics.py) and _apply_fastapi_middlewares (from commands/serve.py) into it, verbatim. Both servers now install their built-in middleware through serve/middleware/register.py::register_middlewares, which reproduces each app's stack exactly: the aggregated server passes adjusted_clock to both middlewares, the disaggregated server to neither. User --middleware entries are still applied after construction and stay outermost. The old import paths are removed with no compatibility shim; no code outside the repository imports them. commands/serve.py keeps an import of _apply_fastapi_middlewares for its own two launcher calls. CI bookkeeping in the same change: the multi-GPU trigger table gains tensorrt_llm/serve/middleware/; legacy-files.txt registers middleware/arrival_time.py and middleware/user_middleware.py with the configs gen-configs regenerates; ruff-legacy-baseline.json carries the moved docstring's one D212; mypy checks the new register.py. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…g request state
Replace the assignment scope["state"] = {} with scope.setdefault("state", {})
in ServerArrivalTimeMiddleware.__call__. State already present when a request
reaches this middleware - written by a user --middleware entry, or copied in by
the ASGI server from a lifespan that yields a dict - is now kept, and
server_arrival_time is added to it. Before, that state was replaced by an empty
dict. Neither trtllm-serve app yields lifespan state today and the built-in
middleware order is unchanged, so no built-in path changes behavior.
Two tests pin the new behavior (state from an outer middleware and from a
lifespan survive); both fail on the previous commit.
Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…lm/serve/_base/msgspec_route.py Generated move: _msgpack_decoder, _MsgspecRequest and _MsgspecRoute leave tensorrt_llm/serve/openai_server.py byte for byte; the module imports _MsgspecRoute back for its app router and keeps no re-export (the three names are private). The two disaggregated test importers now import the new module. OpenAIServer.health, .version and .get_model are removed here and replaced by the system router added in the following commits. tensorrt_llm/serve/_base/msgspec_route.py joins legacy-files.txt (its code comes from a legacy-formatted file), and the generated lint configs are regenerated with scripts/legacy_utils.py gen-configs. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ve the system endpoints through it New structure: tensorrt_llm/serve/_base/ (shared serving infrastructure) and tensorrt_llm/serve/api/ (endpoint packages, one router plus one serving class each; the package __init__ imports nothing so routers load lazily, by role). serve/_base/routing.py provides make_router(), which always uses the msgpack-decoding route class, and attach_routers(), which imports and attaches only the routers whose RouterSpec matches the server role. The first endpoint package, serve/api/ops/system/, serves /health, /version and /v1/models for every role. OpenAIServer builds a SystemServing with the injected _health_probe (today's probe body, SIGINT side effect included) and attaches the routers before the role dispatch; the 12 add_api_route lines for the three paths leave the four role tables. Same routes, methods, route classes, bodies and conflict-relevant order: the recorded route reference is unchanged. The layering tests gain the new packages' contracts, the route-snapshot failure message points new endpoints at the router pattern, and the serve agent docs describe where a new endpoint goes. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…w serve packages tensorrt_llm/serve/_base/ and tensorrt_llm/serve/api/ops/ join the multi-GPU trigger list: the msgpack route class decodes the disaggregated frontend's worker requests, and /health is the disaggregated readiness probe, so changes under these directories keep running the multi-GPU stages that exercise that path. The three new modules join the strict mypy file set, and the type-check pre-commit hook now also triggers on changes under tensorrt_llm/serve/ and tensorrt_llm/commands/serve_cli/, so edits to typed serving modules get local type checking; the hook still checks only the declared file set. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… tests that build routes by hand Two multi-frontend tests build an OpenAIServer without its constructor and call register_routes() themselves. Since /health, /version and /v1/models are attached through the system router by the constructor, those tests no longer saw the three routes: one asserted that /version stays local on an attached frontend (404), the other reported the three per-frontend-safe entries as removed. Both helpers now set the serving object and attach the routers before the hand registration, in the constructor's order, and the route enumeration walks included routers, which FastAPI 0.139 keeps as objects without a path in app.routes. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…n the OpenEngine metrics test The HTTP-enablement case builds a real OpenAIServer through its constructor and looked for /prometheus/metrics by reading .path off every entry of app.routes. Since the constructor attaches the system router, FastAPI 0.139 keeps an included-router object there that has no .path, and the child process failed with AttributeError. The test now matches a GET /prometheus/metrics request against the routes with the public route-matching API, which works on both ends of the pinned FastAPI range. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…d name The check that endpoint packages under tensorrt_llm/serve/api do not import each other read the package off the from-module alone, so `from tensorrt_llm.serve import api` inside an endpoint package passed and `from tensorrt_llm.serve.api.ops import system` inside ops/system failed. It now reads the package off <module>.<name> per imported name, as the layering contracts do, and a synthetic-edge test pins both forms. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…eware package branch as the base for the shared app assembly Base for the shared app assembly: both open PRs it builds on, NVIDIA#21447 (shared middleware package for the aggregated and disaggregated servers) and NVIDIA#21448 (router skeleton and the ops.system endpoints), merged into one branch. Conflicts resolved by keeping both sides: the mypy file list in pyproject.toml and the import-direction contracts in tests/unittest/serve/test_layering.py. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ared assembly of the trtllm-serve apps tensorrt_llm/serve/_base/app_builder.py holds the two functions both servers build on instead of a shared base class. build_http_app() assembles a FastAPI app in one fixed order: the lifespan, the optional route class before any route exists, the exception handlers, the routers in order, and the built-in middleware last so it stays outermost. serve_uvicorn() runs the app through the telemetry-aware uvicorn server, with an optional task that runs next to the server and receives it. The module is typed (added to the mypy file list) and imports only FastAPI, Starlette, uvicorn, the standard library and serve/_telemetry.py. Its tests pin the assembly order, the route class, the middleware stack order, the lifespan and exception-handler pass-through, every uvicorn argument, and the import boundary. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…m-serve apps with build_http_app and serve_uvicorn OpenAIServer and OpenAIDisaggServer construct their FastAPI app through build_http_app() and run uvicorn through serve_uvicorn(). Every difference between the two apps stays an explicit argument at the call site: the msgpack-decoding route class only on the aggregated app; the adjusted clock for the aggregated middleware and none for the disaggregated one; "info" and the 5 s keep-alive for the aggregated server, logger.level and the configured keep-alive for the disaggregated one; the worker-registration task after start only on the aggregated server. Both middleware installers read the server's perf-metrics attributes when they run, after the routes, as the aggregated server did before. No behavior change. The validation handlers keep their bodies and are registered with app.add_exception_handler instead of the decorator; the aggregated server's role dispatch goes unchanged into _attach_role_routes(app), and its post-start closure becomes the method _register_after_serving(server). On the disaggregated app the two built-in middlewares are now added after the routes instead of before; nothing in its route registration adds middleware, so the resulting stack is the same and the middleware-order snapshot is unchanged. Tests: tests/unittest/serve/test_server_uvicorn_settings.py builds both servers through the test factory and pins what each hands to uvicorn (log level, keep-alive, sockets, the post-start task only on the aggregated server). The unit test that patched uvicorn through openai_disagg_server now patches it where serve_uvicorn looks it up and also checks the disaggregated log level. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
1 task done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #21447 ([TRTLLM-17262][refactor] Shared middleware package for the aggregated and disaggregated servers) and #21448 ([TRTLLM-17261][refactor] Add the trtllm-serve router skeleton and move /health, /version and /v1/models into ops.system): the first commits below belong to those two PRs (plus one merge commit that joins them, since they are independent of each other) and disappear from this diff when they merge; this PR's own change is the last 2 commits.
@coderabbitai summary
Description
Both trtllm-serve servers build their FastAPI app and run uvicorn with near-identical code that differs in a handful of deliberate details. This PR extracts that assembly into two shared functions in
tensorrt_llm/serve/_base/app_builder.pyinstead of a shared base class, and makes both servers use them:build_http_app(*, lifespan, routers, dialect_error_renderers, middleware, route_class=None)assembles an app in one fixed order:FastAPI(lifespan=...), the route class (before any route exists), the exception handlers, each router callable in order, and the built-in middleware last so it stays outermost (add_middlewareinserts at the front of the stack).serve_uvicorn(app, *, host, port, sockets=None, log_level, timeout_keep_alive, after_start=None)runs it through the telemetry-aware uvicorn server (serve/_telemetry.py);after_start, when given, runs as a task next to the server and receives it.OpenAIServer.__init__andOpenAIDisaggServer.__init__now callbuild_http_app; their__call__methods callserve_uvicorn. The validation handlers keep their bodies and are registered withapp.add_exception_handler(RequestValidationError, handler)instead of the decorator. The aggregated server's role dispatch moves unchanged into_attach_role_routes(app); its post-start closure becomes the method_register_after_serving(server)with the same body and the same startup-timer names. The disaggregated server gets_attach_routes(app), which callsregister_routes(). Both middleware installers are callables that read the server's perf-metrics attributes when they run, after the routes, as the aggregated server did before.Part of the trtllm-serve refactor (epic TRTLLM-17056, task TRTLLM-17269: cross-cutting pieces and the Responses package). Builds on #21447 (the shared middleware registration both servers call) and #21448 (
serve/_base/,attach_routers, the system endpoints). The per-dialect error renderers land next in thedialect_error_renderersslot, and the eventual composition root calls the same two functions.Behavior
No behavior change.
Every difference between the two apps stays an explicit argument at the call site:
openai_server.py)openai_disagg_server.py)route_class=_MsgspecRoute(everyadd_api_routeroute decodes msgpack)APIRouteregister_middlewares(..., adjusted_clock=...)for both middlewaresregister_middlewares(...)without a clockmount_metrics()insideregister_routes, unchangedapp.mount("/prometheus/metrics", ...)insideregister_routes, unchangedlog_level"info"logger.level, passed through unchangedTIMEOUT_KEEP_ALIVE(5 s)config.server_keep_alive_timeoutafter_start=self._register_after_serving(worker registration with the startup timers)--middlewareSnapshots, unchanged for every configuration (FastAPI 0.136.3 and 0.139.2):
tests/unittest/api_stability/test_serve_routes.py,trtllm_serve_routes.yaml): llm, llm_context, llm_generation, llm_gpt_oss, llm_gpt_oss_harmony_disabled, llm_governor, llm_rl_control, llm_runtime_control, llm_rl_and_runtime_control, llm_perf_metrics, llm_perf_metrics_enable_metrics_env, llm_perf_metrics_output_dir_only, llm_render_endpoints, llm_multi_frontend_attached, mm_encoder, mm_encoder_rl_control, embedding, embedding_runtime_control, disagg_local, disagg_local_perf_metrics, disagg_local_http_cluster_storage, disagg_fleet, disagg_fleet_perf_metrics, disagg_fleet_http_cluster_storage, coordinator, coordinator_http_cluster_storage, visual_gen;tests/unittest/serve/test_middleware_order.py): llm, mm_encoder, embedding, llm_perf_metrics, llm_perf_metrics_output_dir_only, llm_perf_metrics_enable_metrics_env (with the Prometheus instrumentator middleware), disagg_local, disagg_fleet, disagg_local_perf_metrics, disagg_fleet_perf_metrics, coordinator.Beyond the snapshots, a fingerprint of every assembled app across those 27 configurations (route class, endpoint identity, response class, middleware classes and keyword identities against the server's own attributes, exception-handler identities, lifespan identity,
app.statekeys, router route class) is identical before and after this change, and so is everything each__call__hands to uvicorn.The surface that NeMo-RL's subclass of
OpenAIDisaggServerrelies on is unchanged (constructor shape,self.appwith every route registered when__init__returns,__call__(host, port, sockets=None),_wrap_entry_point,_handle_exception,_perf_metrics_collector): the subclassing contract test of #21442,tests/unittest/disaggregated/test_disagg_server_contract.py, passes against this branch (6/6) exactly as at the base commit.OpenAIServer's constructor and.appare unchanged.Not included
serve/coordinator_server.py) and the standalone render app (serve/render/): the coordinator's dual-listener mode and the renderer's blockinguvicorn.rundo not share the aggregated/disaggregated shape, the coordinator file later moves unchanged with the other disaggregated-serving modules, and neither has middleware or validation handlers to share. Both are follow-up candidates.--middlewareCLI handling.asyncio.create_taskof the post-start task is kept as it was.Coordination with other work
tests/unittest/disaggregated/test_openai_disagg_server.pypatcheduvicorn.Config/uvicorn.Serverthroughtensorrt_llm.serve.openai_disagg_server.uvicorn. That module no longer calls uvicorn, so its now-unusedimport uvicorngoes (autoflake would strip it anyway) and the test patchestensorrt_llm.serve._base.app_builder.uvicorn, whereserve_uvicornlooks the names up. It also now asserts the disaggregated log level.build_http_appunchanged. [None][fix] serve: answer a request that fails validation with a readable error #20865 (readable validation errors) is open: the handler stays a closure here and is passed throughdialect_error_renderers; a handler that becomes a method is passed the same way. [None][feat] Support disagg serving for post training #19890 (disaggregated serving for post-training, draft) editsopenai_disagg_server.pytoo.openai_server.py,openai_disagg_server.pyor_telemetry.py; the construction block of both__init__methods and both__call__methods change shape here, so whichever lands second rebases its edits onto the new shape (handler registration viadialect_error_renderers, routes via_attach_role_routes/_attach_routes, uvicorn arguments viaserve_uvicorn).CI bookkeeping
tensorrt_llm/serve/_base/is listed by [TRTLLM-17261][refactor] Add the trtllm-serve router skeleton and move /health, /version and /v1/models into ops.system #21448; both server files are already listed).tensorrt_llm/serve/_base/app_builder.pyadded to[tool.mypy] filesinpyproject.toml.tests/unittest/serve/_base/test_app_builder.pyandtests/unittest/serve/test_server_uvicorn_settings.pyare collected by the existingunittest/serveentry ofl0_cpu.yml(CPU-Generic stages).For reviewers:
git blame -C -C tensorrt_llm/serve/openai_server.pyattributes the relocated handler and post-start bodies to their original commits.Test Coverage
tests/unittest/serve/_base/test_app_builder.py-> 8 passed: route class applies to routes added by routers and only when given; assembly order; built-in middleware outermost after router-added middleware; lifespan and exception-handler pass-through;serve_uvicornpasses host/port/log level/keep-alive/sockets through and schedulesafter_startwith the server; integer log level unchanged; no task whenafter_startis None; the module imports only FastAPI, Starlette, uvicorn, the standard library andserve/_telemetry.py.tests/unittest/serve/test_server_uvicorn_settings.py-> 2 passed: both servers built through the test factory; the aggregated server hands uvicornlog_level="info",timeout_keep_alive=TIMEOUT_KEEP_ALIVE, the sockets, and schedules_register_after_serving; the disaggregated frontend hands itlogger.level, the configured keep-alive, and schedules nothing.pytest -m cpu_only tests/unittest/serve tests/unittest/api_stability/test_serve_routes.py-> 164 passed (route snapshot, middleware-order snapshot, layering contracts, ratchet) on FastAPI 0.139.2; the route snapshot, middleware-order, uvicorn-settings, routing, system-endpoint, layering and disaggregated-server tests -> 131 passed on both FastAPI 0.136.3 and 0.139.2.pytest -m cpu_only tests/unittest/disaggregated-> 1411 passed, 1 skipped, 2 xfailed.tests/unittest/disaggregated/test_disagg_server_contract.py), run against this branch and against the base commit -> 6 passed each.l0_cpu.ymlentry (LLM_MODELS_ROOT=... PYTHONPATH=tests python3 -m test_common.serve.cpu_tests): 66 entries, exit 0, no failures on this branch (unittest/serve: 128 passed, 8 of them new; this run predates the review fixes, which addedtest_server_uvicorn_settings.py, one assertion, one docstring word and the lambda form of the aggregated middleware installer, all covered by the suites above). The same run at the base commit (before this change) had 11 load-induced timeouts while three other test runs shared the machine (8 intest_cluster_storage.py::TestHttpClusterStoragesetup, 2 intest_frontend_lifecycle.py, 1 intest_metrics.py::test_prometheus_contract[multiprocess]); every other entry's count is identical between the two runs. Re-run alone and sequentially: on the branch all three pass (test_cluster_storage.py53 passed,test_frontend_lifecycle.py14 passed,test_metrics.py7 passed); at the base commit the first two still failed the same way, with the cause visible in the output:TestHttpClusterStoragebinds the fixed port 18000 and got[Errno 98] address already in usefrom another concurrent test run on the shared machine, andtest_frontend_group_cleanupis a 90 s gRPC-readiness wait that ran at a load average above 12. Neither test touches the code this PR changes.pytest tests/unittest/api_stability-> 98 passed.bash scripts/run_mypy.sh(strict, repository config) -> Success: no issues found in 21 source files.pre-commit run --files <changed files>twice -> clean on the second run;python scripts/legacy_utils.py check-configs-> up to date.git grep -n "uvicorn.Config\|FastAPI(lifespan" -- tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_disagg_server.py-> nothing.after_start, server not fromcreate_uvicorn_server) were each detected.l0_a10.yml(msgpack requests through the route class) andtest_e2e.py::test_openai_misc_example[pytorch].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-compatibleorapi-breaking. Forapi-breaking, includeBREAKINGin 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