Skip to content

[TRTLLM-12499][feat] Pipelined KVCache transfer for disaggregated serving in Python Cache Transceiver - #15727

Merged
athena-nv merged 5 commits into
NVIDIA:mainfrom
athena-nv:trtllm-12499-pipelined-kvcache-transfer
Sep 2, 2026
Merged

[TRTLLM-12499][feat] Pipelined KVCache transfer for disaggregated serving in Python Cache Transceiver#15727
athena-nv merged 5 commits into
NVIDIA:mainfrom
athena-nv:trtllm-12499-pipelined-kvcache-transfer

Conversation

@athena-nv

@athena-nv athena-nv commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements pipelined prefill transfer for disaggregated serving. Instead of waiting for every
prefill chunk to finish before transferring the request's KV cache, the context server sends the
completed KV blocks after each chunk. This overlaps GPU prefill compute with RDMA transfer and
leaves only the final transfer on the critical path when transfer is faster than prefill.

Baseline
  compute    [ chunk0 ][ chunk1 ][ chunk2 ][ chunk3 ]
  transfer                                            [======= whole prompt =======]
  gen unblocked at                                                                  ^

Pipelined
  compute    [ chunk0 ][ chunk1 ][ chunk2 ][ chunk3 ]
  transfer             [  c0  ]  [  c1  ]  [  c2  ]  [  c3  ]
  gen unblocked at                                           ^

Chunking remains sender-side only. The context server creates multiple KVSendTasks in one
TxSession; the generation server posts one receive for the whole request and completes that
receive when the final-slice result arrives.

Related work by @chienchunhung:
KV cache transfer: Reducing KV Block Residency and Peak Memory Pressure in Disaggregated Serving

Configuration

The feature is gated by CacheTransceiverConfig.enable_pipelined_transfer.

# Context server
context_servers:
  cache_transceiver_config:
    backend: NIXL
    enable_pipelined_transfer: true
  enable_chunked_prefill: true

# Generation server
generation_servers:
  cache_transceiver_config:
    backend: NIXL
    enable_pipelined_transfer: true
  enable_chunked_prefill: true

schedule_style: generation_first

resolve_cache_transceiver_config() in
tensorrt_llm/_torch/pyexecutor/config_utils.py handles runtime-independent selection:

  • With pipelining enabled and no explicit runtime, NIXL auto-selects the Python transceiver.
  • Explicit transceiver_runtime: CPP is rejected.
  • A Python transceiver with a non-NIXL backend is rejected.
  • Resolution runs before cache-manager selection in create_py_executor() and again in
    create_kv_cache_transceiver() for callers that bypass the executor-creation path.

Runtime-dependent restrictions are checked by
KvCacheTransceiverV2._resolve_pipelined_transfer() after the mapping and cache manager exist:

Requirement Reason Enforcement
Python NIXL transceiver Pipelining is implemented by KvCacheTransceiverV2 resolve_cache_transceiver_config()
pipeline_parallel_size == 1 One sender rank must own every layer needed by a chunk _resolve_pipelined_transfer()
kv_cache_bounce_size_mb == 0 Bounce staging coalesces a whole request rather than per-chunk writes _resolve_pipelined_transfer()
No Mamba/hybrid cache manager Recurrent state is mutable request state, not block-addressable chunk state _resolve_pipelined_transfer()
enable_chunked_prefill: true Chunk boundaries come from py_last_context_chunk PyExecutor._validate_request()
beam_width == 1 Chunk projection does not model the packed beam layout PyExecutor._validate_request() and _build_prefill_chunk()
schedule_style == generation_first The generation server must register destination blocks before an intermediate chunk is sent PyExecutor._validate_request()

The last three checks are request checks, limited to disaggregated context-only requests using a
pipelined transceiver. Aggregate/warmup requests and generation-only requests are not rejected.
The disaggregated YAML parser and serve.py do not perform feature-specific startup validation;
the CLI schedule-style override continues to be applied after parsing.

Architecture

Sender-side chunks, monolithic receiver

flowchart LR
    subgraph ctx [Context server]
        exec[PyExecutor._send_kv_async] --> transceiver[respond_and_send_async]
        transceiver --> build[_build_prefill_chunk]
        build --> tx["TxSession: one per request"]
        tx --> c0["KVSendTask 0"]
        tx --> c1["KVSendTask 1"]
        tx --> cn["KVSendTask N-1, is_last_slice"]
    end
    subgraph gen [Generation server]
        rx["RxSession: one per request"] --> task["one whole-request KVRecvTask"]
    end
    c0 -->|RDMA write| task
    c1 -->|RDMA write| task
    cn -->|final RDMA write| task
Loading

request_and_receive_async() is unchanged: the receiver allocates and advertises the full
destination once. Each sender result echoes the receiver's task id, and is_last_slice tells the
receiver when no more chunks remain.

Slice model

KVSlice.token_range distinguishes the two paths:

  • A monolithic slice leaves token_range=None and covers prompt_len.
  • A pipelined slice carries a block-aligned half-open token range and sets
    is_last_slice=True only for the final emitted slice.
  • Layer-group block lists may be empty when that group contributes nothing to the current slice.

SessionArgsBase.prompt_len is the request-wide extent. It is independent of a chunk's
token_range and is required for prompt-wide calculations such as the final sliding-window
boundary.

Deriving a chunk

KvCacheTransceiverV2._build_prefill_chunk() converts the scheduler's
req.py_last_context_chunk token bounds to block coordinates:

first chunk start = 0
later chunk start = floor(chunk_start_token / tokens_per_block)
non-final end     = floor(chunk_end_token / tokens_per_block)
final end         = ceil(chunk_end_token / tokens_per_block)

The first slice starts at block zero so a context-side prefix-reuse hit is included even though
the scheduler starts computing at prepopulated_prompt_len. Non-final partial blocks are deferred
to the next chunk; the final chunk rounds up so the prompt tail is not stranded. If a non-final
chunk completes no transferable block, no slice is emitted.

The final slice is still emitted when its token range is empty. This preserves the final-slice
signal for cases where all remaining attention groups were already sent or contain no blocks.

Sliding-window attention

SWA layers are deliberately not pipelined. Pages can leave the active window between prefill
chunks, so sending each intermediate resident suffix can either transfer stale pages or lose pages
that are still needed in the generation server's final window.

For a layer group whose sliding_window_size < prompt_len:

  • Intermediate slices carry an empty block list for that group.
  • The final slice carries the complete final active window in one transfer.
  • The sender does not project the receiver's whole-prompt list down to the final token range for
    that group; it first trims the receiver list to the source window and then aligns the two lists.

Non-windowed groups continue to transfer chunk by chunk. A model with mixed full-attention and SWA
groups therefore pipelines the full-attention KV while deferring only the SWA groups.

Native sender addressing

project_blocks_to_global_chunk() now lives next to its only production consumer in
tensorrt_llm/_torch/disaggregation/native/transfer.py. It intersects a global chunk interval
with a block list represented as a resident suffix; a non-overlapping list produces an empty
projection.

Sender._build_kv_write_meta() uses the slice end as the coordinate-space extent:

slice_end   = token_range.end for a chunk, otherwise prompt_len
total_blocks = ceil(slice_end / tokens_per_block)
token_start = (total_blocks - resident_block_count) * tokens_per_block

For partial non-SWA chunks, the receiver's whole-prompt block list is projected to the same global
chunk. For the final SWA slice, that projection is skipped so the complete active window remains
addressable. Generation-side prefix reuse and sender/receiver window differences are then handled
by _trim_receiver_window_head() and _align_kv_blocks().

This keeps the addressing correct for:

  • no prefix reuse;
  • context-side prefix reuse only;
  • generation-side prefix reuse only;
  • different cache states on the two servers;
  • incremental cache allocation;
  • full-attention and mixed SWA models.

Result protocol and slice ids

The sender and receiver have different slice namespaces: the sender has one task per prefill
chunk, while the receiver currently has one whole-request task. WriteMeta therefore carries both:

  • slice_id: the local sender task/chunk id, used for sender-side task lookup and diagnostics;
  • receiver_slice_id: the peer's task id copied from RecvReqInfo, used in
    KV_AGENT_RESULT.

The binary wire prefix remains <qqq?Bq>:

instance_rank, unique_rid, receiver_slice_id, is_last_slice, status, transfer_size

RxSession.process_kv_agent_result() validates and indexes receiver_slice_id. The protocol shape
does not change; current receivers advertise task zero, so every sender chunk reports
receiver_slice_id == 0.

TxSession completion and registration races

A TxSession owns all KV tasks and the optional auxiliary task for a request.

  • _has_last_slice is set when a final slice is appended.
  • KV completion requires _has_last_slice, at least one task, and every KV task transferred.
    This prevents an intermediate chunk from making the session appear complete before the final
    task exists.
  • FULLY_TRANSFERRED additionally requires the generation-first auxiliary transfer.
  • has_transferring_tasks() includes both KV and auxiliary tasks.

TxSession.lock atomically appends a task and snapshots currently registered peers. The listener's
late-registration path uses the same lock to save peer info and snapshot existing tasks. Actual
metadata construction and queue dispatch happen after releasing the lock, keeping transfer work
outside the critical section while ensuring a task is seen by either the send path or the replay
path.

Executor and lifecycle integration

Sending from the executor loop

PyExecutor._send_kv_async() runs after each forward step:

  1. Skip a context request already in GENERATION_COMPLETE.
  2. Skip all further sends, including a final send, when that request has a pending cancellation.
  3. Fail a request whose prior send session was retired and can no longer be recreated.
  4. On the final chunk, call async_transfer_manager.start_transfer() before sending, then send the
    final slice and start the final-transfer timeout clock.
  5. On an intermediate chunk, call respond_and_send_async() without moving the request into the
    transfer manager.

respond_and_send_async() creates or reuses the TxSession, builds the next chunk, and finalizes
auxiliary state only when the emitted slice is final. Request state changes to
DISAGG_CONTEXT_TRANS_IN_PROGRESS only at that point; session membership tracks ownership during
earlier chunks.

No additional CUDA synchronization is introduced. The existing sampler event synchronization
completes before _send_kv_async() dispatches the slice.

Cancellation, failure, and safe retirement

Pipelining allows fabric writes while the request is still in CONTEXT_INIT, so request state
alone cannot answer whether its KV pages are safe to release.

  • KvCacheTransceiverV2.has_inflight_transfer() derives ownership from the send/receive session
    maps.
  • _is_request_in_transmission() combines request phase with session ownership, routing
    mid-prefill cancellation through the transceiver.
  • Cancelling a session marks logical terminal state immediately but leaves a task in
    TRANSFERRING until the physical write finishes. INIT KV and auxiliary tasks are failed
    immediately.
  • py_kv_send_session_retired prevents recreating a sender session after teardown has removed the
    peer registration. A later send attempt transitions the request to DISAGG_TRANS_ERROR.

In TP/PP deployments, logical outcome and physical quiescence are reconciled across ranks.
_ctx_consensus_outcome() packs cancelled, failed, completed, and locally quiesced request ids into
the existing outcome allgather, avoiding a second collective per sweep:

  • cancellation or failure is global if any rank reports it;
  • successful completion requires all ranks;
  • a cancelled or failed session is retired only after every participating rank reports no
    transferring KV or auxiliary task.

A bounded wait timeout is nonterminal: the session remains live and retains its pages because a
peer write may still be active. The polling path logs the timeout and retries on a later sweep.

Tests

The updated unit coverage exercises:

  • chunk derivation, unaligned boundaries, prefix reuse, empty chunks, and final-slice signaling;
  • full-attention, SWA-only, and mixed SWA/full-attention source/destination addressing;
  • sender and receiver slice-id separation;
  • TxSession._has_last_slice completion gating;
  • pending cancellation and GENERATION_COMPLETE send suppression;
  • retirement only after KV and auxiliary writers quiesce on all ranks;
  • folded TP/PP outcome and quiescence consensus;
  • runtime/request validation for unsupported combinations.

The old parser-level disaggregated-config tests were removed because feature-specific validation
now occurs in the executor/transceiver, not in extract_disagg_cfg().

Change inventory since 1a80e56eebc420684d7fc90dc14ad8c3a633e42c

File Diff (+/-) Purpose
tensorrt_llm/_torch/disaggregation/base/transfer.py +7/-61 Keep the slice/session data model; move native-only projection logic out
tensorrt_llm/_torch/disaggregation/native/transfer.py +175/-210 Chunk/SWA addressing, dual slice ids, session completion and task dispatch
tensorrt_llm/_torch/disaggregation/transceiver.py +110/-103 Chunk construction, compatibility gates, lifecycle and quiescence consensus
tensorrt_llm/_torch/pyexecutor/config_utils.py +47/-0 Centralized transceiver runtime resolution and backend validation
tensorrt_llm/_torch/pyexecutor/kv_cache_transceiver.py +2/-79 Delegate configuration resolution and runtime-dependent checks
tensorrt_llm/_torch/pyexecutor/llm_request.py +1/-3 Retired-session request flag documentation
tensorrt_llm/_torch/pyexecutor/py_executor.py +28/-22 Request validation and per-step send/cancellation gating
tensorrt_llm/_torch/pyexecutor/py_executor_creator.py +1/-8 Resolve transceiver configuration before cache-manager selection
tensorrt_llm/commands/serve.py +3/-2 Restore schedule-style override behavior
tensorrt_llm/llmapi/disagg_utils.py +3/-44 Remove feature-specific parser validation
tensorrt_llm/llmapi/llm_args.py +4/-3 Document the complete pipelining requirements on the config field
tests/integration/defs/accuracy/test_disaggregated_serving.py +2/-47 Update disaggregated accuracy coverage/configuration
tests/integration/test_lists/test-db/l0_dgx_b200.yml +0/-2 Remove obsolete test-list entries
tests/unittest/disaggregated/test_chunked_transfer.py +295/-232 Consolidated chunking, SWA, lifecycle, and validation tests
tests/unittest/disaggregated/test_disagg_utils.py +0/-93 Remove parser-validation tests
tests/unittest/disaggregated/test_kv_transfer.py +17/-47 Session and auxiliary-transfer behavior
tests/unittest/disaggregated/test_transceiver_bounded_polling.py +86/-20 Quiescence, consensus, cancellation, and bounded polling

The cumulative diff is 781 insertions and 976 deletions across 17 files.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds sender-side pipelined KV transfer for disaggregated serving. The change introduces block projection, separate sender and receiver identifiers, session retirement, configuration validation, executor integration, and unit and integration coverage.

Changes

Pipelined KV transfer

Layer / File(s) Summary
Transfer contracts and block projection
tensorrt_llm/_torch/disaggregation/base/transfer.py
Adds block-coordinate helpers, KVSlice.total_blocks, and required prompt_len values.
Native protocol and session handling
tensorrt_llm/_torch/disaggregation/native/transfer.py
Separates sender and receiver slice IDs, updates the binary result format, serializes dispatch, projects chunk metadata, and centralizes failure results.
Prefill chunk construction and retirement
tensorrt_llm/_torch/disaggregation/transceiver.py
Builds projected prefill chunks, tracks in-flight transfers, and retires sender sessions after completion, failure, or cancellation.
Configuration and executor integration
tensorrt_llm/llmapi/*, tensorrt_llm/_torch/pyexecutor/*, tensorrt_llm/commands/serve.py, tensorrt_llm/_torch/auto_deploy/shim/ad_executor.py
Adds pipelined-transfer settings and validation. Executor scheduling and timeout paths include transceiver-owned transfers.
Validation coverage
tests/unittest/disaggregated/*, tests/integration/defs/accuracy/test_disaggregated_serving.py, tests/integration/test_lists/test-db/l0_dgx_b200.yml, tests/unittest/_torch/executor/*
Covers projection, protocol IDs, replay ordering, lifecycle failures, configuration restrictions, cancellation, timeout behavior, end-to-end transfer, and accuracy cases.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PyExecutor
  participant KvCacheTransceiverV2
  participant TxSession
  participant RxSession
  PyExecutor->>KvCacheTransceiverV2: send prefill chunk
  KvCacheTransceiverV2->>TxSession: send projected KVSlice
  TxSession->>RxSession: deliver KV result with sender and receiver IDs
  RxSession-->>PyExecutor: resolve receiver task and report completion
Loading

Possibly related PRs

Suggested reviewers: brnguyen2, qijune, bowenfu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.01% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: pipelined KV cache transfer for disaggregated serving through the Python Cache Transceiver.
Description check ✅ Passed The description is detailed and relevant. It explains the motivation, configuration requirements, architecture, lifecycle behavior, validation, tests, and known coverage gaps. The template checklist c…
Full details: Description check

Explanation

The description is detailed and relevant. It explains the motivation, configuration requirements, architecture, lifecycle behavior, validation, tests, and known coverage gaps. The template checklist confirmation is not reproduced, but the substantive description and test coverage sections are complete.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tensorrt_llm/_torch/disaggregation/native/transfer.py (1)

488-507: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Mark the task as in-flight before waiting on the CUDA event.

Line 492 waits while the task is still INIT, so cancel_request() can see no TRANSFERRING tasks and free KV pages before the event completes. Move the INIT→TRANSFERRING transition before the event wait, and keep the cancelled/error abort path before synchronization.

Suggested fix
-        # For pipelined prefill-transfer: wait for the GPU forward
-        # to finish writing KV data before starting RDMA.  This
-        # blocks only this worker thread, not the GPU or main thread.
-        if task._slice.cuda_event is not None: # TODO: should I sync after the task status is set to TRANSFERRING?
-            task._slice.cuda_event.synchronize()
-
-        if timer:
-            timer.record_push_end(write_meta.peer_rank)
         # Hold session.lock to serialize the INIT→TRANSFERRING transition with
         # cancel(): prevents cancel_request() from freeing KV pages while a
         # worker is about to write into them.
         with session.lock:
             status = session.status
             if status in (SessionStatus.ERROR, SessionStatus.CANCELLED):
                 should_abort = True
             else:
                 task.status = TaskStatus.TRANSFERRING
                 should_abort = False
+
+        if should_abort:
+            ...
+            return
+
+        # For pipelined prefill-transfer: wait for the GPU forward
+        # to finish writing KV data before starting RDMA.
+        if task._slice.cuda_event is not None:
+            task._slice.cuda_event.synchronize()
+
+        if timer:
+            timer.record_push_end(write_meta.peer_rank)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/_torch/disaggregation/native/transfer.py` around lines 488 -
507, The task transition in transfer.py is happening too late in the
prefill-transfer flow: `task._slice.cuda_event.synchronize()` runs while the
task is still `INIT`, so `cancel_request()` can miss it and free KV pages too
early. In the transfer path around `task`, `session.lock`, and
`TaskStatus.TRANSFERRING`, move the INIT→TRANSFERRING state update (with the
session ERROR/CANCELLED abort check) before waiting on the CUDA event, and keep
the abort branch ahead of synchronization so in-flight work is visible before
any blocking wait.
🧹 Nitpick comments (2)
tensorrt_llm/_torch/disaggregation/transceiver.py (2)

582-585: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant session assignment.

_get_or_create_send_session already inserts the session into self._send_sessions, so re-assigning the return value is redundant (and could mask a future divergence between the two code paths). Mirror the simpler form used in respond_and_send_async.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/_torch/disaggregation/transceiver.py` around lines 582 - 585,
The send-session initialization in transceiver logic has a redundant assignment
because _get_or_create_send_session already stores the session in
self._send_sessions. Update the rid-not-in-self._send_sessions branch in
transceiver.py to follow the same pattern as respond_and_send_async by simply
invoking _get_or_create_send_session(req) for its side effects, then keep
setting _ever_had_send_session and _pipelined_chunk_offsets[rid] as before.

602-604: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Numerous open TODOs in the pipelined-send path before merge.

send_prefill_chunk and respond_and_send_async carry several unresolved TODO(athenac) questions on correctness-critical fields (token_range, mamba_state_index, layer_range, the req.state transition, the offset accumulation "might be a faulty calculation", and the redundancy between the two methods). Since the PR is marked WIP, these need resolution before this is production-ready. I can help draft the offset/metadata handling and consolidate the shared logic into a single helper.

Also applies to: 608-611, 628-631, 657-666, 675-675

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/_torch/disaggregation/transceiver.py` around lines 602 - 604,
The pipelined-send path in transceiver.py still contains unresolved correctness
TODOs in send_prefill_chunk and respond_and_send_async, especially around
token_range, mamba_state_index, layer_range, req.state transitions, and the
offset accumulation logic. Resolve these TODO(athenac) questions by verifying
the metadata semantics, fixing the offset calculation, and making the state
update explicit and correct before merge. Also remove the duplicated logic
between send_prefill_chunk and respond_and_send_async by consolidating the
shared send/metadata assembly into a single helper so the two paths stay
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tensorrt_llm/_torch/disaggregation/native/transfer.py`:
- Around line 728-745: The chunked destination slicing in transfer logic is now
using chunk offsets, but the token alignment still assumes each chunk maps to a
suffix ending at token_range.end. Update the code around the chunked path in
transfer.py and the downstream token-start calculation to derive starts from
chunk_block_offset, or require callers to provide per-chunk KVSlice.token_range
for each chunk. Make sure the block selection and token-range alignment stay
consistent for prefix-cache and SWA cases so the written blocks match the
intended chunk.
- Around line 523-524: The abort/result notification path in transfer.py still
uses write_meta.slice_id, which can conflict with the receiver’s single-task
slice handling. Update the abort send logic in the relevant transfer routine to
mirror the success path by reporting receiver_slice_id as 0 for aborts too, so
the receiver does not see a later-chunk slice ID and hit its slice assertion.
Keep the existing task/event unblocking behavior intact while ensuring the
aborted/failure result is always sent to receiver slice 0.
- Around line 736-743: The chunk-to-destination mapping in transfer.py is too
strict for exhausted layer groups: when len(src_block_ids) is 0, the current
bounds check in the chunk slicing logic still raises on advanced chunk_offset
values. Update the chunk handling around the dst_block_ids slice so empty source
chunks become a no-op and do not trigger the out-of-bounds error; keep the
existing bounds validation for non-empty chunks in the same chunk
offset/full_dst_block_ids path.

In `@tensorrt_llm/_torch/disaggregation/transceiver.py`:
- Line 648: The `respond_and_send_async` skip guard in `transceiver.py` needs
both a lint fix and a logic check: move the `return` onto its own line to
satisfy E701, and verify the condition around `rid in self._send_sessions and
rid not in self._pipelined_chunk_offsets` correctly prevents duplicate sends
while pipelined chunks are still outstanding. If needed, adjust the guard so the
full `_create_kv_slices` resend path only runs when it is safe, using the
existing `_send_sessions` and `_pipelined_chunk_offsets` state to avoid
duplicate transfer.
- Around line 591-611: The chunking logic in send_prefill_chunk() and the
_pipelined_chunk_offsets update can split KV slices on token boundaries that are
not aligned to tokens_per_block, which causes the boundary block to be resent
and offsets to drift. Adjust the prefill chunk selection so every chunk boundary
lands on a KV block boundary (or clamp the sliding-window fallback so it only
overlaps when it evenly divides tokens_per_block), and then recompute
_pipelined_chunk_offsets from the actual block count in the chunk.

In `@tests/unittest/disaggregated/test_kv_transfer.py`:
- Around line 1789-1825: The send/receive flow is using the wrong API shape:
TxSession.send() and RxSession.receive() should be called with a fully populated
KVSlice rather than extra kwargs, and they do not return futures. Update the
test setup around KVSlice, sender_session.send(), and
receiver_sessions/RxSession.receive() to set chunk_block_offset and cuda_event
on the slice object before calling send/receive, then replace the .result()
waits with wait_complete()/wait_complete(blocking=True) on the session or slice
as appropriate.

---

Outside diff comments:
In `@tensorrt_llm/_torch/disaggregation/native/transfer.py`:
- Around line 488-507: The task transition in transfer.py is happening too late
in the prefill-transfer flow: `task._slice.cuda_event.synchronize()` runs while
the task is still `INIT`, so `cancel_request()` can miss it and free KV pages
too early. In the transfer path around `task`, `session.lock`, and
`TaskStatus.TRANSFERRING`, move the INIT→TRANSFERRING state update (with the
session ERROR/CANCELLED abort check) before waiting on the CUDA event, and keep
the abort branch ahead of synchronization so in-flight work is visible before
any blocking wait.

---

Nitpick comments:
In `@tensorrt_llm/_torch/disaggregation/transceiver.py`:
- Around line 582-585: The send-session initialization in transceiver logic has
a redundant assignment because _get_or_create_send_session already stores the
session in self._send_sessions. Update the rid-not-in-self._send_sessions branch
in transceiver.py to follow the same pattern as respond_and_send_async by simply
invoking _get_or_create_send_session(req) for its side effects, then keep
setting _ever_had_send_session and _pipelined_chunk_offsets[rid] as before.
- Around line 602-604: The pipelined-send path in transceiver.py still contains
unresolved correctness TODOs in send_prefill_chunk and respond_and_send_async,
especially around token_range, mamba_state_index, layer_range, req.state
transitions, and the offset accumulation logic. Resolve these TODO(athenac)
questions by verifying the metadata semantics, fixing the offset calculation,
and making the state update explicit and correct before merge. Also remove the
duplicated logic between send_prefill_chunk and respond_and_send_async by
consolidating the shared send/metadata assembly into a single helper so the two
paths stay consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 784a62c4-609a-4545-bc83-7d70311b9078

📥 Commits

Reviewing files that changed from the base of the PR and between a3026c9 and 9aed65c.

📒 Files selected for processing (11)
  • tensorrt_llm/_torch/disaggregation/base/transfer.py
  • tensorrt_llm/_torch/disaggregation/native/transfer.py
  • tensorrt_llm/_torch/disaggregation/transceiver.py
  • tensorrt_llm/_torch/pyexecutor/kv_cache_transceiver.py
  • tensorrt_llm/_torch/pyexecutor/py_executor.py
  • tensorrt_llm/llmapi/llm_args.py
  • tests/integration/defs/accuracy/test_disaggregated_serving.py
  • tests/integration/test_lists/test-db/l0_dgx_b200.yml
  • tests/unittest/disaggregated/test_chunked_transfer.py
  • tests/unittest/disaggregated/test_kv_transfer.py
  • tests/unittest/llmapi/test_llm_args.py

Comment thread tensorrt_llm/_torch/disaggregation/native/transfer.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/native/transfer.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/native/transfer.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/transceiver.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/transceiver.py Outdated
Comment thread tests/unittest/disaggregated/test_kv_transfer.py Outdated
@xinhe-nv

Copy link
Copy Markdown
Collaborator

@coderabbitai check the test coverage, including function test, perf test, stress test and multi nodes test.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Changed/mentioned test files =="
for f in \
  tests/unittest/disaggregated/test_chunked_transfer.py \
  tests/unittest/disaggregated/test_kv_transfer.py \
  tests/unittest/llmapi/test_llm_args.py \
  tests/integration/defs/accuracy/test_disaggregated_serving.py \
  tests/integration/test_lists/test-db/l0_dgx_b200.yml \
  tensorrt_llm/_torch/disaggregation/transceiver.py \
  tensorrt_llm/_torch/pyexecutor/py_executor.py \
  tensorrt_llm/_torch/pyexecutor/kv_cache_transceiver.py
do
  if [ -f "$f" ]; then
    echo "-- outline: $f"
    ast-grep outline "$f" --view expanded || true
    echo
  else
    echo "Missing: $f"
  fi
done

echo "== Search for perf/stress/multi-node/disaggregated related tests =="
rg -n -i -C2 \
  '(perf|performance|benchmark|stress|soak|multi[-_ ]node|multinode|disaggregated serving|disaggregated|chunked|pipelined)' \
  tests tensorrt_llm/_torch | sed -n '1,260p'

echo
echo "== Inspect relevant slices from changed tests =="
sed -n '1,260p' tests/unittest/disaggregated/test_chunked_transfer.py
echo "-----"
sed -n '1,260p' tests/unittest/disaggregated/test_kv_transfer.py
echo "-----"
sed -n '1,220p' tests/integration/defs/accuracy/test_disaggregated_serving.py
echo "-----"
sed -n '1,220p' tests/unittest/llmapi/test_llm_args.py
echo "-----"
sed -n '1,220p' tests/integration/test_lists/test-db/l0_dgx_b200.yml

echo
echo "== Inspect relevant implementation slices for testability points =="
sed -n '1,260p' tensorrt_llm/_torch/disaggregation/transceiver.py
echo "-----"
sed -n '1,220p' tensorrt_llm/_torch/pyexecutor/py_executor.py
echo "-----"
sed -n '1,220p' tensorrt_llm/_torch/pyexecutor/kv_cache_transceiver.py

Length of output: 144072


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Direct test references for new pipelined/chunked entry points =="
rg -n -C2 \
  '_maybe_send_prefill_chunk|send_prefill_chunk|enable_pipelined_transfer|respond_and_send_async|request_and_receive_async|_create_kv_slices|create_kv_cache_transceiver' \
  tests tensorrt_llm/_torch | sed -n '1,260p'

echo
echo "== Disaggregated multi-node / stress / soak style tests in test tree =="
rg -n -i -C2 \
  '(multi[-_ ]node|multinode|soak|stress|long[-_ ]run|longevity|burn[-_ ]in)' \
  tests | sed -n '1,220p'

echo
echo "== Show pipelined accuracy test and nearby YAML entries =="
sed -n '720,820p' tests/integration/defs/accuracy/test_disaggregated_serving.py
echo "-----"
rg -n -C2 'chunked_kv_transfer|pipelined_kv_transfer' tests/integration/test_lists/test-db/l0_dgx_b200.yml

Length of output: 49120


@xinhe-nv I checked the coverage. Short version: function coverage is decent, but perf/stress/multi-node coverage for this new feature is still incomplete.

What is covered well

Function / unit tests

  • tests/unittest/disaggregated/test_chunked_transfer.py
    • Covers KVSendTask, TxSession, RxSession
    • Covers multi-slice status/error handling
    • Covers enable_pipelined_transfer
    • Covers the “skip re-send” branch in respond_and_send_async
  • tests/unittest/disaggregated/test_kv_transfer.py
    • Covers _create_kv_slices
    • Covers chunk integrity across layer groups
    • Covers mamba_state_index propagation
    • Adds end-to-end chunked and pipelined transfer tests

Integration / accuracy

  • tests/integration/defs/accuracy/test_disaggregated_serving.py
    • test_chunked_kv_transfer_nixl_python_accuracy at Line 744
    • test_pipelined_kv_transfer_nixl_python_accuracy at Line 787

Gaps I found

1. Some new entry points are not directly tested
I did not find direct tests for:

  • tensorrt_llm._torch.disaggregation.transceiver.KvCacheTransceiverV2.send_prefill_chunk
  • tensorrt_llm._torch.pyexecutor.py_executor.PyExecutor._maybe_send_prefill_chunk
  • tensorrt_llm._torch.pyexecutor.kv_cache_transceiver.create_kv_cache_transceiver
  • tensorrt_llm._torch.disaggregation.transceiver.KvCacheTransceiverV2.request_and_receive_async

So the core mechanics are exercised indirectly, but several newly added integration points are still missing targeted tests.

2. Pipelined accuracy test does not appear to be scheduled in the shown pre-merge list

  • tests/integration/test_lists/test-db/l0_dgx_b200.yml includes the chunked accuracy test at Line 20 and Line 21
  • I did not find the new test_pipelined_kv_transfer_nixl_python_accuracy in that YAML

So pipelined accuracy exists in code, but it does not look wired into this pre-merge list yet.

3. No PR-specific perf validation
I found general perf infrastructure in the repo, but nothing added here that validates the PR goal:

  • no latency assertion
  • no overlap/hidden-transfer metric
  • no benchmark comparing pipelined vs non-pipelined transfer

Given the PR objective is explicitly performance-oriented, this is a notable gap.

4. No PR-specific stress coverage
The repo has general stress suites, including disaggregated stress entries in:

  • tests/integration/test_lists/qa/llm_function_stress.txt

But I did not find stress coverage specifically targeting:

  • chunked KV transfer
  • pipelined KV transfer
  • repeated chunk-send / cancel / failure / long-run stability for this path

5. No PR-specific multi-node coverage
The repo does have multi-node disaggregated tests, e.g.:

  • tests/unittest/llmapi/apps/_test_disagg_serving_multi_nodes.py
  • tests/unittest/llmapi/apps/_test_disagg_serving_multi_nodes_service_discovery.py
  • multi-node perf YAMLs under tests/integration/test_lists/test-db/

But I did not find multi-node tests specifically exercising this PR’s new chunked/pipelined KV transfer path.

Overall assessment

  • Function test coverage: good
  • Accuracy coverage: present for both chunked and pipelined
  • Pre-merge coverage: incomplete, because pipelined accuracy does not appear scheduled
  • Perf coverage: missing for this feature
  • Stress coverage: missing for this feature
  • Multi-node coverage: missing for this feature

Recommended follow-ups

  1. Add direct unit tests for:
    • send_prefill_chunk
    • _maybe_send_prefill_chunk
    • create_kv_cache_transceiver auto-selection / warnings
    • request_and_receive_async
  2. Add the pipelined accuracy test to the appropriate test-db YAML.
  3. Add at least one perf sanity test for:
    • pipelined vs non-pipelined transfer
    • visible transfer latency / overlap benefit
  4. Add a stress case for repeated chunked/pipelined disaggregated serving.
  5. Add a multi-node disaggregated case specifically for pipelined KV transfer.

If you want, I can turn this into a concrete review comment with prioritized test additions.

@athena-nv
athena-nv force-pushed the trtllm-12499-pipelined-kvcache-transfer branch from 7705b98 to 37f1bdd Compare July 7, 2026 18:10
@athena-nv
athena-nv requested a review from a team as a code owner July 7, 2026 18:10
@athena-nv
athena-nv requested a review from nvchenghaoz July 7, 2026 18:10
@athena-nv athena-nv changed the title [TRTLLM-12499][feat] (WIP) Add support for pipelined KVCache transfer for disaggregated serving in Python Cache Transceiver [TRTLLM-12499][feat] Pipelined KVCache transfer for disaggregated serving in Python Cache Transceiver Jul 7, 2026
@athena-nv
athena-nv force-pushed the trtllm-12499-pipelined-kvcache-transfer branch from 37f1bdd to 7173ea6 Compare July 7, 2026 21:02
@athena-nv
athena-nv force-pushed the trtllm-12499-pipelined-kvcache-transfer branch 3 times, most recently from d3de936 to aa98b71 Compare July 10, 2026 19:38
Comment thread tensorrt_llm/_torch/pyexecutor/py_executor.py Outdated
Comment thread tensorrt_llm/_torch/pyexecutor/py_executor.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/native/transfer.py Outdated
Comment thread tensorrt_llm/_torch/disaggregation/transceiver.py Outdated
@Tabrizian
Tabrizian requested a review from Shixiaowei02 July 13, 2026 06:45
Comment thread tensorrt_llm/_torch/pyexecutor/py_executor.py Outdated
Comment thread tests/integration/test_lists/test-db/l0_dgx_b200.yml Outdated
Comment thread tensorrt_llm/_torch/disaggregation/transceiver.py Outdated
Comment thread tests/unittest/disaggregated/test_chunked_transfer.py Outdated
…ving in Python Cache Transceiver

Instead of waiting for all prefill chunks to complete before starting KV cache
transfer, each chunk's KV data is transferred to the generation server
immediately after its prefill completes. This overlaps GPU compute with RDMA
transfer, hiding transfer latency behind prefill computation. Only the last
chunk's transfer remains on the critical path.

The feature is gated behind `enable_pipelined_transfer` on
`CacheTransceiverConfig` and is implemented in `KvCacheTransceiverV2` only. It
requires `schedule_style: generation_first`, `enable_chunked_prefill: true`,
`beam_width == 1`, the NIXL backend, `kv_cache_bounce_size_mb == 0`,
`pipeline_parallel_size == 1` on the sender, and a non-Mamba/hybrid cache
manager. Each requirement is enforced at startup or per request.

Squashed from 15 commits:

- Chunking is sender-side only; the generation server posts a single receive
  covering the whole prompt and completes on `is_last_slice`.
- `KVSlice` now describes one chunk rather than one whole request, gaining
  `total_blocks` and a meaningful `is_last_slice`. `prompt_len` became required
  on the session args so SWA can compute the stale-block boundary.
- `project_blocks_to_global_chunk` intersects ranges instead of indexing, so
  resident-suffix block lists (sliding window groups, prefix reuse, incremental
  allocation) project correctly onto a global chunk.
- The first slice always extends back to block 0, so a context-side prefix-reuse
  hit does not leave `[0, prepopulated_prompt_len)` unsent.
- Source blocks are capped at the computed chunk boundary before SWA trimming,
  normalizing V1's full-prompt reservation against V2's incremental allocation.
- `KV_AGENT_RESULT` carries `sender_slice_id` and `receiver_slice_id`
  separately, making per-chunk RDMA failures attributable. Behavior-neutral for
  the monolithic receiver.
- KV transfer activity is modeled by transceiver session membership rather than
  `LlmRequestState`, so mid-prefill cancellation and transfer-timeout monitoring
  work during the pipelined phase.
- A retired send session cannot be silently re-created, since closing it drops
  the peer's `RecvReqInfo` and the receiver never re-registers.
- `TxSession.dispatch_lock` serializes chunk dispatch across the executor thread
  and the late-peer replay path, so a newer slice cannot reach a peer's queue
  ahead of an older one.
- Transceiver configuration resolution happens early and idempotently, and
  backend/runtime compatibility validation is centralized.

Signed-off-by: Athena Cai <athenac@nvidia.com>

Simpilify _build_kv_write_meta logic

Signed-off-by: Athena Cai <athenac@nvidia.com>

Carry the pipelined chunk window as a TokenRange

The chunk cursor on KVSlice goes back to the existing TokenRange rather
than a new ChunkCoords dataclass, so the slice keeps one field for "how
far does this slice reach" instead of gaining a second vocabulary for it.
The window is still decided in block space by _build_prefill_chunk, so
the range it emits is block-aligned and the sender asserts that before
dividing it back out. TokenRange now admits an empty range, which a chunk
clamped past the end of the prompt produces.

Signed-off-by: Athena Cai <athenac@nvidia.com>

Drop cancellation, timeout, and failure-path chunked transfer tests

Removes the error-path coverage from test_chunked_transfer.py: the
cancelled-request send gates, the transfer-timeout sweeps, the session
ERROR/FAILED status cases, and the retired-send-session block. The file
now covers chunk projection, slice-id addressing, dispatch ordering, and
the pipelined config gates only.

Signed-off-by: Athena Cai <athenac@nvidia.com>

Restore WriteMeta's single slice_id field

WriteMeta carried a sender_slice_id/receiver_slice_id pair so the sender
could log its own chunk index while addressing the peer's task on the
wire. Only the peer's index is needed, so the field goes back to the
pre-existing slice_id and _deliver_kv_to_agent resolves the send task
from write_meta.task instead of indexing the session by chunk. Sender
logs lose per-chunk attribution.

Signed-off-by: Athena Cai <athenac@nvidia.com>

Remove KVSlice.total_blocks

Signed-off-by: Athena Cai <athenac@nvidia.com>

Round down at block boundaries

Signed-off-by: Athena Cai <athenac@nvidia.com>
Signed-off-by: Athena Cai <athenac@nvidia.com>
… request state.

2. Executor records the failed request.
3. After the current sampling/state-update phase—the next safe boundary—it sets DISAGG_TRANS_ERROR and runs normal cleanup.
4. The request is excluded before the next scheduling pass.

Signed-off-by: Athena Cai <athenac@nvidia.com>
@athena-nv

Copy link
Copy Markdown
Contributor Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70523 [ run ] triggered by Bot. Commit: 6032c4a Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70523 [ run ] completed with state FAILURE. Commit: 6032c4a
/LLM/main/L0_MergeRequest_PR pipeline #57735 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

Signed-off-by: Athena Cai <athenac@nvidia.com>
@athena-nv

Copy link
Copy Markdown
Contributor Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70592 [ run ] triggered by Bot. Commit: fc2e4c8 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70592 [ run ] completed with state SUCCESS. Commit: fc2e4c8
/LLM/main/L0_MergeRequest_PR pipeline #57796 completed with status: 'UNSTABLE'

CI Report

⚠️ Multi-GPU Label Required:
Multi-GPU tests require the ci: full pre-merge approved label on this PR. Ask a member of NVIDIA/trt-llm-ci-approvers to add the label, then re-trigger CI with the same bot command (no rebase needed).

⚠️ 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

Signed-off-by: Athena Cai <athenac@nvidia.com>
@athena-nv

Copy link
Copy Markdown
Contributor Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70813 [ run ] triggered by Bot. Commit: a16213b Link to invocation

@chienchunhung chienchunhung left a comment

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.

Regarding the ownership lifecycle, we don't need duplicate the implementation in this PR now that others (#17720 and #18041) started enforcing them. Let's make sure TRTLLM-15184 documents the remaining timeout/cancellation issue; specifically retaining Tx KV, Rx KV, and auxiliary resources until all physical operations drain.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70813 [ run ] completed with state FAILURE. Commit: a16213b
/LLM/main/L0_MergeRequest_PR pipeline #57994 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

@athena-nv

Copy link
Copy Markdown
Contributor Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70891 [ run ] triggered by Bot. Commit: a16213b Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70891 [ run ] completed with state FAILURE. Commit: a16213b
/LLM/main/L0_MergeRequest_PR pipeline #58060 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

@Shixiaowei02

Copy link
Copy Markdown
Collaborator

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70960 [ run ] triggered by Bot. Commit: a16213b Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #70960 [ run ] completed with state SUCCESS. Commit: a16213b
/LLM/main/L0_MergeRequest_PR pipeline #58122 completed with status: 'SUCCESS'

CI Report

Link to invocation

@nv-xtf nv-xtf left a comment

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.

LGTM

"""Return whether this rank supports pipelined KV transfer."""
if not cfg.enable_pipelined_transfer:
return False
blockers = []

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.

Could this list also cover the capacity scheduler policy? Under max utilization the C++ scheduler can pause a context request past its first chunk and the executor frees its blocks in the same iteration, while an earlier chunk is still being read, and the KV connector already blocks that policy in the same way.

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.

Will be addressed in subsequent PRs.

== LlmRequestState.DISAGG_GENERATION_TRANS_IN_PROGRESS):
return True
return (self.kv_cache_transceiver is not None
and self.kv_cache_transceiver.has_inflight_transfer(request))

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 a session existing from the first chunk this answer now depends on whether a worker thread has picked the newest task up yet, so tensor parallel ranks can disagree and split the batch; returning false unconditionally for a context only request and letting the rank uniform retirement finish it would avoid that.

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.

Will be addressed in subsequent PRs.

@SimengLiu-nv SimengLiu-nv left a comment

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.

LGTM for KVCM changes.

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

Labels

api-compatible Accepted LLM API contract change that is backwards-compatible ci: full pre-merge approved

Projects

None yet

Development

Successfully merging this pull request may close these issues.