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
47 changes: 46 additions & 1 deletion rtp_llm/config/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
init_quant_config,
)
from rtp_llm.multimodal.multimodal_mixin_register import get_multimodal_mixin_cls
from rtp_llm.ops import DataType, KvCacheDataType
from rtp_llm.ops import DataType, HybridAttentionType, KvCacheDataType
from rtp_llm.ops import ModelConfig as CppModelConfig
from rtp_llm.ops import TaskType
from rtp_llm.utils.base_model_datatypes import VitParameters
Expand Down Expand Up @@ -78,6 +78,7 @@ class ModelConfig(CppModelConfig):
"phy2log_path",
"lora_infos",
"headwise_config",
"attention_value_scale",
}

# Known C++ ModelConfig members (from ModelConfig.h)
Expand Down Expand Up @@ -251,6 +252,9 @@ def _eval_kv_cache_mem_size(self) -> float:
# Get kv_cache_dtype from attn_config
kv_cache_dtype_enum = self.attn_config.kv_cache_dtype
kv_cache_bytes = 1 if kv_cache_dtype_enum == KvCacheDataType.FP8 else 2
hybrid_config = self.hybrid_attention_config
if hybrid_config.enable_hybrid_attention:
return self._eval_hybrid_kv_cache_mem_size(kv_cache_bytes)
kv_cache_size = (
2
* self.num_layers
Expand All @@ -261,6 +265,43 @@ def _eval_kv_cache_mem_size(self) -> float:
)
return kv_cache_size

def _eval_hybrid_kv_cache_mem_size(self, kv_cache_bytes: int) -> float:
"""KV cache size for a model whose layers do not share one attention shape.

Global and sliding-window layers differ in KV head count, K and V may differ in
head dimension, and a windowed layer never holds more than its window, so the
homogeneous ``2 * num_layers * kv_head_num * size_per_head`` estimate is wrong on
all three counts.
"""
swa_config = self.hybrid_attention_config.swa_attention_config
pattern = self.hybrid_attention_config.hybrid_attention_types
ga_layers = sum(1 for t in pattern if t != HybridAttentionType.SLIDING_WINDOW)

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.

[P2] 混合注意力 KV 显存估算有三处计量口径问题

(1) ga_layers = sum(1 for t in pattern if t != SLIDING_WINDOW)(:278)把 LINEAR 层按全长 KV 计费,而仓库已有权威分类器 rtp_llm/models/hybrid_kv_cache.py:22-25== LINEAR 三分;qwen3_next / kimi_linear 均置 enable_hybrid_attention=True 会命中该分支(数值与旧公式恒等故非回归,但 docstring 声称按层型精确计量并未对 LINEAR 兑现)。(2) 窗口层读 swa_config.swa_kv_head_num(:285),全局层却读 attn_config.kv_head_num(:295),而语义对应的 ga_kv_head_num 未被使用——build_layer_attn_configs:141 等运行时路径用的正是后者,两者分叉时估算会静默偏离。(3) pattern 为空时 ga_layers=swa_layers=0,静默返回 0 且无日志。

建议: 复用 hybrid_kv_cache.py 的三分口径显式区分 LINEAR / SLIDING_WINDOW / 全局(LINEAR 计 0 或按 linear_attention_config 状态尺寸计费;若本次不修请在 docstring 写明已知偏差);GA 分支改用 swa_config.ga_kv_head_num or self.attn_config.kv_head_num,或在 _parse_swa_config 中断言二者相等;并与 apply_layer_num_override 保持一致校验 len(pattern) == self.num_layers,不满足时抛 ValueError 或退回同构公式并打 warning,不要静默产出 0。

swa_layers = len(pattern) - ga_layers

k_head_size = self.attn_config.size_per_head
v_head_size = self.attn_config.v_size_per_head or k_head_size
kv_head_size = k_head_size + v_head_size

swa_kv_head_num = swa_config.swa_kv_head_num or self.attn_config.kv_head_num
# A windowed layer only ever keeps the last window_size tokens resident.
swa_tokens = (
min(swa_config.window_size, self.max_seq_len)
if swa_config.window_size > 0
else self.max_seq_len
)

ga_bytes = (
ga_layers
* self.attn_config.kv_head_num
* kv_head_size
* kv_cache_bytes
* self.max_seq_len
)
swa_bytes = (
swa_layers * swa_kv_head_num * kv_head_size * kv_cache_bytes * swa_tokens
)
return ga_bytes + swa_bytes

def _eval_runtime_buffer_mem_size(self) -> float:
"""Evaluate runtime buffer memory size."""
input_buffer = self.max_seq_len * self.hidden_size
Expand Down Expand Up @@ -566,6 +607,10 @@ def __init__(self, *args, **kwargs):
self.mm_related_params = VitParameters()
self.quant_config = None

# Checkpoint-provided scale applied to V before attention. None means the
# checkpoint does not scale V, which is the case for every model but MiMo V2.5.
self.attention_value_scale: Optional[float] = None

def apply_override_args(self, json_model_override_args: str) -> None:
"""Apply model override arguments to ModelConfig.

Expand Down
7 changes: 5 additions & 2 deletions rtp_llm/cpp/cache/CacheConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ std::string cacheGroupPolicySummary(const CacheGroupPolicy& policy) {
<< ", charge_to_paged_budget=" << policy.charge_to_paged_budget
<< ", memory_placement=" << static_cast<int>(policy.memory_placement)
<< ", active_tail_blocks=" << policy.active_tail_blocks
<< ", prefix_reuse_window_tokens=" << policy.prefix_reuse_window_tokens
<< ", validate_tail_blocks=" << policy.validate_tail_blocks
<< ", cp_mapping=" << static_cast<int>(policy.cp_mapping) << ", cp_slice=" << static_cast<int>(policy.cp_slice)
<< '}';
Expand Down Expand Up @@ -130,8 +131,10 @@ bool CacheConfig::samePolicy(const CacheGroupPolicy& lhs, const CacheGroupPolicy
&& lhs.evict_policy == rhs.evict_policy && lhs.reservable == rhs.reservable
&& lhs.explicit_block_num == rhs.explicit_block_num
&& lhs.charge_to_paged_budget == rhs.charge_to_paged_budget && lhs.memory_placement == rhs.memory_placement
&& lhs.active_tail_blocks == rhs.active_tail_blocks && lhs.validate_tail_blocks == rhs.validate_tail_blocks
&& lhs.cp_mapping == rhs.cp_mapping && lhs.cp_slice == rhs.cp_slice;
&& lhs.active_tail_blocks == rhs.active_tail_blocks
&& lhs.prefix_reuse_window_tokens == rhs.prefix_reuse_window_tokens
&& lhs.validate_tail_blocks == rhs.validate_tail_blocks && lhs.cp_mapping == rhs.cp_mapping
&& lhs.cp_slice == rhs.cp_slice;
}

void CacheConfig::setTopology(std::vector<GroupBase> new_groups, std::vector<LayerBase> new_layers) {
Expand Down
10 changes: 7 additions & 3 deletions rtp_llm/cpp/cache/CacheGroupType.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ struct CacheGroupPolicy {
bool charge_to_paged_budget = false;
CacheMemoryPlacement memory_placement = CacheMemoryPlacement::DEVICE;
uint32_t active_tail_blocks = 0;
bool validate_tail_blocks = true;
CpBlockMappingMode cp_mapping = CpBlockMappingMode::NONE;
CpBlockSliceMode cp_slice = CpBlockSliceMode::NONE;
// Number of token positions covered by the SWA prefix-reuse window. This
// is intentionally separate from active_tail_blocks: the latter controls
// allocation/retention, while this value controls cache-key matching.
uint32_t prefix_reuse_window_tokens = 0;
bool validate_tail_blocks = true;
CpBlockMappingMode cp_mapping = CpBlockMappingMode::NONE;
CpBlockSliceMode cp_slice = CpBlockSliceMode::NONE;
};

// One cache-store registration step: pair a cache key from the full logical
Expand Down
93 changes: 67 additions & 26 deletions rtp_llm/cpp/cache/HybridKVCacheAllocator.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "rtp_llm/cpp/cache/HybridKVCacheAllocator.h"

#include <algorithm>
#include <cstdint>
#include <unordered_map>
#include <unordered_set>

Expand Down Expand Up @@ -56,6 +57,32 @@ BlockIndicesType validBlocksAfter(const BlockIndicesType& blocks, size_t begin)
return valid;
}

// Return the first canonical cache-key block needed by the SWA window when
// `end` is the last reused block. Cache keys are in canonical units under CP,
// so one key covers cp_scale raw blocks. A zero window keeps the historical
// single-tail behavior used by generic SWA groups that do not declare a
// prefix-reuse window; MiMo supplies the explicit window in its descriptor.
int swaMatchBegin(const KVCacheGroup& group, int end, int cp_scale) {

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.

[P1] SWA 多块前缀复用(swaMatchBegin + CP 槽位映射)无任何单测覆盖

新逻辑含两处易错算术:swaMatchBegin()(end+1)*block_tokens - (window-1) 反推起始块(:74-78),以及非 compact CP 布局下 logical_pos = (canonical_pos+1)*cp_scale - 1(:223-224)。全仓 grep prefix_reuse_window_tokens / swaMatchBegin,仅命中 mimo_v25.py、KVCacheSpecDesc、CacheGroupType、CacheConfig、ConfigInit、pyi 与本文件,rtp_llm/cpp/cache/test/ 下 0 命中;本 PR 唯一的 cache 测试改动只覆盖张量视图。取错块不会崩溃,只会让 attention 读到窗口外 KV,或在窗口内留下 NULL 空洞(plan kernel 替入保留块 0),属静默错答。

建议:HybridTypeKVCacheAllocatorTest.cc(已有 SWA policy 脚手架)补 reuseCache 用例:(1) window_tokens=0 与旧单 tail 行为逐位等价;(2) 窗口跨 2~3 个 block 时 [begin,pos] 全部落位、更早槽位保持 NULL;(3) 窗口内任一 key 未命中时 pos 逐级回退;(4) 边界值 window_tokens=1window_tokens <= seq_size_per_blockbeginmax(0,...) 截断至 0。在 HybridKVCacheAllocatorCPShardTest.cccp_size>1 下 compact 与非 compact 两种 logical_pos 映射断言。建议同时把 swaMatchBegin 暴露为可直接单测的纯函数入口。

const auto window_tokens = group.policy().prefix_reuse_window_tokens;
if (window_tokens == 0) {
return end;
}
if (window_tokens == 1) {

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.

[P3] swaMatchBegin 中 window_tokens==1 的特例分支与通用公式完全等价

if (window_tokens == 1) { return end + 1; }(:70-72)。代入通用公式:history_tokens = 0history_start = first_new_token = (end+1)*block_tokensbegin = (end+1)*block_tokens / block_tokens = end + 1,逐字相同。该分支既不改变行为也不规避除零(block_tokens 已由 std::max(1, ...) 保底),只多一条读者需单独验证语义的路径。

建议: 删除 window_tokens == 1 分支,仅保留 window_tokens == 0 的兼容早退与通用公式;若担心读者误解「窗口为 1 时不复用任何块」,改为在通用公式上方补一行注释说明该退化结果。

Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用

return end + 1;
}

const int block_tokens = std::max(1, group.seqSizePerBlock() * std::max(cp_scale, 1));
const int64_t first_new_token = static_cast<int64_t>(end + 1) * block_tokens;
const int64_t history_tokens = static_cast<int64_t>(window_tokens - 1);
const int64_t history_start = std::max<int64_t>(0, first_new_token - history_tokens);
return std::max(0, static_cast<int>(history_start / block_tokens));
}

struct SwaMatch {
int begin = 0;
BlockIndicesType blocks;
};

} // namespace

bool HybridKVCacheAllocator::skipReuseCacheGroup(int gid) const {
Expand Down Expand Up @@ -102,14 +129,14 @@ int HybridKVCacheAllocator::reuseCache(const CacheKeysType& cach
full_matched_blocks[static_cast<size_t>(gid)] = std::move(match_result.block_indices);
}

int pos = min_full_reuse_blocks - 1;
std::vector<BlockIdxType> linear_tail_blocks(linear_group_ids_.size(), NULL_BLOCK_IDX);
std::vector<BlockIndicesType> swa_tail_blocks(swa_group_ids_.size());
const bool has_tail_groups = !linear_group_ids_.empty() || !swa_group_ids_.empty();
int pos = min_full_reuse_blocks - 1;
std::vector<BlockIdxType> linear_tail_blocks(linear_group_ids_.size(), NULL_BLOCK_IDX);
std::vector<SwaMatch> swa_tail_matches(swa_group_ids_.size());
const bool has_tail_groups = !linear_group_ids_.empty() || !swa_group_ids_.empty();
for (; pos >= 0 && has_tail_groups; --pos) {
bool all_tail_groups_matched = true;
std::vector<BlockIdxType> candidate_linear_tail_blocks(linear_group_ids_.size(), NULL_BLOCK_IDX);
std::vector<BlockIndicesType> candidate_swa_tail_blocks(swa_group_ids_.size());
bool all_tail_groups_matched = true;
std::vector<BlockIdxType> candidate_linear_tail_blocks(linear_group_ids_.size(), NULL_BLOCK_IDX);
std::vector<SwaMatch> candidate_swa_tail_matches(swa_group_ids_.size());
for (size_t i = 0; i < linear_group_ids_.size(); ++i) {
const int gid = linear_group_ids_[i];
auto result =
Expand All @@ -128,17 +155,28 @@ int HybridKVCacheAllocator::reuseCache(const CacheKeysType& cach
if (skipReuseCacheGroup(gid)) {
continue;
}
auto result =
kv_cache_groups_[static_cast<size_t>(gid)]->matchSingleKey(cache_keys[static_cast<size_t>(pos)]);
if (result.block_indices.empty()) {
all_tail_groups_matched = false;
const auto& group = *kv_cache_groups_[static_cast<size_t>(gid)];
const int begin = swaMatchBegin(group, pos, cp_scale);
auto& match = candidate_swa_tail_matches[i];
match.begin = begin;
if (begin <= pos) {

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.

[P3] 复用回退循环对同一 cache key 重复 matchSingleKey,最坏开销随声明窗口线性放大

外层 for (; pos >= 0 && has_tail_groups; --pos)(:136)每次迭代都对 SWA 组重新扫描 [begin, pos] 全区间(:164-171),相邻迭代的查询区间高度重叠却不复用结果。改动前每个 pos 只做 1 次 matchSingleKey,现在做 window_blocks 次,最坏总量为 min_full_reuse_blocks × window_blocks。MiMo 当前 window=128 / page=64 仅约 2 倍,且未提供 benchmark,故按 P3 记录;但代码对窗口大小无上限约束,更大窗口下放大系数会同比上升。

建议: 把已查询的 key_pos → block_idx 结果缓存在循环外的 std::unordered_map(或按 key_pos 下标的 vector)中跨 pos 复用;或改为先自顶向下一次性求出每个 key 的 SWA 命中情况,再用「最长连续命中后缀」直接推出可行 pos,把复杂度降回 O(keys)。

Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值)

match.blocks.reserve(static_cast<size_t>(pos - begin + 1));
for (int key_pos = begin; key_pos <= pos; ++key_pos) {
auto result = group.matchSingleKey(cache_keys[static_cast<size_t>(key_pos)]);
if (result.block_indices.empty()) {
all_tail_groups_matched = false;
break;
}
match.blocks.push_back(result.block_indices[0]);
}
}
if (!all_tail_groups_matched) {
break;
}
candidate_swa_tail_blocks[i].push_back(result.block_indices[0]);
}
if (all_tail_groups_matched) {
linear_tail_blocks = std::move(candidate_linear_tail_blocks);
swa_tail_blocks = std::move(candidate_swa_tail_blocks);
swa_tail_matches = std::move(candidate_swa_tail_matches);
break;
}
}
Expand Down Expand Up @@ -176,10 +214,17 @@ int HybridKVCacheAllocator::reuseCache(const CacheKeysType& cach
if (skipReuseCacheGroup(gid)) {
continue;
}
const size_t tail_begin =
static_cast<size_t>(std::max(group_reuse_len - static_cast<int>(swa_tail_blocks[i].size()), 0));
for (size_t j = 0; j < swa_tail_blocks[i].size(); ++j) {
kv_resource.mutableBlockIds(0, gid).setAt(tail_begin + j, swa_tail_blocks[i][j]);
const auto& match = swa_tail_matches[i];
for (size_t j = 0; j < match.blocks.size(); ++j) {
const int canonical_pos = match.begin + static_cast<int>(j);
// Compact-last-rank SWA uses one slot per canonical key. The
// non-compact layout keeps cp_size logical slots per key, and the
// canonical key owns the last slot in that group.
const int logical_pos =
cpCompactSwaGroup(gid, cp_mapper) ? canonical_pos : (canonical_pos + 1) * cp_scale - 1;
if (logical_pos >= 0 && logical_pos < group_reuse_len) {

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.

[P3] SWA 复用的越界写入被静默丢弃,窗口全失配又会清零 FULL 复用且无日志或指标

两处:(1) if (logical_pos >= 0 && logical_pos < group_reuse_len)(:225)对越界窗口块静默 continue。按当前三条路径推导该分支不可达(非 CP:canonical_pos ≤ pos;CP 非 compact:(pos+1)*cp_scale-1 = logical_reuse_len-1;CP compact:≤ pos),但公式后续演进破坏该不变量时,结果是窗口内留下 NULL 空洞、plan kernel 替入保留块 0,产生静默错误输出而非可观测失败。(2) 窗口匹配是「每个 pos 全命中或全否」,一路失败到 pos<0reuse_blocks_len=0return 0(:184-187)——连已成功匹配的 FULL 组前缀复用也一并丢弃;该路径无 warning 也无指标,表现为 TTFT 静默退化。

建议: 把 :225 的静默边界改为显式不变量断言(RTP_LLM_CHECK_WITH_INFO(...) 后无条件 setAt),使不变量破坏在开发期即暴露。同时当 has_tail_groups && min_full_reuse_blocks > 0 && reuse_blocks_len == 0 时打印一次可聚合的 warning 或上报一个 counter,使「声明了窗口但复用全失效」可被观测。

kv_resource.mutableBlockIds(0, gid).setAt(static_cast<size_t>(logical_pos), match.blocks[j]);
}
}
}
return reuse_blocks_len;
Expand Down Expand Up @@ -259,19 +304,15 @@ MallocResult HybridKVCacheAllocator::initMallocForCommonLen(const MallocInfo& ma
original_sizes[static_cast<size_t>(gid)] = kv_resource->blocksNum(0, gid);
}
for (int gid = 0; gid < kv_resource->groupNums(); ++gid) {
auto& block_ids_0 = kv_resource->mutableBlockIds(0, gid);
const int group_seq_len = cpEffectiveSeqLenForGroup(cp_mapper, config_, gid, common_seq_len);
const auto& group = kv_cache_groups_[static_cast<size_t>(gid)];
auto& block_ids_0 = kv_resource->mutableBlockIds(0, gid);
const int group_seq_len = cpEffectiveSeqLenForGroup(cp_mapper, config_, gid, common_seq_len);
const auto& group = kv_cache_groups_[static_cast<size_t>(gid)];
// Snapshot the slot count before the call so a failure can report this
// group's exact physical request in the error_code=602 record.
const int blocks_before = static_cast<int>(block_ids_0.blocksNum());
if (!group->malloc(block_ids_0, group_seq_len, malloc_info.reuse_cache, 0)) {
logMallocFailure(malloc_info,
"init_group_malloc",
0,
gid,
false,
group->needBlocksNum(group_seq_len, blocks_before, 0));
logMallocFailure(
malloc_info, "init_group_malloc", 0, gid, false, group->needBlocksNum(group_seq_len, blocks_before, 0));
rollbackInitMalloc(*kv_resource, referenced_blocks, original_sizes);
return {false, 0};
}
Expand Down
21 changes: 17 additions & 4 deletions rtp_llm/cpp/cache/HybridPoolConfigCreator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ void validateHybridPoolDescs(const ModelConfig& model_config, uint32_t kernel_to
}
}

uint32_t mhaLocalKvHeadNum(const ModelConfig& model_config, const ParallelismConfig& parallelism_config) {
uint32_t mhaLocalKvHeadNum(const KVCacheSpecDesc& desc,

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.

[P2] local KV head 计算公式在 spec 与 pool config 两处重复实现,仅靠注释维持一致

mhaLocalKvHeadNum()(:73-85)与 MHAKVCacheSpec::build()(MHAKVCacheSpec.h:33-58)各自实现同一段逻辑:override 优先取值 + (kv%tp==0)?kv/tp:kv/gcd(kv,tp)。本 PR 把重复面从一处扩大到两处,唯一约束是注释「Must match MHAKVCacheSpec::build()」。二者一旦分叉,group.local_kv_head_num 与实际给 block 定尺的 spec 脱节;下游 OpDefs.h:192 只校验 k_block_elems % (local_kv_heads*seq) == 0,恰好整除时会静默算出错误 head_dim,表现为张量视图形状错位而非清晰报错。

建议: 收敛为单一入口,例如在 MHAKVCacheSpec 上提供 static uint32_t resolveLocalKvHeads(const KVCacheSpecDesc&, const AttentionConfigs&, const ParallelismConfig&),由 build()mhaLocalKvHeadNum() 共同调用,以类型系统而非注释保证一致性。

const ModelConfig& model_config,
const ParallelismConfig& parallelism_config) {
const auto attn_tp = std::max<int64_t>(1, parallelism_config.get_attn_tp_size());
const uint32_t tp = static_cast<uint32_t>(attn_tp);
const uint32_t kv = static_cast<uint32_t>(model_config.attn_config.kv_head_num);
// Must match MHAKVCacheSpec::build(): a per-desc override describes a layer kind whose
// KV head count differs from the model-wide one, and the group's local head count has
// to agree with the spec that sized its blocks.
const uint32_t kv = desc.kv_head_num_override != 0 ? desc.kv_head_num_override :
static_cast<uint32_t>(model_config.attn_config.kv_head_num);
RTP_LLM_CHECK_WITH_INFO(kv > 0, "local kv head num requires positive kv_head_num");
return (kv % tp == 0) ? kv / tp : kv / std::gcd(kv, tp);
}
Expand All @@ -98,7 +104,7 @@ uint32_t localKvHeadNumForDesc(const KVCacheSpecDesc& desc,
const ParallelismConfig& parallelism_config) {
switch (desc.cache_type) {
case KVCacheSpecType::MultiHeadAttention:
return mhaLocalKvHeadNum(model_config, parallelism_config);
return mhaLocalKvHeadNum(desc, model_config, parallelism_config);
case KVCacheSpecType::LinearAttention:
return linearLocalKvHeadNum(model_config, parallelism_config);
case KVCacheSpecType::MultiHeadLatentAttention:
Expand Down Expand Up @@ -239,7 +245,14 @@ void setupIndependentPoolSizes(CacheConfig& config, bool is_mtp) {
group_kv_block_stride_bytes[gid] = kv_stride;
group_kv_scale_stride_bytes[gid] = scale_stride;
const auto type = config.typeForGroup(gid);
const bool is_paged_group = type == CacheGroupType::FULL || type == CacheGroupType::LINEAR;
// A sliding-window group counts too when it is an ordinary paged pool: it draws
// its block count from the same global budget (finalizeBlockNums gives it
// global_block_num / linear_step), so its bytes belong in the per-block cost. DSv4
// instead backs its window with a fixed-allocation state cache, which is sized
// outside the paged budget and must stay excluded.
const bool is_state_cache = spec->type == KVCacheSpecType::OpaqueState;
const bool is_paged_group = type == CacheGroupType::FULL || type == CacheGroupType::LINEAR
|| (type == CacheGroupType::SWA && !is_state_cache);
if (is_paged_group && !config.usesExplicitIndependentBlocks(gid)) {
total_kv_block_bytes += static_cast<size_t>(layer_count) * kv_stride;
total_scale_block_bytes += static_cast<size_t>(layer_count) * scale_stride;

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.

📍 实际位置 rtp_llm/cpp/cache/HybridPoolConfigCreator.cc:321(不在 diff 展示范围内,就近挂载)

[P2] SWA 组开启前缀复用后保留全长 KV,窗口收益消失且与注释、离线估算假设均矛盾

createHybridAttentionPoolConfig 硬编码 config.linear_step = 1。在 SWAKVCacheGroup 中 step=1 使 step_hit = ((i+1)%1)==0 恒真:shouldAllocateBlock(SWAKVCacheGroup.cc:22-23)在 reuse 打开时对每个槽位都分配,removeSkippedBlocks(:229-231)对所有槽位 continue、不释放任何块。而 MiMo 的 SWA desc 显式设 enable_prefix_reuse=True(mimo_v25.py:80),故请求级占用等于全长,并非该 desc 注释(mimo_v25.py:64-67)所称「occupancy bounded by active_tail_blocks」。同时 _eval_hybrid_kv_cache_mem_size(model_config.py:287-291)按 min(window_size, max_seq_len) 计量 SWA 层...

建议: 先确定取舍再统一两侧口径:若要保留窗口收益,为该 desc 提供独立于全局 linear_step 的保留步长,或让 active_tail_blocks 在 reuse 打开时仍参与释放判定;若接受全长保留,则修正 mimo_v25.py:64-67 的注释,并把 _eval_hybrid_kv_cache_mem_size 的 SWA 分支改为按实际保留量计量。无论哪种,请在 PR 描述中给出开启前缀复用前后 block_num / 最大并发的实测对比,便于容量回滚决策。

Checklist: [6.1] 回滚路径:风险行为存在运维回滚手段

Expand Down
3 changes: 3 additions & 0 deletions rtp_llm/cpp/cache/KVCacheSpecDesc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ CacheGroupPolicy SpecBuilder::groupPolicy(const KVCacheSpecDesc& desc) {
if (desc.tail->active_tail_blocks.has_value()) {
policy.active_tail_blocks = *desc.tail->active_tail_blocks;
}
if (desc.tail->prefix_reuse_window_tokens.has_value()) {
policy.prefix_reuse_window_tokens = *desc.tail->prefix_reuse_window_tokens;
}
if (desc.tail->validate_tail_blocks.has_value()) {
policy.validate_tail_blocks = *desc.tail->validate_tail_blocks;
}
Expand Down
9 changes: 9 additions & 0 deletions rtp_llm/cpp/cache/KVCacheSpecDesc.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct CacheMemoryPolicyDesc {
struct CacheTailPolicyDesc {
std::optional<uint32_t> active_tail_blocks;
std::optional<bool> validate_tail_blocks;
std::optional<uint32_t> prefix_reuse_window_tokens;
};

struct CacheCpPolicyDesc {
Expand Down Expand Up @@ -72,6 +73,14 @@ struct KVCacheSpecDesc {
size_t block_stride_bytes_alignment = 0;
uint32_t block_stride_alignment_min_entries = 0;

// MHA geometry overrides; 0 means "take it from SpecBuildContext.attn_config".
// attn_config carries one global head count and one head dimension, which is not
// enough for models whose layer kinds disagree. MiMo V2.5 needs both: 4 KV heads on
// its global-attention layers versus 8 on its sliding-window layers, and V head dim
// 128 against QK 192.
uint32_t kv_head_num_override = 0;
uint32_t v_size_per_head_override = 0;

std::optional<CacheGroupType> group_type;
std::optional<CacheReusePolicyDesc> reuse;
std::optional<CacheCapacityPolicyDesc> capacity;
Expand Down
Loading