Skip to content
Open
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
10 changes: 10 additions & 0 deletions tensorrt_llm/executor/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ def is_shutdown(self) -> bool:
"""Return True if the executor is shutting down or fatally errored."""
return self.doing_shutdown or self._fatal_error is not None

@property
def fatal_error(self) -> Optional[BaseException]:
"""The unrecoverable engine error recorded by ``_set_fatal_error``, or None."""
return self._fatal_error

@property
def engine_dead(self) -> bool:
"""Whether the engine is known dead (an unrecoverable error was recorded)."""
return self._fatal_error is not None

def check_health(self) -> bool:
"""Check whether the executor is healthy and able to process requests.

Expand Down
9 changes: 9 additions & 0 deletions tensorrt_llm/executor/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,15 @@ def check_health(self) -> bool:

return True

@property
def engine_dead(self) -> bool:
"""Whether the engine is known dead (a worker died or a fatal error was recorded).

``_mark_engine_dead`` can set the sticky flag before the fatal error
is recorded, so check both.
"""
return self._engine_dead or self._fatal_error is not None

def _set_fatal_error(self, error: BaseException) -> None:
"""Record the fatal error, then unblock every pending request.

Expand Down
24 changes: 10 additions & 14 deletions tensorrt_llm/grpc/openengine/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,19 @@ async def _get_kv_cache_load(self) -> dict:
def _engine_is_healthy(self) -> bool:
"""Readiness via the predicate the engine's own HTTP /health uses.

`_check_health` is stronger than the `is_shutdown()` guard generate_async
`check_health()` is stronger than the `is_shutdown()` guard generate_async
raises on: it drains the executor's error queue, so a fatal error queued
but not yet promoted is caught, and the proxy polls MPI worker liveness.
It also covers encode-only engines, where `_executor` is None.
It also covers encode-only engines, where there is no generation executor.
"""
check = getattr(self._llm, "_check_health", None)
if callable(check):
try:
return bool(check())
except (RuntimeError, ValueError, OSError) as error:
logger.warning(f"OpenEngine health check reported a failure: {error}")
return False
except (TypeError, AttributeError) as error:
logger.error(f"OpenEngine health check contract failed: {error}")
return False
executor = getattr(self._llm, "_executor", None)
return executor is not None and not executor.is_shutdown()
try:
return bool(self._llm.check_health())
except (RuntimeError, ValueError, OSError) as error:
logger.warning(f"OpenEngine health check reported a failure: {error}")
return False
except (TypeError, AttributeError) as error:
logger.error(f"OpenEngine health check contract failed: {error}")
return False

# -- Runtime metadata ---------------------------------------------------

Expand Down
14 changes: 3 additions & 11 deletions tensorrt_llm/grpc/openengine/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,7 @@ async def _refresh_inference_health(self) -> None:
except (RuntimeError, ValueError, OSError, CoordinationError) as error:
logger.warning(f"OpenEngine inference health check failed: {error}")
self._engine_healthy = False
executor = getattr(self._llm, "_executor", None)
if executor is not None and (
getattr(executor, "_fatal_error", None) is not None
or getattr(executor, "_engine_dead", False)
):
if self._llm.engine_dead:
self._fatal_engine_failure = True
if self._stopping:
return
Expand Down Expand Up @@ -412,11 +408,7 @@ def healthy() -> bool:
)

def terminal_engine_failure() -> bool:
executor = getattr(llm, "_executor", None)
return executor is not None and (
getattr(executor, "_fatal_error", None) is not None
or getattr(executor, "_engine_dead", False)
)
return llm.engine_dead

async def supervise() -> None:
while not stop_event.is_set():
Expand All @@ -428,7 +420,7 @@ async def supervise() -> None:
return
if server is None:
engine_ready = await asyncio.wait_for(
asyncio.to_thread(llm._check_health),
asyncio.to_thread(llm.check_health),
timeout=_HEALTH_CHECK_TIMEOUT_SECONDS,
)
if server is None and not engine_ready:
Expand Down
29 changes: 15 additions & 14 deletions tensorrt_llm/grpc/smg/request_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,21 @@ async def health_check(self) -> Tuple[bool, str]:
if self.llm is None:
return False, "LLM not initialized"

# Check executor health (includes error queue, MPI worker
# liveness, and fatal error state — not just doing_shutdown).
if hasattr(self.llm, "_executor"):
if self.llm._executor is None:
return False, "Executor is not available"
if not self.llm._executor.check_health():
error_msg = "Executor is unhealthy"
if self.llm._executor._fatal_error is not None:
exc = self.llm._executor._fatal_error
lines = str(exc).splitlines()
short = (lines[0] if lines else type(exc).__name__)[:200]
error_msg = f"{type(exc).__name__}: {short}"
logger.error(f"Health check fatal error: {repr(exc)}")
return False, error_msg
# An encode-only LLM has no generation executor; SMG serves
# generate requests only.
if self.llm.args.encode_only is True:
return False, "Executor is not available"
# check_health() drains queued errors and checks MPI worker
# liveness and the fatal error state, not just shutdown.
if not self.llm.check_health():
error_msg = "Executor is unhealthy"
exc = self.llm.fatal_error
if exc is not None:
lines = str(exc).splitlines()
short = (lines[0] if lines else type(exc).__name__)[:200]
error_msg = f"{type(exc).__name__}: {short}"
logger.error(f"Health check fatal error: {repr(exc)}")
return False, error_msg

return True, "OK"
except Exception as e:
Expand Down
58 changes: 55 additions & 3 deletions tensorrt_llm/llmapi/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ def _multimodal_params_have_encoder_features(
llm_id (str): The unique ID of the LLM instance.
disaggregated_params (dict): The disaggregated parameters of the LLM instance.
startup_metrics (dict): The startup metrics reported by worker rank 0.
fatal_error (BaseException, optional): The unrecoverable engine error the executor recorded, or None.
shutdown_started (bool): Whether shutdown of the generation executor has begun.
engine_dead (bool): Whether the engine is known dead (a worker died or a fatal error was recorded).
"""


Expand Down Expand Up @@ -1785,11 +1788,20 @@ def _record_usage_shutdown(self) -> None:
except Exception as exc:
logger.debug(f"Usage telemetry shutdown tracking failed: {exc}")

def _check_health(self) -> bool:
"""Check if the LLM is healthy.
@set_api_status("prototype")
def check_health(self) -> bool:
"""Check whether the LLM can serve requests.

For ``encode_only=True`` the LLM is healthy while its encoder executor
exists. Otherwise this delegates to the generation executor's
``check_health()``, which drains queued background errors (a fatal one
is recorded, see ``fatal_error``, and starts executor shutdown) and,
for the multi-process proxy, polls worker liveness. Without a
generation executor (never built, or after ``shutdown()``) the LLM is
unhealthy.

Returns:
bool: True if the executor is running and healthy, False otherwise.
bool: True if the LLM is healthy, False otherwise.
"""
if self._encode_only:
return (hasattr(self, "_encoder_executor")
Expand All @@ -1799,6 +1811,46 @@ def _check_health(self) -> bool:

return False

# Kept for one release for out-of-tree callers that look this name up
# (ai-dynamo components/src/dynamo/trtllm/engine.py); use check_health().
_check_health = check_health

@property
@set_api_status("prototype")
def fatal_error(self) -> Optional[BaseException]:
"""The unrecoverable engine error the executor recorded, if any.

None when no fatal error was recorded or the LLM has no generation
executor (``encode_only=True``, or after ``shutdown()``).
"""
executor = getattr(self, "_executor", None)
return None if executor is None else executor.fatal_error

@property
@set_api_status("prototype")
def shutdown_started(self) -> bool:
"""Whether shutdown of the generation executor has begun.

True once ``shutdown()`` was called or the executor started shutting
down after a fatal error. False for ``encode_only=True``, which has no
generation executor.
"""
executor = getattr(self, "_executor", None)
if executor is None:
return not getattr(self, "_encode_only", False)
return executor.doing_shutdown

@property
@set_api_status("prototype")
def engine_dead(self) -> bool:
"""Whether the engine is known dead (a worker died or a fatal error was recorded).

False when the LLM has no generation executor (``encode_only=True``,
or after ``shutdown()``).
"""
executor = getattr(self, "_executor", None)
return False if executor is None else executor.engine_dead

@staticmethod
def _shutdown_wrapper(self_ref):
# Retrieve the instance if it still exists
Expand Down
25 changes: 13 additions & 12 deletions tensorrt_llm/serve/openai_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1227,9 +1227,11 @@ def _check_health(self) -> bool:
batcher = self.embedding_batcher
if batcher is not None and not batcher.is_alive():
return False
if hasattr(self.generator, '_check_health'):
return self.generator._check_health()
return True
if self._is_visual_gen:
# VisualGen has no check_health(), so its /health always
# answers 200.
return True
return self.generator.check_health()

def register_routes(self):
self.app.add_api_route("/health", self.health, methods=["GET"])
Expand Down Expand Up @@ -1727,15 +1729,14 @@ async def health(self) -> Response:
# its graceful shutdown sequence. The doing_shutdown guard
# ensures we only send SIGINT once — repeated health probes
# won't stack signals during an in-progress teardown.
executor = getattr(self.generator, '_executor', None)
if executor is not None and getattr(executor, '_fatal_error',
None) is not None:
if not getattr(executor, 'doing_shutdown', True):
logger.error(
"Health check detected fatal engine error, initiating "
f"server shutdown: {executor._fatal_error}")
_record_generator_termination(self.generator)
signal.raise_signal(signal.SIGINT)
fatal_error = (None if self._is_visual_gen else
self.generator.fatal_error)
if fatal_error is not None and not self.generator.shutdown_started:
logger.error(
"Health check detected fatal engine error, initiating "
f"server shutdown: {fatal_error}")
_record_generator_termination(self.generator)
signal.raise_signal(signal.SIGINT)
return Response(
status_code=503,
content=
Expand Down
6 changes: 6 additions & 0 deletions tests/integration/test_lists/test-db/l0_cpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ l0_cpu:
- unittest/grpc/openengine/test_openengine.py
- unittest/grpc/openengine/test_metrics.py
- unittest/grpc/openengine/test_servicer.py
- unittest/grpc/openengine/test_private_access_lint_openengine.py
- unittest/grpc/smg/test_smg_health.py
- unittest/grpc/smg/test_smg.py::test_streaming_deltas_preserve_sequences_logprobs_and_final_tokens
- unittest/grpc/smg/test_smg.py::test_streaming_empty_token_history
- unittest/grpc/test_grpc_optional.py::test_protobuf_warning_in_fresh_process
Expand Down Expand Up @@ -295,6 +297,7 @@ l0_cpu:
- unittest/llmapi/test_kv_cache_dtype_override.py
- unittest/llmapi/test_llm_args.py
- unittest/llmapi/test_gms_args.py
- unittest/llmapi/test_llm_public_runtime_api.py
- unittest/llmapi/test_mx_args.py
- unittest/llmapi/test_custom_tokenizer_aliases.py
# Reads the Muse Spark Open tokenizer files from LLM_MODELS_ROOT.
Expand Down Expand Up @@ -334,6 +337,9 @@ l0_cpu:
- unittest/quantization/test_mode.py
- unittest/scaffolding
- unittest/scripts
- unittest/serve/api/visual_gen/test_private_access_lint_visual_gen.py
- unittest/serve/test_private_access_lint.py
- unittest/serve/test_serve_health.py
- unittest/tools
- unittest/usage
- unittest/utils/test_logger.py
Expand Down
13 changes: 13 additions & 0 deletions tests/unittest/api_stability/references/llm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@ methods:
default: 2
return_annotation: tensorrt_llm.executor.result.IterationResult
status: beta
check_health:
parameters: {}
return_annotation: bool
status: prototype
get_data_transceiver_state:
parameters: {}
return_annotation: bytes
Expand Down Expand Up @@ -584,3 +588,12 @@ properties:
startup_metrics:
annotation: dict
default: None
fatal_error:
annotation: Optional[BaseException]
default: inspect._empty
shutdown_started:
annotation: bool
default: inspect._empty
engine_dead:
annotation: bool
default: inspect._empty
Loading
Loading