From 21f7d56a770e85b40a0b0d8632de88a6bd44e7c0 Mon Sep 17 00:00:00 2001 From: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> Date: Wed, 3 Dec 2025 03:16:06 +0000 Subject: [PATCH 1/6] [None][fix] Preserving ip:port for trtllm-serve before initializing llm Signed-off-by: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> --- tensorrt_llm/commands/serve.py | 66 +++++++++++++++-------------- tensorrt_llm/serve/openai_server.py | 5 ++- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/tensorrt_llm/commands/serve.py b/tensorrt_llm/commands/serve.py index affa88dc4e26..0eb0d1901a42 100644 --- a/tensorrt_llm/commands/serve.py +++ b/tensorrt_llm/commands/serve.py @@ -3,6 +3,7 @@ import json import os import signal # Added import +import socket import subprocess # nosec B404 import sys from pathlib import Path @@ -176,37 +177,40 @@ def launch_server( backend = llm_args["backend"] model = llm_args["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 - - # AutoDeploy does not support build_config - llm_args.pop("build_config", None) - llm = AutoDeployLLM(**llm_args) - elif backend == 'tensorrt' or backend == 'trt': - llm_args.pop("backend") - llm = LLM(**llm_args) - else: - raise click.BadParameter( - f"{backend} is not a known backend, check help for available options.", - param_hint="backend") - - server = OpenAIServer(llm=llm, - model=model, - tool_parser=tool_parser, - server_role=server_role, - metadata_server_cfg=metadata_server_cfg, - disagg_cluster_config=disagg_cluster_config, - multimodal_server_config=multimodal_server_config, - chat_template=chat_template) - - # Optionally disable GC (default: not disabled) - if os.getenv("TRTLLM_SERVER_DISABLE_GC", "0") == "1": - gc.disable() - - asyncio.run(server(host, port)) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((host, port)) + + 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 + + # AutoDeploy does not support build_config + llm_args.pop("build_config", None) + llm = AutoDeployLLM(**llm_args) + elif backend == 'tensorrt' or backend == 'trt': + llm_args.pop("backend") + llm = LLM(**llm_args) + else: + raise click.BadParameter( + f"{backend} is not a known backend, check help for available options.", + param_hint="backend") + + server = OpenAIServer(llm=llm, + model=model, + tool_parser=tool_parser, + server_role=server_role, + metadata_server_cfg=metadata_server_cfg, + disagg_cluster_config=disagg_cluster_config, + multimodal_server_config=multimodal_server_config, + chat_template=chat_template) + + # Optionally disable GC (default: not disabled) + if os.getenv("TRTLLM_SERVER_DISABLE_GC", "0") == "1": + gc.disable() + + asyncio.run(server(host, port, sockets=[s])) def launch_mm_encoder_server( diff --git a/tensorrt_llm/serve/openai_server.py b/tensorrt_llm/serve/openai_server.py index dcaa23494980..5886a7674c4f 100644 --- a/tensorrt_llm/serve/openai_server.py +++ b/tensorrt_llm/serve/openai_server.py @@ -3,6 +3,7 @@ import os import re import signal +import socket import traceback from collections import deque from contextlib import asynccontextmanager @@ -990,7 +991,7 @@ async def create_stream_response(generator, request: ResponsesRequest, sampling_ return JSONResponse(content={"detail": "None"}) - async def __call__(self, host, port): + async def __call__(self, host, port, sockets: list[socket.socket] = None): # Store the binding address for server registration self.binding_addr = f"http://{host}:{port}" self.host = host @@ -1000,4 +1001,4 @@ async def __call__(self, host, port): port=port, log_level="info", timeout_keep_alive=TIMEOUT_KEEP_ALIVE) - await uvicorn.Server(config).serve() + await uvicorn.Server(config).serve(sockets=sockets) From d21b90ce916d1c59c6dae37058454d6e4f230f95 Mon Sep 17 00:00:00 2001 From: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> Date: Fri, 5 Dec 2025 06:29:14 +0000 Subject: [PATCH 2/6] Update error message Signed-off-by: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> --- tensorrt_llm/commands/serve.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tensorrt_llm/commands/serve.py b/tensorrt_llm/commands/serve.py index 0eb0d1901a42..716e27bda4fc 100644 --- a/tensorrt_llm/commands/serve.py +++ b/tensorrt_llm/commands/serve.py @@ -178,7 +178,10 @@ def launch_server( backend = llm_args["backend"] model = llm_args["model"] with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind((host, port)) + try: + s.bind((host, port)) + except OSError as e: + raise RuntimeError(f"Failed to bind socket to {host}:{port}: {e}") if backend == 'pytorch': llm_args.pop("build_config", None) From 319e1ef1f4c70dd135b891b4e2e2fab62a014579 Mon Sep 17 00:00:00 2001 From: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> Date: Fri, 5 Dec 2025 07:25:45 +0000 Subject: [PATCH 3/6] Fix new tests Signed-off-by: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> --- tensorrt_llm/serve/openai_server.py | 2 +- .../accuracy/test_disaggregated_serving.py | 19 +------------------ tests/integration/defs/common.py | 19 +++++++++++++++++++ .../defs/disaggregated/test_disaggregated.py | 6 ++++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/tensorrt_llm/serve/openai_server.py b/tensorrt_llm/serve/openai_server.py index 5886a7674c4f..e64c5d20df69 100644 --- a/tensorrt_llm/serve/openai_server.py +++ b/tensorrt_llm/serve/openai_server.py @@ -991,7 +991,7 @@ async def create_stream_response(generator, request: ResponsesRequest, sampling_ return JSONResponse(content={"detail": "None"}) - async def __call__(self, host, port, sockets: list[socket.socket] = None): + async def __call__(self, host, port, sockets: list[socket.socket] | None = None): # Store the binding address for server registration self.binding_addr = f"http://{host}:{port}" self.host = host diff --git a/tests/integration/defs/accuracy/test_disaggregated_serving.py b/tests/integration/defs/accuracy/test_disaggregated_serving.py index e00e8850ee0f..3d7ed84dfdfb 100644 --- a/tests/integration/defs/accuracy/test_disaggregated_serving.py +++ b/tests/integration/defs/accuracy/test_disaggregated_serving.py @@ -13,8 +13,8 @@ import pytest import requests import yaml +from defs.common import revise_disaggregated_server_config_urls_with_free_ports -from tensorrt_llm._utils import get_free_port from tensorrt_llm.executor.result import GenerationResultBase from tensorrt_llm.llmapi import CompletionOutput, RequestOutput, SamplingParams from tensorrt_llm.llmapi.llm_args import LlmArgs @@ -68,23 +68,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): return False -def revise_disaggregated_server_config_urls_with_free_ports( - disaggregated_server_config: Dict[str, Any]) -> Dict[str, Any]: - num_ctx_ports = len(disaggregated_server_config["context_servers"]["urls"]) - num_gen_ports = len( - disaggregated_server_config["generation_servers"]["urls"]) - - disaggregated_server_config['port'] = get_free_port() - disaggregated_server_config["context_servers"]["urls"] = [ - f"localhost:{get_free_port()}" for _ in range(num_ctx_ports) - ] - disaggregated_server_config["generation_servers"]["urls"] = [ - f"localhost:{get_free_port()}" for _ in range(num_gen_ports) - ] - - return disaggregated_server_config - - @contextlib.contextmanager def launch_disaggregated_llm( disaggregated_server_config: Dict[str, Any], diff --git a/tests/integration/defs/common.py b/tests/integration/defs/common.py index 9b78e1bdee57..4986f7617cfb 100644 --- a/tests/integration/defs/common.py +++ b/tests/integration/defs/common.py @@ -20,10 +20,12 @@ import time from difflib import SequenceMatcher from pathlib import Path +from typing import Any from packaging import version from tensorrt_llm import LLM as LLM_torch +from tensorrt_llm._utils import get_free_port from tensorrt_llm.executor.request import LoRARequest from tensorrt_llm.lora_manager import LoraConfig from tensorrt_llm.sampling_params import SamplingParams @@ -1147,3 +1149,20 @@ def wait_for_server(host, port, timeout_seconds=180): except (socket.error, ConnectionRefusedError, OSError): time.sleep(2) return False + + +def revise_disaggregated_server_config_urls_with_free_ports( + disaggregated_server_config: dict[str, Any]) -> dict[str, Any]: + num_ctx_ports = len(disaggregated_server_config["context_servers"]["urls"]) + num_gen_ports = len( + disaggregated_server_config["generation_servers"]["urls"]) + + disaggregated_server_config['port'] = get_free_port() + disaggregated_server_config["context_servers"]["urls"] = [ + f"localhost:{get_free_port()}" for _ in range(num_ctx_ports) + ] + disaggregated_server_config["generation_servers"]["urls"] = [ + f"localhost:{get_free_port()}" for _ in range(num_gen_ports) + ] + + return disaggregated_server_config diff --git a/tests/integration/defs/disaggregated/test_disaggregated.py b/tests/integration/defs/disaggregated/test_disaggregated.py index 3be534531ab3..d9d90c6383ae 100644 --- a/tests/integration/defs/disaggregated/test_disaggregated.py +++ b/tests/integration/defs/disaggregated/test_disaggregated.py @@ -22,7 +22,8 @@ import pytest import yaml -from defs.common import wait_for_server +from defs.common import ( + revise_disaggregated_server_config_urls_with_free_ports, wait_for_server) from defs.conftest import (get_sm_version, llm_models_root, skip_arm, skip_no_hopper) from defs.trt_test_alternative import check_call, check_output, popen @@ -277,7 +278,8 @@ def get_test_config(test_desc, example_dir, test_root): raise ValueError(f"Invalid test description: {test_desc}, " f"valid descriptions are: {config_map.keys()}") - return config_map[test_desc] + return revise_disaggregated_server_config_urls_with_free_ports( + config_map[test_desc]) def get_extra_llm_config(config, suffix, cwd): From 9964511fae6f8498cd23497c7281e52e0649c61d Mon Sep 17 00:00:00 2001 From: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:20:57 +0000 Subject: [PATCH 4/6] update Signed-off-by: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> --- .../defs/disaggregated/test_disaggregated.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/integration/defs/disaggregated/test_disaggregated.py b/tests/integration/defs/disaggregated/test_disaggregated.py index d9d90c6383ae..2be9a6cf998f 100644 --- a/tests/integration/defs/disaggregated/test_disaggregated.py +++ b/tests/integration/defs/disaggregated/test_disaggregated.py @@ -278,8 +278,17 @@ def get_test_config(test_desc, example_dir, test_root): raise ValueError(f"Invalid test description: {test_desc}, " f"valid descriptions are: {config_map.keys()}") - return revise_disaggregated_server_config_urls_with_free_ports( - config_map[test_desc]) + # Revise the config file to use free ports + new_config = None + with open(config_map[test_desc][1], 'r') as f: + config = yaml.safe_load(f) + new_config = revise_disaggregated_server_config_urls_with_free_ports( + config) + + with open(config_map[test_desc][1], 'w') as f: + yaml.dump(new_config, f) + + return config_map[test_desc] def get_extra_llm_config(config, suffix, cwd): From b5859a71b5d09937b98045690bfe36b10e5f9dff Mon Sep 17 00:00:00 2001 From: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:45:26 +0000 Subject: [PATCH 5/6] Fix more tests Signed-off-by: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> --- tests/integration/defs/common.py | 26 ++++++++----- .../defs/disaggregated/test_disaggregated.py | 37 +++++++++++-------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/tests/integration/defs/common.py b/tests/integration/defs/common.py index 4986f7617cfb..243801a8c9e9 100644 --- a/tests/integration/defs/common.py +++ b/tests/integration/defs/common.py @@ -1153,16 +1153,22 @@ def wait_for_server(host, port, timeout_seconds=180): def revise_disaggregated_server_config_urls_with_free_ports( disaggregated_server_config: dict[str, Any]) -> dict[str, Any]: - num_ctx_ports = len(disaggregated_server_config["context_servers"]["urls"]) - num_gen_ports = len( - disaggregated_server_config["generation_servers"]["urls"]) - + # Revise serve port disaggregated_server_config['port'] = get_free_port() - disaggregated_server_config["context_servers"]["urls"] = [ - f"localhost:{get_free_port()}" for _ in range(num_ctx_ports) - ] - disaggregated_server_config["generation_servers"]["urls"] = [ - f"localhost:{get_free_port()}" for _ in range(num_gen_ports) - ] + + # Revise context and generation server urls + ctx_urls = disaggregated_server_config["context_servers"]["urls"] + gen_urls = disaggregated_server_config["generation_servers"]["urls"] + url_map = dict() + for url in set(ctx_urls + gen_urls): + url_map[url] = (url.split(':')[0], get_free_port()) + + for i, url in enumerate(ctx_urls): + disaggregated_server_config["context_servers"]["urls"][ + i] = f"{url_map[url][0]}:{url_map[url][1]}" + + for i, url in enumerate(gen_urls): + disaggregated_server_config["generation_servers"]["urls"][ + i] = f"{url_map[url][0]}:{url_map[url][1]}" return disaggregated_server_config diff --git a/tests/integration/defs/disaggregated/test_disaggregated.py b/tests/integration/defs/disaggregated/test_disaggregated.py index 2be9a6cf998f..07a5adc9874b 100644 --- a/tests/integration/defs/disaggregated/test_disaggregated.py +++ b/tests/integration/defs/disaggregated/test_disaggregated.py @@ -28,7 +28,7 @@ skip_no_hopper) from defs.trt_test_alternative import check_call, check_output, popen -from tensorrt_llm._utils import mpi_disabled +from tensorrt_llm._utils import get_free_port, mpi_disabled from tensorrt_llm.logger import logger @@ -144,12 +144,12 @@ def validate_timing_metrics(perf_metrics_item, request_context=""): return True -def get_disagg_server_url_from_cfg(config_file: str) -> str: +def get_disagg_server_url_from_cfg(config_file: str) -> tuple[str, int]: with open(config_file, 'r') as file: config = yaml.safe_load(file) server_host = config.get('hostname', 'localhost') server_port = config.get('port', 8000) - return f"http://{server_host}:{server_port}" + return server_host, server_port def get_test_config(test_desc, example_dir, test_root): @@ -285,10 +285,11 @@ def get_test_config(test_desc, example_dir, test_root): new_config = revise_disaggregated_server_config_urls_with_free_ports( config) - with open(config_map[test_desc][1], 'w') as f: + temp_fd, new_config_file = tempfile.mkstemp(suffix=f'_{test_desc}.yaml') + with os.fdopen(temp_fd, 'w') as f: yaml.dump(new_config, f) - return config_map[test_desc] + return (config_map[test_desc][0], new_config_file) def get_extra_llm_config(config, suffix, cwd): @@ -492,7 +493,8 @@ def run_disaggregated_test(example_dir, 'trtllm-serve', 'disaggregated', '--server_start_timeout', str(server_start_timeout), '-c', config_file ] - server_url = get_disagg_server_url_from_cfg(config_file) + server_host, server_port = get_disagg_server_url_from_cfg(config_file) + server_url = f"http://{server_host}:{server_port}" try: if not use_ray: @@ -549,8 +551,8 @@ def run_disaggregated_test(example_dir, env=run_env, cwd=cwd)) - if not wait_for_server("localhost", - 8000, + if not wait_for_server(server_host, + server_port, timeout_seconds=server_start_timeout): raise RuntimeError( f"Disaggregated server failed to start within {server_start_timeout} seconds" @@ -1580,6 +1582,7 @@ def run_disaggregated_benchmark(example_dir, 'trtllm-serve', 'disaggregated', '--server_start_timeout', str(server_start_timeout), '-c', config_file ] + server_host, server_port = get_disagg_server_url_from_cfg(config_file) try: with ( # Start workers open('output_workers.log', 'w') as output_workers, @@ -1633,9 +1636,9 @@ def run_disaggregated_benchmark(example_dir, '--max-concurrency', str(max_concurrency), '--host', - 'localhost', + server_host, '--port', - '8000', + str(server_port), '--ignore-eos', '--no-test-input', '--percentile-metrics', @@ -1677,7 +1680,7 @@ def get_config_for_benchmark(model_root, backend): serve_config = { "model": model_root, "hostname": "localhost", - "port": 8000, + "port": get_free_port(), "backend": "pytorch", "context_servers": { "num_instances": 1, @@ -1691,7 +1694,7 @@ def get_config_for_benchmark(model_root, backend): "backend": backend, "max_tokens_in_buffer": 512, }, - "urls": ["localhost:8001"] + "urls": [f"localhost:{get_free_port()}"] }, "generation_servers": { "num_instances": 1, @@ -1704,7 +1707,7 @@ def get_config_for_benchmark(model_root, backend): "backend": backend, "max_tokens_in_buffer": 512, }, - "urls": ["localhost:8002"] + "urls": [f"localhost:{get_free_port()}"] } } return serve_config @@ -1735,6 +1738,7 @@ def run_disaggregated_genai_perf(config_file, ] artifact_dir = os.path.join(cwd or ".", "benchmark-results") + server_host, server_port = get_disagg_server_url_from_cfg(config_file) try: with (open('output_workers.log', 'w') as output_workers, @@ -1751,8 +1755,9 @@ def run_disaggregated_genai_perf(config_file, cwd=cwd) as server_proc): # Wait for server to be ready - if not wait_for_server( - "localhost", 8000, timeout_seconds=server_start_timeout): + if not wait_for_server(server_host, + server_port, + timeout_seconds=server_start_timeout): raise RuntimeError( f"Disaggregated server did not become ready within {server_start_timeout} seconds" ) @@ -1762,7 +1767,7 @@ def run_disaggregated_genai_perf(config_file, 'genai-perf', 'profile', '--model', model_path, '--tokenizer', model_path, '--endpoint-type', 'chat', '--endpoint', '/v1/chat/completions', '--streaming', '--url', - 'localhost:8000', '--synthetic-input-tokens-mean', + f'{server_host}:{server_port}', '--synthetic-input-tokens-mean', str(input_tokens), '--synthetic-input-tokens-stddev', '0', '--output-tokens-mean', str(output_tokens), '--output-tokens-stddev', '0', From 40f33d752fe9a5178ed61fc0d703628d98ff555b Mon Sep 17 00:00:00 2001 From: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> Date: Sat, 6 Dec 2025 04:20:08 +0000 Subject: [PATCH 6/6] Fix more tests Signed-off-by: Junyi Xu <219237550+JunyiXu-nv@users.noreply.github.com> --- tests/integration/defs/common.py | 17 +++++++++++++++++ .../defs/disaggregated/test_disaggregated.py | 18 ++++-------------- .../defs/disaggregated/test_workers.py | 2 ++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/tests/integration/defs/common.py b/tests/integration/defs/common.py index 243801a8c9e9..5ff8799fb3b4 100644 --- a/tests/integration/defs/common.py +++ b/tests/integration/defs/common.py @@ -17,11 +17,13 @@ import platform import re import socket +import tempfile import time from difflib import SequenceMatcher from pathlib import Path from typing import Any +import yaml from packaging import version from tensorrt_llm import LLM as LLM_torch @@ -1172,3 +1174,18 @@ def revise_disaggregated_server_config_urls_with_free_ports( i] = f"{url_map[url][0]}:{url_map[url][1]}" return disaggregated_server_config + + +def revise_disagg_config_file_with_free_ports(disagg_config_file: str) -> str: + # Revise the config file to use free ports + new_config = None + with open(disagg_config_file, 'r') as f: + config = yaml.safe_load(f) + new_config = revise_disaggregated_server_config_urls_with_free_ports( + config) + + temp_fd, new_config_file = tempfile.mkstemp(suffix='.yaml') + with os.fdopen(temp_fd, 'w') as f: + yaml.dump(new_config, f) + + return new_config_file diff --git a/tests/integration/defs/disaggregated/test_disaggregated.py b/tests/integration/defs/disaggregated/test_disaggregated.py index 07a5adc9874b..b2db88f0d24e 100644 --- a/tests/integration/defs/disaggregated/test_disaggregated.py +++ b/tests/integration/defs/disaggregated/test_disaggregated.py @@ -22,8 +22,8 @@ import pytest import yaml -from defs.common import ( - revise_disaggregated_server_config_urls_with_free_ports, wait_for_server) +from defs.common import (revise_disagg_config_file_with_free_ports, + wait_for_server) from defs.conftest import (get_sm_version, llm_models_root, skip_arm, skip_no_hopper) from defs.trt_test_alternative import check_call, check_output, popen @@ -278,18 +278,8 @@ def get_test_config(test_desc, example_dir, test_root): raise ValueError(f"Invalid test description: {test_desc}, " f"valid descriptions are: {config_map.keys()}") - # Revise the config file to use free ports - new_config = None - with open(config_map[test_desc][1], 'r') as f: - config = yaml.safe_load(f) - new_config = revise_disaggregated_server_config_urls_with_free_ports( - config) - - temp_fd, new_config_file = tempfile.mkstemp(suffix=f'_{test_desc}.yaml') - with os.fdopen(temp_fd, 'w') as f: - yaml.dump(new_config, f) - - return (config_map[test_desc][0], new_config_file) + return (config_map[test_desc][0], + revise_disagg_config_file_with_free_ports(config_map[test_desc][1])) def get_extra_llm_config(config, suffix, cwd): diff --git a/tests/integration/defs/disaggregated/test_workers.py b/tests/integration/defs/disaggregated/test_workers.py index c4fb51f63db3..b1b537ec35d8 100644 --- a/tests/integration/defs/disaggregated/test_workers.py +++ b/tests/integration/defs/disaggregated/test_workers.py @@ -9,6 +9,7 @@ import aiohttp import pytest import yaml +from defs.common import revise_disagg_config_file_with_free_ports from defs.conftest import skip_no_hopper from defs.trt_test_alternative import popen from transformers import AutoTokenizer @@ -42,6 +43,7 @@ def run_disaggregated_workers( num_ranks: Optional[int] = None ) -> Tuple[Generator[subprocess.Popen, None, None], List[str], List[str]]: + config_file = revise_disagg_config_file_with_free_ports(config_file) ctx_servers, gen_servers = get_ctx_gen_server_urls_from_cfg(config_file) # TODO: auto detect num_ranks