Skip to content

[None][feat] Add shared KV transfer ownership and progress - #20268

Draft
chienchunhung wants to merge 3 commits into
NVIDIA:mainfrom
chienchunhung:dev/ri-02-transfer-manager
Draft

chienchunhung wants to merge 3 commits into
NVIDIA:mainfrom
chienchunhung:dev/ri-02-transfer-manager

Conversation

@chienchunhung

@chienchunhung chienchunhung commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add an acquired-lease holder that separates transfer completion from permission to consume fetched KV. Return writes only after fixed delivery results, ended access, and orchestration authorization.

Scope

  • TransferManager retains leases, backend attempts, and the originating lender's bound return endpoint; it reports operation facts and returned bounds.
  • Preserve sticky abandonment, release reads/discarded writes after access ends, and retain attempts until late results are observed.
  • Never poll a ready lease again. Stop the lender domain after an operational return failure, retain unresolved resources, and never retry or release the failed return twice.
  • Keep serialized provider shutdown, owner-thread finalizers, Lifecycle arbitration, and optional executor/coordinator progress hooks.

Request lending, lender progress, distributed return agreement, readiness, and recovery remain with orchestration. This foundation does not activate a provider; DEVICE support and qualified nonfatal containment remain separate integration work.

Verification

Revision: 58b8c9be9b5e4307481d419b2c89da4c965dee15.

  • 1,222 passed, 0 remaining failures, 4 inherited hardware skips, across the full suite and focused rerun on NVIDIA A30 with normal pytest imports and plugins.
  • The full suite at 1acbe240 passed 1,216 cases; six GPU cases failed at a stale test-fixture constructor. The corrected six-case module passed at this revision. Production and all other test files are byte-identical between those revisions.
  • Ran python3 -m pytest -c tests/unittest/pytest.ini -v -ra over holder/coordinator/loop, real-manager transfer, backend/lender, Lifecycle, executor cleanup, generation/capacity, and forward-error suites. Four inherited foundation cases require Blackwell. No provider or distributed qualification is claimed.
  • Used the native wheel built for [TRTLLM-16186][feat] Lend KV cache pages and recurrent state in place #20534 at bc07138a5, with matching candidate native/build sources. Repository hooks, GitHub pre-commit/DCO/metadata checks, and a fresh independent review passed. Full CI launched; results are pending.

Notes

Stacked directly on #20266 at dce241c32ab9b8b61fc40589274680d5ab734dbc, which uses #20534 at bc07138a5f8ca79088a9a6c1aece3eebdd9d95ed. Lifecycle #19822 and backend contract #20117 are already merged in that foundation.

One implementation commit follows the assembly prerequisite. Implementation-only diff.

  • Contribution checklist reviewed: private interfaces, focused tests, no new dependencies or public configuration fields.

@chienchunhung

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #77704 [ run ] triggered by Bot. Commit: b44c0bc Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #77704 [ run ] completed with state SUCCESS. Commit: b44c0bc
/LLM/main/L0_MergeRequest_PR pipeline #64058 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@chienchunhung

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #77929 [ run ] triggered by Bot. Commit: 73f380f Link to invocation

@chienchunhung
chienchunhung force-pushed the dev/ri-02-transfer-manager branch 2 times, most recently from e26a87d to 4da6155 Compare October 15, 2026 00:25
@chienchunhung

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #77936 [ run ] triggered by Bot. Commit: 4da6155 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #77929 [ run ] completed with state ABORTED. Commit: 73f380f

Link to invocation


Backend shutdown alone runs on a daemon worker. Backend methods must satisfy
their nonblocking contract, while the supplied watchdog runs independently.
The initial policy remains conservative executor-fatal containment.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For host staging, suggest isolating the binding rather than failing the executor: keep lease, hold and registration, mark nothing delivered, stop submitting past a retention threshold, and recompute locally.

Comment thread tensorrt_llm/_torch/disaggregation/transfer_manager.py Outdated
return "backend does not support the requested direction"
return None

def _lend(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lender requires lending outside scheduling and batch preparation. Running lend, mark and cancel from advance() at the loop head would make that hold by construction.

*,
key: RequestKey,
) -> StartResult:
"""Publish committed state; a granted read may outlive the producer's exit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A request with an in-place write lease or a lent state row must not run until release. Could the holder publish such blocking leases so scheduling can hold those requests?

deadline if self._shutdown_deadline is None else min(self._shutdown_deadline, deadline)
)
for key in tuple(self.requests):
self.cancel(key, CancelReason.REQUEST_EXIT)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shutdown reuses REQUEST_EXIT; a separate reason would let parked requests get RECOMPUTE or REJECTED. On request exit, could not-yet-submitted in-place reads be abandoned, since their locks pin scheduling?

if threading.get_ident() != expected or self._shutdown_deadline is not None:
raise RuntimeError("backend assembly is unavailable on this thread or after shutdown")

def assemble(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a separate thread and finalize, could the loop call shutdown(0) each round and spend the remaining budget on the shutdown path, with holds returned on the manager thread?

coordinator = self.__dict__.get("_disagg_coordinator")
if coordinator is not None and not coordinator.shared_shutdown_complete(
):
raise RuntimeError(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raising here skips the KV connector and manager shutdowns, where the lender decides retention (it keeps pools while holds or locks remain). Suggest logging the incomplete cleanup and continuing.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #77936 [ run ] completed with state FAILURE. Commit: 4da6155
/LLM/main/L0_MergeRequest_PR pipeline #64241 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

Link to invocation

Comment thread tensorrt_llm/_torch/disaggregation/transfer_manager.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/transfer_manager.py Outdated
Comment thread tensorrt_llm/_torch/pyexecutor/py_executor.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/orchestration/coordinator.py
Comment thread tensorrt_llm/_torch/disaggregation/transfer_manager.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/transfer_manager.py Outdated
@Shixiaowei02
Shixiaowei02 requested a review from bo-nv October 15, 2026 08:54
Comment thread tensorrt_llm/_torch/disaggregation/transfer_manager.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/orchestration/acquisition.py Outdated
# A violating provider cannot convert a cancellation error into ended access.
self._stop_binding(record.binding, f"backend cancel failed: {error}")

def advance(self, now: float) -> tuple[RuntimeUpdate, ...]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admission would need a per-iteration view of holding costs: requests a lease keeps from running, pages leases keep after a request is freed (per pool group), and leases retained per binding.

manager = self._shared_transfer_manager
return manager is None or manager.shutdown_status().complete

def _advance_shared_transfers(self) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 trtllm-bench comparison (main, feature off, on but idle) before a profile is enabled; on but idle should stay within about 1% step time.

manager = self._shared_transfer_manager
return manager is not None and manager.pending()

def take_shared_updates(self) -> Tuple[RuntimeUpdate, ...]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-rank agreement (resume intervals, release, admission numbers) could travel as one message on the per-iteration allgather the transceiver already does, choosing the group as native does (pp_allgather under attention DP). Every rank joins each iteration, including one whose lend failed; no rank joins when the feature is off.

)
except ValueError as error:
return Rejected(str(error))
except Exception as error:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest one error ladder across layers: lender argument errors are bugs (fail the request and log an error); refusals that depend on state arrive as leases that fail at the call (request level); only undocumented lender exceptions disable the domain. Request- or binding-level problems should never stop the domain or the executor.

raise RuntimeError("a shared transfer manager is already attached")
self._shared_transfer_manager = manager

def has_pending_shared_transfers(self) -> bool:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

@@ -0,0 +1,919 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Owner-loop transfer ledger, independent of request scheduling and backend progress."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest a minimal metrics set: per binding, submissions, deliveries, recompute fallbacks, retained leases and their age, late releases and isolations; per domain, whether it is disabled; transfer bytes and latency; and transfer end kinds in the logs.

@chienchunhung
chienchunhung requested review from pcastonguay and removed request for bo-nv October 15, 2026 16:03
@chienchunhung
chienchunhung force-pushed the dev/ri-02-transfer-manager branch 2 times, most recently from dbdf18b to 90f5443 Compare October 15, 2026 23:35

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #79382 [ run ] triggered by Bot. Commit: ca16f04 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #79382 [ run ] completed with state SUCCESS. Commit: ca16f04
/LLM/main/L0_MergeRequest_PR pipeline #65377 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

Link to invocation



@dataclass(frozen=True)
class LifecycleBinding:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RetirementWatchdog makes executor-fatal containment the only policy: one expired grace closes admission for every binding. Suggest not using the #19822 lifecycle package: deadlines belong to orchestration, access-end evidence and peer isolation to backends, lease holding and binding stop to the holder, and only local contradictions trip the executor.

if record.abandoned:
self._mark(record, consume=False)
if record.access_ended:
self._release(record)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each rank returns a lease once its own evidence allows, so ranks can return in different rounds, while #20534 requires distributed transfers to return in the same iteration. Suggest collect() reporting local facts, orchestration agreeing on them over the per-round allgather, and settle(tickets) returning the same leases on every rank.

binding.lifecycle = lifecycle.watchdog.create_owner(-1, "receive", lifecycle.timeout_s)
self.bindings[binding_id] = binding
try:
binding.backend = factory(namespace.regions, self.notify, config, cleanup.post)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the holder builds backends, receiving bindings one at a time. Suggest construction in the assembly (_assembly.py): it calls each factory with data only, never the lender, resolves optional protocols once, and builds the holder once with every binding and lender, so the holder never calls a factory.

self._stop_binding(binding_id, f"backend construction failed: {error}")
raise

def submit(self, lease: Lease, intent: TransferIntent) -> SubmitResult:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred and Rejected hand the lease back, so ownership depends on the result. Suggest take(lease, mode, hint, timeout_s=None) -> LeaseTicket, which always takes the lease: a binding not accepting, shutdown or a stopped lender becomes a fact, only TypeError and ValueError raise, and no call scans every operation.

Comment thread tensorrt_llm/_torch/disaggregation/transfer_manager.py Outdated
except Exception as error:
self._stop_binding(record.intent.binding_id, f"backend cancel failed: {error}")

def advance(self, now: float) -> tuple[OperationObservation, ...]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

advance(now) touches every operation every round and polls leases after their view was returned, which the Lease contract rules out. Suggest four calls per round: collect(), settle(tickets) after cross-rank agreement, advance(unsettled_lenders) as the only Lender.progress() caller, and finish(), so idle rounds cost O(1) and busy rounds O(changes).

self._binding_deadline(binding, fault)
self._launch_shutdown(binding, self._clock() + binding.lifecycle_binding.timeout_s)

def begin_shutdown(self, deadline: float) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest replacing the per-binding shutdown threads with shutdown(budget_s) -> HolderShutdown on the calling thread, repeatable and never raising: each call drives Backend.shutdown(budget), returns leases on evidence, and calls release_hold() once a binding's shutdown() returns empty with no lease unreturned. A binding stopped mid-run retires likewise, via shutdown(0) each round.



@dataclass(frozen=True)
class TransferIntent:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TransferIntent is built per operation, so each submission allocates, re-checks profile_id and memory_kind, and projects the view. Suggest a shared SubmitMode built once per kind (binding, direction, Urgency.URGENT or RECLAIM, reuse bit), with the hint and relative timeout_s passed to take, and the lease view handed to the backend unchanged.



@dataclass(frozen=True)
class OperationObservation:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One OperationObservation per changed operation allocates on every change. Suggest one columnar LeaseFacts batch per call: ticket and Fact bit arrays, bounds of returned write leases, and sparse FactDetails only for failures and attribution, each with a top-level FailureClass that orchestration decides on; DomainFailure becomes Fact.LENDER_STOPPED.

def _progress_operation(self, record: OperationRecord) -> None:
"""Observe delivery and access independently without driving provider I/O."""
binding = self.bindings[record.intent.binding_id]
if record.timeout.is_set():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the holder decides expiry, after which late rows never count. Superseding the earlier suggestion to mark nothing delivered, orchestration keeps the only deadline, per request, and on expiry resumes the request at the settled bounds if access ended, otherwise waits, moves it to a new cache, or fails it.

record.submission = Submission.REJECTED
self._abandon(record, str(error))
except Exception as error:
record.submission = Submission.UNCERTAIN

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding the lease on an uncertain submission is right; always stopping the whole binding is not. Suggest marking the ticket Fact.UNCERTAIN and taking the binding effect from a per-binding containment level: stop the binding, or isolate the failed peer. A stopped binding retires once its per-round shutdown(0) returns empty.

fault: str | None = None


class TransferManager:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest one lease holder shared by peer, store and third-party backends: it only takes leases, reports facts, and returns each lease exactly once on access-end evidence. Reason: two holders mean two state machines for the same ownership rules, so every safety fix lands twice and the two can drift apart.

self._shared_transfer_manager: Optional["TransferManager"] = None
self._shared_updates: List["OperationObservation"] = []

def attach_shared_transfer_manager(self, manager: "TransferManager") -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest leaving DisaggTransferCoordinator and 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.

len(self.waiting_queue) == 0 and not self._has_pending_connector_transfers()
len(self.waiting_queue) == 0 and not self._has_pending_transfers()

def _has_pending_transfers(self) -> bool:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest gating stopping, draining and idle waits on state all ranks agreed on in the same round, not on rank-local holder state. Reason: the loop assumes every rank sees the stop condition together; a rank-local gate lets ranks exit in different rounds and leaves the others waiting in collectives.

"""Increment one payload-free owner-thread operation counter."""
self.counters[key] = self.counters.get(key, 0) + 1

def notify(self) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest one wake signal per backend, raised only after the backend has listed what changed, so that each round reads only those changes and an idle round does nothing. Reason: a single shared notify cannot say what changed, so per-round cost grows with transfers in flight rather than with events.

class TransferManager:
"""Hold operations on one manager thread while backend I/O progresses independently.

There is no production activation here: request admission, loop-head lending,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest lending at the executor event that makes KV available or needed, admission for receives and step completion for sends, and passing that step's ready event along. Reason: lending at the loop head delays every transfer by up to a round and cannot tell when the KV was actually written.

Signed-off-by: Shixiaowei02 <39303645+Shixiaowei02@users.noreply.github.com>
@chienchunhung
chienchunhung force-pushed the dev/ri-02-transfer-manager branch from ca16f04 to 25860d7 Compare October 23, 2026 23:09
@chienchunhung chienchunhung changed the title [None][feat] Add shared KV transfer ownership and progress (RI-02) [None][feat] Add shared KV transfer ownership and progress Oct 23, 2026
@chienchunhung
chienchunhung force-pushed the dev/ri-02-transfer-manager branch from 25860d7 to 3131de2 Compare October 23, 2026 23:14
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
@chienchunhung
chienchunhung force-pushed the dev/ri-02-transfer-manager branch from 3131de2 to 1acbe24 Compare October 23, 2026 23:15
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
@chienchunhung
chienchunhung force-pushed the dev/ri-02-transfer-manager branch from 1acbe24 to 58b8c9b Compare October 23, 2026 23:31

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #80287 [ run ] triggered by Bot. Commit: 58b8c9b Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #80287 [ run ] completed with state FAILURE. Commit: 58b8c9b
/LLM/main/L0_MergeRequest_PR pipeline #66079 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants