Skip to content

Commit cd7963b

Browse files
committed
[TRTLLM-12721][fix] Bound disagg transfer status polling
Signed-off-by: Chien-Chun Hung <2679986+chienchunhung@users.noreply.github.com>
1 parent 1f8b710 commit cd7963b

2 files changed

Lines changed: 201 additions & 29 deletions

File tree

cpp/tensorrt_llm/batch_manager/cacheTransceiver.cpp

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
#include "tensorrt_llm/runtime/utils/mpiUtils.h"
5454
#include "tensorrt_llm/runtime/utils/pgUtils.h"
5555
#include <algorithm>
56+
#include <chrono>
5657
#include <cstddef>
5758
#include <numeric>
5859
#include <unordered_map>
@@ -66,6 +67,20 @@ std::mutex CacheTransceiver::mDllMutex;
6667
namespace
6768
{
6869

70+
constexpr int kTransferFuturePollIntervalMs = 10;
71+
72+
// Finite status checks are scheduler polls, not terminal deadlines. Keep each
73+
// slice short so the caller can yield back to scheduling and transfer progress.
74+
std::chrono::milliseconds getTransferFuturePollInterval(std::optional<int> const& configuredTimeoutMs)
75+
{
76+
auto waitMs = kTransferFuturePollIntervalMs;
77+
if (configuredTimeoutMs.has_value())
78+
{
79+
waitMs = std::max(1, std::min(configuredTimeoutMs.value(), kTransferFuturePollIntervalMs));
80+
}
81+
return std::chrono::milliseconds(waitMs);
82+
}
83+
6984
using RequestIdType = LlmRequest::RequestIdType;
7085

7186
enum class TransferConsensusState : std::uint64_t
@@ -700,13 +715,13 @@ void updateKVCacheTransferBW(std::shared_ptr<CacheTransceiverComm> const& mComm,
700715
RequestStatuses CacheTransceiver::checkContextTransferStatus(
701716
std::optional<int> const& atLeastRequestNum, bool markComplete)
702717
{
703-
bool blockAll = !atLeastRequestNum.has_value();
718+
bool const blockAll = !atLeastRequestNum.has_value();
704719
std::optional<int> senderFutureTimeoutMs = std::nullopt;
705-
// If blockAll is true, we want to block and not use a timeout
706-
if (!blockAll && mCacheTransceiverConfig.has_value())
720+
if (mCacheTransceiverConfig.has_value())
707721
{
708722
senderFutureTimeoutMs = mCacheTransceiverConfig->getKvTransferSenderFutureTimeoutMs();
709723
}
724+
auto const futurePollInterval = getTransferFuturePollInterval(senderFutureTimeoutMs);
710725
// Observe-only: WARN per-request when the wall-clock transfer time exceeds
711726
// kvTransferTimeoutMs. No cancellation, eviction, or state transition.
712727
std::optional<int> kvTransferTimeoutMs = std::nullopt;
@@ -770,27 +785,26 @@ RequestStatuses CacheTransceiver::checkContextTransferStatus(
770785
for (auto it = mSenderFutures.begin(); it != mSenderFutures.end();)
771786
{
772787
auto& [request, future] = *it;
788+
auto const requestId = request->mRequestId;
773789
if (kvTransferTimeoutMs.has_value())
774790
{
775791
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
776792
LlmRequest::getSteadyClockNow() - request->getKvCacheTransferStart());
777793
auto elapsedMs = static_cast<long>(elapsed.count());
778-
if (elapsedMs > kvTransferTimeoutMs.value() && mTimedOutSenderIds.insert(request->mRequestId).second)
794+
if (elapsedMs > kvTransferTimeoutMs.value() && mTimedOutSenderIds.insert(requestId).second)
779795
{
780796
TLLM_LOG_WARNING(
781797
"Context KV cache transfer for request %ld exceeded configured timeout: "
782798
"elapsed %ld ms > limit %d ms (observe-only).",
783-
request->mRequestId, elapsedMs, kvTransferTimeoutMs.value());
799+
requestId, elapsedMs, kvTransferTimeoutMs.value());
784800
}
785801
}
786-
if (blockAll || (toCompleteIdSet.find(request->mRequestId) != toCompleteIdSet.end()))
802+
if (blockAll || (toCompleteIdSet.find(requestId) != toCompleteIdSet.end()))
787803
{
788-
auto const requestId = request->mRequestId;
789804
try
790805
{
791-
// Wait for up to a specified timeout
792-
auto status = future.wait_for(std::chrono::milliseconds(senderFutureTimeoutMs.value_or(0)));
793-
if (status == std::future_status::ready || !senderFutureTimeoutMs.has_value())
806+
auto const status = blockAll ? std::future_status::ready : future.wait_for(futurePollInterval);
807+
if (status == std::future_status::ready)
794808
{
795809
future.get();
796810
bool const failed = request->getState() == LlmRequestState::kDISAGG_TRANS_ERROR;
@@ -800,8 +814,10 @@ RequestStatuses CacheTransceiver::checkContextTransferStatus(
800814
}
801815
else if (status == std::future_status::timeout)
802816
{
803-
TLLM_LOG_WARNING("Timed out waiting for context KV cache transfer after %d milliseconds.",
804-
senderFutureTimeoutMs.value());
817+
TLLM_LOG_DEBUG(
818+
"Context KV cache transfer for request %ld is not ready after %ld ms wait slice; keeping it "
819+
"in progress.",
820+
requestId, static_cast<long>(futurePollInterval.count()));
805821
++it;
806822
}
807823
else
@@ -866,7 +882,7 @@ RequestStatuses CacheTransceiver::checkContextTransferStatus(
866882

867883
void CacheTransceiver::checkGenTransferStatus(std::optional<int> const& atLeastRequestNum)
868884
{
869-
bool blockAll = !atLeastRequestNum.has_value();
885+
bool const blockAll = !atLeastRequestNum.has_value();
870886
std::vector<LlmRequest::RequestIdType> genTransferReadyRequestIds;
871887
for (auto&& [request, future] : mRequesterFutures)
872888
{
@@ -977,6 +993,13 @@ void CacheTransceiver::checkGenTransferStatus(std::optional<int> const& atLeastR
977993
" checkGenTransferStatus toCompleteIdSet size: %zu, atLeastRequestNum: %d ", toCompleteIdSet.size(),
978994
atLeastRequestNum.value_or(0));
979995
}
996+
std::optional<int> requesterFutureTimeoutMs = std::nullopt;
997+
if (mCacheTransceiverConfig.has_value())
998+
{
999+
requesterFutureTimeoutMs = mCacheTransceiverConfig->getKvTransferSenderFutureTimeoutMs();
1000+
}
1001+
auto const futurePollInterval = getTransferFuturePollInterval(requesterFutureTimeoutMs);
1002+
9801003
// Observe-only: gen-side mirror of the context-side timeout WARN.
9811004
std::optional<int> kvTransferTimeoutMs = std::nullopt;
9821005
if (mCacheTransceiverConfig.has_value())
@@ -992,28 +1015,48 @@ void CacheTransceiver::checkGenTransferStatus(std::optional<int> const& atLeastR
9921015
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
9931016
LlmRequest::getSteadyClockNow() - request->getKvCacheTransferStart());
9941017
auto elapsedMs = static_cast<long>(elapsed.count());
995-
if (elapsedMs > kvTransferTimeoutMs.value() && mTimedOutRequesterIds.insert(request->mRequestId).second)
1018+
if (elapsedMs > kvTransferTimeoutMs.value() && mTimedOutRequesterIds.insert(requestId).second)
9961019
{
9971020
TLLM_LOG_WARNING(
9981021
"Generation KV cache transfer for request %ld exceeded configured timeout: "
9991022
"elapsed %ld ms > limit %d ms (observe-only).",
1000-
request->mRequestId, elapsedMs, kvTransferTimeoutMs.value());
1023+
requestId, elapsedMs, kvTransferTimeoutMs.value());
10011024
}
10021025
}
10031026
if (blockAll || toCompleteIdSet.find(requestId) != toCompleteIdSet.end())
10041027
{
10051028
try
10061029
{
1007-
it->second.get();
1008-
bool const failed = request->getState() == LlmRequestState::kDISAGG_TRANS_ERROR;
1009-
if (failed)
1030+
auto const status = blockAll ? std::future_status::ready : it->second.wait_for(futurePollInterval);
1031+
if (status == std::future_status::ready)
10101032
{
1011-
// The receiver uses the error state as a local transfer-failed signal.
1012-
// Keep that signal local until the consensus outcome commits it globally.
1013-
request->setState(LlmRequestState::kDISAGG_GENERATION_TRANS_IN_PROGRESS);
1033+
it->second.get();
1034+
bool const failed = request->getState() == LlmRequestState::kDISAGG_TRANS_ERROR;
1035+
if (failed)
1036+
{
1037+
// The receiver uses the error state as a local transfer-failed signal.
1038+
// Keep that signal local until the consensus outcome commits it globally.
1039+
request->setState(LlmRequestState::kDISAGG_GENERATION_TRANS_IN_PROGRESS);
1040+
}
1041+
recordLocalTransferOutcome(requestId, request, failed, mCompletedRequesterRequestIds,
1042+
mFailedRequesterRequestIds, mRequesterRequestsAwaitingConsensus);
1043+
}
1044+
else if (status == std::future_status::timeout)
1045+
{
1046+
TLLM_LOG_DEBUG(
1047+
"Generation KV cache transfer for request %ld is not ready after %ld ms wait slice; keeping "
1048+
"it in progress.",
1049+
requestId, static_cast<long>(futurePollInterval.count()));
1050+
++it;
1051+
continue;
1052+
}
1053+
else
1054+
{
1055+
TLLM_LOG_ERROR(
1056+
"Future returned unexpected status for request %ld. Recording as failed.", requestId);
1057+
recordLocalTransferOutcome(requestId, request, /*failed=*/true, mCompletedRequesterRequestIds,
1058+
mFailedRequesterRequestIds, mRequesterRequestsAwaitingConsensus);
10141059
}
1015-
recordLocalTransferOutcome(requestId, request, failed, mCompletedRequesterRequestIds,
1016-
mFailedRequesterRequestIds, mRequesterRequestsAwaitingConsensus);
10171060
}
10181061
catch (std::exception const& e)
10191062
{

tests/unittest/others/test_kv_cache_transceiver.py

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
AttentionTypeCpp = tensorrt_llm.bindings.internal.batch_manager.AttentionType
2727
LlmRequestType = tensorrt_llm.bindings.internal.batch_manager.LlmRequestType
2828
DataType = tensorrt_llm.bindings.DataType
29+
DEFAULT_KV_TRANSFER_TIMEOUT_S = (
30+
CacheTransceiverConfig.model_fields["kv_transfer_timeout_ms"].default /
31+
1000.0)
32+
KV_TRANSFER_COMPLETION_MARGIN_S = 10.0
2933

3034

3135
def create_kv_cache_manager(mapping, dtype):
@@ -54,6 +58,20 @@ def fill_kv_cache_buffer(kv_cache_manager):
5458
buffer.copy_(random_values)
5559

5660

61+
def wait_for_transfer_completion(poll_fn,
62+
is_done_fn,
63+
timeout_s=DEFAULT_KV_TRANSFER_TIMEOUT_S +
64+
KV_TRANSFER_COMPLETION_MARGIN_S):
65+
deadline = time.monotonic() + timeout_s
66+
while time.monotonic() < deadline:
67+
poll_fn()
68+
if is_done_fn():
69+
return
70+
time.sleep(0.01)
71+
poll_fn()
72+
assert is_done_fn(), "Timed out waiting for KV cache transfer completion"
73+
74+
5775
@pytest.fixture(scope="function")
5876
def ctx_gen_kv_cache_dtype(request):
5977
if request.param == "ctx_fp8_gen_fp8":
@@ -155,8 +173,21 @@ def test_kv_cache_transceiver_single_process(ctx_gen_kv_cache_dtype,
155173
# send gen request
156174
kv_cache_transceiver_gen.request_and_receive_async(gen_request)
157175

158-
kv_cache_transceiver_ctx.check_context_transfer_status(1)
159-
kv_cache_transceiver_gen.check_gen_transfer_status(1)
176+
completed_ctx_ids = set()
177+
178+
def poll_transfers():
179+
completed, failed = kv_cache_transceiver_ctx.check_context_transfer_status(
180+
1)
181+
assert failed == []
182+
completed_ctx_ids.update(completed)
183+
kv_cache_transceiver_gen.check_gen_transfer_status(1)
184+
185+
def transfers_done():
186+
return (ctx_request.py_request_id in completed_ctx_ids
187+
and gen_request.state
188+
== LlmRequestState.DISAGG_GENERATION_TRANS_COMPLETE)
189+
190+
wait_for_transfer_completion(poll_transfers, transfers_done)
160191

161192
assert torch.equal(
162193
kv_cache_manager_gen.get_buffers(0),
@@ -323,8 +354,23 @@ def test_async_transfer_keeps_llm_request_alive():
323354
"the requester future is in flight")
324355

325356
# Drive the transfer to completion so the harness tears down cleanly.
326-
transceiver_ctx.check_context_transfer_status(1)
327-
transceiver_gen.check_gen_transfer_status(1)
357+
completed_ctx_ids = set()
358+
359+
def poll_transfers():
360+
completed, failed = transceiver_ctx.check_context_transfer_status(1)
361+
assert failed == []
362+
completed_ctx_ids.update(completed)
363+
transceiver_gen.check_gen_transfer_status(1)
364+
365+
def transfers_done():
366+
ctx_request = ctx_ref()
367+
gen_request = gen_ref()
368+
return (ctx_request is not None and gen_request is not None
369+
and ctx_request.py_request_id in completed_ctx_ids
370+
and gen_request.state
371+
== LlmRequestState.DISAGG_GENERATION_TRANS_COMPLETE)
372+
373+
wait_for_transfer_completion(poll_transfers, transfers_done)
328374

329375

330376
def _build_ctx_request_for_timeout_test(request_id: int) -> LlmRequest:
@@ -428,6 +474,76 @@ def test_kv_transfer_timeout_silent_when_unset(capfd):
428474
transceiver_ctx.cancel_request(ctx_request)
429475

430476

477+
@pytest.mark.timeout(60)
478+
def test_context_transfer_bounded_poll_keeps_request_in_progress():
479+
"""A bounded transfer poll must not make a non-ready request terminal."""
480+
mapping = Mapping(world_size=1, rank=0)
481+
dist = Distributed.get(mapping)
482+
kv_cache_manager_ctx = create_kv_cache_manager(mapping, DataType.HALF)
483+
kv_cache_manager_gen = create_kv_cache_manager(mapping, DataType.HALF)
484+
485+
cache_transceiver_config = CacheTransceiverConfig(backend="DEFAULT",
486+
max_tokens_in_buffer=512)
487+
transceiver_ctx = create_kv_cache_transceiver(mapping, dist,
488+
kv_cache_manager_ctx,
489+
AttentionTypeCpp.DEFAULT,
490+
cache_transceiver_config)
491+
transceiver_gen = create_kv_cache_transceiver(mapping, dist,
492+
kv_cache_manager_gen,
493+
AttentionTypeCpp.DEFAULT,
494+
cache_transceiver_config)
495+
496+
fill_kv_cache_buffer(kv_cache_manager_ctx)
497+
498+
ctx_request = _build_ctx_request_for_timeout_test(request_id=100)
499+
kv_cache_manager_ctx.impl.add_sequence_batch(
500+
[(ctx_request.py_request_id, ctx_request.prompt_len, 1)], [ctx_request])
501+
502+
transceiver_ctx.respond_and_send_async(ctx_request)
503+
504+
start = time.monotonic()
505+
completed_request_ids, error_request_ids = (
506+
transceiver_ctx.check_context_transfer_status(1))
507+
elapsed = time.monotonic() - start
508+
509+
assert elapsed < 1.0, (
510+
f"Bounded poll should yield back to the executor quickly; "
511+
f"elapsed={elapsed:.3f}s")
512+
assert completed_request_ids == []
513+
assert error_request_ids == []
514+
assert ctx_request.state == LlmRequestState.DISAGG_CONTEXT_TRANS_IN_PROGRESS
515+
516+
sampling_params = SamplingParams()
517+
gen_request = LlmRequest(
518+
request_id=100,
519+
max_new_tokens=1,
520+
input_tokens=list(range(256)),
521+
sampling_config=tensorrt_llm.bindings.SamplingConfig(
522+
sampling_params._get_sampling_config()),
523+
is_streaming=False,
524+
llm_request_type=LlmRequestType.LLMREQUEST_TYPE_GENERATION_ONLY,
525+
context_phase_params=ctx_request.context_phase_params)
526+
527+
kv_cache_manager_gen.impl.add_sequence_batch(
528+
[(gen_request.py_request_id, gen_request.prompt_len, 1)], [gen_request])
529+
transceiver_gen.request_and_receive_async(gen_request)
530+
531+
completed_ctx_ids = set()
532+
533+
def poll_transfers():
534+
completed, failed = transceiver_ctx.check_context_transfer_status(1)
535+
assert failed == []
536+
completed_ctx_ids.update(completed)
537+
transceiver_gen.check_gen_transfer_status(1)
538+
539+
def transfers_done():
540+
return (ctx_request.py_request_id in completed_ctx_ids
541+
and gen_request.state
542+
== LlmRequestState.DISAGG_GENERATION_TRANS_COMPLETE)
543+
544+
wait_for_transfer_completion(poll_transfers, transfers_done)
545+
546+
431547
def create_hybrid_cache_manager(mapping,
432548
dtype,
433549
mamba_conv_dtype=torch.float16,
@@ -629,8 +745,21 @@ def test_hybrid_cache_transceiver_single_process(backend, hybrid_dtypes,
629745

630746
cache_transceiver_gen.request_and_receive_async(gen_request)
631747

632-
cache_transceiver_ctx.check_context_transfer_status(1)
633-
cache_transceiver_gen.check_gen_transfer_status(1)
748+
completed_ctx_ids = set()
749+
750+
def poll_transfers():
751+
completed, failed = cache_transceiver_ctx.check_context_transfer_status(
752+
1)
753+
assert failed == []
754+
completed_ctx_ids.update(completed)
755+
cache_transceiver_gen.check_gen_transfer_status(1)
756+
757+
def transfers_done():
758+
return (ctx_request.py_request_id in completed_ctx_ids
759+
and gen_request.state
760+
== LlmRequestState.DISAGG_GENERATION_TRANS_COMPLETE)
761+
762+
wait_for_transfer_completion(poll_transfers, transfers_done)
634763

635764
assert torch.equal(
636765
hybrid_cache_manager_gen.get_buffers(0),

0 commit comments

Comments
 (0)