Skip to content

Commit 1bc798d

Browse files
committed
[https://nvbugs/6336747][fix] Fail fast when executor worker stalls
* Why? A stuck or disconnected executor worker left the proxy blocked indefinitely: the request queue uses an unbounded send HWM with no send timeout, so request_queue.put -> socket.send never returned once the worker stopped draining, and the error monitor never tripped. In CI this could surface as a ~1h hang ending in an opaque timeout kill. The stall itself is non-deterministic and not yet root-caused. * What? Make the failure fast and legible instead: - Bound request submission: poll the socket for send-readiness and check worker liveness, raising RequestError if the worker has not accepted the request within a timeout. - Add a progress watchdog to the error monitor that marks the worker stalled and aborts in-flight requests when no result arrives while requests are outstanding. - Honor the previously-ignored timeout in GenerationResult.result() and bound the per-request wait in the VideoMME evaluator. - On a detected stall, signal the worker (SIGUSR1/faulthandler) to dump all thread stacks so the next occurrence is diagnosable. This mitigates the hang and captures worker state; it does not fix the underlying intermittent stall. Signed-off-by: William Zhang <133824995+2ez4bz@users.noreply.github.com>
1 parent 4768e6f commit 1bc798d

9 files changed

Lines changed: 283 additions & 24 deletions

File tree

tensorrt_llm/evaluate/audio_asr.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from typing import Any, Iterable, NamedTuple, Optional
2121

2222
import soundfile
23-
from tqdm import tqdm
2423

2524
import tensorrt_llm.profiler as profiler
2625
from tensorrt_llm.inputs import (
@@ -36,7 +35,13 @@
3635
from tensorrt_llm.logger import logger
3736
from tensorrt_llm.sampling_params import SamplingParams
3837

39-
from .interface import Evaluator, get_chat_template_kwargs, get_model_context
38+
from .interface import (
39+
RESULT_WAIT_TIMEOUT_SECS,
40+
Evaluator,
41+
get_chat_template_kwargs,
42+
get_model_context,
43+
)
44+
from .progress import tqdm_with_time_prefix
4045

4146

4247
class MultimodalASRSample(NamedTuple):
@@ -174,15 +179,19 @@ def evaluate(
174179
input_context = self._make_input_context(llm)
175180
dataset = _load_local_hf_dataset(self.dataset_path, self.split)
176181
num_samples = self._get_num_samples(dataset)
177-
samples = list(tqdm(self._iter_samples(dataset), desc="Loading samples", total=num_samples))
182+
samples = list(
183+
tqdm_with_time_prefix(
184+
self._iter_samples(dataset), desc="Loading samples", total=num_samples
185+
)
186+
)
178187
inputs = [
179188
self._make_input(llm, sample, input_context)
180-
for sample in tqdm(samples, desc="Loading inputs")
189+
for sample in tqdm_with_time_prefix(samples, desc="Loading inputs")
181190
]
182191
futures = []
183192
references = []
184193
scoring_samples = []
185-
for sample, request_input in tqdm(
194+
for sample, request_input in tqdm_with_time_prefix(
186195
zip(samples, inputs, strict=True), desc="Submitting requests", total=len(samples)
187196
):
188197
params = (
@@ -197,7 +206,10 @@ def evaluate(
197206
)
198207
references.append(sample.transcript)
199208
scoring_samples.append(_sample_for_scoring(sample))
200-
outputs = [future.result() for future in tqdm(futures, desc="Fetching responses")]
209+
outputs = [
210+
future.result(timeout=RESULT_WAIT_TIMEOUT_SECS)
211+
for future in tqdm_with_time_prefix(futures, desc="Fetching responses")
212+
]
201213

202214
profiler.stop("trtllm exec")
203215
elapsed_time = profiler.elapsed_time_in_sec("trtllm exec")

tensorrt_llm/evaluate/interface.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@
2929
from ..logger import logger
3030
from ..sampling_params import SamplingParams
3131

32+
# Per-request upper bound (seconds) on how long an evaluator waits for a single response before
33+
# failing fast. A stalled or dead executor worker would otherwise block `future.result()`
34+
# indefinitely, turning an evaluation into potential hangs.
35+
# This is a backstop: it is intentionally larger than the executor's stall watchdog
36+
# (`TLLM_EXECUTOR_STALL_TIMEOUT_SECS`, default 300s) so the watchdog's more-informative
37+
# `RequestError` normally surfaces first; no healthy single request should come close to it.
38+
RESULT_WAIT_TIMEOUT_SECS = float(
39+
os.environ.get("TLLM_EVAL_RESULT_TIMEOUT_SECS", "600"))
40+
3241

3342
def get_chat_template_kwargs(
3443
template_owner: Any,
@@ -145,7 +154,7 @@ def evaluate(self,
145154
auxiliaries.append(aux)
146155
results = []
147156
for output in tqdm(outputs, desc="Fetching responses"):
148-
results.append(output.result())
157+
results.append(output.result(timeout=RESULT_WAIT_TIMEOUT_SECS))
149158

150159
if self.output_dir:
151160
dump_inference_results(self.output_dir, results,

tensorrt_llm/evaluate/lm_eval.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
from ..llmapi import RequestOutput
4545
from ..logger import logger
4646
from ..sampling_params import SamplingParams
47-
from .interface import (Evaluator, dump_inference_results,
48-
get_chat_template_kwargs)
47+
from .interface import (RESULT_WAIT_TIMEOUT_SECS, Evaluator,
48+
dump_inference_results, get_chat_template_kwargs)
4949

5050
# NOTE: lm_eval uses "<image>" as the default image placeholder
5151
# https://github.com/EleutherAI/lm-evaluation-harness/blob/7f04db12d2f8e7a99a0830d99eb78130e1ba2122/lm_eval/models/hf_vlms.py#L25
@@ -176,7 +176,7 @@ def generate_until(self, requests, disable_tqdm: bool = False) -> List[str]:
176176
for output in tqdm(results,
177177
desc="Fetching responses",
178178
disable=disable_tqdm):
179-
outputs.append(output.result())
179+
outputs.append(output.result(timeout=RESULT_WAIT_TIMEOUT_SECS))
180180

181181
if self.output_dir:
182182
dump_inference_results(self.output_dir, outputs,
@@ -434,7 +434,7 @@ def generate_until(self, requests, disable_tqdm: bool = False) -> List[str]:
434434
for output in tqdm(results,
435435
desc="Fetching responses",
436436
disable=disable_tqdm):
437-
outputs.append(output.result())
437+
outputs.append(output.result(timeout=RESULT_WAIT_TIMEOUT_SECS))
438438

439439
if self.output_dir:
440440
dump_inference_results(self.output_dir, outputs,

tensorrt_llm/evaluate/progress.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
from datetime import datetime
16+
from typing import Any
17+
18+
from tqdm import tqdm
19+
20+
_TIME_PREFIX_BAR_FORMAT = "{current_time} {l_bar}{bar}{r_bar}"
21+
22+
23+
class _TimePrefixTqdm(tqdm):
24+
@property
25+
def format_dict(self) -> dict[str, Any]:
26+
format_dict = super().format_dict
27+
format_dict["current_time"] = datetime.now().strftime("%H:%M:%S")
28+
return format_dict
29+
30+
31+
def tqdm_with_time_prefix(*args: Any, **kwargs: Any) -> _TimePrefixTqdm:
32+
"""Return a tqdm progress bar with the current time rendered before the description."""
33+
kwargs.setdefault("bar_format", _TIME_PREFIX_BAR_FORMAT)
34+
return _TimePrefixTqdm(*args, **kwargs)

tensorrt_llm/executor/ipc.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,22 @@ def poll(self, timeout: int) -> bool:
181181
else:
182182
return False
183183

184+
def poll_send(self, timeout: float) -> bool:
185+
"""Return True if a message can be sent within *timeout* seconds.
186+
187+
Args:
188+
timeout (float): Timeout in seconds.
189+
190+
Returns:
191+
For a PAIR socket whose peer has disconnected, the socket enters mute state, and this
192+
returns `False` instead of blocking, letting callers detect a dead/disconnected peer
193+
rather than blocking forever in `send()`.
194+
"""
195+
self.setup_lazily()
196+
self._check_thread_safety()
197+
return bool(
198+
self.socket.poll(timeout=int(timeout * 1000), flags=zmq.POLLOUT))
199+
184200
def put(self, obj: Any, routing_id: Optional[bytes] = None):
185201
self.setup_lazily()
186202
self._check_thread_safety()

0 commit comments

Comments
 (0)