Skip to content

[TRTLLM-17267][refactor] Move the embeddings, mm_encoder and tokenize handlers into serving classes - #21575

Draft
JunyiXu-nv wants to merge 31 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-embeddings-mm-encoder
Draft

JunyiXu-nv wants to merge 31 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-embeddings-mm-encoder

Conversation

@JunyiXu-nv

@JunyiXu-nv JunyiXu-nv commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #21544 ("Split openai_protocol into a package") and #21445 ("Add public runtime accessors for the remaining serving-frontend private accesses"): the first commits below belong to those PRs and disappear from this diff when they merge; this PR's own change is the last 7 commits, on top of the merge of the two (d2e8afb843).

@coderabbitai summary

Description

Part of the trtllm-serve refactor (epic TRTLLM-17056, task TRTLLM-17267: move the remaining handlers off OpenAIServer into one serving class per endpoint). This PR moves the EMBEDDING role's POST /v1/embeddings, the MM_ENCODER role's POST /v1/chat/completions and the LLM roles' POST /_internal/tokenize off OpenAIServer into EmbeddingServing, MMEncoderServing and TokenizeServing under tensorrt_llm/serve/api/openai/{embeddings,mm_encoder,tokenize}/, each with an api_router.py created by make_router() and attached through ROUTERS for exactly the roles the route had. It creates the serving bases in tensorrt_llm/serve/_base/serving.py: BaseServing (holds create_error_response) and GenerateBaseServing (holds await_disconnected and _attach_multimodal_data; the base of the completions, chat and Responses serving classes that follow), and moves the module helper _record_generator_termination there. tensorrt_llm/serve/encode_batcher.py moves whole to tensorrt_llm/serve/api/openai/embeddings/batcher.py; the old module is deleted and every import of it is rewritten. The composition root (OpenAIServer.__init__) builds RenderResources once after _init_llm and injects it, constructs the serving objects inside the role branches, puts them on app.state, and attaches the routers after the role switch, where each router can check that its serving object exists.

Builds on #21448 (the router skeleton and ops.system), #21406 (route, middleware-order and shim-identity snapshots and the fixture factory), #21544 (the protocol package) and #21445 (the public runtime accessors that replaced the private engine reads in the moved code).

Why the tokenize route moves in this PR: the handler needs only the tokenizer and has no blocker beyond this PR's, and moving it now keeps the completions and chat moves to their large handlers. Why the composition root keeps self.embedding_batcher: the lifespan starts and stops the batcher and the health probe reports it; the attribute is bound to EmbeddingServing.embedding_batcher, the same object (tested).

Behavior

No behavior change. Every route keeps its path, method, msgpack route class and role set (the route snapshot and the middleware-order snapshot are unchanged). Notes a reviewer may want to check:

  • openai_mm_encoder no longer calls RenderResources.from_server(self) per request; it uses the RenderResources the composition root built once after _init_llm. Equivalent: every attribute from_server reads is set before __init__ returns and never reassigned, and harmony_adapter is never set on the MM_ENCODER role (only the harmony chat path sets it, on the LLM role).
  • A multimodal-encoder request whose result carries no embedding handle answers 400 today, not the 500 the handler builds for it: the nested create_mm_embedding_response returns the 500 error response, the handler then calls .model_dump() on it, and the AttributeError is answered by the handler's except Exception as a 400. The new test pins today's 400; the fix is a separate change (see "Coordination notes").
  • FastAPI's validation-error body ({"error": ...}) embeds the route function's source location; that text changes with any move and is not a contract.

Moves

Pure-move check (head `600bcd503b`, run from a clean checkout)
## Move spec

```yaml
file_moves:
- from: tensorrt_llm/serve/encode_batcher.py
  to: tensorrt_llm/serve/api/openai/embeddings/batcher.py
  group: A
  shim: false
moves:
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/_base/serving.py
  group: B
  append: true
  symbols:
  - _record_generator_termination
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/_base/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: BaseServing
    names:
    - create_error_response
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/_base/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: GenerateBaseServing
    names:
    - _attach_multimodal_data
    - await_disconnected
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/openai/embeddings/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: EmbeddingServing
    names:
    - _init_embedding_batcher
    - _normalize_embedding_input
    - openai_embedding
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/openai/mm_encoder/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: MMEncoderServing
    names:
    - openai_mm_encoder
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/openai/tokenize/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: TokenizeServing
    names:
    - tokenize
rewrites:
- symbol: tensorrt_llm/serve/api/openai/mm_encoder/serving.py::MMEncoderServing.openai_mm_encoder
  old: RenderResources.from_server(self)
  new: self._render_resources
borrowed:
- name: create_error_response
  alias: staticmethod(BaseServing.create_error_response)
- name: _attach_multimodal_data
  alias: staticmethod(GenerateBaseServing._attach_multimodal_data)
- name: await_disconnected
  alias: GenerateBaseServing.await_disconnected
replaced:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_embedding_routes
  by: tensorrt_llm/serve/api/openai/embeddings/api_router.py::openai_embedding
  reason: its last route is served by the embeddings router; /health, /version and
    /v1/models are already served by serve/api/ops/system
added:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._generate_serving_kwargs
  reason: constructor arguments of every GenerateBaseServing, read once from the server
- symbol: tensorrt_llm/serve/_base/serving.py::BaseServing
  reason: serving base class; holds create_error_response
- symbol: tensorrt_llm/serve/_base/serving.py::GenerateBaseServing
  reason: base of the generate serving classes
- symbol: tensorrt_llm/serve/_base/serving.py::GenerateBaseServing.__init__
  reason: explicit dependencies instead of server attributes, so missing wiring fails
    at construction
- symbol: tensorrt_llm/serve/api/openai/embeddings/serving.py::EmbeddingServing
  reason: serving class of /v1/embeddings
- symbol: tensorrt_llm/serve/api/openai/embeddings/serving.py::EmbeddingServing.__init__
  reason: explicit dependencies; builds the batcher
- symbol: tensorrt_llm/serve/api/openai/mm_encoder/serving.py::MMEncoderServing
  reason: serving class of the multimodal-encoder /v1/chat/completions
- symbol: tensorrt_llm/serve/api/openai/mm_encoder/serving.py::MMEncoderServing.__init__
  reason: explicit dependencies, injected RenderResources
- symbol: tensorrt_llm/serve/api/openai/tokenize/serving.py::TokenizeServing
  reason: serving class of /_internal/tokenize
- symbol: tensorrt_llm/serve/api/openai/tokenize/serving.py::TokenizeServing.__init__
  reason: explicit dependencies
- symbol: tensorrt_llm/serve/api/openai/embeddings/api_router.py::*
  reason: new router module
- symbol: tensorrt_llm/serve/api/openai/mm_encoder/api_router.py::*
  reason: new router module
- symbol: tensorrt_llm/serve/api/openai/tokenize/api_router.py::*
  reason: new router module
modified:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.__init__
  reason: RenderResources built once; serving objects built in the role branches;
    attach_routers after the role switch
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_routes
  reason: /_internal/tokenize comes from the tokenize router
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_mm_encoder_routes
  reason: /v1/chat/completions comes from the multimodal-encoder router
- symbol: tensorrt_llm/serve/_base/routing.py::ROUTERS
  reason: the embeddings, multimodal-encoder and tokenize routers join the routing
    table for their role groups
rewrite_internal_imports: true

Pure-move check: base d2e8afb, head 600bcd5, tools 517fc468330a

Symbol From To Result
ROUTERS tensorrt_llm/serve/_base/routing.py tensorrt_llm/serve/_base/routing.py modified (declared)
EncodeBatcher tensorrt_llm/serve/encode_batcher.py tensorrt_llm/serve/api/openai/embeddings/batcher.py verbatim
EncodeFn tensorrt_llm/serve/encode_batcher.py tensorrt_llm/serve/api/openai/embeddings/batcher.py verbatim
InputTooLongError tensorrt_llm/serve/encode_batcher.py tensorrt_llm/serve/api/openai/embeddings/batcher.py verbatim
QueueFullError tensorrt_llm/serve/encode_batcher.py tensorrt_llm/serve/api/openai/embeddings/batcher.py verbatim
_QueuedRequest tensorrt_llm/serve/encode_batcher.py tensorrt_llm/serve/api/openai/embeddings/batcher.py verbatim
OpenAIServer._attach_multimodal_data tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/serving.py verbatim
OpenAIServer.await_disconnected tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/serving.py verbatim
OpenAIServer.create_error_response tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/serving.py verbatim
_record_generator_termination tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/serving.py verbatim
OpenAIServer.register_embedding_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/embeddings/api_router.py replaced
OpenAIServer._init_embedding_batcher tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/embeddings/serving.py verbatim
OpenAIServer._normalize_embedding_input tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/embeddings/serving.py verbatim
OpenAIServer.openai_embedding tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/embeddings/serving.py verbatim
OpenAIServer.openai_mm_encoder tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/mm_encoder/serving.py rewritten
OpenAIServer.tokenize tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/tokenize/serving.py verbatim
OpenAIServer.__init__ tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer._attach_multimodal_data tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py borrowed
OpenAIServer.await_disconnected tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py borrowed
OpenAIServer.create_error_response tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py borrowed
OpenAIServer.register_mm_encoder_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer.register_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
BaseServing — tensorrt_llm/serve/_base/serving.py added
GenerateBaseServing.__init__ — tensorrt_llm/serve/_base/serving.py added
GenerateBaseServing — tensorrt_llm/serve/_base/serving.py added
tensorrt_llm/serve/api/openai/embeddings/api_router.py::* — tensorrt_llm/serve/api/openai/embeddings/api_router.py added
EmbeddingServing.__init__ — tensorrt_llm/serve/api/openai/embeddings/serving.py added
EmbeddingServing — tensorrt_llm/serve/api/openai/embeddings/serving.py added
tensorrt_llm/serve/api/openai/mm_encoder/api_router.py::* — tensorrt_llm/serve/api/openai/mm_encoder/api_router.py added
MMEncoderServing.__init__ — tensorrt_llm/serve/api/openai/mm_encoder/serving.py added
MMEncoderServing — tensorrt_llm/serve/api/openai/mm_encoder/serving.py added
tensorrt_llm/serve/api/openai/tokenize/api_router.py::* — tensorrt_llm/serve/api/openai/tokenize/api_router.py added
TokenizeServing.__init__ — tensorrt_llm/serve/api/openai/tokenize/serving.py added
TokenizeServing — tensorrt_llm/serve/api/openai/tokenize/serving.py added
OpenAIServer._generate_serving_kwargs — tensorrt_llm/serve/openai_server.py added

Replaced

  • OpenAIServer.register_embedding_routes -> tensorrt_llm/serve/api/openai/embeddings/api_router.py::openai_embedding: its last route is served by the embeddings router; /health, /version and /v1/models are already served by serve/api/ops/system

Added

  • OpenAIServer._generate_serving_kwargs: constructor arguments of every GenerateBaseServing, read once from the server
  • BaseServing: serving base class; holds create_error_response
  • GenerateBaseServing: base of the generate serving classes
  • GenerateBaseServing.__init__: explicit dependencies instead of server attributes, so missing wiring fails at construction
  • EmbeddingServing: serving class of /v1/embeddings
  • EmbeddingServing.__init__: explicit dependencies; builds the batcher
  • MMEncoderServing: serving class of the multimodal-encoder /v1/chat/completions
  • MMEncoderServing.__init__: explicit dependencies, injected RenderResources
  • TokenizeServing: serving class of /_internal/tokenize
  • TokenizeServing.__init__: explicit dependencies
  • tensorrt_llm/serve/api/openai/embeddings/api_router.py::*: new router module
  • tensorrt_llm/serve/api/openai/mm_encoder/api_router.py::*: new router module
  • tensorrt_llm/serve/api/openai/tokenize/api_router.py::*: new router module

Rewritten

  • OpenAIServer.openai_mm_encoder (tensorrt_llm/serve/api/openai/mm_encoder/serving.py): RenderResources.from_server(self) -> self._render_resources

Modified (declared)

  • OpenAIServer.__init__: RenderResources built once; serving objects built in the role branches; attach_routers after the role switch
    --- base
    +++ head
    @@ -94,6 +94,7 @@
    self._init_visual_gen()
    else:
    self._init_llm(chat_template)
    •    self._render_resources = RenderResources.from_server(self)
      

      @asynccontextmanager
      async def lifespan(app: FastAPI):
      @@ -166,20 +167,29 @@
      detail = detail[:4096] + ' ... [truncated %d bytes]' % (len(detail) - 4096)
      return JSONResponse(status_code=400, content={'error': detail})
      self.app.state.ops_serving_system = SystemServing(model=self.model, health_probe=self._health_probe)

    • attach_routers(self.app, ServerCapabilities.for_role(self.server_role))
      if self.server_role is ServerRole.VISUAL_GEN:
      assert self._is_visual_gen, 'generator must be a VisualGen for VISUAL_GEN server'
      self.register_visual_gen_routes()
      elif self.server_role is ServerRole.MM_ENCODER:
      assert isinstance(self.generator, MultimodalEncoder), 'generator must be a MultimodalEncoder for multimodal encoder'
    •    from tensorrt_llm.serve.api.openai.mm_encoder import api_router as mm_encoder_router
      
    •    from tensorrt_llm.serve.api.openai.mm_encoder.serving import MMEncoderServing
      
    •    setattr(self.app.state, mm_encoder_router.APP_STATE_KEY, MMEncoderServing(render_resources=self._render_resources, multimodal_server_config=self.multimodal_server_config, allow_request_chat_template=self.allow_request_chat_template, **self._generate_serving_kwargs()))
         self.register_mm_encoder_routes()
      
      elif self.server_role is ServerRole.EMBEDDING:
      assert getattr(self.generator.args, 'encode_only', False), 'generator must be an encode_only=True LLM for the embedding server'
    •    self._init_embedding_batcher()
      
    •    self.register_embedding_routes()
      
    •    from tensorrt_llm.serve.api.openai.embeddings import api_router as embeddings_router
      
    •    from tensorrt_llm.serve.api.openai.embeddings.serving import EmbeddingServing
      
    •    embedding_serving = EmbeddingServing(model=self.model, generator=self.generator, embedding_max_queue_delay=self._embedding_max_queue_delay, embedding_max_queue_size=self._embedding_max_queue_size)
      
    •    self.embedding_batcher = embedding_serving.embedding_batcher
      
    •    setattr(self.app.state, embeddings_router.APP_STATE_KEY, embedding_serving)
      
      else:
    •    from tensorrt_llm.serve.api.openai.tokenize import api_router as tokenize_router
      
    •    from tensorrt_llm.serve.api.openai.tokenize.serving import TokenizeServing
      
    •    setattr(self.app.state, tokenize_router.APP_STATE_KEY, TokenizeServing(model=self.model, tokenizer=self.tokenizer))
         self._register_launcher_forwarding_routes()
         self.register_routes()
      
    • attach_routers(self.app, ServerCapabilities.for_role(self.server_role))
      if self._collect_perf_metrics:
      self.app.add_middleware(PerfMetricsMiddleware, expose_headers=self._expose_perf_metrics, writer=self._perf_metrics_writer, adjusted_clock=self._adjusted_steady_clock)
      self.app.add_middleware(ServerArrivalTimeMiddleware, adjusted_clock=self._adjusted_steady_clock)
  • OpenAIServer.register_routes: /_internal/tokenize comes from the tokenize router
    --- base
    +++ head
    @@ -28,7 +28,6 @@
    self.app.add_api_route('/v1/responses', self.openai_responses, methods=['POST'])
    self.app.add_api_route('/v1/responses/{response_id}', self.openai_responses_get_response, methods=['GET'])
    self.app.add_api_route('/v1/responses/{response_id}', self.openai_responses_delete_response, methods=['DELETE'])
    • self.app.add_api_route('/_internal/tokenize', self.tokenize, methods=['POST'])
      self.app.add_api_route('/start_profile', self.start_profile, methods=['POST'])
      self.app.add_api_route('/stop_profile', self.stop_profile, methods=['POST'])
      self._register_runtime_control_routes()
  • OpenAIServer.register_mm_encoder_routes: /v1/chat/completions comes from the multimodal-encoder router
    --- base
    +++ head
    @@ -1,3 +1,2 @@
    def register_mm_encoder_routes(self):
    self.app.add_api_route('/metrics', self.get_iteration_stats, methods=['GET'])
    • self.app.add_api_route('/v1/chat/completions', self.openai_mm_encoder, methods=['POST'])
  • ROUTERS: the embeddings, multimodal-encoder and tokenize routers join the routing table for their role groups
    --- base
    +++ head
    @@ -1 +1 @@
    -ROUTERS: tuple[RouterSpec, ...] = (RouterSpec('tensorrt_llm.serve.api.ops.system.api_router', roles=ALL_ROLE_GROUPS),)
    +ROUTERS: tuple[RouterSpec, ...] = (RouterSpec('tensorrt_llm.serve.api.ops.system.api_router', roles=ALL_ROLE_GROUPS), RouterSpec('tensorrt_llm.serve.api.openai.embeddings.api_router', roles=frozenset({RoleGroup.EMBEDDING})), RouterSpec('tensorrt_llm.serve.api.openai.mm_encoder.api_router', roles=frozenset({RoleGroup.MM_ENCODER})), RouterSpec('tensorrt_llm.serve.api.openai.tokenize.api_router', roles=frozenset({RoleGroup.LLM})))

13 definitions moved verbatim; 0 differences; 1 rewritten (declared); 1 files moved; 3 borrowed; 1 replaced; 13 added; 4 modified (declared); shim exports 0 names


</details>

Declared transformations: one rewrite in `MMEncoderServing.openai_mm_encoder` (`RenderResources.from_server(self)` → `self._render_resources`, equivalence above); three helpers that moved to the bases stay reachable on `OpenAIServer` as aliases of the moved objects while chat, completions and Responses still call them (`create_error_response = staticmethod(BaseServing.create_error_response)`, `_attach_multimodal_data = staticmethod(GenerateBaseServing._attach_multimodal_data)`, `await_disconnected = GenerateBaseServing.await_disconnected`; `tests/unittest/serve/test_openai_server_borrowed_helpers.py` pins the identity); `register_embedding_routes` is deleted, its route is served by the embeddings router. The nested functions `encode_fn` and `create_mm_embedding_response` move inside their methods unchanged.

Moves without a shim (no code outside the repository imports the old path; release notes): `tensorrt_llm.serve.encode_batcher.EncodeBatcher`, `.EncodeFn`, `.InputTooLongError`, `.QueueFullError` → `tensorrt_llm.serve.api.openai.embeddings.batcher`. History: `git log --follow tensorrt_llm/serve/api/openai/embeddings/batcher.py` for the whole-file move; `git blame -C -C` on the serving modules for the split.

### Removed from OpenAIServer

- `openai_embedding`, `_normalize_embedding_input`, `_init_embedding_batcher` → `EmbeddingServing` (`tensorrt_llm/serve/api/openai/embeddings/serving.py`)
- `register_embedding_routes` → deleted; `tensorrt_llm/serve/api/openai/embeddings/api_router.py` serves `POST /v1/embeddings`
- `openai_mm_encoder` → `MMEncoderServing` (`tensorrt_llm/serve/api/openai/mm_encoder/serving.py`)
- `tokenize` → `TokenizeServing` (`tensorrt_llm/serve/api/openai/tokenize/serving.py`)
- `create_error_response` → `BaseServing`; `await_disconnected`, `_attach_multimodal_data` → `GenerateBaseServing` (`tensorrt_llm/serve/_base/serving.py`); all three stay reachable on `OpenAIServer` as aliases
- module helper `_record_generator_termination` → `tensorrt_llm/serve/_base/serving.py` (`openai_server.py` imports it for its remaining callers)

New on `OpenAIServer`: `_generate_serving_kwargs()`, the one place that reads the constructor arguments of every `GenerateBaseServing` from the server. RL frameworks that construct `OpenAIServer` directly: the removed methods are gone from the instance; the routes are served through `server.app`.

### Coordination notes

- `tensorrt_llm/serve/_base/serving.py` is also created by #21552 ("Move the VisualGen handlers into serve/api/visual_gen"): the `BaseServing` class body is byte-identical in both, the module docstring differs. Whichever of the two lands second gets an add/add conflict on this file and adopts the landed module's docstring when it is regenerated on the latest main before merge.
- `openai_server.py` keeps `from tensorrt_llm.serve.api.openai.embeddings.batcher import EncodeBatcher` at module level for the `Optional[EncodeBatcher]` annotation of `self.embedding_batcher` (the layering tests allow it). Follow-up for a later hand-written change: a `TYPE_CHECKING` guard.
- The missing-handle 400 above is a latent product defect, recorded in the refactor's follow-up list; it is fixed outside the refactor so that this PR changes no behavior.
- On the branch this PR is stacked on, 8 tests fail on the integration base (`d2e8afb843`) before and after this PR's commits; nothing in this PR causes them. `tests/unittest/serve/test_serve_health.py` (from #21445) calls `OpenAIServer.health`, which #21448 renamed to `_health_probe` (6 tests); whichever of #21448 and #21445 lands second updates that test. The private-access lint (from #21393, which #21445 is stacked on) cannot classify `main`'s `_PreprocessingWork._release(result)` call in `tensorrt_llm/grpc/openengine/servicer.py` (#21037, merged after #21393's base; 2 cases); #21393 classifies it when it is rebased on the latest `main`. Neither is part of this PR.
- `/openapi.json`: the three routes now register after the role's own table, so the order of `paths` changes; operation ids and schemas do not (same function names, parameters and body models).
- Type checking: the three `api_router.py` modules join the mypy file list; `tensorrt_llm/serve/_base/serving.py` does not (it holds moved functions without annotations, which strict mypy rejects), nor do the `serving.py` and `batcher.py` modules (moved code).
- Open PRs that edit the moved code at this time, to be absorbed by the regeneration on the latest main before merge: #19550 and #19624 (native Qwen3 reranking, which shares the encode batcher; if either lands first the batcher moves to `tensorrt_llm/serve/_base/` instead), #20739 (input-processing timing metrics, edits `openai_mm_encoder`), #12746 (several served model names), #16252 (KV cache disk tier), #19461, #19605, #20616, #17541. Merged and absorbed: #20775, #20626 (`_attach_multimodal_data`), #20269, #20815.

### CI bookkeeping

- Multi-GPU table (`getMultiGpuFileChanged` in `jenkins/L0_MergeRequest.groovy`): added `tensorrt_llm/serve/api/openai/embeddings/`, `tensorrt_llm/serve/api/openai/mm_encoder/`, `tensorrt_llm/serve/api/openai/tokenize/`; `tensorrt_llm/serve/openai_server.py` stays.
- CODEOWNERS: none (`/tensorrt_llm/serve` covers the new directories).
- `legacy-files.txt`: `tensorrt_llm/serve/_base/serving.py`, `tensorrt_llm/serve/api/openai/{embeddings,mm_encoder,tokenize}/serving.py` (Group B: moved code from `openai_server.py`); `batcher.py` and the `api_router.py` modules are Group A. Derived configs regenerated with `scripts/legacy_utils.py gen-configs`; `ruff-legacy-baseline.json`: no change needed.
- mypy `files` (`pyproject.toml`): the three `api_router.py` modules.
- Test lists: none; the new tests under `tests/unittest/serve/` are collected by the `unittest/serve` entry of `l0_cpu.yml` and carry `pytest.mark.cpu_only`.
- `tensorrt_llm/serve/AGENTS.md`: the constructor rule of serving classes (keyword-only arguments without defaults; no `getattr(self, ...)` in new serving code).

## Test Coverage

New (all CPU, through the real constructor and routers, no `object.__new__`): `tests/unittest/serve/api/openai/{embeddings,mm_encoder,tokenize}/test_serving.py` (constructor requires each dependency; end-to-end requests incl. msgpack bodies; error statuses; the batcher the health probe reads; the injected `RenderResources` carrying the server chat template; the tokenize route on the LLM roles only), `tests/unittest/serve/_base/test_serving.py`, `tests/unittest/serve/test_openai_server_borrowed_helpers.py`; `test_serving_classes_share_the_server_error_factory` in `tests/unittest/llmapi/apps/test_context_length_error.py`; `tests/unittest/llmapi/apps/test_serve_lazy_import.py` also checks that importing `tensorrt_llm.serve` loads no endpoint package. Updated: `tests/unittest/llmapi/apps/test_encode_batcher.py` (new import path), `tests/unittest/serve/test_serve_runtime_api_calls.py` (builds `EmbeddingServing`; patches the termination telemetry where the moved helper looks it up), `tests/unittest/grpc/openengine/test_metrics.py` and the two multi-frontend tests (their constructor re-enactments provide the tokenize serving object), the fakes in `tests/test_common/serve/`.

Run on CPU (no GPU on the host), on the current head:

- `pytest -m cpu_only tests/unittest/api_stability tests/unittest/serve` (route snapshot, middleware order, shim identity, layering, private-access lint, `object.__new__` ratchet and agents-md checks included): 272 passed, 8 failed, the 8 tests of "Coordination notes", which fail identically on the integration base.
- `tests/unittest/llmapi/apps` and `tests/unittest/disaggregated`: 3966 passed, 44 failed, all in `tests/unittest/disaggregated/test_context_first_profile.py`, `test_lifecycle_activation.py` and `test_peer.py`, which fail identically on #21406's head in this environment.
- `pytest -rs tests/unittest/llmapi/apps/test_encode_batcher.py tests/unittest/api_stability` with `tests/unittest/llmapi/apps/test_harmony_parsing.py` and `test_harmony_channel_validation.py`: 197 passed, nothing skipped as not importable.
- `pre-commit run --files <all changed files>` twice: clean both times. `git grep -n "serve[./]encode_batcher" -- .` and `git grep -n "_encoder_executor\|from_server(self)" -- tensorrt_llm/serve/api/`: nothing.

Run on the previous base of the stack:

- `python3 -m test_common.serve.cpu_tests` (every serve CPU test, one pytest process per `l0_cpu.yml` entry): 71 entries, 4316 passed; the only failures were the 6 `test_serve_health.py` tests. The two multi-frontend files were re-run after their last fix: 15 passed.
- Route and router tests on FastAPI 0.136.3 and 0.139.2: 119 passed on each.
- `bash scripts/run_mypy.sh`: no issues in 23 source files. `python scripts/legacy_utils.py check-configs`: up to date.
- Property checks of the hand-written wiring (which roles get which router, router delegation, the base's constructor arguments) with Hypothesis; 7 planted bugs all detected.

Left to CI (GPU): `test_e2e.py::test_openai_mmencoder_example`, `tests/unittest/llmapi/apps/_test_openai_embeddings.py`, `test_e2e.py::test_openai_misc_example[pytorch]` (covers `/_internal/tokenize`).

## 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](https://github.com/NVIDIA/TensorRT-LLM/blob/main/CODING_GUIDELINES.md) to the best of your knowledge.
- Test cases are provided for new code paths (see [test instructions](https://github.com/NVIDIA/TensorRT-LLM/tree/main/tests#1-how-does-the-ci-work))
- 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](https://github.com/NVIDIA/TensorRT-LLM/blob/main/.github/CODEOWNERS) updated if ownership changes
- Documentation updated as needed
- Update [tava architecture diagram](https://github.com/NVIDIA/TensorRT-LLM/blob/main/.github/tava_architecture_diagram.md) if there is a significant design change in PR.
- The reviewers assigned automatically/manually are appropriate for the PR.


- [x] 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](https://claude.com/claude-code)

…ss lint for the serving frontends

The HTTP server, the SMG gRPC frontend and the OpenEngine gRPC frontend
learned engine health by reading private members (_check_health, _executor,
_fatal_error, _engine_dead). Add the public health API on the classes that
own those members and switch all three frontends to it, with no behavior
change:

- BaseLLM.check_health() (public rename of _check_health; the private name
  stays for one release as an alias for out-of-tree callers that look it
  up), BaseLLM.fatal_error, BaseLLM.shutdown_started, BaseLLM.engine_dead.
- GenerationExecutor.fatal_error and engine_dead;
  GenerationExecutorProxy.engine_dead also reads the sticky worker-death
  flag set before the fatal error is recorded.

Add a private-access lint (tests/unittest/serve/test_private_access_lint.py)
that counts cross-package private accesses in tensorrt_llm/serve,
tensorrt_llm/commands and tensorrt_llm/grpc against a checked-in baseline
(tests/unittest/serve/private_access_baseline.yaml) that only shrinks, with
companion test files under tests/unittest/serve/api/visual_gen/ and
tests/unittest/grpc/openengine/ so VisualGen-only and OpenEngine-only pull
requests keep running the check.

Replace the three frontend health mirror tests with tests of the production
code, rename the _check_health test doubles to the public name, and register
the new CPU test files in l0_cpu.yml.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…erving-frontend private accesses

The serving frontends (tensorrt_llm/serve, tensorrt_llm/commands, the
OpenEngine gRPC frontend) read private members of LLM, the executors,
CompletionOutput, SamplingParams, MultimodalDataTracker,
OpenAIDisaggServer and PyExecutor. Give each remaining read a thin
public entry point on the class that owns the member, switch the
frontends to it with no behavior change, and shrink the private-access
baseline from 50 recorded accesses to 19. The reads of the two private
LlmArgs attributes stay (exposing them is an LLM-args change for its
own PR); the remaining baseline entries belong to other in-flight work.

New public members (status prototype where api-stability tracks them):
LLM.record_fatal_error / multi_frontend_attach_info /
multi_frontend_ipc_dir / engine_in_worker_process / hf_model_dir /
hf_model_config / resource_governor_queue / encode_limits /
get_kv_cache_load / get_openengine_discovery;
GenerationExecutor.record_fatal_error and multi_frontend_ipc_dir;
CompletionOutput.spec_dec_counters and text_diff_offset (and the
SpecDecCounters rename); SamplingParams.set_request_provided_fields and
stop_word_ids; MultimodalDataTracker.multimodal_server_config;
OpenAIDisaggServer.address; PyExecutor.set_openengine_discovery and
set_openengine_node_server; registered_model_type() in inputs/registry.

Also adds tensorrt_llm/llmapi/engine_client.py: the EngineClient
protocol naming the engine operations every frontend shares (generate,
health, load), checked by tests rather than at runtime.

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

The shut-down-LLM case of the launcher-lost watchdog test now calls the
production method unbound on a stand-in object whose generator double
defines the public members the method reads, the form the file's other
stand-in tests use. New tests must not build servers with object.__new__.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…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>
…on the current main

The disaggregated frontend now registers its routes with KvHintsRoute
(NVIDIA#20269), which strips kv_hints from request bodies unless the server
trusts its ingress. Regenerate the route reference: the six
disaggregated configurations record KvHintsRoute where they recorded
APIRoute; their route sets and order are unchanged.

The route-class invariant, which a regenerated reference cannot hide,
now requires KvHintsRoute on every API route of the disaggregated
frontend and APIRoute on the coordinator app. Dropping the KV-hint
policy from the frontend fails it even after a regeneration, and both
apps still reject the aggregated server's msgpack route class.

The object.__new__ baseline records the sites on main at the merge base:
test_kv_hints.py builds one server that way (NVIDIA#20269), and
test_harmony_parsing.py no longer builds any (NVIDIA#21176).

The harmony parsing tests are now registered as a whole file (NVIDIA#21411),
so the node-id entry that the CPU-selection test used as its example is
gone. The node-id check now runs select_entries on a one-entry list of
its own, so it no longer depends on which node-id entries l0_cpu.yml
holds.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ail on silent regressions

The route snapshot now requires every configuration other than the three
invalid ones to build, so a configuration that starts failing at construction
cannot pass through a regenerated reference. The object.__new__ ratchet
resolves 'from ... import <server class> as <name>' aliases and records each
baseline site as its enclosing function or class plus the server class, so
swapping one site for another in the same file no longer passes. The baseline
keeps the same 52 sites in the same 26 files.

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>
…e/api/visual_gen

Move the eight VisualGen request and response models (ImageGenerationRequest,
ImageEditRequest, ImageObject, ImageGenerationResponse, MediaReferenceItem,
VideoGenerationRequest, VideoJob, VideoJobList) out of
tensorrt_llm/serve/openai_protocol.py into the new package
tensorrt_llm/serve/api/visual_gen/ (protocol.py), unchanged. openai_protocol
keeps the eight names importable through a lazy module __getattr__: importing
it no longer loads the VisualGen models or tensorrt_llm.visual_gen, and the
re-export is part of its own interface, so it never warns and has no removal
release. The imports only the moved block used (UploadFile, AliasChoices,
MediaRole) leave openai_protocol with it.

Every importer in the repository now imports the new module; the benchmarking
guide links the new file. protocol.py joins legacy-files.txt (its code comes
from a legacy-formatted file) and the derived lint configs are regenerated.

No behavior change. OpenAPI component names may change with the move; they are
not a contract.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…isualGen team in CODEOWNERS

VisualGen serving code now lives under tensorrt_llm/serve/api/visual_gen/. The
new rule, placed after the /tensorrt_llm/serve rule so that it wins, makes
@NVIDIA/trt-llm-torch-visual-gen-devs the owner of that directory: changes there
need the VisualGen team's approval and no longer need runtime-devs.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…y re-export under a visual_gen path

Add tests/unittest/api_stability/visual_gen/test_visual_gen_serve_api.py: it
runs the checks of test_serve_api.py on the models of
references/trtllm_serve_api.yaml that are defined in
tensorrt_llm.serve.api.visual_gen.protocol, and checks that
tensorrt_llm.serve.openai_protocol re-exports the eight VisualGen names lazily
(importing it does not load the VisualGen module; unknown names still raise
AttributeError). The file lives under a visual_gen/ path and has its own
l0_cpu.yml entry so that CI selection for VisualGen-only pull requests keeps
running it; the reference YAML is unchanged.

Add the eight identity rows of the VisualGen names to the VisualGen shim
identity table: identity only, no warning and no ban on the old path, because
the re-export is openai_protocol's permanent interface.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…its no-warning contract

Resolve every pinned model with the full gate's own loader, so a VisualGen
model pinned in the reference YAML that is later renamed or removed fails this
gate too instead of being dropped silently; the four known models stay a
secondary check. Make the lazy-load probe's diagnostic best-effort and
identity-based, so a failure prints which modules hold the VisualGen classes
instead of crashing on module-level __getattr__ hooks while walking
sys.modules. In the same fresh-interpreter probe, assert that reading each of
the eight names from openai_protocol emits no FutureWarning or
DeprecationWarning.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… one submodule per API

tensorrt_llm/serve/openai_protocol.py held every OpenAI-dialect request and
response model plus the helpers that turn them into engine parameters. It
becomes the package tensorrt_llm/serve/openai_protocol/ with one submodule per
API: common, sampling, tools, chat, completions, embeddings, responses,
tokenize, models, kv_cache, control and disagg. Every definition moves
unchanged (93 definitions; generated by an AST tool and checked definition by
definition against the former module).

The package __init__.py is a lazy facade: a name-to-module table and a module
__getattr__ import a submodule only when one of its names is first read, so
every import from tensorrt_llm.serve.openai_protocol keeps working for every
name the former module had, the underscore helpers that tests import included,
and the VisualGen models re-exported from
tensorrt_llm.serve.api.visual_gen.protocol. The facade is the package's own
interface, not a compatibility shim: no warning, no removal release, and no
importer in the repository changes. Its TYPE_CHECKING block imports the public
names from their submodules so type checkers resolve them; the VisualGen names
are left out of it on purpose and appear only as strings in the table, so that
importing the facade never loads the VisualGen package. DisaggScheduleStyle,
which the former module imported and two importers took from it, stays
reachable through the facade.

Submodules import each other directly, never through the package, along an
acyclic graph: common imports no sibling; sampling, tools, models, tokenize,
embeddings and control import common; completions and responses import common
and sampling; chat imports common, sampling and tools; kv_cache imports
common, tools and chat; disagg imports common, completions and chat. torch is
imported only by sampling and disagg, openai_harmony only by chat, and the
serving-extension registry only by sampling, so code that needs only the tool
types no longer loads the chat or Responses models.

The 12 submodules are registered in legacy-files.txt (their code comes from a
legacy-formatted file) and the deleted module's line is removed; the derived
lint configs are regenerated with scripts/legacy_utils.py gen-configs. The
facade is new code and stays under the default formatter.

No behavior change. OpenAPI component names may change for classes that share
a short name with an openai type, because pydantic qualifies them with the
module path; they are not a contract.

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

tests/unittest/serve/openai_protocol/test_openai_protocol_facade.py keeps the
package tensorrt_llm/serve/openai_protocol/ honest: every name the former
single-file module defined (plus DisaggScheduleStyle) is reachable through the
facade and is the object its submodule defines; the facade table is frozen at
those names; __all__, dir() and the TYPE_CHECKING block agree with the table;
unknown names raise AttributeError; the submodule import graph stays inside
the allowed acyclic graph and no submodule imports the package itself;
importing the package loads no submodule, importing tools loads neither chat
nor responses nor completions, and reading ChatCompletionRequest does not load
the serving extensions; the private helpers that other tests import through
the package stay importable.

tests/unittest/serve/api/visual_gen/test_openai_protocol_facade_visual_gen.py
holds the VisualGen assertions under a visual_gen/ path so that CI selection
runs them for VisualGen-only changes: the eight VisualGen models map to
tensorrt_llm.serve.api.visual_gen.protocol, nothing but a lookup of one of
them loads that module, and the facade names it only as strings in its table,
never in an import statement.

tests/unittest/serve/test_layering.py gains the contract that the protocol
package imports no handler module (servers, handlers and routers import the
models, never the reverse), and tensorrt_llm/serve/AGENTS.md tells new code to
import the submodule that defines a model rather than the package.

Both test files are collected by the existing unittest/serve and
unittest/serve/api/visual_gen/ entries of l0_cpu.yml and carry the cpu_only
marker.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…s as strings and bind the test's culprit list unconditionally

The table comment in tensorrt_llm/serve/openai_protocol/__init__.py said that a
TYPE_CHECKING import of the VisualGen models would load their package at run
time, which a TYPE_CHECKING block never does. The real reason is the static
import-isolation check that lets CI run only the VisualGen tests for
VisualGen-only changes: it forbids an import edge from this package to the
VisualGen package even under TYPE_CHECKING, so the facade names that module
only as a string.

In tests/unittest/serve/openai_protocol/test_openai_protocol_facade.py the
list of submodules that import the serving extensions was bound only on the
failing path and read in the assertion message; it is now bound before the
branch, so an unconditional message can never raise UnboundLocalError.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…junyix-feat-serve-embeddings-mm-encoder

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

# Conflicts:
#	pyproject.toml
#	tensorrt_llm/grpc/openengine/streaming.py
#	tensorrt_llm/serve/openai_protocol.py
#	tensorrt_llm/serve/openai_server.py
#	tests/integration/test_lists/test-db/l0_cpu.yml
#	tests/unittest/serve/__init__.py
#	tests/unittest/serve/api/__init__.py
#	tests/unittest/serve/api/visual_gen/__init__.py
…gs, multimodal encoder and tokenize

Create tensorrt_llm/serve/_base/serving.py with BaseServing and
GenerateBaseServing, and the EmbeddingServing, MMEncoderServing and
TokenizeServing skeletons under tensorrt_llm/serve/api/openai/. Every
dependency is a keyword-only constructor argument. The new Group B files
are registered in legacy-files.txt and the generated lint configs are
regenerated.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…tokenize handlers into their serving classes

Generated move. OpenAIServer loses openai_embedding,
_normalize_embedding_input, _init_embedding_batcher (to EmbeddingServing),
openai_mm_encoder (to MMEncoderServing) and tokenize (to TokenizeServing);
register_embedding_routes is deleted, its route is served by the
embeddings router. create_error_response, _attach_multimodal_data and
await_disconnected move to the serving bases in
tensorrt_llm/serve/_base/serving.py and stay reachable on OpenAIServer
as aliases of the moved objects while chat and completions still call
them. _record_generator_termination moves to the same module.
tensorrt_llm/serve/encode_batcher.py becomes
tensorrt_llm/serve/api/openai/embeddings/batcher.py; the old module is
deleted and every import of it is rewritten.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…tokenize routers and serving objects

Each new package gets an api_router.py built with make_router() and a
RouterSpec in ROUTERS for exactly the roles the route had. The
composition root builds RenderResources once after _init_llm and
injects it, constructs the serving objects inside the role branches,
puts them on app.state, and attaches the routers after the role switch,
where every attach_router can check its app.state key. The two route
registrations the routers now serve leave register_routes and
register_mm_encoder_routes.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…enize serving classes on CPU

Construction tests (every dependency is a required keyword-only argument),
route tests through the real composition root and routers (msgpack bodies
included), the batcher reference the health probe reads, the injected
RenderResources carrying the server chat template, the serving bases and
the borrowed-helper identity on OpenAIServer. The encode-only fake now
encodes fixed embeddings and scripted outputs can carry disaggregated
params; the app factory passes the chat-template knobs. The runtime-API
tests build EmbeddingServing and patch the termination telemetry where
the moved helper looks it up; the lazy-import guard also checks that
importing tensorrt_llm.serve loads no endpoint package.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… and the serve guide

The multi-GPU trigger table gains the three package prefixes, so code
moved out of tensorrt_llm/serve/openai_server.py keeps its trigger; the
three router modules join the mypy file list; the serve guide states the
constructor rule of serving classes.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… stub provide the tokenizer

The test runs the real OpenAIServer constructor with _init_llm replaced
by a stub. The constructor now builds the tokenize serving object for
the LLM role from self.tokenizer, so the stub sets it too; a missing
dependency fails at construction on purpose instead of at request time.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ition root with the tokenize serving object

Both tests build a partial OpenAIServer and register its routes the way
the constructor does. The constructor now puts the tokenize serving object
on app.state and attaches the role's routers after the local table, so the
re-enactments do the same; the tokenize router refuses to attach without
its serving object.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant