Repository navigation
[TRTLLM-17272][feat] Add public runtime accessors for the remaining serving-frontend private accesses - #21445
Draft
JunyiXu-nv wants to merge 4 commits into
Draft
JunyiXu-nv wants to merge 4 commits into
JunyiXu-nv wants to merge 4 commits into
Conversation
…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>
This was referenced Nov 3, 2026
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.
@coderabbitai summary
Description
Stacked on #21393 (TRTLLM-17272 health-check + private-access lint, "[TRTLLM-17272][feat] Add public LLM health queries and a private-access lint for the serving frontends"): the first 2 commits below belong to that PR and disappear from this diff when it merges; this PR's own change is the last 2 commits.
The serving frontends (HTTP
tensorrt_llm/serve, the CLItensorrt_llm/commands, the OpenEngine gRPC frontendtensorrt_llm/grpc/openengine) still read private members ofLLM, the executors,CompletionOutput,SamplingParams,MultimodalDataTracker,OpenAIDisaggServerandPyExecutor. This PR gives each remaining read a thin public entry point on the class that owns the member, switches the frontends to it with no behavior change, and shrinks the private-access-lint baseline from 50 recorded accesses to 19. It also addstensorrt_llm/llmapi/engine_client.py: anEngineClientprotocol naming the engine-level operations every frontend shares (generate, health, load), not enforced at runtime (the private-access lint is the enforcement) and deliberately without role-specific members such asencode_limits. After this PR, moving the HTTP handlers into serving classes carries only public calls.Part of the trtllm-serve refactor: public runtime API for the frontends (epic TRTLLM-17056, task TRTLLM-17272). Builds on #21393.
Converted accesses (31 of 50; old read → new public member)
serve/_frontend_processes.py_spawn_attached_frontendsllm._executor+ isinstance + attach-info checkLLM.multi_frontend_attach_info()(sameValueError, byte-identical text)serve/openai_server.py_record_generator_terminationgetattr(generator, "_executor", None)isinstance proxyLLM.engine_in_worker_processserve/openai_server.py_init_llmgenerator._hf_model_dir+ tokenizer-name fallbackLLM.hf_model_dir(one fallback rule, see below)serve/openai_server.pyregister_routesgenerator._executor.resource_governor_queueLLM.resource_governor_queueserve/openai_server.py_init_embedding_batchergenerator._encoder_executor.model_enginelimitsLLM.encode_limits(EncodeLimitsdataclass;encode()reads the same definition)serve/openai_server.py_on_launcher_lostgenerator._executor+executor._set_fatal_error(...)LLM.record_fatal_error()(+ existingLLM.shutdown_started); executor sideGenerationExecutor.record_fatal_error()serve/chat_utils.py(4 sites)mm_data_tracker._multimodal_server_configMultimodalDataTracker.multimodal_server_configserve/openai_protocol.pysampling_params._set_request_provided_fields(...)SamplingParams.set_request_provided_fields(...)(public rename, same body)serve/postprocess_handlers.py(4 sites)getattr(output, '_spec_dec_counters', None)CompletionOutput.spec_dec_counters(direct attribute; the test doubles gained the member)serve/postprocess_handlers.pyoutput._last_text_lenCompletionOutput.text_diff_offsetcommands/serve.py_bind_launcher_udsllm._executor+getattr(..., "_multi_frontend_ipc_dir", None)LLM.multi_frontend_ipc_dir(executor side:GenerationExecutor.multi_frontend_ipc_dir, proxy override)commands/serve.py_run_fleet_worker_implserver._config.hostname,server._config.portOpenAIDisaggServer.addressgrpc/openengine/capabilities.pygetattr(llm, "_hf_model_config", None)LLM.hf_model_config(read stays a tolerantgetattrfor non-LLM doubles)grpc/openengine/capabilities.pygetattr(type(input_processor), "_registered_model_type", None)registered_model_type()intensorrt_llm/inputs/registry.py, next to the decorator that stamps itgrpc/openengine/control.py_get_kv_cache_load_executor+ tolerant getterLLM.get_kv_cache_load()grpc/openengine/control.pyGetServerInfo_executor+ tolerant gettersLLM.get_kv_cache_capacity()behind anargs.encode_onlyguard;LLM.get_openengine_discovery()grpc/openengine/control.py_requested_sources_executor+ tolerant getterLLM.get_openengine_discovery()grpc/openengine/formatting.py_token_holdbackgetattr(sampling_params, "_stop_word_ids", None)SamplingParams.stop_word_idsgrpc/openengine/node.pyexecutor._openengine_discovery = ...,executor._openengine_node_server = ...(writes)PyExecutor.set_openengine_discovery(),PyExecutor.set_openengine_node_server()grpc/openengine/request_mapping.pysampling_params._set_request_provided_fields(...)SamplingParams.set_request_provided_fields(...)grpc/openengine/server.pyOpenEngineServer.startself._llm._executor.get_kv_cache_capacityLLM.get_kv_cache_capacity()Stays in the baseline, for other owners:
_postprocess_result(9, owned by the post-processing strategy unification),._done(5, owned by the publicfinishedswitch), the VisualGenoutput._save_bytes(1, public VisualGen output encoder follow-up),KVCacheEventManager._hash_cache_salt(1, needs a public nanobind binding from the KV-cache-manager owners), and OpenEngine's three reads of the privateLlmArgsattributes_enable_routing_load/_openengine_discovery— those two are deliberately absent from the public LLM configuration and telemetry schema (llmapi/llm_args.py), so exposing them is an LLM-args change for its own PR;llmapi/llm_args.pyis not edited here. Baseline: 50 → 19 accesses;MAX_TOTALlowered to 19.New public members (all
status="prototype"where api-stability tracks them)LLM.multi_frontend_attach_info() -> dict— the attach payload for serving frontends sharing this LLM's engine; raises the exact startupValueErrorotherwise.LLM.multi_frontend_ipc_dir -> Optional[str]— the launcher-owned multi-frontend IPC directory, or None.LLM.engine_in_worker_process -> bool— whether generation runs through the IPC executor proxy (engine in separate MPI worker processes).LLM.record_fatal_error(error) -> bool— records an unrecoverable engine error (first one wins); False without a generation executor. Executor side:GenerationExecutor.record_fatal_error().LLM.hf_model_dir -> Union[str, Path]—self._hf_model_dir or self.args.model, the rule_try_load_generation_config_explicit_valuesalready used; now the only copy.LLM.hf_model_config -> Optional[transformers.PretrainedConfig]— the loaded HF config.LLM.resource_governor_queue -> Optional[Any]— the generation executor's queue; raisesRuntimeErrorwithout a generation executor.LLM.encode_limits -> Optional[EncodeLimits]— the limitsencode()checks (max_batch_size / max_num_tokens / max_seq_len);encode()and the HTTP embedding batcher now read one definition.LLM.get_kv_cache_load() -> dict,LLM.get_openengine_discovery() -> dict—{}without a generation executor (keeps OpenEngine's tolerant behavior exactly).CompletionOutput.spec_dec_counters -> Optional[SpecDecCounters](_SpecDecCountersrenamed public: a public property should not expose a private type; the class is used only insideexecutor/result.py),CompletionOutput.text_diff_offset -> int.SamplingParams.set_request_provided_fields(Iterable[str])(public rename, no private alias: its only callers are the two frontends),SamplingParams.stop_word_idsproperty.MultimodalDataTracker.multimodal_server_configproperty.OpenAIDisaggServer.address -> tuple[str, int](OpenAIServer.host/porthold the address passed to__call__, so those names would mean something different).PyExecutor.set_openengine_discovery(),PyExecutor.set_openengine_node_server()— the node-discovery hook's two writes; the fields stay private for the executor's own reads.registered_model_type(processor_cls)intensorrt_llm/inputs/registry.py.tensorrt_llm/llmapi/engine_client.py— theEngineClientprotocol (new module, added to the strict-mypy file list inpyproject.toml).Names chosen here for the API review committee to confirm:
engine_in_worker_process,record_fatal_error,multi_frontend_ipc_dir,hf_model_config,text_diff_offset,set_request_provided_fields,address,set_openengine_discovery/set_openengine_node_server,SpecDecCounters,registered_model_type.Behavior
No behavior change on any path a working configuration reaches. Four differences exist only on unreachable paths:
OpenAIServer._init_llmused a tokenizer-name_or_pathfallback whengenerator._hf_model_dirwas falsy;LLM.hf_model_dirfalls back toargs.modelinstead._hf_model_diris assigned during construction from the model loader and is a directory in every path, so the old fallback never ran.LLM.resource_governor_queueraisesRuntimeErrorwithout a generation executor where the old inline read raisedAttributeError(an encode-only LLM served in the LLM role already failed at that point).start()with KV events configured on an encode-only LLM now raisesRuntimeErrorfromLLM.get_kv_cache_capacity()where it raisedAttributeErrorbefore; that configuration already failed at startup.GetServerInfo's capacity query goes throughLLM.get_kv_cache_capacity(); afterllm.shutdown()that raises where the old tolerantgetattrchain silently skipped the block. Unreachable:OpenEngineServer.stop()runs beforellm.shutdown()in the launcher, so no RPC is served once the executor is gone. (Encode-only LLMs are handled by the explicitargs.encode_onlyguard, exactly as the old executor-less path skipped the block.)LLM.resource_governor_queuereadsself._executordirectly rather than throughgetattr(..., None): the api-stability snapshotter (ClassSnapshot.from_inspect) evaluates every attribute on an attribute-less__new__instance and tolerates onlyAttributeError, so the property raisesAttributeErroron a bare instance andRuntimeErroronly on a built LLM whose executor is gone; a test pins the bare-instance behavior.Moves
None.
CI bookkeeping
tensorrt_llm/llmapi/engine_client.pyand the new test file are Group A).tests/integration/test_lists/test-db/l0_cpu.ymlgainsunittest/serve/test_serve_runtime_api_calls.py(next to the other serve entries).pyproject.toml[tool.mypy] filesgainstensorrt_llm/llmapi/engine_client.py(triggers the OSS compliance scan).api-stability
tests/unittest/api_stability/references/llm.yamlgains four methods (record_fatal_error,multi_frontend_attach_info,get_kv_cache_load,get_openengine_discovery) and six properties (encode_limits,engine_in_worker_process,hf_model_config,hf_model_dir,multi_frontend_ipc_dir,resource_governor_queue);completion_output.yamlgainsspec_dec_countersandtext_diff_offset;sampling_params.yamlgains theset_request_provided_fieldsmethod and thestop_word_idsproperty.api_stability_core.pyaddsIterableto itscollections.abcimport so the reference annotation evaluates.references_committed/is unchanged, so only the non-committed committee reviews; please apply theapi-compatiblelabel.In-flight PRs on the same files
grpc/openengine/capabilities.pyare converted here.grpc/openengine/formatting.py— textual-conflict risk only, whichever lands second rebases (_token_holdbacknow reads the publicstop_word_ids).llmapi/llm.pyandreferences/llm.yaml— additive on both sides, whichever lands second rebases.Test Coverage
New and extended tests (all CPU, registered):
tests/unittest/serve/test_serve_runtime_api_calls.py(new,cpu_only, registered inl0_cpu.yml): productionOpenAIServermethods on stand-in objects — the embedding batcher takesencode_limits's three limits;register_routesbuilds the resource governor from the public queue and skips it forNone;_init_llmfeedshf_model_dirtoAutoProcessor.from_pretrainedandload_pretrained_config;_record_generator_terminationclassifies worker-process vs in-process engines and stays silent for doubles without the member.tests/unittest/llmapi/test_llm_public_runtime_api.py(extended): all newLLMmembers onobject.__new__(LLM)objects with real executor classes — attach-info payload/threeValueErrorcases with exact text, IPC dir (including the attached-frontend proxy without the field),engine_in_worker_processfor both proxies and in-process executors,hf_model_dirfallback,hf_model_config, governor-queue forwarding andRuntimeError,encode_limitsandencode()rejecting an oversized batch, load/discovery forwarding and empties,record_fatal_errorfirst-one-wins and no-executor no-op, and theEngineClientconformance tests (member kinds andgenerate_asyncparameter set/defaults).tests/unittest/llmapi/test_executor.py:CompletionOutput.spec_dec_counters(None default, set value, result-path identity) andtext_diff_offset(=len(text) - len(text_diff)).tests/unittest/llmapi/test_sampling_params.py:set_request_provided_fieldskeeps only sampler fields and gates model defaulting exactly as before;stop_word_idsis None before_setupand the encoded ids after.tests/unittest/llmapi/apps/test_chat_utils.py:multimodal_server_configreturns the configured object and a default when none was given.tests/unittest/disaggregated/test_openai_disagg_service.py:OpenAIDisaggServer.addressis the configured(hostname, port).tests/unittest/_torch/pyexecutor/test_py_executor.py: the two OpenEngine setters store what the getter and shutdown read.test_reasoning_prompt_resolution.py,test_openai_disagg_service.py,test_openai_server_iteration_stats.py,test_anthropic_routes.py,openai_server_stub.py,test_serve_multi_frontend_helpers.py(with a new already-shut-down case),test_control.py,test_node.py,test_openengine.py,test_multimodal.py,test_frontend_lifecycle.py.Commands and results (CPU-only dev container, no GPU):
Property-based check (Hypothesis) on the request-field recording path: recording equals the sampler-field intersection for arbitrary name lists, and model defaulting fills exactly the unrecorded, non-null config fields; four planted bugs (recording junk fields, inverting the recorded check, letting a JSON null override, never applying defaults) were each detected by the properties.
GPU coverage is left to CI: the embeddings e2e (
test_encode_batcher.py), the multi-frontend tests,test_e2e.py::test_openai_health, the disagg e2e blocks inl0_a10.yml, andunittest/grpc/openengine/test_capability_conformance.py.PR Checklist
Please review the following before submitting your PR:
[JIRA ticket/NVBugs ID/GitHub issue/None][type] Summary of the change.🤖 Generated with Claude Code