Repository navigation
[None][feat] Add shared KV transfer ownership and progress #20268
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,11 @@ | |
| if TYPE_CHECKING: | ||
| import torch | ||
|
|
||
| from tensorrt_llm._torch.disaggregation.transfer_manager import ( | ||
| DomainFailure, | ||
| OperationObservation, | ||
| TransferManager, | ||
| ) | ||
| from tensorrt_llm._torch.pyexecutor.scheduler.scheduler import ScheduledRequests | ||
|
|
||
|
|
||
|
|
@@ -131,6 +136,61 @@ def __init__( | |
| self._unpublished_gen_receives: Dict[ | ||
| int, Tuple[LlmRequest, Optional["torch.cuda.Event"]] | ||
| ] = {} | ||
| self._shared_transfer_manager: Optional["TransferManager"] = None | ||
| self._shared_updates: List["OperationObservation"] = [] | ||
|
|
||
| def attach_shared_transfer_manager(self, manager: "TransferManager") -> None: | ||
| """Attach the shared holder on its owner thread before submitting work. | ||
|
|
||
| Args: | ||
| manager: Executor-local holder assembled for a supported profile. | ||
|
|
||
| Raises: | ||
| RuntimeError: A different holder is already attached. | ||
| """ | ||
| if ( | ||
| self._shared_transfer_manager is not None | ||
| and self._shared_transfer_manager is not manager | ||
| ): | ||
| raise RuntimeError("a shared transfer manager is already attached") | ||
| self._shared_transfer_manager = manager | ||
|
|
||
| def has_pending_shared_transfers(self) -> bool: | ||
|
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. While both paths exist, suggest one writer per request at a time: a context request may fetch a prefix through the shared path before it computes, and the native send starts only after that fetch has returned its leases; a generation request receives through native only. |
||
| """Return whether shared work still needs owner-loop progress.""" | ||
| manager = self._shared_transfer_manager | ||
| return manager is not None and manager.pending() | ||
|
|
||
| def take_shared_updates(self) -> Tuple["OperationObservation", ...]: | ||
| """Drain local observations for orchestration to reconcile across ranks. | ||
|
|
||
| Returns: | ||
| Operation facts; request associations and readiness belong to orchestration. | ||
| """ | ||
| updates = tuple(self._shared_updates) | ||
| self._shared_updates.clear() | ||
| return updates | ||
|
|
||
| def take_shared_domain_failures(self) -> Tuple["DomainFailure", ...]: | ||
| """Drain lender-wide stops for orchestration to apply at its recovery boundary.""" | ||
| manager = self._shared_transfer_manager | ||
| return () if manager is None else manager.take_domain_failures() | ||
|
|
||
| def begin_shared_shutdown(self) -> None: | ||
| """Close shared admission while keeping the owner loop alive for cleanup.""" | ||
| manager = self._shared_transfer_manager | ||
| if manager is not None: | ||
| manager.begin_shutdown(time.monotonic() + manager.shutdown_timeout_s) | ||
|
|
||
| def shared_shutdown_complete(self) -> bool: | ||
| """Return whether shared resources permit manager teardown.""" | ||
| manager = self._shared_transfer_manager | ||
| return manager is None or manager.shutdown_status().complete | ||
|
|
||
| def _advance_shared_transfers(self) -> None: | ||
|
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. With no shared manager attached this path should cost nothing: no collectives, no threads, no events. Suggest a test that pins that, and a |
||
| """Observe shared work locally without entering native collectives.""" | ||
| manager = self._shared_transfer_manager | ||
| if manager is not None: | ||
| self._shared_updates.extend(manager.advance(time.monotonic())) | ||
|
|
||
| # -- loop head ----------------------------------------------------------- | ||
|
|
||
|
|
@@ -145,6 +205,7 @@ def handle_errors_synced(self) -> None: | |
| otherwise the downstream ``tp_gather`` in ``_enqueue_responses`` | ||
| deadlocks or leaves peer replicas running. | ||
| """ | ||
| self._advance_shared_transfers() | ||
| pending_ids = self.take_pending_context_failures() | ||
| pending_requests = ( | ||
| [req for req in self._registry.active_requests() if get_unique_rid(req) in pending_ids] | ||
|
|
@@ -851,13 +912,15 @@ def pace_idle(self) -> None: | |
| ) | ||
| if waiting_on_transfer: | ||
| time.sleep(0.001) | ||
|
chienchunhung marked this conversation as resolved.
|
||
| elif self.has_pending_shared_transfers(): | ||
| self._shared_transfer_manager.wait_for_progress() | ||
|
|
||
|
|
||
| class NoopDisaggCoordinator(DisaggTransferCoordinator): | ||
| """Coordinator used when the executor has no KV cache transceiver. | ||
|
|
||
| A null object: no state, no dependencies, no side effects. The loops call | ||
| every entry point unconditionally, so each one is a no-op here. Transceiver | ||
| Native entry points are no-ops. An explicitly attached shared holder still | ||
| progresses without a native transceiver. Transceiver | ||
| enablement is rank-uniform within a collective group, so every rank skips | ||
| the same collectives symmetrically. | ||
| """ | ||
|
|
@@ -875,7 +938,7 @@ def __init__(self) -> None: | |
| ) | ||
|
|
||
| def handle_errors_synced(self) -> None: | ||
| return None | ||
| self._advance_shared_transfers() | ||
|
|
||
| def admit(self, fitting_gen_init: List[LlmRequest]) -> Tuple[List[LlmRequest], bool]: | ||
| return fitting_gen_init, False | ||
|
|
@@ -941,4 +1004,5 @@ def forget_request(self, request_id: int) -> None: | |
| return None | ||
|
|
||
| def pace_idle(self) -> None: | ||
| return None | ||
| if self.has_pending_shared_transfers(): | ||
| self._shared_transfer_manager.wait_for_progress() | ||
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.
Suggest leaving
DisaggTransferCoordinatorand the no-op coordinator unchanged, and adding the new path behind an executor-facing interface whose null implementation does nothing. Reason: mixing new and old logic in one class means neither path can be switched off or deleted on its own, and the null object stops being null.