From 4d557a69d47cc133d7579e57b4ef8bc4b192a72d Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:12:03 +0800 Subject: [PATCH 1/8] [None][refactor] Organize SMG gRPC adapter by protocol Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- tensorrt_llm/commands/serve.py | 136 +----------------- tensorrt_llm/grpc/__init__.py | 75 +--------- tensorrt_llm/grpc/smg/__init__.py | 16 +++ tensorrt_llm/grpc/smg/bindings.py | 20 +++ .../request_manager.py} | 6 +- tensorrt_llm/grpc/smg/server.py | 134 +++++++++++++++++ .../{grpc_servicer.py => smg/servicer.py} | 6 +- .../test_grpc.py => grpc/smg/test_smg.py} | 10 +- 8 files changed, 189 insertions(+), 214 deletions(-) create mode 100644 tensorrt_llm/grpc/smg/__init__.py create mode 100644 tensorrt_llm/grpc/smg/bindings.py rename tensorrt_llm/grpc/{grpc_request_manager.py => smg/request_manager.py} (98%) create mode 100644 tensorrt_llm/grpc/smg/server.py rename tensorrt_llm/grpc/{grpc_servicer.py => smg/servicer.py} (99%) rename tests/unittest/{llmapi/test_grpc.py => grpc/smg/test_smg.py} (99%) diff --git a/tensorrt_llm/commands/serve.py b/tensorrt_llm/commands/serve.py index 65691acbf91c..8d3bc067c046 100644 --- a/tensorrt_llm/commands/serve.py +++ b/tensorrt_llm/commands/serve.py @@ -57,9 +57,6 @@ # Global variable to store the Popen object of the child process _child_p_global: Optional[subprocess.Popen] = None -# Bound gRPC messages while leaving room for multimodal image payloads. -_GRPC_MAX_MESSAGE_LENGTH_BYTES = 32 * 1024 * 1024 - def _pop_bool_config_option(config: dict[str, Any], key: str) -> bool: return validate_config_bool(config.pop(key, False), key) @@ -631,129 +628,6 @@ def launch_server( _terminate_attached_frontends(frontend_children) -def launch_grpc_server(host: str, - port: int, - llm_args: dict, - served_model_name: Optional[str] = None): - """ - Launch a gRPC server for TensorRT-LLM. - - This provides a high-performance gRPC interface designed for external routers - (e.g., sgl-router) using pre-tokenized input and raw token ID output. - - Args: - host: Host to bind to - port: Port to bind to - llm_args: Arguments for LLM initialization (from get_llm_args) - served_model_name: Custom model name for API responses (defaults to model path) - """ - import grpc - - try: - from grpc_reflection.v1alpha import reflection - REFLECTION_AVAILABLE = True - except ImportError: - REFLECTION_AVAILABLE = False - - from tensorrt_llm.grpc import trtllm_service_pb2, trtllm_service_pb2_grpc - from tensorrt_llm.grpc.grpc_request_manager import GrpcRequestManager - from tensorrt_llm.grpc.grpc_servicer import TrtllmServiceServicer - - async def serve_grpc_async(): - logger.info("Initializing TensorRT-LLM gRPC server...") - - backend = llm_args.get("backend") - model_path = served_model_name or llm_args.get("model", "") - - if backend == "pytorch": - llm_args.pop("build_config", None) - llm = PyTorchLLM(**llm_args) - elif backend == "_autodeploy": - from tensorrt_llm._torch.auto_deploy import LLM as AutoDeployLLM - llm_args.pop("build_config", None) - llm = AutoDeployLLM(**llm_args) - else: - raise click.BadParameter( - f"{backend} is not a known backend, check help for available options.", - param_hint="backend") - - logger.info("Model loaded successfully") - - # Create request manager - request_manager = GrpcRequestManager(llm) - - # Create servicer - servicer = TrtllmServiceServicer(request_manager, model_path=model_path) - - # Create gRPC server - server = grpc.aio.server( - options=[ - ("grpc.max_send_message_length", - _GRPC_MAX_MESSAGE_LENGTH_BYTES), - ("grpc.max_receive_message_length", - _GRPC_MAX_MESSAGE_LENGTH_BYTES), - ("grpc.keepalive_time_ms", 30000), # 30s keepalive - ("grpc.keepalive_timeout_ms", 10000), # 10s timeout - ("grpc.keepalive_permit_without_calls", True), - ("grpc.http2.min_recv_ping_interval_without_data_ms", 10000), - ], ) - - # Add servicer to server - trtllm_service_pb2_grpc.add_TrtllmServiceServicer_to_server( - servicer, server) - - # Enable reflection for grpcurl and other tools - if REFLECTION_AVAILABLE: - service_names = ( - trtllm_service_pb2.DESCRIPTOR.services_by_name["TrtllmService"]. - full_name, - reflection.SERVICE_NAME, - ) - reflection.enable_server_reflection(service_names, server) - logger.info("gRPC reflection enabled") - - # Bind to address - address = f"{host}:{port}" - server.add_insecure_port(address) - - # Start server - await server.start() - logger.info(f"TensorRT-LLM gRPC server started on {address}") - logger.info("Server is ready to accept requests") - - # Handle shutdown signals - loop = asyncio.get_running_loop() - stop_event = asyncio.Event() - - def signal_handler(): - logger.info("Received shutdown signal") - stop_event.set() - - for sig in (signal.SIGTERM, signal.SIGINT): - loop.add_signal_handler(sig, signal_handler) - - # Serve until shutdown signal - try: - await stop_event.wait() - except KeyboardInterrupt: - logger.info("Interrupted by user") - finally: - logger.info("Shutting down TensorRT-LLM gRPC server...") - - # Stop gRPC server - await server.stop(grace=5.0) - logger.info("gRPC server stopped") - - # Shutdown LLM - if hasattr(llm, "shutdown"): - llm.shutdown() - logger.info("LLM engine stopped") - - logger.info("Shutdown complete") - - uvloop.run(serve_grpc_async()) - - def launch_mm_encoder_server( host: str, port: int, @@ -1478,10 +1352,12 @@ def _serve_llm(): f"Argument '{name}' is not supported when running in gRPC mode. " f"The gRPC server is designed for use with external routers that handle " f"these features (e.g., tool parsing, chat templates).") - launch_grpc_server(host, - port, - llm_args, - served_model_name=served_model_name) + from tensorrt_llm.grpc.smg.server import launch_smg_server + + launch_smg_server(host, + port, + llm_args, + served_model_name=served_model_name) else: # Default: launch OpenAI HTTP server launch_server( diff --git a/tensorrt_llm/grpc/__init__.py b/tensorrt_llm/grpc/__init__.py index d75315bd0605..4900715bc879 100644 --- a/tensorrt_llm/grpc/__init__.py +++ b/tensorrt_llm/grpc/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,75 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -r"""TensorRT-LLM gRPC module for high-performance communication with external routers. - -This module provides a gRPC server interface that accepts pre-tokenized requests -and returns raw token IDs, enabling efficient binary communication with Rust-based -routers like sgl-router. - -Key Features: -- Pre-tokenized input (no Python tokenization overhead) -- Raw token ID output (no Python detokenization overhead) -- Streaming support with delta tokens -- Full sampling parameter support -- Guided decoding (JSON schema, regex, grammar) -- LoRA and prompt tuning support -- Disaggregated inference support - -Proto definitions are provided by the smg-grpc-proto package (pip install smg-grpc-proto). - -Usage: - python -m tensorrt_llm.commands.serve /path/to/model \ - --grpc \ - --host 0.0.0.0 \ - --port 50051 -""" - -# Try to import generated protobuf modules from smg-grpc-proto package -try: - from smg_grpc_proto.generated import trtllm_service_pb2, trtllm_service_pb2_grpc - - PROTOS_AVAILABLE = True -except ImportError: - PROTOS_AVAILABLE = False - trtllm_service_pb2 = None - trtllm_service_pb2_grpc = None - -# Try to import request manager -try: - from .grpc_request_manager import ( - GrpcRequestManager, - create_disaggregated_params_from_proto, - create_lora_request_from_proto, - create_sampling_params_from_proto, - ) - - REQUEST_MANAGER_AVAILABLE = True -except ImportError: - REQUEST_MANAGER_AVAILABLE = False - GrpcRequestManager = None - create_sampling_params_from_proto = None - create_lora_request_from_proto = None - create_disaggregated_params_from_proto = None - -# Try to import servicer -try: - from .grpc_servicer import TrtllmServiceServicer - - SERVICER_AVAILABLE = True -except ImportError: - SERVICER_AVAILABLE = False - TrtllmServiceServicer = None - -__all__ = [ - "PROTOS_AVAILABLE", - "REQUEST_MANAGER_AVAILABLE", - "SERVICER_AVAILABLE", - "trtllm_service_pb2", - "trtllm_service_pb2_grpc", - "GrpcRequestManager", - "TrtllmServiceServicer", - "create_sampling_params_from_proto", - "create_lora_request_from_proto", - "create_disaggregated_params_from_proto", -] +"""gRPC protocol integrations for TensorRT-LLM.""" diff --git a/tensorrt_llm/grpc/smg/__init__.py b/tensorrt_llm/grpc/smg/__init__.py new file mode 100644 index 000000000000..b78fe0dff49d --- /dev/null +++ b/tensorrt_llm/grpc/smg/__init__.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""SMG gRPC protocol integration for TensorRT-LLM.""" diff --git a/tensorrt_llm/grpc/smg/bindings.py b/tensorrt_llm/grpc/smg/bindings.py new file mode 100644 index 000000000000..d7f5ae798c87 --- /dev/null +++ b/tensorrt_llm/grpc/smg/bindings.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generated protobuf bindings used by the SMG adapter.""" + +from smg_grpc_proto.generated import trtllm_service_pb2, trtllm_service_pb2_grpc + +__all__ = ["trtllm_service_pb2", "trtllm_service_pb2_grpc"] diff --git a/tensorrt_llm/grpc/grpc_request_manager.py b/tensorrt_llm/grpc/smg/request_manager.py similarity index 98% rename from tensorrt_llm/grpc/grpc_request_manager.py rename to tensorrt_llm/grpc/smg/request_manager.py index ca2e59eb1f36..7b849f4d7b8a 100644 --- a/tensorrt_llm/grpc/grpc_request_manager.py +++ b/tensorrt_llm/grpc/smg/request_manager.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""gRPC Request Manager for TensorRT-LLM. +"""Request manager for the TensorRT-LLM SMG gRPC adapter. Manages request lifecycle for gRPC requests, converting between protobuf and TensorRT-LLM types. Designed for high-performance communication with @@ -35,7 +35,7 @@ from tensorrt_llm.logger import logger from tensorrt_llm.sampling_params import GuidedDecodingParams, SamplingParams -from . import trtllm_service_pb2 as pb2 +from .bindings import trtllm_service_pb2 as pb2 class GrpcRequestManager: diff --git a/tensorrt_llm/grpc/smg/server.py b/tensorrt_llm/grpc/smg/server.py new file mode 100644 index 000000000000..56b2fd29b737 --- /dev/null +++ b/tensorrt_llm/grpc/smg/server.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Server lifecycle for the TensorRT-LLM SMG gRPC adapter.""" + +import asyncio +import signal +from typing import Any + +import click +import grpc +import uvloop + +from tensorrt_llm import LLM as PyTorchLLM +from tensorrt_llm.logger import logger + +from .bindings import trtllm_service_pb2, trtllm_service_pb2_grpc +from .request_manager import GrpcRequestManager +from .servicer import TrtllmServiceServicer + +_GRPC_MAX_MESSAGE_LENGTH_BYTES = 32 * 1024 * 1024 + + +def launch_smg_server( + host: str, + port: int, + llm_args: dict[str, Any], + served_model_name: str | None = None, +) -> None: + """Launch the SMG gRPC server. + + Args: + host: Host to bind to. + port: Port to bind to. + llm_args: Arguments for LLM initialization. + served_model_name: Model name returned by discovery RPCs. Defaults to + the model path. + """ + try: + from grpc_reflection.v1alpha import reflection + except ImportError: + reflection = None + + async def serve_grpc_async() -> None: + logger.info("Initializing TensorRT-LLM SMG gRPC server...") + + backend = llm_args.get("backend") + model_path = served_model_name or llm_args.get("model", "") + + if backend == "pytorch": + llm_args.pop("build_config", None) + llm = PyTorchLLM(**llm_args) + elif backend == "_autodeploy": + from tensorrt_llm._torch.auto_deploy import LLM as AutoDeployLLM + + llm_args.pop("build_config", None) + llm = AutoDeployLLM(**llm_args) + else: + raise click.BadParameter( + f"{backend} is not a known backend, check help for available options.", + param_hint="backend", + ) + + logger.info("Model loaded successfully") + + request_manager = GrpcRequestManager(llm) + servicer = TrtllmServiceServicer(request_manager, model_path=model_path) + + server = grpc.aio.server( + options=[ + ("grpc.max_send_message_length", _GRPC_MAX_MESSAGE_LENGTH_BYTES), + ("grpc.max_receive_message_length", _GRPC_MAX_MESSAGE_LENGTH_BYTES), + ("grpc.keepalive_time_ms", 30000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", True), + ("grpc.http2.min_recv_ping_interval_without_data_ms", 10000), + ] + ) + trtllm_service_pb2_grpc.add_TrtllmServiceServicer_to_server(servicer, server) + + if reflection is not None: + service_names = ( + trtllm_service_pb2.DESCRIPTOR.services_by_name["TrtllmService"].full_name, + reflection.SERVICE_NAME, + ) + reflection.enable_server_reflection(service_names, server) + logger.info("gRPC reflection enabled") + + address = f"{host}:{port}" + server.add_insecure_port(address) + await server.start() + logger.info(f"TensorRT-LLM SMG gRPC server started on {address}") + logger.info("Server is ready to accept requests") + + loop = asyncio.get_running_loop() + stop_event = asyncio.Event() + + def signal_handler() -> None: + logger.info("Received shutdown signal") + stop_event.set() + + for sig in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(sig, signal_handler) + + try: + await stop_event.wait() + except KeyboardInterrupt: + logger.info("Interrupted by user") + finally: + logger.info("Shutting down TensorRT-LLM SMG gRPC server...") + await server.stop(grace=5.0) + logger.info("gRPC server stopped") + + if hasattr(llm, "shutdown"): + llm.shutdown() + logger.info("LLM engine stopped") + logger.info("Shutdown complete") + + uvloop.run(serve_grpc_async()) + + +__all__ = ["launch_smg_server"] diff --git a/tensorrt_llm/grpc/grpc_servicer.py b/tensorrt_llm/grpc/smg/servicer.py similarity index 99% rename from tensorrt_llm/grpc/grpc_servicer.py rename to tensorrt_llm/grpc/smg/servicer.py index 4fd05aab59b9..5b7d0561dbd3 100644 --- a/tensorrt_llm/grpc/grpc_servicer.py +++ b/tensorrt_llm/grpc/smg/servicer.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""gRPC Servicer for TensorRT-LLM. +"""Servicer for the TensorRT-LLM SMG gRPC adapter. Implements the TrtllmService gRPC service for high-performance communication with external routers (e.g., sgl-router) using pre-tokenized input. @@ -31,8 +31,8 @@ from tensorrt_llm.inputs.media_io import _load_and_convert_image from tensorrt_llm.logger import logger -from . import trtllm_service_pb2, trtllm_service_pb2_grpc -from .grpc_request_manager import ( +from .bindings import trtllm_service_pb2, trtllm_service_pb2_grpc +from .request_manager import ( GrpcRequestManager, create_disaggregated_params_from_proto, create_lora_request_from_proto, diff --git a/tests/unittest/llmapi/test_grpc.py b/tests/unittest/grpc/smg/test_smg.py similarity index 99% rename from tests/unittest/llmapi/test_grpc.py rename to tests/unittest/grpc/smg/test_smg.py index cf69a99a0514..f55a3a5addee 100644 --- a/tests/unittest/llmapi/test_grpc.py +++ b/tests/unittest/grpc/smg/test_smg.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Unit tests for gRPC server components.""" +"""Unit tests for the SMG gRPC adapter.""" import asyncio import io @@ -25,18 +25,18 @@ from PIL import Image from tensorrt_llm import LLM -from tensorrt_llm.grpc import trtllm_service_pb2 as pb2 -from tensorrt_llm.grpc.grpc_request_manager import ( +from tensorrt_llm.grpc.smg.bindings import trtllm_service_pb2 as pb2 +from tensorrt_llm.grpc.smg.request_manager import ( GrpcRequestManager, create_disaggregated_params_from_proto, create_lora_request_from_proto, create_sampling_params_from_proto, ) -from tensorrt_llm.grpc.grpc_servicer import TrtllmServiceServicer +from tensorrt_llm.grpc.smg.servicer import TrtllmServiceServicer from tensorrt_llm.llmapi import KvCacheConfig # isort: off -sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..") +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../..") from utils.llm_data import llm_models_root # isort: on From 67b292e8b2a81726cfbc45d9115ae8dee58ecbf4 Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:26:27 +0800 Subject: [PATCH 2/8] [None][refactor] Make smg-grpc-proto an optional dependency Move the SMG gRPC adapter's smg-grpc-proto package out of the default install into an opt-in extra (tensorrt_llm[grpc-smg]), so a default TensorRT-LLM install no longer ships a gateway-specific protobuf package. - requirements.txt: drop smg-grpc-proto; add it to requirements-dev.txt so it stays present in every CI/dev environment (test coverage preserved). - setup.py: add the grpc-smg extra. - grpc/smg/bindings.py, commands/serve.py: guard the import and emit an actionable "pip install tensorrt_llm[grpc-smg]" hint instead of a bare ImportError. - tests: importorskip-guard test_smg.py; add test_grpc_optional.py covering the actionable-error and present-path smoke; register both under l0_a10. Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- requirements-dev.txt | 4 ++ requirements.txt | 1 - setup.py | 5 ++ tensorrt_llm/commands/serve.py | 8 ++- tensorrt_llm/grpc/smg/bindings.py | 9 ++- .../integration/test_lists/test-db/l0_a10.yml | 2 + tests/unittest/grpc/smg/test_smg.py | 20 +++++-- tests/unittest/grpc/test_grpc_optional.py | 58 +++++++++++++++++++ 8 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 tests/unittest/grpc/test_grpc_optional.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 3f2b3340844b..5218957b1a7e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -56,3 +56,7 @@ hf-transfer==0.1.9 line_profiler # workaround to prevent picking up 20260602.2.5 which pulls in NumPy 2.5.0 numpy-typing-compat!=20260602.2.5 +# The SMG gRPC adapter is an optional runtime extra (tensorrt_llm[grpc-smg]) and +# is intentionally not in requirements.txt. Keep it in the dev/test environment +# so the SMG adapter tests run in CI without shipping it in the default install. +smg-grpc-proto>=0.4.2 diff --git a/requirements.txt b/requirements.txt index e592848fc725..be790f1c782f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -93,7 +93,6 @@ cuda-tile>=1.0.1 nvidia-cuda-tileiras>=13.1,<13.2 etcd-sdk-python==0.0.7 python-multipart -smg-grpc-proto>=0.4.2 cache-dit>=1.3.5 librosa msgpack diff --git a/setup.py b/setup.py index e71819c66fb9..557a1dd4c2bb 100644 --- a/setup.py +++ b/setup.py @@ -130,6 +130,10 @@ def has_ext_modules(self): Path("requirements-dev-windows.txt" if on_windows else "requirements-dev.txt")) mx_deps = ["modelexpress==0.4.1"] +# Optional SMG gRPC adapter dependency. Not part of the default install; opt in +# with `pip install tensorrt_llm[grpc-smg]`. Also kept in requirements-dev.txt so +# the adapter is present (and tested) in every CI/dev environment. +grpc_smg_deps = ["smg-grpc-proto>=0.4.2"] constraints_file = Path("constraints.txt") if constraints_file.exists(): constraints, _ = parse_requirements(constraints_file) @@ -475,6 +479,7 @@ def extract_from_precompiled(precompiled_location: str, package_data: list[str], extras_require={ "devel": devel_deps, "mx": mx_deps, + "grpc-smg": grpc_smg_deps, }, zip_safe=True, install_requires=required_deps, diff --git a/tensorrt_llm/commands/serve.py b/tensorrt_llm/commands/serve.py index 8d3bc067c046..7af761e70d53 100644 --- a/tensorrt_llm/commands/serve.py +++ b/tensorrt_llm/commands/serve.py @@ -1352,7 +1352,13 @@ def _serve_llm(): f"Argument '{name}' is not supported when running in gRPC mode. " f"The gRPC server is designed for use with external routers that handle " f"these features (e.g., tool parsing, chat templates).") - from tensorrt_llm.grpc.smg.server import launch_smg_server + try: + from tensorrt_llm.grpc.smg.server import launch_smg_server + except ImportError as e: + raise ValueError( + "gRPC serving with the SMG protocol requires the optional " + "'smg-grpc-proto' package. Install it with: " + "pip install tensorrt_llm[grpc-smg]") from e launch_smg_server(host, port, diff --git a/tensorrt_llm/grpc/smg/bindings.py b/tensorrt_llm/grpc/smg/bindings.py index d7f5ae798c87..9ade5c74c586 100644 --- a/tensorrt_llm/grpc/smg/bindings.py +++ b/tensorrt_llm/grpc/smg/bindings.py @@ -15,6 +15,13 @@ """Generated protobuf bindings used by the SMG adapter.""" -from smg_grpc_proto.generated import trtllm_service_pb2, trtllm_service_pb2_grpc +try: + from smg_grpc_proto.generated import trtllm_service_pb2, trtllm_service_pb2_grpc +except ImportError as e: + raise ImportError( + "The SMG gRPC adapter requires the optional 'smg-grpc-proto' package, " + "which is not part of the default TensorRT-LLM installation. Install it " + "with: pip install tensorrt_llm[grpc-smg]" + ) from e __all__ = ["trtllm_service_pb2", "trtllm_service_pb2_grpc"] diff --git a/tests/integration/test_lists/test-db/l0_a10.yml b/tests/integration/test_lists/test-db/l0_a10.yml index b3b8bdb0e9fa..7ba5d900374e 100644 --- a/tests/integration/test_lists/test-db/l0_a10.yml +++ b/tests/integration/test_lists/test-db/l0_a10.yml @@ -47,6 +47,8 @@ l0_a10: - unittest/others/test_cache_transceiver_precheck_run.py - unittest/others/test_lora_manager.py - unittest/others/test_tracing.py + - unittest/grpc/test_grpc_optional.py + - unittest/grpc/smg/test_smg.py - unittest/disaggregated/test_coordinator_e2e.py - unittest/disaggregated/test_coordinator_worker.py - unittest/disaggregated/test_agent_multi_backends.py diff --git a/tests/unittest/grpc/smg/test_smg.py b/tests/unittest/grpc/smg/test_smg.py index f55a3a5addee..4852788d6bc6 100644 --- a/tests/unittest/grpc/smg/test_smg.py +++ b/tests/unittest/grpc/smg/test_smg.py @@ -19,21 +19,31 @@ import os import sys -import grpc import pytest import torch from PIL import Image from tensorrt_llm import LLM -from tensorrt_llm.grpc.smg.bindings import trtllm_service_pb2 as pb2 -from tensorrt_llm.grpc.smg.request_manager import ( +from tensorrt_llm.llmapi import KvCacheConfig + +# The SMG adapter depends on the optional 'smg-grpc-proto' package +# (pip install tensorrt_llm[grpc-smg]). Skip the whole module cleanly when it is +# absent so collection does not fail in an environment without the extra. +pytest.importorskip( + "smg_grpc_proto", + reason="SMG gRPC adapter extra not installed (pip install tensorrt_llm[grpc-smg])", +) + +import grpc # noqa: E402 + +from tensorrt_llm.grpc.smg.bindings import trtllm_service_pb2 as pb2 # noqa: E402 +from tensorrt_llm.grpc.smg.request_manager import ( # noqa: E402 GrpcRequestManager, create_disaggregated_params_from_proto, create_lora_request_from_proto, create_sampling_params_from_proto, ) -from tensorrt_llm.grpc.smg.servicer import TrtllmServiceServicer -from tensorrt_llm.llmapi import KvCacheConfig +from tensorrt_llm.grpc.smg.servicer import TrtllmServiceServicer # noqa: E402 # isort: off sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../..") diff --git a/tests/unittest/grpc/test_grpc_optional.py b/tests/unittest/grpc/test_grpc_optional.py new file mode 100644 index 000000000000..220dad2223f6 --- /dev/null +++ b/tests/unittest/grpc/test_grpc_optional.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Guards for the optional SMG gRPC extra (``pip install tensorrt_llm[grpc-smg]``). + +These run correctly with or without the extra installed: the "missing" case is +simulated so it is meaningful in every environment, and the "present" case is +guarded with ``importorskip``. +""" + +import sys + +import pytest + + +def test_smg_bindings_missing_gives_actionable_error(monkeypatch): + """A missing 'smg-grpc-proto' yields an actionable install hint. + + Importing the SMG bindings must fail with a ``pip install + tensorrt_llm[grpc-smg]`` hint rather than a bare ImportError. + """ + # Force the optional package to look absent regardless of whether it is + # actually installed in this environment (setting a sys.modules entry to + # None makes ``import smg_grpc_proto`` raise ImportError). + monkeypatch.setitem(sys.modules, "smg_grpc_proto", None) + monkeypatch.delitem(sys.modules, "smg_grpc_proto.generated", raising=False) + # Evict any cached bindings module, otherwise ``import`` is a cache hit that + # returns without re-executing the guard. + monkeypatch.delitem(sys.modules, "tensorrt_llm.grpc.smg.bindings", raising=False) + + with pytest.raises(ImportError, match=r"tensorrt_llm\[grpc-smg\]"): + import tensorrt_llm.grpc.smg.bindings # noqa: F401 + + +def test_smg_bindings_present_smoke(): + """When the extra is installed, the bindings import cleanly. + + They must expose the pb2 modules the SMG adapter depends on. + """ + pytest.importorskip( + "smg_grpc_proto", + reason="SMG gRPC adapter extra not installed", + ) + from tensorrt_llm.grpc.smg import bindings + + assert bindings.trtllm_service_pb2 is not None + assert bindings.trtllm_service_pb2_grpc is not None From 8bd088ee11430936f1c283cc4cf48992d1679169 Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:26:05 +0800 Subject: [PATCH 3/8] [None][fix] Address SMG gRPC review feedback Use the existing devel dependency group, preserve unrelated import failures, and guarantee server and LLM cleanup when gRPC binding or startup fails. Declare protobuf directly because etcd-sdk-python omits its generated bindings runtime dependency, and add regression coverage for the optional import boundary and startup cleanup. Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- requirements-dev.txt | 5 +- requirements.txt | 2 + setup.py | 5 -- tensorrt_llm/commands/serve.py | 12 +-- tensorrt_llm/grpc/smg/bindings.py | 9 +- tensorrt_llm/grpc/smg/server.py | 91 ++++++++++---------- tests/unittest/grpc/smg/test_smg.py | 6 +- tests/unittest/grpc/test_grpc_optional.py | 100 +++++++++++++++++++--- 8 files changed, 155 insertions(+), 75 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5218957b1a7e..4feee5c10e1d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -56,7 +56,6 @@ hf-transfer==0.1.9 line_profiler # workaround to prevent picking up 20260602.2.5 which pulls in NumPy 2.5.0 numpy-typing-compat!=20260602.2.5 -# The SMG gRPC adapter is an optional runtime extra (tensorrt_llm[grpc-smg]) and -# is intentionally not in requirements.txt. Keep it in the dev/test environment -# so the SMG adapter tests run in CI without shipping it in the default install. +# The SMG gRPC adapter is intentionally not part of the default installation. +# Keep it in the devel extra so the adapter is available to developers and CI. smg-grpc-proto>=0.4.2 diff --git a/requirements.txt b/requirements.txt index be790f1c782f..061e6e040fad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -92,6 +92,8 @@ llist cuda-tile>=1.0.1 nvidia-cuda-tileiras>=13.1,<13.2 etcd-sdk-python==0.0.7 +# etcd-sdk-python imports google.protobuf but omits it from its package metadata. +protobuf>=5.27.2 python-multipart cache-dit>=1.3.5 librosa diff --git a/setup.py b/setup.py index 557a1dd4c2bb..e71819c66fb9 100644 --- a/setup.py +++ b/setup.py @@ -130,10 +130,6 @@ def has_ext_modules(self): Path("requirements-dev-windows.txt" if on_windows else "requirements-dev.txt")) mx_deps = ["modelexpress==0.4.1"] -# Optional SMG gRPC adapter dependency. Not part of the default install; opt in -# with `pip install tensorrt_llm[grpc-smg]`. Also kept in requirements-dev.txt so -# the adapter is present (and tested) in every CI/dev environment. -grpc_smg_deps = ["smg-grpc-proto>=0.4.2"] constraints_file = Path("constraints.txt") if constraints_file.exists(): constraints, _ = parse_requirements(constraints_file) @@ -479,7 +475,6 @@ def extract_from_precompiled(precompiled_location: str, package_data: list[str], extras_require={ "devel": devel_deps, "mx": mx_deps, - "grpc-smg": grpc_smg_deps, }, zip_safe=True, install_requires=required_deps, diff --git a/tensorrt_llm/commands/serve.py b/tensorrt_llm/commands/serve.py index 7af761e70d53..d982b8f992b7 100644 --- a/tensorrt_llm/commands/serve.py +++ b/tensorrt_llm/commands/serve.py @@ -13,6 +13,7 @@ import sys import time import uuid +from importlib.util import find_spec from pathlib import Path from typing import Any, Dict, NamedTuple, Optional, Sequence, Set @@ -1112,7 +1113,8 @@ def launch_visual_gen_server( is_flag=True, default=False, help="Run gRPC server instead of OpenAI HTTP server. " - "gRPC server accepts pre-tokenized requests and returns raw token IDs.", + "gRPC server accepts pre-tokenized requests and returns raw token IDs. " + "Requires the tensorrt_llm[devel] extra.", status="prototype") @stability_option( "--served_model_name", @@ -1352,13 +1354,13 @@ def _serve_llm(): f"Argument '{name}' is not supported when running in gRPC mode. " f"The gRPC server is designed for use with external routers that handle " f"these features (e.g., tool parsing, chat templates).") - try: - from tensorrt_llm.grpc.smg.server import launch_smg_server - except ImportError as e: + if find_spec("smg_grpc_proto") is None: raise ValueError( "gRPC serving with the SMG protocol requires the optional " "'smg-grpc-proto' package. Install it with: " - "pip install tensorrt_llm[grpc-smg]") from e + 'pip install "tensorrt_llm[devel]"') + + from tensorrt_llm.grpc.smg.server import launch_smg_server launch_smg_server(host, port, diff --git a/tensorrt_llm/grpc/smg/bindings.py b/tensorrt_llm/grpc/smg/bindings.py index 9ade5c74c586..8bab785c1f12 100644 --- a/tensorrt_llm/grpc/smg/bindings.py +++ b/tensorrt_llm/grpc/smg/bindings.py @@ -17,11 +17,14 @@ try: from smg_grpc_proto.generated import trtllm_service_pb2, trtllm_service_pb2_grpc -except ImportError as e: - raise ImportError( +except ModuleNotFoundError as e: + if e.name != "smg_grpc_proto": + raise + raise ModuleNotFoundError( "The SMG gRPC adapter requires the optional 'smg-grpc-proto' package, " "which is not part of the default TensorRT-LLM installation. Install it " - "with: pip install tensorrt_llm[grpc-smg]" + 'with: pip install "tensorrt_llm[devel]"', + name=e.name, ) from e __all__ = ["trtllm_service_pb2", "trtllm_service_pb2_grpc"] diff --git a/tensorrt_llm/grpc/smg/server.py b/tensorrt_llm/grpc/smg/server.py index 56b2fd29b737..608baa47428f 100644 --- a/tensorrt_llm/grpc/smg/server.py +++ b/tensorrt_llm/grpc/smg/server.py @@ -50,7 +50,9 @@ def launch_smg_server( """ try: from grpc_reflection.v1alpha import reflection - except ImportError: + except ModuleNotFoundError as e: + if e.name != "grpc_reflection": + raise reflection = None async def serve_grpc_async() -> None: @@ -75,58 +77,59 @@ async def serve_grpc_async() -> None: logger.info("Model loaded successfully") - request_manager = GrpcRequestManager(llm) - servicer = TrtllmServiceServicer(request_manager, model_path=model_path) - - server = grpc.aio.server( - options=[ - ("grpc.max_send_message_length", _GRPC_MAX_MESSAGE_LENGTH_BYTES), - ("grpc.max_receive_message_length", _GRPC_MAX_MESSAGE_LENGTH_BYTES), - ("grpc.keepalive_time_ms", 30000), - ("grpc.keepalive_timeout_ms", 10000), - ("grpc.keepalive_permit_without_calls", True), - ("grpc.http2.min_recv_ping_interval_without_data_ms", 10000), - ] - ) - trtllm_service_pb2_grpc.add_TrtllmServiceServicer_to_server(servicer, server) - - if reflection is not None: - service_names = ( - trtllm_service_pb2.DESCRIPTOR.services_by_name["TrtllmService"].full_name, - reflection.SERVICE_NAME, + server = None + try: + request_manager = GrpcRequestManager(llm) + servicer = TrtllmServiceServicer(request_manager, model_path=model_path) + + server = grpc.aio.server( + options=[ + ("grpc.max_send_message_length", _GRPC_MAX_MESSAGE_LENGTH_BYTES), + ("grpc.max_receive_message_length", _GRPC_MAX_MESSAGE_LENGTH_BYTES), + ("grpc.keepalive_time_ms", 30000), + ("grpc.keepalive_timeout_ms", 10000), + ("grpc.keepalive_permit_without_calls", True), + ("grpc.http2.min_recv_ping_interval_without_data_ms", 10000), + ] ) - reflection.enable_server_reflection(service_names, server) - logger.info("gRPC reflection enabled") + trtllm_service_pb2_grpc.add_TrtllmServiceServicer_to_server(servicer, server) - address = f"{host}:{port}" - server.add_insecure_port(address) - await server.start() - logger.info(f"TensorRT-LLM SMG gRPC server started on {address}") - logger.info("Server is ready to accept requests") + if reflection is not None: + service_names = ( + trtllm_service_pb2.DESCRIPTOR.services_by_name["TrtllmService"].full_name, + reflection.SERVICE_NAME, + ) + reflection.enable_server_reflection(service_names, server) + logger.info("gRPC reflection enabled") - loop = asyncio.get_running_loop() - stop_event = asyncio.Event() + address = f"{host}:{port}" + server.add_insecure_port(address) + await server.start() + logger.info(f"TensorRT-LLM SMG gRPC server started on {address}") + logger.info("Server is ready to accept requests") - def signal_handler() -> None: - logger.info("Received shutdown signal") - stop_event.set() + loop = asyncio.get_running_loop() + stop_event = asyncio.Event() - for sig in (signal.SIGTERM, signal.SIGINT): - loop.add_signal_handler(sig, signal_handler) + def signal_handler() -> None: + logger.info("Received shutdown signal") + stop_event.set() + + for sig in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(sig, signal_handler) - try: await stop_event.wait() - except KeyboardInterrupt: - logger.info("Interrupted by user") finally: logger.info("Shutting down TensorRT-LLM SMG gRPC server...") - await server.stop(grace=5.0) - logger.info("gRPC server stopped") - - if hasattr(llm, "shutdown"): - llm.shutdown() - logger.info("LLM engine stopped") - logger.info("Shutdown complete") + try: + if server is not None: + await server.stop(grace=5.0) + logger.info("gRPC server stopped") + finally: + if hasattr(llm, "shutdown"): + llm.shutdown() + logger.info("LLM engine stopped") + logger.info("Shutdown complete") uvloop.run(serve_grpc_async()) diff --git a/tests/unittest/grpc/smg/test_smg.py b/tests/unittest/grpc/smg/test_smg.py index 4852788d6bc6..78248fb0d83b 100644 --- a/tests/unittest/grpc/smg/test_smg.py +++ b/tests/unittest/grpc/smg/test_smg.py @@ -27,11 +27,11 @@ from tensorrt_llm.llmapi import KvCacheConfig # The SMG adapter depends on the optional 'smg-grpc-proto' package -# (pip install tensorrt_llm[grpc-smg]). Skip the whole module cleanly when it is -# absent so collection does not fail in an environment without the extra. +# (pip install "tensorrt_llm[devel]"). Skip the whole module cleanly when it is +# absent so collection does not fail in an environment without the dependency. pytest.importorskip( "smg_grpc_proto", - reason="SMG gRPC adapter extra not installed (pip install tensorrt_llm[grpc-smg])", + reason='SMG gRPC adapter dependency not installed (pip install "tensorrt_llm[devel]")', ) import grpc # noqa: E402 diff --git a/tests/unittest/grpc/test_grpc_optional.py b/tests/unittest/grpc/test_grpc_optional.py index 220dad2223f6..b67785ef5563 100644 --- a/tests/unittest/grpc/test_grpc_optional.py +++ b/tests/unittest/grpc/test_grpc_optional.py @@ -12,14 +12,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Guards for the optional SMG gRPC extra (``pip install tensorrt_llm[grpc-smg]``). +"""Optional-dependency and lifecycle tests for the SMG gRPC adapter. -These run correctly with or without the extra installed: the "missing" case is +These run correctly with or without the dependency installed: the "missing" case is simulated so it is meaningful in every environment, and the "present" case is guarded with ``importorskip``. """ +import asyncio +import builtins +import importlib import sys +import types +from unittest.mock import AsyncMock, MagicMock import pytest @@ -28,31 +33,102 @@ def test_smg_bindings_missing_gives_actionable_error(monkeypatch): """A missing 'smg-grpc-proto' yields an actionable install hint. Importing the SMG bindings must fail with a ``pip install - tensorrt_llm[grpc-smg]`` hint rather than a bare ImportError. + "tensorrt_llm[devel]"`` hint rather than a bare ImportError. """ - # Force the optional package to look absent regardless of whether it is - # actually installed in this environment (setting a sys.modules entry to - # None makes ``import smg_grpc_proto`` raise ImportError). - monkeypatch.setitem(sys.modules, "smg_grpc_proto", None) + real_import = builtins.__import__ + + def import_without_smg(name, globals=None, locals=None, fromlist=(), level=0): + if name == "smg_grpc_proto.generated": + raise ModuleNotFoundError("No module named 'smg_grpc_proto'", name="smg_grpc_proto") + return real_import(name, globals, locals, fromlist, level) + + monkeypatch.delitem(sys.modules, "smg_grpc_proto", raising=False) monkeypatch.delitem(sys.modules, "smg_grpc_proto.generated", raising=False) - # Evict any cached bindings module, otherwise ``import`` is a cache hit that - # returns without re-executing the guard. monkeypatch.delitem(sys.modules, "tensorrt_llm.grpc.smg.bindings", raising=False) + monkeypatch.setattr(builtins, "__import__", import_without_smg) - with pytest.raises(ImportError, match=r"tensorrt_llm\[grpc-smg\]"): + with pytest.raises(ModuleNotFoundError, match=r"tensorrt_llm\[devel\]") as exc_info: import tensorrt_llm.grpc.smg.bindings # noqa: F401 + assert exc_info.value.name == "smg_grpc_proto" + + +def test_smg_bindings_preserves_unrelated_import_error(monkeypatch): + """A transitive dependency failure is not rewritten as an install hint.""" + smg_package = types.ModuleType("smg_grpc_proto") + smg_package.__path__ = [] + generated_package = types.ModuleType("smg_grpc_proto.generated") + + def missing_protobuf(_name): + raise ModuleNotFoundError("No module named 'google.protobuf'", name="google.protobuf") + + generated_package.__getattr__ = missing_protobuf + monkeypatch.setitem(sys.modules, "smg_grpc_proto", smg_package) + monkeypatch.setitem(sys.modules, "smg_grpc_proto.generated", generated_package) + monkeypatch.delitem(sys.modules, "tensorrt_llm.grpc.smg.bindings", raising=False) + + with pytest.raises(ModuleNotFoundError) as exc_info: + importlib.import_module("tensorrt_llm.grpc.smg.bindings") + + assert exc_info.value.name == "google.protobuf" + assert "tensorrt_llm[devel]" not in str(exc_info.value) + def test_smg_bindings_present_smoke(): - """When the extra is installed, the bindings import cleanly. + """When the dependency is installed, the bindings import cleanly. They must expose the pb2 modules the SMG adapter depends on. """ pytest.importorskip( "smg_grpc_proto", - reason="SMG gRPC adapter extra not installed", + reason="SMG gRPC adapter dependency not installed", ) from tensorrt_llm.grpc.smg import bindings assert bindings.trtllm_service_pb2 is not None assert bindings.trtllm_service_pb2_grpc is not None + + +@pytest.mark.cpu_only +@pytest.mark.parametrize("failure_point", ["bind", "start"]) +def test_smg_server_startup_failure_cleans_up(monkeypatch, failure_point): + """Binding and startup failures stop both the gRPC server and LLM.""" + pytest.importorskip( + "smg_grpc_proto", + reason="SMG gRPC adapter dependency not installed", + ) + from tensorrt_llm.grpc.smg import server as server_module + + llm = MagicMock() + grpc_server = MagicMock() + grpc_server.start = AsyncMock() + grpc_server.stop = AsyncMock() + + if failure_point == "bind": + grpc_server.add_insecure_port.side_effect = RuntimeError("bind failed") + else: + grpc_server.add_insecure_port.return_value = 8000 + grpc_server.start.side_effect = RuntimeError("start failed") + + monkeypatch.setitem(sys.modules, "grpc_reflection", None) + monkeypatch.delitem(sys.modules, "grpc_reflection.v1alpha", raising=False) + monkeypatch.setattr(server_module.uvloop, "run", asyncio.run) + monkeypatch.setattr(server_module, "PyTorchLLM", MagicMock(return_value=llm)) + monkeypatch.setattr(server_module, "GrpcRequestManager", MagicMock()) + monkeypatch.setattr(server_module, "TrtllmServiceServicer", MagicMock()) + monkeypatch.setattr(server_module.grpc.aio, "server", MagicMock(return_value=grpc_server)) + monkeypatch.setattr( + server_module.trtllm_service_pb2_grpc, + "add_TrtllmServiceServicer_to_server", + MagicMock(), + ) + + with pytest.raises(RuntimeError, match=f"{failure_point} failed"): + server_module.launch_smg_server( + host="127.0.0.1", + port=8000, + llm_args={"backend": "pytorch", "model": "test-model"}, + ) + + grpc_server.stop.assert_awaited_once_with(grace=5.0) + llm.shutdown.assert_called_once_with() From f7c03dc8caf165697eefa1421cea302cf491380e Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:17:48 +0800 Subject: [PATCH 4/8] [None][fix] Fix SMG gRPC test failures: vocab_size lookup and fixture GPU overlap - GetModelInfo returned vocab_size=0 because TransformersTokenizer does not override vocab_size; the wrapper attribute resolves to PreTrainedTokenizerBase's abstract property which raises NotImplementedError, making hasattr() False. Read vocab_size from the inner HF tokenizer instead. - The two e2e fixtures were module-scoped, so the TinyLlama LLM was still holding ~11 GiB when the Qwen3-VL fixture created its LLM, OOMing on A10. Scope both fixtures to their class so the first LLM is shut down before the second starts. Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- tensorrt_llm/grpc/smg/request_manager.py | 14 ++++++++++++-- tests/unittest/grpc/smg/test_smg.py | 12 ++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tensorrt_llm/grpc/smg/request_manager.py b/tensorrt_llm/grpc/smg/request_manager.py index 7b849f4d7b8a..40e7ec09f822 100644 --- a/tensorrt_llm/grpc/smg/request_manager.py +++ b/tensorrt_llm/grpc/smg/request_manager.py @@ -211,8 +211,18 @@ def get_model_config(self) -> Dict[str, Any]: # Try to get tokenizer info if hasattr(self.llm, "tokenizer") and self.llm.tokenizer is not None: - if hasattr(self.llm.tokenizer, "vocab_size"): - config["vocab_size"] = self.llm.tokenizer.vocab_size + # TransformersTokenizer wraps the HF tokenizer without + # overriding vocab_size, so the wrapper attribute resolves to + # PreTrainedTokenizerBase's abstract property (raises + # NotImplementedError). Read from the inner tokenizer. + tokenizer = self.llm.tokenizer + inner_tokenizer = getattr(tokenizer, "tokenizer", tokenizer) + try: + vocab_size = inner_tokenizer.vocab_size + except (AttributeError, NotImplementedError): + vocab_size = None + if vocab_size: + config["vocab_size"] = int(vocab_size) # Try to get max context length from various sources if hasattr(self.llm, "args") and self.llm.args is not None: diff --git a/tests/unittest/grpc/smg/test_smg.py b/tests/unittest/grpc/smg/test_smg.py index 78248fb0d83b..8bd0fb35317a 100644 --- a/tests/unittest/grpc/smg/test_smg.py +++ b/tests/unittest/grpc/smg/test_smg.py @@ -652,12 +652,15 @@ def get_model_path(model_name): return str(llm_models_root() / model_name) -@pytest.fixture(scope="module") +@pytest.fixture(scope="class") def grpc_service(): """Create a real LLM, request manager, and servicer for e2e testing. Uses TinyLlama-1.1B for minimal GPU resource usage. - Shared across all tests in this module. + Shared across all tests in the class; class scope (not module) so the + LLM is shut down and its GPU memory released before the multimodal + class below creates its own LLM — with module scope both models are + alive at once and the second one OOMs on A10. """ model_path = get_model_path(default_model_name) llm = LLM( @@ -830,12 +833,13 @@ def get_test_image_path(): return str(llm_models_root() / "multimodals" / "test_data" / "seashore.png") -@pytest.fixture(scope="module") +@pytest.fixture(scope="class") def grpc_vlm_service(): """Create a real VLM LLM, request manager, and servicer for multimodal e2e testing. Uses Qwen3-VL-8B-Instruct for vision-language model testing. - Shared across all tests in this module. + Shared across all tests in the class; class scope so this LLM does not + coexist with the one from grpc_service (see note there). """ model_path = get_model_path(vlm_model_name) llm = LLM( From f32d9ea5503d0b0455dbeb51e83c6cee1c30aa89 Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:50:31 +0800 Subject: [PATCH 5/8] [None][refactor] Expose the SMG adapter via a dedicated grpc-smg extra Move the smg-grpc-proto pin from requirements-dev.txt into setup.py as the single source of truth. Add a user-facing grpc-smg extra for SMG gateway deployments; the devel extra aggregates it so developers and CI still get the adapter. Update install hints and tests accordingly. Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- requirements-dev.txt | 3 --- setup.py | 8 +++++++- tensorrt_llm/commands/serve.py | 4 ++-- tensorrt_llm/grpc/smg/bindings.py | 2 +- tests/unittest/grpc/smg/test_smg.py | 6 +++--- tests/unittest/grpc/test_grpc_optional.py | 6 +++--- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4feee5c10e1d..3f2b3340844b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -56,6 +56,3 @@ hf-transfer==0.1.9 line_profiler # workaround to prevent picking up 20260602.2.5 which pulls in NumPy 2.5.0 numpy-typing-compat!=20260602.2.5 -# The SMG gRPC adapter is intentionally not part of the default installation. -# Keep it in the devel extra so the adapter is available to developers and CI. -smg-grpc-proto>=0.4.2 diff --git a/setup.py b/setup.py index deaeacc793e1..28c7b9f84fd0 100644 --- a/setup.py +++ b/setup.py @@ -141,6 +141,11 @@ def has_ext_modules(self): Path("requirements-dev-windows.txt" if on_windows else "requirements-dev.txt")) mx_deps = ["modelexpress==0.4.1"] +# Gateway protocol adapters are opt-in extras: the default installation must +# not carry any gateway protobuf package. The single source of truth for each +# gateway pin lives here; the devel extra aggregates them so developers and CI +# get the adapters without a separate install step. +grpc_smg_deps = ["smg-grpc-proto>=0.4.2"] constraints_file = Path("constraints.txt") if constraints_file.exists(): constraints, _ = parse_requirements(constraints_file) @@ -484,8 +489,9 @@ def extract_from_precompiled(precompiled_location: str, package_data: list[str], }, scripts=['tensorrt_llm/llmapi/trtllm-llmapi-launch'], extras_require={ - "devel": devel_deps, + "devel": devel_deps + grpc_smg_deps, "mx": mx_deps, + "grpc-smg": grpc_smg_deps, }, zip_safe=True, install_requires=required_deps, diff --git a/tensorrt_llm/commands/serve.py b/tensorrt_llm/commands/serve.py index f732bff826a8..867b50511689 100644 --- a/tensorrt_llm/commands/serve.py +++ b/tensorrt_llm/commands/serve.py @@ -1124,7 +1124,7 @@ def launch_visual_gen_server( default=False, help="Run gRPC server instead of OpenAI HTTP server. " "gRPC server accepts pre-tokenized requests and returns raw token IDs. " - "Requires the tensorrt_llm[devel] extra.", + "Requires the tensorrt_llm[grpc-smg] extra.", status="prototype") @stability_option( "--served_model_name", @@ -1370,7 +1370,7 @@ def _serve_llm(): raise ValueError( "gRPC serving with the SMG protocol requires the optional " "'smg-grpc-proto' package. Install it with: " - 'pip install "tensorrt_llm[devel]"') + 'pip install "tensorrt_llm[grpc-smg]"') from tensorrt_llm.grpc.smg.server import launch_smg_server diff --git a/tensorrt_llm/grpc/smg/bindings.py b/tensorrt_llm/grpc/smg/bindings.py index 8bab785c1f12..3bc24ad0e87c 100644 --- a/tensorrt_llm/grpc/smg/bindings.py +++ b/tensorrt_llm/grpc/smg/bindings.py @@ -23,7 +23,7 @@ raise ModuleNotFoundError( "The SMG gRPC adapter requires the optional 'smg-grpc-proto' package, " "which is not part of the default TensorRT-LLM installation. Install it " - 'with: pip install "tensorrt_llm[devel]"', + 'with: pip install "tensorrt_llm[grpc-smg]"', name=e.name, ) from e diff --git a/tests/unittest/grpc/smg/test_smg.py b/tests/unittest/grpc/smg/test_smg.py index 8bd0fb35317a..ca813d40b6d6 100644 --- a/tests/unittest/grpc/smg/test_smg.py +++ b/tests/unittest/grpc/smg/test_smg.py @@ -27,11 +27,11 @@ from tensorrt_llm.llmapi import KvCacheConfig # The SMG adapter depends on the optional 'smg-grpc-proto' package -# (pip install "tensorrt_llm[devel]"). Skip the whole module cleanly when it is -# absent so collection does not fail in an environment without the dependency. +# (pip install "tensorrt_llm[grpc-smg]"). Skip the whole module cleanly when it +# is absent so collection does not fail in an environment without the dependency. pytest.importorskip( "smg_grpc_proto", - reason='SMG gRPC adapter dependency not installed (pip install "tensorrt_llm[devel]")', + reason='SMG gRPC adapter dependency not installed (pip install "tensorrt_llm[grpc-smg]")', ) import grpc # noqa: E402 diff --git a/tests/unittest/grpc/test_grpc_optional.py b/tests/unittest/grpc/test_grpc_optional.py index b67785ef5563..16ae17f941f0 100644 --- a/tests/unittest/grpc/test_grpc_optional.py +++ b/tests/unittest/grpc/test_grpc_optional.py @@ -33,7 +33,7 @@ def test_smg_bindings_missing_gives_actionable_error(monkeypatch): """A missing 'smg-grpc-proto' yields an actionable install hint. Importing the SMG bindings must fail with a ``pip install - "tensorrt_llm[devel]"`` hint rather than a bare ImportError. + "tensorrt_llm[grpc-smg]"`` hint rather than a bare ImportError. """ real_import = builtins.__import__ @@ -47,7 +47,7 @@ def import_without_smg(name, globals=None, locals=None, fromlist=(), level=0): monkeypatch.delitem(sys.modules, "tensorrt_llm.grpc.smg.bindings", raising=False) monkeypatch.setattr(builtins, "__import__", import_without_smg) - with pytest.raises(ModuleNotFoundError, match=r"tensorrt_llm\[devel\]") as exc_info: + with pytest.raises(ModuleNotFoundError, match=r"tensorrt_llm\[grpc-smg\]") as exc_info: import tensorrt_llm.grpc.smg.bindings # noqa: F401 assert exc_info.value.name == "smg_grpc_proto" @@ -71,7 +71,7 @@ def missing_protobuf(_name): importlib.import_module("tensorrt_llm.grpc.smg.bindings") assert exc_info.value.name == "google.protobuf" - assert "tensorrt_llm[devel]" not in str(exc_info.value) + assert "tensorrt_llm[grpc-smg]" not in str(exc_info.value) def test_smg_bindings_present_smoke(): From 6df2c7058563e877aec437e359fb49dd578bbf6c Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:48:24 +0800 Subject: [PATCH 6/8] [None][refactor] Install the grpc-smg extra in the release docker image The CI test environment is provisioned at image granularity: the release image installs the wheel, and test stages only add requirements-dev.txt, which no longer carries smg-grpc-proto. Bake the grpc-smg extra into the wheel install (alongside mx) so unittest/grpc/smg keeps running in CI instead of being silently skipped by importorskip once images rebuild. Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- docker/Dockerfile.multi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.multi b/docker/Dockerfile.multi index 828859395043..ba736e269347 100644 --- a/docker/Dockerfile.multi +++ b/docker/Dockerfile.multi @@ -126,7 +126,7 @@ WORKDIR /app/tensorrt_llm RUN --mount=type=cache,target=/root/.cache/pip --mount=type=bind,from=wheel,source=/src/tensorrt_llm/build,target=/tmp/wheel \ TRTLLM_WHEEL=$(find /tmp/wheel -maxdepth 1 -name 'tensorrt_llm*.whl' -print -quit) && \ test -n "${TRTLLM_WHEEL}" && \ - pip install "${TRTLLM_WHEEL}[mx]" + pip install "${TRTLLM_WHEEL}[mx,grpc-smg]" RUN --mount=type=bind,source=README.md,target=/mnt/ctx/README.md \ --mount=type=bind,source=docs,target=/mnt/ctx/docs \ From f7adfde3edae00cea65e4129f7f0f036c7e89890 Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:16:14 +0800 Subject: [PATCH 7/8] [None][refactor] Provision gateway adapter dependencies per requirements file Give each gRPC gateway a dedicated requirements-.txt as the single source of truth for its dependency pins, starting with requirements-grpc-smg.txt; setup.py parses it into the grpc-smg extra, and the file is owned by OSS compliance like the other requirements files. A requirements file may carry gateway-specific options such as an --extra-index-url without touching the default dependency graph, which a setup.py literal cannot express, so the OpenEngine adapter can declare its BSR-resolved pins the same way. CI test environments install exactly zero or one gateway file per stage, routed by whether the gateway's pins co-resolve with the default environment. SMG co-resolves, so the shared K8s and SLURM provisioning paths install its file next to requirements-dev.txt; this restores the package that unittest/grpc/smg needs after its move out of requirements.txt (without it the module is skipped at collection and pytest exit code 5 fails the stage, pipeline 52819). A gateway whose pins conflict with the default environment installs its file behind a dedicated stage guard instead, as documented at the provisioning site. Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- .github/CODEOWNERS | 1 + docker/Dockerfile.multi | 2 +- jenkins/L0_Test.groovy | 15 +++++++++++++++ jenkins/scripts/slurm_install.sh | 1 + requirements-grpc-smg.txt | 1 + setup.py | 10 ++++++---- 6 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 requirements-grpc-smg.txt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1652e81c5853..e7ce18708a8f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -478,6 +478,7 @@ /jenkins/license_cpp.json @NVIDIA/trt-llm-infra-devs @NVIDIA/trt-llm-oss-compliance /pyproject.toml @NVIDIA/trt-llm-oss-compliance /requirements-dev.txt @NVIDIA/trt-llm-oss-compliance +/requirements-grpc-smg.txt @NVIDIA/trt-llm-oss-compliance /requirements.txt @NVIDIA/trt-llm-oss-compliance /setup.py @NVIDIA/trt-llm-oss-compliance /tests/unittest/api_stability/ @NVIDIA/trt-llm-noncommitted-api-review-committee diff --git a/docker/Dockerfile.multi b/docker/Dockerfile.multi index ba736e269347..4fb2535bd5a0 100644 --- a/docker/Dockerfile.multi +++ b/docker/Dockerfile.multi @@ -110,7 +110,7 @@ COPY scripts scripts COPY tensorrt_llm tensorrt_llm COPY triton_kernels triton_kernels COPY 3rdparty 3rdparty -COPY .gitmodules setup.py requirements.txt requirements-dev.txt constraints.txt README.md ./ +COPY .gitmodules setup.py requirements.txt requirements-dev.txt requirements-grpc-smg.txt constraints.txt README.md ./ ENV CCACHE_DIR=/root/.cache/ccache # Build the TRT-LLM wheel diff --git a/jenkins/L0_Test.groovy b/jenkins/L0_Test.groovy index a3a8a6910166..291bf59a7fb1 100644 --- a/jenkins/L0_Test.groovy +++ b/jenkins/L0_Test.groovy @@ -4227,6 +4227,20 @@ def runLLMTestlistOnPlatformImpl(pipeline, platform, testList, config=VANILLA_CO sh "cd ${llmSrc} && sed -i 's#tensorrt~=.*\$#tensorrt#g' requirements.txt && cat requirements.txt" } trtllm_utils.llmExecStepWithRetry(pipeline, script: "cd ${llmSrc} && pip3 install -r requirements-dev.txt") + // Gateway adapters are opt-in extras excluded from requirements.txt; + // each gateway declares its pins in a dedicated + // requirements-.txt, and a test stage installs exactly + // zero or one gateway file so every adapter is tested under the + // dependency set its real opt-in users receive. A gateway whose + // pins co-resolve with the default environment (SMG today) is + // installed in the shared stages so its unit tests run from the + // regular shard pool instead of being skipped at collection; a + // gateway whose pins conflict with the default environment (for + // example a protobuf major-version floor or a custom package + // index) must instead install its file behind a dedicated stage + // guard and skip this one (see the Ray install below for the + // stage-scoped pattern). + trtllm_utils.llmExecStepWithRetry(pipeline, script: "cd ${llmSrc} && pip3 install -r requirements-grpc-smg.txt") trtllm_utils.llmExecStepWithRetry(pipeline, script: "pip3 install opencv-python-headless") if (stageName.contains("-Ray-")) { trtllm_utils.llmExecStepWithRetry(pipeline, script: "pip3 install ray[default]==2.55.1") @@ -4764,6 +4778,7 @@ def runLLMBuild( } trtllm_utils.llmExecStepWithRetry(pipeline, script: "#!/bin/bash \n" + "cd tensorrt_llm/ && pip3 install -r requirements-dev.txt") + trtllm_utils.llmExecStepWithRetry(pipeline, script: "#!/bin/bash \n" + "cd tensorrt_llm/ && pip3 install -r requirements-grpc-smg.txt") if (env.alternativeTRT) { trtllm_utils.replaceWithAlternativeTRT(env.alternativeTRT, cpver) } diff --git a/jenkins/scripts/slurm_install.sh b/jenkins/scripts/slurm_install.sh index 27c591760f5c..91ca8145bec6 100644 --- a/jenkins/scripts/slurm_install.sh +++ b/jenkins/scripts/slurm_install.sh @@ -31,6 +31,7 @@ slurm_install_setup() { fi retry_command --timeout 2700 bash -c "pip3 install --retries 10 opencv-python-headless" retry_command --timeout 2700 bash -c "cd $llmSrcNode && pip3 install --retries 10 -r requirements-dev.txt" + retry_command --timeout 2700 bash -c "cd $llmSrcNode && pip3 install --retries 10 -r requirements-grpc-smg.txt" retry_command --timeout 2700 bash -c "cd $resourcePathNode && pip3 install --retries 10 --force-reinstall --no-deps TensorRT-LLM/tensorrt_llm-*.whl" gpuUuids=$(nvidia-smi -q | grep "GPU UUID" | awk '{print $4}' | tr '\n' ',' || true) hostNodeName="${HOST_NODE_NAME:-$(hostname -f || hostname)}" diff --git a/requirements-grpc-smg.txt b/requirements-grpc-smg.txt new file mode 100644 index 000000000000..94c9efb9fcd0 --- /dev/null +++ b/requirements-grpc-smg.txt @@ -0,0 +1 @@ +smg-grpc-proto>=0.4.2 diff --git a/setup.py b/setup.py index 28c7b9f84fd0..d4cac2103ed7 100644 --- a/setup.py +++ b/setup.py @@ -142,10 +142,12 @@ def has_ext_modules(self): if on_windows else "requirements-dev.txt")) mx_deps = ["modelexpress==0.4.1"] # Gateway protocol adapters are opt-in extras: the default installation must -# not carry any gateway protobuf package. The single source of truth for each -# gateway pin lives here; the devel extra aggregates them so developers and CI -# get the adapters without a separate install step. -grpc_smg_deps = ["smg-grpc-proto>=0.4.2"] +# not carry any gateway protobuf package. Each gateway owns a dedicated +# requirements-.txt as the single source of truth for its pins; CI +# stages that exercise a gateway install that file explicitly, and the file +# may carry gateway-specific options (such as an --extra-index-url) without +# affecting the default dependency graph. +grpc_smg_deps, _ = parse_requirements(Path("requirements-grpc-smg.txt")) constraints_file = Path("constraints.txt") if constraints_file.exists(): constraints, _ = parse_requirements(constraints_file) From 64cb830137d90a666a4e8d626f72e4accf83c241 Mon Sep 17 00:00:00 2001 From: junq <22017000+QiJune@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:09:02 +0800 Subject: [PATCH 8/8] [None][refactor] Do not preinstall the grpc-smg extra in the release image Install the release image with [mx] only; gateway users add pip install "tensorrt_llm[grpc-smg]" on top, same as wheel users. Co-Authored-By: Claude Fable 5 Signed-off-by: junq <22017000+QiJune@users.noreply.github.com> --- docker/Dockerfile.multi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.multi b/docker/Dockerfile.multi index 4fb2535bd5a0..838c731e9a17 100644 --- a/docker/Dockerfile.multi +++ b/docker/Dockerfile.multi @@ -126,7 +126,7 @@ WORKDIR /app/tensorrt_llm RUN --mount=type=cache,target=/root/.cache/pip --mount=type=bind,from=wheel,source=/src/tensorrt_llm/build,target=/tmp/wheel \ TRTLLM_WHEEL=$(find /tmp/wheel -maxdepth 1 -name 'tensorrt_llm*.whl' -print -quit) && \ test -n "${TRTLLM_WHEEL}" && \ - pip install "${TRTLLM_WHEEL}[mx,grpc-smg]" + pip install "${TRTLLM_WHEEL}[mx]" RUN --mount=type=bind,source=README.md,target=/mnt/ctx/README.md \ --mount=type=bind,source=docs,target=/mnt/ctx/docs \