Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 254 additions & 5 deletions cpp/tests/unit_tests/batch_manager/kvCacheManagerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8386,16 +8386,17 @@ TEST_F(KVCacheManagerTest, StaticLinearHybridAllocationTest)
// ownership tracking.
///////////////////////////////////////////////////////////////////////////////

// Helper: create a KVCacheManager for batch tests.
// tokensPerBlock=4, 16 primary blocks, block reuse enabled, partial reuse enabled.
static auto makeBatchTestKVCacheManager(std::shared_ptr<tensorrt_llm::runtime::CudaStream> const& stream)
// Helper: create a KVCacheManager for batch tests. The default pool geometry is
// tokensPerBlock=4, 16 primary blocks, no secondary blocks. Tests that certify
// local offload can opt into secondary blocks without duplicating the manager
// construction used by the ownership tests below.
static auto makeBatchTestKVCacheManager(std::shared_ptr<tensorrt_llm::runtime::CudaStream> const& stream,
SizeType32 blocksInPrimaryPool = 16, SizeType32 blocksInSecondaryPool = 0)
{
auto constexpr numLayers = 1;
auto constexpr numKvHeads = 1;
auto constexpr sizePerHead = 16;
auto constexpr tokensPerBlock = 4;
auto constexpr blocksInPrimaryPool = 16;
auto constexpr blocksInSecondaryPool = 0;
auto constexpr maxNumSequences = 16;
auto constexpr beamWidth = 1;
auto constexpr maxAttentionWindow = tokensPerBlock * 8;
Expand Down Expand Up @@ -8426,6 +8427,254 @@ static void seedAndRelease(KVCacheManager& mgr, LlmRequest::RequestIdType reqId,
(void) mgr.removeSequence(reqId, req);
}

TEST_F(KVCacheManagerTest, AddSequenceBatchLeavesOneFinalContextTokenAfterReuse)
{
auto const stream = std::make_shared<tr::CudaStream>();
auto mgr = makeBatchTestKVCacheManager(stream);
auto constexpr beamWidth = 1;
auto constexpr promptLen = 9;
auto constexpr seedRequestId = 0;
auto constexpr requestId = 1;
auto const inputTokens = std::make_shared<VecTokens>(VecTokens{0, 1, 2, 3, 4, 5, 6, 7, 8});

seedAndRelease(*mgr, seedRequestId, inputTokens);

auto req = std::make_shared<LlmRequest>(
LlmRequest::RequestIdType{requestId}, /*maxNewTokens=*/2, inputTokens, tr::SamplingConfig{beamWidth}, false);
auto const initialState = req->getState();
auto const requestType = req->getLlmRequestType();

mgr->addSequenceBatch({{{requestId, promptLen, beamWidth}}}, {std::ref(*req)});

EXPECT_EQ(req->getPrepopulatedPromptLen(), promptLen - 1);
EXPECT_EQ(req->getContextCurrentPosition(), promptLen - 1);
EXPECT_EQ(req->getContextRemainingLength(), 1);
EXPECT_EQ(req->getContextChunkSize(), 1);
EXPECT_TRUE(req->isLastContextChunk());
EXPECT_EQ(req->getState(), initialState);
EXPECT_EQ(req->getLlmRequestType(), requestType);

tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*req);
EXPECT_NO_THROW(static_cast<void>(mgr->removeSequence(requestId, req)));
}

TEST_F(KVCacheManagerTest, AddSequenceBatchPreservesDraftTokensOnFinalContextAfterReuse)
{
auto const stream = std::make_shared<tr::CudaStream>();
auto mgr = makeBatchTestKVCacheManager(stream);
auto constexpr beamWidth = 1;
auto constexpr promptLen = 9;
auto constexpr seedRequestId = 0;
auto constexpr requestId = 1;
auto const inputTokens = std::make_shared<VecTokens>(VecTokens{0, 1, 2, 3, 4, 5, 6, 7, 8});

seedAndRelease(*mgr, seedRequestId, inputTokens);

auto req = std::make_shared<LlmRequest>(
LlmRequest::RequestIdType{requestId}, /*maxNewTokens=*/2, inputTokens, tr::SamplingConfig{beamWidth}, false);
auto const draftTokens = std::make_shared<VecTokens>(VecTokens{42});
req->setDraftTokens(draftTokens);
auto const initialState = req->getState();
auto const requestType = req->getLlmRequestType();

mgr->addSequenceBatch({{{requestId, promptLen, beamWidth}}}, {std::ref(*req)});

EXPECT_EQ(req->getPrepopulatedPromptLen(), promptLen - 1);
EXPECT_EQ(req->getContextCurrentPosition(), promptLen - 1);
EXPECT_EQ(req->getContextRemainingLength(), 1);
EXPECT_EQ(req->getContextChunkSize(), 1);
EXPECT_EQ(req->getNumDraftTokens(), 1);
EXPECT_EQ(req->getState(), initialState);
EXPECT_EQ(req->getLlmRequestType(), requestType);

tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*req);
EXPECT_NO_THROW(static_cast<void>(mgr->removeSequence(requestId, req)));
}

TEST_F(KVCacheManagerTest, AddSequenceBatchPreservesGuidanceAndContextLogitsAfterReuse)
{
auto const stream = std::make_shared<tr::CudaStream>();
auto mgr = makeBatchTestKVCacheManager(stream);
auto constexpr promptLen = 9;
auto constexpr reusableLen = promptLen - 1;
auto constexpr beamWidth = 1;
auto const inputTokens = std::make_shared<VecTokens>(VecTokens{0, 1, 2, 3, 4, 5, 6, 7, 8});
tr::SamplingConfig const samplingConfig{beamWidth};

auto seedReq = std::make_shared<LlmRequest>(
LlmRequest::RequestIdType{0}, SizeType32{1}, inputTokens, samplingConfig, /*isStreaming=*/false);
mgr->addSequenceBatch({{{0, promptLen, beamWidth}}}, {std::ref(*seedReq)});
tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*seedReq);
(void) mgr->removeSequence(0, seedReq);

tle::OutputConfig outputConfig;
outputConfig.returnContextLogits = true;
auto const guidedParams = tle::GuidedDecodingParams(tle::GuidedDecodingParams::GuideType::kREGEX, R"([0-9]+)");
tle::Request executorRequest(
*inputTokens, /*maxTokens=*/1, /*streaming=*/false, tle::SamplingConfig{}, outputConfig);
executorRequest.setGuidedDecodingParams(guidedParams);
auto req = std::make_shared<LlmRequest>(LlmRequest::RequestIdType{1}, executorRequest);
auto const semanticState = req->getState();
auto const semanticType = req->getLlmRequestType();

mgr->addSequenceBatch({{{1, promptLen, beamWidth}}}, {std::ref(*req)});

EXPECT_EQ(req->getContextCurrentPosition(), reusableLen);
EXPECT_EQ(req->getContextRemainingLength(), 1);
EXPECT_EQ(req->getContextChunkSize(), 1);
EXPECT_TRUE(req->getReturnContextLogits());
ASSERT_TRUE(req->getGuidedDecodingParams().has_value());
EXPECT_EQ(req->getGuidedDecodingParams().value(), guidedParams);
EXPECT_EQ(req->getState(), semanticState);
EXPECT_EQ(req->getLlmRequestType(), semanticType);

tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*req);
EXPECT_NO_THROW(static_cast<void>(mgr->removeSequence(1, req)));
}

TEST_F(KVCacheManagerTest, AddSequenceBatchOnboardsOffloadedPrefixForFinalContextToken)
{
auto const stream = std::make_shared<tr::CudaStream>();
auto mgr = makeBatchTestKVCacheManager(stream, /*blocksInPrimaryPool=*/16, /*blocksInSecondaryPool=*/4);
auto constexpr beamWidth = 1;
auto constexpr promptLen = 9;
auto constexpr reusableLen = promptLen - 1;
auto const inputTokens = std::make_shared<VecTokens>(VecTokens{0, 1, 2, 3, 4, 5, 6, 7, 8});
tr::SamplingConfig const samplingConfig{beamWidth};

// Seed two reusable full blocks and remember them before the sequence is
// released. Moving both blocks to secondary memory forces the next batch
// through the real local-onboard path instead of GPU-only radix reuse.
auto seedReq = std::make_shared<LlmRequest>(
LlmRequest::RequestIdType{0}, SizeType32{0}, inputTokens, samplingConfig, /*isStreaming=*/false);
mgr->addSequenceBatch({{{0, promptLen, beamWidth}}}, {std::ref(*seedReq)});
auto const windowSize = theOnlyWindowSize(*mgr);
auto const seedBlockIds = mgr->getSequence(0).getCacheBlockIds(windowSize).at(0);
ASSERT_EQ(seedBlockIds.size(), 3);
tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*seedReq);
(void) mgr->removeSequence(0, seedReq);

// KVCacheManager intentionally exposes BlockManager as read-only. This
// test needs to force a specific reusable block into the secondary tier,
// so keep the mutation local to the fixture instead of widening the
// production interface solely for test setup.
auto& blockManager = const_cast<BlockManager&>(mgr->getBlockManager());
for (auto const blockId : {seedBlockIds[0], seedBlockIds[1]})
{
auto block = blockManager.getBlockById(blockId, windowSize);
ASSERT_TRUE(block->isPrimary());
blockManager.offloadBlock(block, windowSize);
EXPECT_FALSE(block->isPrimary());
}
stream->synchronize();

// Two requests claiming the same host-resident prefix in one IFB batch
// must both stop at the final prompt token. They may share the immutable
// prefix, but each needs private writable capacity for that final token.
auto req1 = std::make_shared<LlmRequest>(
LlmRequest::RequestIdType{1}, SizeType32{1}, inputTokens, samplingConfig, /*isStreaming=*/false);
auto req2 = std::make_shared<LlmRequest>(
LlmRequest::RequestIdType{2}, SizeType32{1}, inputTokens, samplingConfig, /*isStreaming=*/false);
auto const req1State = req1->getState();
auto const req2State = req2->getState();
auto const req1Type = req1->getLlmRequestType();
auto const req2Type = req2->getLlmRequestType();

mgr->addSequenceBatch({{{1, promptLen, beamWidth}, {2, promptLen, beamWidth}}}, {std::ref(*req1), std::ref(*req2)});
// refreshBlocks joins all onboard/copy work to the execution stream. A
// model forward or CUDA graph replay enqueued after this point can consume
// the restored prefix without a host-side synchronization.
mgr->refreshBlocks();
stream->synchronize();

for (auto const& req : {req1, req2})
{
EXPECT_EQ(req->getPrepopulatedPromptLen(), reusableLen);
EXPECT_EQ(req->getContextCurrentPosition(), reusableLen);
EXPECT_EQ(req->getContextRemainingLength(), 1);
EXPECT_EQ(req->getContextChunkSize(), 1);
EXPECT_TRUE(req->isLastContextChunk());
}
EXPECT_EQ(req1->getState(), req1State);
EXPECT_EQ(req2->getState(), req2State);
EXPECT_EQ(req1->getLlmRequestType(), req1Type);
EXPECT_EQ(req2->getLlmRequestType(), req2Type);

auto const req1BlockIds = mgr->getSequence(1).getCacheBlockIds(windowSize).at(0);
auto const req2BlockIds = mgr->getSequence(2).getCacheBlockIds(windowSize).at(0);
ASSERT_EQ(req1BlockIds.size(), 3);
ASSERT_EQ(req2BlockIds.size(), 3);
EXPECT_EQ(req1BlockIds[0], req2BlockIds[0]);
EXPECT_EQ(req1BlockIds[1], req2BlockIds[1]);
EXPECT_NE(req1BlockIds[2], req2BlockIds[2]);

// Exercise the failure path for one claimant, then complete the other.
// A subsequent request must still reuse the prefix, proving cancellation
// did not leave stale ownership or invalidate the source blocks.
EXPECT_NO_THROW(static_cast<void>(mgr->removeSequence(1, std::nullopt)));
tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*req2);
EXPECT_NO_THROW(static_cast<void>(mgr->removeSequence(2, req2)));

auto req3 = std::make_shared<LlmRequest>(
LlmRequest::RequestIdType{3}, SizeType32{1}, inputTokens, samplingConfig, /*isStreaming=*/false);
mgr->addSequenceBatch({{{3, promptLen, beamWidth}}}, {std::ref(*req3)});
EXPECT_EQ(req3->getContextCurrentPosition(), reusableLen);
EXPECT_EQ(req3->getContextRemainingLength(), 1);
tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*req3);
EXPECT_NO_THROW(static_cast<void>(mgr->removeSequence(3, req3)));
EXPECT_TRUE(blockManager.verifyQueueIntegrity(windowSize));
}

TEST_F(KVCacheManagerTest, AddSequenceBatchLeavesOneFinalMultimodalContextTokenAfterReuse)
{
auto const stream = std::make_shared<tr::CudaStream>();
auto mgr = makeBatchTestKVCacheManager(stream);
auto constexpr beamWidth = 1;
auto constexpr promptLen = 9;
auto constexpr seedRequestId = 0;
auto constexpr requestId = 1;
auto constexpr mropePositionDelta = 7;
auto const inputTokens = std::make_shared<VecTokens>(VecTokens{0, 1, 2, 3, 4, 5, 6, 7, 8});
auto const multimodalHashes = std::make_shared<std::vector<std::vector<SizeType32>>>(
std::vector<std::vector<SizeType32>>{{1, 2, 3, 4, 5, 6, 7, 8}});
auto const multimodalPositions = std::make_shared<std::vector<SizeType32>>(std::vector<SizeType32>{1});
auto const multimodalLengths = std::make_shared<std::vector<SizeType32>>(std::vector<SizeType32>{4});
tr::SamplingConfig const samplingConfig{beamWidth};
auto const makeRequest = [&](LlmRequest::RequestIdType reqId, SizeType32 maxNewTokens)
{
return std::make_shared<LlmRequest>(reqId, maxNewTokens, inputTokens, samplingConfig, /*isStreaming=*/false,
std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt,
std::nullopt, multimodalHashes, multimodalPositions, multimodalLengths, std::nullopt, std::nullopt,
std::nullopt, mropePositionDelta);
};

auto seedReq = makeRequest(seedRequestId, /*maxNewTokens=*/0);
mgr->addSequenceBatch({{{seedRequestId, promptLen, beamWidth}}}, {std::ref(*seedReq)});
tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*seedReq);
(void) mgr->removeSequence(seedRequestId, seedReq);

auto req = makeRequest(requestId, /*maxNewTokens=*/2);
auto const initialState = req->getState();
auto const requestType = req->getLlmRequestType();

mgr->addSequenceBatch({{{requestId, promptLen, beamWidth}}}, {std::ref(*req)});

EXPECT_EQ(req->getPrepopulatedPromptLen(), promptLen - 1);
EXPECT_EQ(req->getContextCurrentPosition(), promptLen - 1);
EXPECT_EQ(req->getContextRemainingLength(), 1);
EXPECT_EQ(req->getContextChunkSize(), 1);
EXPECT_TRUE(req->isLastContextChunk());
EXPECT_EQ(req->getState(), initialState);
EXPECT_EQ(req->getLlmRequestType(), requestType);
ASSERT_TRUE(req->getMropePositionDeltas().has_value());
EXPECT_EQ(req->getMropePositionDeltas().value(), mropePositionDelta);
ASSERT_TRUE(req->getMultimodalHashes().has_value());
EXPECT_EQ(req->getMultimodalHashes().value(), multimodalHashes);

tensorrt_llm::testing::KvCacheManagerTestUtil::simulatePrefillCompletion(*req);
EXPECT_NO_THROW(static_cast<void>(mgr->removeSequence(requestId, req)));
}

// Test 1: Two requests in a batch, both partially match the same leaf block.
// The tracker should assign reuse to the last request and bump the first to copy.
TEST_F(KVCacheManagerTest, BatchAddSequence_LeafPartialThenPartial)
Expand Down
53 changes: 41 additions & 12 deletions tensorrt_llm/_torch/pyexecutor/cuda_graph_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,17 @@ def _create_shared_static_tensors(self):
(max_total_tokens, ), device="cuda", dtype=torch.long)

def _get_seq_len_mode(
self,
batch: ScheduledRequests,
new_tensors_device: Optional[SampleStateTensors] = None):
self,
batch: ScheduledRequests,
new_tensors_device: Optional[SampleStateTensors] = None,
promoted_context_request_ids: frozenset[int] = frozenset()
) -> bool:
"""Select the sparse-attention graph family for the execution view.

``promoted_context_request_ids`` contains semantic final-context rows
that the model engine temporarily placed in the generation list. It is
empty for the existing generation-only path.
"""
if (isinstance(self.sparse_config, SeqLenAwareSparseAttentionConfig)
and self.sparse_config.needs_separate_short_long_cuda_graphs()):
# Some sparse attention algorithms need to use different forward paths for short and long sequences.
Expand All @@ -202,8 +210,16 @@ def _get_seq_len_mode(
is_spec_request = get_draft_token_length(
request) > 0 or next_draft_tokens_device is not None
num_draft_tokens = self.spec_config.max_draft_len if is_spec_request else 0
if request.py_request_id in promoted_context_request_ids:
# A promoted context row may retain overlap bookkeeping
# such as py_batch_idx from an earlier context chunk. That
# state describes the previous batch, not the sequence
# length of the final prompt token executed by this graph.
# Use the authoritative context cursor so graph keying
# matches the decode-shaped input prepared for this row.
total_seq_len = request.context_current_position + 1
# First draft
if request.py_is_first_draft:
elif request.py_is_first_draft:
# get_num_tokens is O(1); len(get_tokens(0)) marshals the
# whole O(seq_len) VecTokens into a Python list just for len.
total_seq_len = request.get_num_tokens(0)
Expand All @@ -229,15 +245,20 @@ def _get_seq_len_mode(
return short_seq_len_mode

def get_graph_key(
self,
batch: ScheduledRequests,
new_tensors_device: Optional[SampleStateTensors] = None,
spec_resource_manager: Optional[BaseResourceManager] = None,
spec_metadata: Optional[SpecMetadata] = None):
self,
batch: ScheduledRequests,
new_tensors_device: Optional[SampleStateTensors] = None,
spec_resource_manager: Optional[BaseResourceManager] = None,
spec_metadata: Optional[SpecMetadata] = None,
promoted_context_request_ids: frozenset[int] = frozenset()
) -> KeyType:
batch_size = batch.batch_size

# Get the sequence length mode.
short_seq_len_mode = self._get_seq_len_mode(batch, new_tensors_device)
# Keep the graph-key tuple unchanged; promoted IDs only correct the
# sequence length observed by sparse short/long graph selection.
short_seq_len_mode = self._get_seq_len_mode(
batch, new_tensors_device, promoted_context_request_ids)

# Spec one-engine sampler has two code paths (argmax fast-path vs
# advanced sampling kernel). Include this in the key so we capture
Expand Down Expand Up @@ -305,14 +326,19 @@ def maybe_get_cuda_graph(
draft_tokens_cuda: Optional[torch.Tensor] = None,
new_tensors_device: Optional[SampleStateTensors] = None,
spec_resource_manager: Optional[BaseResourceManager] = None,
) -> Tuple[Optional[Any], Optional[Any], Optional[Tuple[int, int, bool]]]:
promoted_context_request_ids: frozenset[int] = frozenset(),
) -> Tuple[Optional[Any], Optional[Any], Optional[KeyType]]:
"""
Determines if the current batch can be run with a CUDA graph.

Returns a tuple containing:
- The attn_metadata for the graph, if applicable.
- The spec_metadata for the graph, if applicable.
- The key for the graph, if applicable.

``promoted_context_request_ids`` is execution-view metadata. It does
not change request state or type and is used only to build a graph key
consistent with the final-context token that will be executed.
"""
# disable when doing statistic
if ExpertStatistic.should_record():
Expand Down Expand Up @@ -342,8 +368,11 @@ def maybe_get_cuda_graph(
# do carry a delta must first populate the model-side cache for their
# current seq slot before graph replay.
return None, None, None
# Propagate the execution-view identity through graph lookup. Existing
# callers pass the empty default and retain generation-only behavior.
key = self.get_graph_key(batch, new_tensors_device,
spec_resource_manager, spec_metadata)
spec_resource_manager, spec_metadata,
promoted_context_request_ids)

if key in self.graph_metadata:
return self.graph_metadata[key][
Expand Down
Loading
Loading