-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[TRTLLM-14010][feat] report KV cache transfer state on executor hangs #16300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,21 @@ | ||
| # 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. | ||
| import os | ||
| import time | ||
| import uuid | ||
| from collections import defaultdict | ||
| from collections import Counter, defaultdict | ||
| from itertools import chain | ||
| from typing import Any, Callable, Dict, List, Optional, cast | ||
|
|
||
|
|
@@ -150,6 +164,48 @@ def _exchange_rank_info(self): | |
| logger.info(f"layer_num_per_pp: {layer_num_per_pp}") | ||
| logger.info(f"self._context_info_endpoint: {self._context_info_endpoint}") | ||
|
|
||
| def get_status_dump(self) -> str: | ||
| """Return a one-line summary of transceiver state for debugging hangs.""" | ||
|
|
||
| def summarize( | ||
| sessions: Dict[int, Any], | ||
| include_receiver_ready: bool, | ||
| ) -> str: | ||
| sessions_snapshot = list(sessions.values()) | ||
| status_counts = Counter() | ||
| receiver_ready = 0 | ||
| for session in sessions_snapshot: | ||
| status = session.status | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we check |
||
| if isinstance(status, SessionStatus): | ||
| status_counts[status] += 1 | ||
| else: | ||
| status_counts["unknown"] += 1 | ||
|
|
||
| if include_receiver_ready: | ||
| receiver_ready += int(bool(session.receiver_ready)) | ||
|
|
||
| fields = [ | ||
| f"sessions={len(sessions_snapshot)}", | ||
| f"init={status_counts[SessionStatus.INIT]}", | ||
| f"ready_to_transfer={status_counts[SessionStatus.READY]}", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The receive side never yields the ready state. |
||
| f"transferring={status_counts[SessionStatus.TRANSFERRING]}", | ||
| f"kv_transferred={status_counts[SessionStatus.KV_TRANSFERRED]}", | ||
| f"fully_transferred={status_counts[SessionStatus.FULLY_TRANSFERRED]}", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This field stays |
||
| f"error={status_counts[SessionStatus.ERROR]}", | ||
| f"cancelled={status_counts[SessionStatus.CANCELLED]}", | ||
| f"unknown={status_counts['unknown']}", | ||
| ] | ||
| if include_receiver_ready: | ||
| fields.append(f"peer_ready={receiver_ready}/{len(sessions_snapshot)}") | ||
| return ", ".join(fields) | ||
|
|
||
| tx_status = summarize(self._send_sessions, include_receiver_ready=True) | ||
| rx_status = summarize(self._recv_sessions, include_receiver_ready=False) | ||
| return ( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you consider adding a few stuck request ids and the oldest session's age, so context-side and generation-side logs can be joined? |
||
| f"KV cache transceiver | backend=NIXL | TX({tx_status}) | RX({rx_status}) | " | ||
| f"waiting_for_peer_info={len(self._wait_reqs)}" | ||
| ) | ||
|
|
||
| def shutdown(self): | ||
| if getattr(self, "_shutdown", False): | ||
| return | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,6 +100,7 @@ def __init__( | |
| self.lock = threading.Lock() | ||
| self.active = False | ||
| self._detected = False | ||
| self._status_providers: list[Callable[[], str]] = [] | ||
|
|
||
| def start(self): | ||
| """Enable hang detection.""" | ||
|
|
@@ -113,13 +114,38 @@ def run_loop(): | |
| self.loop_thread = threading.Thread(target=run_loop, daemon=True, name="hang_detector_loop") | ||
| self.loop_thread.start() | ||
|
|
||
| async def _detect_hang(self): | ||
| def register_status_provider(self, provider: Callable[[], str]) -> None: | ||
| """Register a nonblocking callable that returns status to dump on hang detection.""" | ||
| with self.lock: | ||
| self._status_providers.append(provider) | ||
|
|
||
| async def _detect_hang(self) -> None: | ||
| await asyncio.sleep(self.timeout) | ||
| with self.lock: | ||
| self._detected = True | ||
| logger.error(f"Hang detected after {self.timeout} seconds.") | ||
| status_providers = tuple(self._status_providers) | ||
|
|
||
| # All diagnostics are best-effort: nothing may prevent on_detected() | ||
| # (hard-kill propagation) from firing. | ||
| _best_effort_log_error(f"Hang detected after {self.timeout} seconds.") | ||
| for provider in status_providers: | ||
| try: | ||
| status = provider() | ||
| if status: | ||
| _best_effort_log_error(status) | ||
| except Exception as error: # noqa: BLE001 - isolate diagnostic providers | ||
| _best_effort_log_error( | ||
| f"HangDetector: status provider failed with {type(error).__name__}: {error}" | ||
| ) | ||
| try: | ||
| print_all_stacks() | ||
| self.on_detected() | ||
| except Exception: # noqa: BLE001 - stack dump must not block hard kill | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we log one line here? |
||
| pass | ||
|
|
||
| # Set _detected last so observers (and tests) see it only once | ||
| # diagnostics are done and on_detected is about to fire. | ||
| with self.lock: | ||
| self._detected = True | ||
| self.on_detected() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we consider moving it in to |
||
|
|
||
| def detected(self): | ||
| """Return True if hang is detected.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments are outdated.