Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/commands/trtllm-serve/trtllm-serve.rst
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ Start the server and specify the ``--config`` argument with the path to the YAML

After sending at least one inference request to the server, you can fetch runtime iteration statistics by polling the ``/metrics`` endpoint.
Since the statistics are stored in an internal queue and removed once retrieved, it's recommended to poll the endpoint shortly after each request and store the results if needed.
Each statistic is returned to only one caller, so poll the endpoint from a single client: when several clients poll at once, one of them may receive an empty list.

.. code-block:: bash

Expand Down
63 changes: 60 additions & 3 deletions tensorrt_llm/serve/openai_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from tensorrt_llm.executor import CppExecutorError
from tensorrt_llm.executor.postproc_worker import PostprocArgs, PostprocParams
from tensorrt_llm.executor.request import DEFAULT_REQUEST_PRIORITY
from tensorrt_llm.executor.rpc import RPCError
from tensorrt_llm.executor.utils import (CONTEXT_LENGTH_EXCEEDED_CODE,
is_context_length_exceeded_message)
from tensorrt_llm.inputs import prompt_inputs
Expand Down Expand Up @@ -649,6 +650,19 @@ def __init__(
thread_name_prefix="trtllm_media_load",
)
BaseMediaIO.set_executor(self._media_load_executor)
# /metrics reads iteration stats with a blocking call that waits up to
# 2 s. A dedicated thread keeps those waits off the default executor,
# which request handling also uses; get_iteration_stats runs at most
# one read on it at a time.
self._metrics_stats_executor = ThreadPoolExecutor(
max_workers=1,
thread_name_prefix="trtllm_metrics_stats",
)
self._metrics_stats_read: Optional[asyncio.Future] = None
# Whether a scrape is awaiting the last read, and whether a scrape has
# returned its stats.
self._metrics_stats_read_waiting = False
self._metrics_stats_read_consumed = True

model_dir = Path(model)
if model_dir.exists() and model_dir.is_dir():
Expand Down Expand Up @@ -799,6 +813,10 @@ async def lifespan(app: FastAPI):
if self.resource_governor is not None:
self.resource_governor.close()
self.generator.shutdown()
# A stats read still in flight fails fast now that the executor is
# down; the /metrics handler already treats that as no stats.
self._metrics_stats_executor.shutdown(wait=False,
cancel_futures=True)

self.app = FastAPI(lifespan=lifespan)
self.app.router.route_class = _MsgspecRoute
Expand Down Expand Up @@ -1746,6 +1764,11 @@ async def get_iteration_stats(self) -> JSONResponse:
stats.append(stat)
return JSONResponse(content=stats)

# Without iteration stats the engine produces none, so there is
# nothing to wait for.
if not getattr(self.generator.args, "enable_iter_perf_stats", True):
return JSONResponse(content=[])

# When the background collector loop is active it is the sole
# consumer of the engine stats queue; serve /metrics from the tee
# buffer it populates so we do not race it for queue items. Racing
Expand All @@ -1758,9 +1781,43 @@ async def get_iteration_stats(self) -> JSONResponse:
return JSONResponse(content=stats)

# Legacy path: no background collector -> read the queue directly.
stats = []
async for stat in self.generator.get_stats_async(2):
stats.append(stat)
# The read waits up to 2 s for stats inside a blocking executor call,
# so it runs on the metrics thread; on the event loop it would stall
# every response being streamed for as long as it waits.
# One read at a time: a scrape that arrives while a read is in flight
# returns no stats instead of queueing behind it, so scraping an idle
# server faster than one read per 2 s cannot build a backlog. A read
# drains the queue and hands everything to its own scrape, so when two
# scrapers poll at once, one of them regularly gets an empty list.
read = self._metrics_stats_read
if read is not None and (not read.done()
or self._metrics_stats_read_waiting):
# In flight, or finished with its scrape about to take the stats.
return JSONResponse(content=[])
if (read is None or self._metrics_stats_read_consumed
or read.cancelled()):
read = asyncio.get_running_loop().run_in_executor(
self._metrics_stats_executor, self.generator.get_stats, 2)
self._metrics_stats_read = read
self._metrics_stats_read_consumed = False
# Otherwise the last read finished after its scrape went away, and
# this scrape takes the stats it drained rather than losing them.
self._metrics_stats_read_waiting = True
try:
# Shielded so a scrape that goes away leaves the read running
# until its thread finishes, and its stats for the next scrape.
stats = await asyncio.shield(read)
except RPCError as e:
# The executor is unreachable, e.g. while it shuts down.
logger.debug(f"Failed to fetch iteration stats: {e}")
stats = []
except Exception:
# The read failed; the next scrape starts a new one.
self._metrics_stats_read_consumed = True
raise
finally:
self._metrics_stats_read_waiting = False
self._metrics_stats_read_consumed = True
return JSONResponse(content=stats)

async def get_energy_metrics(self) -> JSONResponse:
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_lists/test-db/l0_cpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ l0_cpu:
- unittest/llmapi/apps/test_harmony_parsing.py::TestStripIncompleteMessagesReporting
- unittest/llmapi/apps/test_kimi_serve_extensions.py
- unittest/llmapi/apps/test_openai_chat_disagg_multimodal.py
- unittest/llmapi/apps/test_openai_server_iteration_stats.py
- unittest/llmapi/apps/test_reasoning_prompt_resolution.py
# test_request_metrics.py as a whole is not registered on any list, so only the
# two cases added with the KV-transfer fix are selected here. Registering the
Expand Down
Loading
Loading