Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fb3ad4b
[None][feat] BREAKING: Support layer-level reuse policies for attenti…
jiaganc Oct 27, 2026
e6a1dd7
[None][fix] Exclude unselected optional SSM groups from reuse statistics
jiaganc Oct 27, 2026
46cd921
[None][doc] Explain SSM allocation branch ordering
jiaganc Oct 27, 2026
ff87edc
[None][refactor] Simplify required SSM cache level assignment
jiaganc Oct 27, 2026
13a5c26
[None][feat] Support buffer-level reuse policy for SSM layers (By Agent)
VALLIS-NERIA Oct 27, 2026
54d010c
[None][fix] Keep SSM reuse policies when dropping zero-size buffers (…
VALLIS-NERIA Oct 27, 2026
dd58f34
Support per-buffer lifecycle queries and migrate cache storage metadata
jiaganc Oct 28, 2026
892c6c8
Cover split-buffer scratch allocation and transfer membership
jiaganc Oct 28, 2026
7ef9931
Reject ambiguous connector layer lookup and cover split transfer layout
jiaganc Oct 28, 2026
0e8a9cc
Preserve group membership order and pack unequal scratch shares safely
jiaganc Oct 28, 2026
a4b257e
Register lifecycles only for configured buffers
jiaganc Oct 28, 2026
16773f0
Defer Python consumer adoption of per-buffer reuse policies
jiaganc Oct 28, 2026
3e3ce5d
Remove deferred buffer adoption comments from Python consumers
jiaganc Oct 28, 2026
74e17aa
Remove V2 reuse policy section from V1 cache manager guide
jiaganc Oct 28, 2026
360a2f2
Wait for offload before overwriting recycled slots in tests
jiaganc Oct 28, 2026
4f26fed
Document attention and SSM buffer reuse policy overrides
jiaganc Oct 28, 2026
13ff239
Clarify lifecycle policy helpers and cache group API contracts
jiaganc Oct 29, 2026
2295af0
Adapt buffer reuse policies to upstream sparse cache APIs
jiaganc Oct 30, 2026
d0911a1
Validate suspended SSM working state with page holders
jiaganc Oct 30, 2026
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
38 changes: 35 additions & 3 deletions cpp/tensorrt_llm/batch_manager/kv_cache_manager_v2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ them before changing `batch.*`, `sparseCache.*` or the index conversion.
- Prefer composition when it expresses ownership or containment. Use
inheritance only for a real IS-A relationship or required virtual dispatch,
such as `CommittedPage : Page` and `EventManager : EventSink`.
- Preserve strongly typed indices. `LayerGroupId` is a public lifecycle
- Preserve strongly typed indices. `CacheGroupId` (and legacy `LayerGroupId`) is a public lifecycle
semantic, `PoolGroupIndex` is a storage-layout index, `BlockOrdinal` is a
sequence position, and `SlotId`/page indices address storage. Do not convert
or compare them implicitly.
Expand Down Expand Up @@ -93,9 +93,41 @@ must eventually be `close()`d. Its normal flow is:
for later requests; `stopCommitting()` finalizes this process.
5. Suspend or close the request, returning pages to holding/eviction ownership.

`LifeCycleRegistry` maps model layers to lifecycle groups. Attention lifecycles
`LifeCycleRegistry` maps layer buffers to lifecycle groups. Attention lifecycles
may have sliding-window and sink-token rules; SSM lifecycles represent a
recurrent-state checkpoint. A pool-group index is a storage-layout index and is
recurrent-state checkpoint. Both layer configs expose `reusePolicy`: `REQUIRED`
constrains the prefix match, `OPTIONAL` offers state at the required endpoint for
explicit selection on first resume, and `PRIVATE` never publishes reusable pages.
A buffer may override its layer's default through `BufferConfig::reusePolicy`.
This applies to attention and SSM buffers, so one model layer may span several
lifecycle groups. Use `getBufferGroupId(layerId, role)` and `bufferGrouping()`
(`get_buffer_group_id` and `buffer_grouping` in Python) for exact membership.
These APIs return or index by `CacheGroupId`, a public alias of `LifeCycleId`.
`LayerGroupId` remains a lifecycle alias for legacy APIs.
The legacy `getLayerGroupId(layerId)` and `layerGrouping()` are deprecated in
documentation only: uniform layers retain their results, while a split layer
fails a `TLLM_CHECK_WITH_INFO` (for the queried layer or the entire partition,
respectively), exposed as `RuntimeError` in Python. No deprecation warnings are emitted.

Page conversion reads `BufferAttr::scratchPagesPerBlock`, counting only the
layer's buffers in that coalesced slot. Scratch capacity uses the maximum exact
layer share across pools within each lifecycle. Layer counts are temporary
layout calculations, not persistent `(layer, lifecycle)` attributes.

Execution backends that share a page or state index across coupled buffers must
validate that they use the same policy; independent policies require independent
index tables. In particular, the default attention view couples K/V and scales,
and Mamba kernels couple recurrent, convolution, PLE, and replay state. Native
KVCM supports independent policies for callers that provide those indices.
The sparse fetch path configured by `sparseCache` still uses one group per layer;
its setup uses the checked layer lookup and rejects split layers.

SSM lifecycles are deduplicated by policy, so a manager can have up to three SSM
groups; iterate `ssmLifeCycleIds()` for working state and use
`requiredSsmLifeCycleId()` only for prefix pruning.
Optional SSM checkpoints require an exact endpoint match, including partial
blocks. Fresh or unselected recurrent state must be initialized or reconstructed
by the caller before inference. A pool-group index is a storage-layout index and is
not interchangeable with a layer ID or lifecycle ID.

Sparsity is a property of a pool, not of a lifecycle: a layer's buffers may be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ std::vector<SharedPtr<Block>> Block::clearStaleBlocksAfterPageUnlink(

// If this is a full-attention block or a sink block: evict subtree.
// Mirrors Python: pages = remove_subtree(self)
if (alc && alc->requiresPrefixCoverage()
if (alc && requiresPrefixCoverage(*alc)
&& (!alc->windowSize.has_value() || block.ordinal() < BlockOrdinal{alc->numSinkBlocks}))
{
pruneStart = block.prev;
Expand Down Expand Up @@ -899,7 +899,7 @@ std::vector<BlockRadixTree::MatchResult> BlockRadixTree::pruneMatch(
bool shortened = false;
for (auto [lcId, attn] : attnLcs)
{
if (!attn->requiresPrefixCoverage())
if (!requiresPrefixCoverage(*attn))
{
continue;
}
Expand Down Expand Up @@ -973,7 +973,7 @@ BlockRadixTree::ReuseMatch BlockRadixTree::match(
auto rawMatched = matchTokenPath(reuseScope, tokens, knownNoDigest, enablePartialMatch);
// Content-divergence depth is measured before page or recurrent-snapshot pruning.
int const numReusableTokensBeforePruning = numMatchedTokens(rawMatched, mTokensPerBlock);
auto const ssmLcId = mLifeCycles.ssmLifeCycleId();
auto const ssmLcId = mLifeCycles.requiredSsmLifeCycleId();
// Page requirements depend on the final endpoint. Back off before pruning
// so SWA coverage and recurrent snapshots are validated at that endpoint.
if (backoff > 0)
Expand Down
47 changes: 32 additions & 15 deletions cpp/tensorrt_llm/batch_manager/kv_cache_manager_v2/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,21 @@ inline std::optional<size_t> cacheTierMaxQuota(CacheTierConfig const& cfg)
return host != nullptr ? host->maxQuota : std::nullopt;
}

//! Controls whether a lifecycle participates in prefix matching and page reuse.
enum class ReusePolicy
{
//! Commit reusable pages and require live-window coverage when matching a prefix.
REQUIRED = 0,
//! Keep request-local working pages; never publish or reuse them through the radix tree.
PRIVATE = 1,
//! Commit reusable pages without constraining the REQUIRED prefix match. The caller selects
//! available groups at resume; missing or unselected groups use fresh pages for recomputation.
//! These pages may be evicted independently without removing the REQUIRED prefix.
OPTIONAL = 2,
};

// ---------------------------------------------------------------------------
// Buffer configuration (one KV buffer inside an attention layer).
// Buffer configuration (one buffer inside an attention or SSM layer).
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -193,6 +206,15 @@ struct BufferConfig
//! densely packed, so a padded page is not expressible.
int sparseUnitChunks = 1;

//! Reuse policy of this buffer; nullopt inherits the layer's policy. Overrides can
//! place buffers of one attention or SSM layer in different lifecycle groups.
std::optional<ReusePolicy> reusePolicy;

[[nodiscard]] ReusePolicy effectiveReusePolicy(ReusePolicy layerPolicy) const noexcept
{
return reusePolicy.value_or(layerPolicy);
}

void validate() const;

//! Where this buffer's selection units sit inside a page, given how many the layer
Expand Down Expand Up @@ -292,19 +314,6 @@ inline void validateNoDuplicateBufferRoles(std::vector<BufferConfig> const& buff

} // namespace detail

//! Controls whether an attention lifecycle participates in prefix matching and page reuse.
enum class AttentionReusePolicy
{
//! Commit reusable pages and require live-window coverage when matching a prefix.
REQUIRED = 0,
//! Keep request-local working pages; never publish or reuse them through the radix tree.
PRIVATE = 1,
//! Commit reusable pages without constraining the REQUIRED prefix match. The caller selects
//! available groups at resume; missing or unselected groups use fresh pages for recomputation.
//! These pages may be evicted independently without removing the REQUIRED prefix.
OPTIONAL = 2,
};

struct AttentionLayerConfig
{
static constexpr LayerType type = LayerType::ATTENTION;
Expand All @@ -319,7 +328,8 @@ struct AttentionLayerConfig
std::optional<int> numSinkTokens;

// Private working pages never constrain prefix matching or enter the tree.
AttentionReusePolicy reusePolicy = AttentionReusePolicy::REQUIRED;
// Every buffer of an attention layer uses this policy.
ReusePolicy reusePolicy = ReusePolicy::REQUIRED;
//! The sparse fetch path for this layer's sparse buffers, if it has one.
std::optional<SparseCacheConfig> sparseCache;

Expand All @@ -341,6 +351,8 @@ struct SsmLayerConfig

LayerId layerId = 0;
std::vector<BufferConfig> buffers;
// Default policy of the layer's buffers; BufferConfig::reusePolicy may override it per buffer.
ReusePolicy reusePolicy = ReusePolicy::REQUIRED;

void validate() const
{
Expand All @@ -362,6 +374,11 @@ struct SsmLayerConfig

using LayerConfig = std::variant<AttentionLayerConfig, SsmLayerConfig>;

inline std::vector<BufferConfig> const& layerBuffers(LayerConfig const& layer)
{
return std::visit([](auto const& cfg) -> std::vector<BufferConfig> const& { return cfg.buffers; }, layer);
}

// ---------------------------------------------------------------------------
// KVCacheDesc — describes one KV cache request's capacity and history length.
// Mirrors _config.py::KVCacheDesc.
Expand Down
Loading
Loading