Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -912,8 +912,29 @@ std::vector<BlockRadixTree::MatchResult> BlockRadixTree::pruneMatch(
return matched;
}

namespace
{
// Drop `backoff` tokens off the tail of a match, dropping whole blocks while the backoff
// outruns them. Only the tail shrinks, so pruneMatch's "leading entries are full blocks"
// invariant still holds.
void backOffMatch(std::vector<BlockRadixTree::MatchResult>& matched, int backoff)
{
while (backoff > 0 && !matched.empty())
{
auto& last = matched.back();
if (last.numMatchedTokens > backoff)
{
last.numMatchedTokens -= backoff;
return;
}
backoff -= last.numMatchedTokens;
matched.pop_back();
}
}
} // namespace

BlockRadixTree::ReuseMatch BlockRadixTree::match(
ReuseScope const& reuseScope, TokenSpan tokens, bool knownNoDigest, bool enablePartialMatch) const
ReuseScope const& reuseScope, TokenSpan tokens, bool knownNoDigest, bool enablePartialMatch, int backoff) const
{
auto rawMatched = matchTokenPath(reuseScope, tokens, knownNoDigest, enablePartialMatch);
auto const ssmLcId = mLifeCycles.ssmLifeCycleId();
Expand All @@ -925,7 +946,11 @@ BlockRadixTree::ReuseMatch BlockRadixTree::match(
{
attnOnlyTokens = numMatchedTokens(pruneMatch(rawMatched, std::nullopt), mTokensPerBlock);
}
auto const matched = pruneMatch(std::move(rawMatched), ssmLcId);
auto matched = pruneMatch(std::move(rawMatched), ssmLcId);
if (backoff > 0)
{
backOffMatch(matched, backoff);
}
ReuseMatch result{};
result.numTokens = numMatchedTokens(matched, mTokensPerBlock);
result.numLookupTokens = static_cast<int>(tokens.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,9 @@ class BlockRadixTree
// knownNoDigest: from external text_only knowledge, never a scan (see Hasher::update).
// Takes a non-owning TokenSpan so a zero-copy int32 token buffer can be matched without
// allocating/copying (the hot path). Callers holding a std::vector pass toSpan(vec).
// backoff: tokens trimmed off the tail of the match (see KVCacheManagerConfig::reuseMatchBackoff).
ReuseMatch match(ReuseScope const& reuseScope, TokenSpan tokens, bool knownNoDigest = false,
bool enablePartialMatch = false) const;
bool enablePartialMatch = false, int backoff = 0) const;

// Detach all cached blocks. ~Block() releases pages when the last owner drops a block.
void clear();
Expand Down
10 changes: 10 additions & 0 deletions cpp/tensorrt_llm/batch_manager/kv_cache_manager_v2/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,16 @@ struct KVCacheManagerConfig
// Try to reuse tokens from partially matched blocks.
bool enablePartialReuse = true;

// Tokens dropped from the tail of every prefix match.
//
// For a pool whose KV at position i is a function of tokens [0, i] this is 0: a match of
// m tokens proves all m are reusable. Set it to D when the pool also holds state that
// reads D tokens ahead -- one-model speculative decoding draft layers -- where a match
// of m only describes the first m - D positions.
//
// Applied inside the match so a single tree walk yields the usable depth.
int reuseMatchBackoff = 0;

// Constraint-based memory partitioning.
std::vector<BatchDesc> constraints; // batches that must always be supportable
std::optional<BatchDesc> typicalStep; // typical step for initial ratio computation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ std::shared_ptr<KvCache> KvCacheManager::createKvCache(ReuseScope reuseScope, To
BlockRadixTree::ReuseMatch KvCacheManager::matchReuse(
ReuseScope const& reuseScope, TokenSpan inputTokens, bool knownNoDigest) const
{
return mRadixTree->match(reuseScope, inputTokens, knownNoDigest, enablePartialMatch());
return mRadixTree->match(reuseScope, inputTokens, knownNoDigest, enablePartialMatch(), mConfig.reuseMatchBackoff);
}

int KvCacheManager::probeReuse(ReuseScope reuseScope, TokenSpan inputTokens, bool knownNoDigest) const
Expand Down
11 changes: 7 additions & 4 deletions cpp/tensorrt_llm/nanobind/batch_manager/kvCacheManagerV2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1613,7 +1613,7 @@ void KvCacheManagerV2Bindings::initBindings(nb::module_& m)
.def(
"__init__",
[](kv::KVCacheManagerConfig* cfg, int tokensPerBlock, std::vector<kv::CacheTierConfig> cacheTiers,
nb::list layers, float maxUtilForResume, bool enablePartialReuse,
nb::list layers, float maxUtilForResume, bool enablePartialReuse, int reuseMatchBackoff,
std::optional<kv::BatchDesc> typicalStep, std::vector<kv::BatchDesc> constraints,
std::optional<std::vector<float>> initialPoolRatio,
std::optional<kv::SwaScratchReuseConfig> swaScratchReuse, bool commitMinSnapshot, bool enableStats,
Expand All @@ -1632,6 +1632,7 @@ void KvCacheManagerV2Bindings::initBindings(nb::module_& m)
}
cfg->maxUtilForResume = maxUtilForResume;
cfg->enablePartialReuse = enablePartialReuse;
cfg->reuseMatchBackoff = reuseMatchBackoff;
cfg->typicalStep = std::move(typicalStep);
cfg->constraints = std::move(constraints);
cfg->initialPoolRatio = std::move(initialPoolRatio);
Expand All @@ -1645,14 +1646,16 @@ void KvCacheManagerV2Bindings::initBindings(nb::module_& m)
},
nb::arg("tokens_per_block"), nb::arg("cache_tiers"), nb::arg("layers"),
nb::arg("max_util_for_resume") = 0.97f, nb::arg("enable_partial_reuse") = true,
nb::arg("typical_step") = std::nullopt, nb::arg("constraints") = std::vector<kv::BatchDesc>{},
nb::arg("initial_pool_ratio").none() = std::nullopt, nb::arg("swa_scratch_reuse").none() = std::nullopt,
nb::arg("commit_min_snapshot") = false, nb::arg("enable_stats") = true, nb::arg("text_only") = false)
nb::arg("reuse_match_backoff") = 0, nb::arg("typical_step") = std::nullopt,
nb::arg("constraints") = std::vector<kv::BatchDesc>{}, nb::arg("initial_pool_ratio").none() = std::nullopt,
nb::arg("swa_scratch_reuse").none() = std::nullopt, nb::arg("commit_min_snapshot") = false,
nb::arg("enable_stats") = true, nb::arg("text_only") = false)
.def_rw("tokens_per_block", &kv::KVCacheManagerConfig::tokensPerBlock)
.def_rw("cache_tiers", &kv::KVCacheManagerConfig::cacheTiers)
.def_rw("layers", &kv::KVCacheManagerConfig::layers)
.def_rw("max_util_for_resume", &kv::KVCacheManagerConfig::maxUtilForResume)
.def_rw("enable_partial_reuse", &kv::KVCacheManagerConfig::enablePartialReuse)
.def_rw("reuse_match_backoff", &kv::KVCacheManagerConfig::reuseMatchBackoff)
.def_rw("typical_step", &kv::KVCacheManagerConfig::typicalStep)
.def_rw("constraints", &kv::KVCacheManagerConfig::constraints)
.def_rw("initial_pool_ratio", &kv::KVCacheManagerConfig::initialPoolRatio,
Expand Down
37 changes: 33 additions & 4 deletions tensorrt_llm/_torch/pyexecutor/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@
from ..hostfunc import set_low_latency_dispatch
from ..model_config import ModelConfig
from ..models.modeling_multimodal_mixin import MultimodalModelMixin
from ..speculative import (get_num_extra_kv_tokens, get_num_spec_layers,
get_spec_decoder, should_use_separate_draft_kv_cache)
from ..speculative import (draft_prompt_lookahead, get_num_extra_kv_tokens,
get_num_spec_layers, get_spec_decoder,
should_use_separate_draft_kv_cache)
from ..utils import is_gdn_replay_enabled
from .config_utils import (MambaKVCacheParams, extract_mamba_kv_cache_params,
get_layer_attention_window, is_gemma4_hybrid,
Expand Down Expand Up @@ -548,6 +549,10 @@ class KvCacheCreator:
# must be divided rather than handed out whole.
_OFFLOAD_TIER_BUDGET_ATTRS = ("host_cache_size", "disk_cache_size")

# Paired-reuse protocol flag. ``build_managers`` resolves it once and hands
# it to both constructors; the managers must not re-derive it.
_joint_kv_cache_reuse = False

def __init__(
self,
*,
Expand Down Expand Up @@ -1402,6 +1407,7 @@ def _create_kv_cache_manager(
execution_stream=self._execution_stream,
layer_mask=spec_dec_layer_mask,
is_disagg=self._is_disagg,
joint_kv_cache_reuse=self._joint_kv_cache_reuse,
)

if not self._skip_est:
Expand Down Expand Up @@ -1455,6 +1461,17 @@ def _should_create_separate_draft_kv_cache(self) -> bool:
return False
return should_use_separate_draft_kv_cache(self._speculative_config)

def _joint_reuse_supported(self) -> bool:
"""Span half of the pairing decision; ``build_managers`` ANDs it with
``has_separate_one_model_draft``. ``False`` = leave the run unpaired.
"""
if not self._is_kv_cache_manager_v2:
return False
if not getattr(self._kv_cache_manager_cls,
"_supports_joint_kv_cache_reuse", False):
return False
return draft_prompt_lookahead(self._speculative_config) is not None

def _get_effective_draft_config(self) -> ModelConfig:
"""
Return the ModelConfig to use for draft KV cache creation.
Expand Down Expand Up @@ -1589,6 +1606,7 @@ def _create_one_model_draft_kv_cache_manager(
layer_mask=spec_dec_layer_mask,
num_layers=num_draft_layers,
is_disagg=self._is_disagg,
joint_kv_cache_reuse=self._joint_kv_cache_reuse,
)

def _get_target_and_draft_cache_costs(
Expand Down Expand Up @@ -2018,6 +2036,15 @@ def build_managers(self,
self_kv_cache_config, cross_kv_cache_config = self._split_kv_cache_budget_for_cross(
)

has_separate_one_model_draft = (
self._draft_model_engine is None
and self._should_create_separate_draft_kv_cache())
# One term per axis: a draft pool exists to pair with, block reuse is on,
# and the span is known. Anything outside keeps its unpaired path.
self._joint_kv_cache_reuse = (has_separate_one_model_draft and
self_kv_cache_config.enable_block_reuse
and self._joint_reuse_supported())

# Estimation managers are throwaway probes whose pools only hold dummy
# requests, so an explicit offload tier would reserve capacity the probe
# cannot fill. Encoder-decoder runs skip estimation, so dropping the
Expand All @@ -2031,7 +2058,7 @@ def build_managers(self,
# Split combined KV cache budgets before creating managers.
has_draft = (
self._draft_model_engine is not None # two-model
or self._should_create_separate_draft_kv_cache()) # one-model
or has_separate_one_model_draft) # one-model
draft_kv_cache_config = None
if has_draft:
# The GPU split applies when each manager sizes its pools from
Expand Down Expand Up @@ -2220,7 +2247,8 @@ def _create_kv_cache_manager(
num_kv_heads: Optional[Union[int, List[int]]] = None,
head_dim: Optional[int] = None,
kv_cache_type=None,
is_disagg: bool = False) -> KVCacheManager:
is_disagg: bool = False,
joint_kv_cache_reuse: bool = False) -> KVCacheManager:
"""
Returns:
A KVCacheManager instance for the given model engine or model config
Expand Down Expand Up @@ -2351,6 +2379,7 @@ def _create_kv_cache_manager(
manager_extra_kwargs = {}
if issubclass(kv_cache_manager_cls, KVCacheManagerV2):
manager_extra_kwargs["enable_stats"] = enable_kv_cache_stats
manager_extra_kwargs["joint_kv_cache_reuse"] = joint_kv_cache_reuse
if issubclass(kv_cache_manager_cls, MambaHybridCacheManagerV2):
manager_extra_kwargs["is_disagg"] = is_disagg

Expand Down
Loading
Loading