Prhub

#35045 [Model Runner V2] Support sharing kv cache layers

原始 PR 作者 njhill 合并时间 2026-05-23 06:18 文件变更 4 提交数 3 评论 6 代码增减 +54 / -19

执行摘要

支持 Model Runner V2 中 KV 缓存层的共享

为 MRV2 提供跨 attention 层共享 KV 缓存的能力,典型用例是模型内部存在键值重复的层(如某些深层结构或 MTP 场景),避免重复分配显存。PR body 提到已通过 test_kv_sharing_fast_prefill.py 验证。

值得精读,因涉及 MRV2 核心缓存架构的扩展。建议后续引入显式的依赖顺序解析(如拓扑排序)替代字典迭代顺序,以消除潜在顺序依赖风险。同时补充单元测试覆盖共享层在 _allocate_kv_cache 中的绑定逻辑。

讨论亮点
  1. 顺序依赖风险:gemini-code-assist[bot] 指出 _allocate_kv_cache 中映射共享层时依赖字典迭代顺序,若目标层出现在共享层之后会触发 KeyError,建议显式解析共享链。该评论未被后续 commit 修复(故 status 为 unresolved)。
  2. assert 放宽:TheEpicDolphin 质疑将断言从 == 改为 <= 可能掩盖初始化问题。作者 njhill 回复已修复(实际 commit 中改为 <=,并认为在共享场景下合理)。

实现拆解

  1. 新增共享层发现函数:在 attn_utils.py 中添加 get_shared_kv_cache_layers(),遍历所有 Attention 层,若存在 kv_sharing_target_layer_name 属性则记录映射关系。
  2. 扩展 KV cache group:在 init_attn_backend() 开头调用 add_kv_sharing_layers_to_kv_cache_groups(),将共享层追加到目标层的 layer_names 中,使其在后继阶段正确处理 attention 分组。
  3. 分配缓存时的共享绑定_allocate_kv_cache() 新增 shared_layers 参数,在完成每层的 tensor reshape 后,依据映射将共享层指向目标层的 KV cache tensor。
  4. 防御性改进utils.pyadd_kv_sharing_layers_to_kv_cache_groups() 增加空字典提前返回;config/vllm.pyget_layers_from_vllm_config() 使用 Iterable 类型并用 dict.get 避免 key 不存在。
  5. 调用链适配model_runner.pyinitialize_kv_cache()init_kv_cache 传递 vllm_config,使其内部能获取共享层配置。
文件 模块 状态 重要度
vllm/v1/worker/gpu/attn_utils.py 注意力层 modified 7.37
vllm/v1/worker/utils.py 通用工具 modified 4.99
vllm/config/vllm.py 配置 modified 5.76
vllm/v1/worker/gpu/model_runner.py 模型运行器 modified 4.77

关键符号

get_shared_kv_cache_layers init_attn_backend _allocate_kv_cache add_kv_sharing_layers_to_kv_cache_groups get_layers_from_vllm_config

关键源码片段

vllm/v1/worker/gpu/attn_utils.py core-logic

核心改动文件:新增 `get_shared_kv_cache_layers` 函数、修改 `init_attn_backend` 以整合共享层、修改 `_allocate_kv_cache` 以处理共享映射,是整个功能的主线。

# vllm/v1/worker/gpu/attn_utils.py ( 关键片段 )def get_kv_cache_spec(vllm_config: VllmConfig) -> dict[str, KVCacheSpec]:
    """获取每个 attention 层的 KV cache spec,但跳过那些由共享目标提供缓存的层。"""
    kv_cache_spec: dict[str, KVCacheSpec] = {}
    layer_type = cast(type[Any], AttentionLayerBase)
    attn_layers = get_layers_from_vllm_config(vllm_config, layer_type)
    for layer_name, attn_module in attn_layers.items():
        # 检查是否存在 kv_sharing_target_layer_name 属性——有此属性则表示该层
        # 不自己分配 KV cache,而是复用目标层的缓存。
        if getattr(attn_module, "kv_sharing_target_layer_name", None):
            continue
        if spec := attn_module.get_kv_cache_spec(vllm_config):
            kv_cache_spec[layer_name] = spec
    return kv_cache_spec
​
​
def get_shared_kv_cache_layers(vllm_config: VllmConfig):
    """收集所有声明了`kv_sharing_target_layer_name`的层,返回 {共享层: 目标层} 映射。"""
    attn_layers = get_layers_from_vllm_config(vllm_config, Attention)
    return {
        layer_name: kv_tgt_layer
        for layer_name, attn_module in attn_layers.items()
        if (kv_tgt_layer := attn_module.kv_sharing_target_layer_name)
    }
​
​
def init_attn_backend(...):
    # ...
    # 在 Phase 1 之前,将共享层加入目标组的 layer_names,以确保后续
    # attention group 的发现能覆盖所有参与 layer。
    add_kv_sharing_layers_to_kv_cache_groups(
        get_shared_kv_cache_layers(vllm_config),
        kv_cache_config.kv_cache_groups
    )
    # ...
​
​
def _allocate_kv_cache(
    kv_cache_config: KVCacheConfig,
    shared_layers: dict[str, str], # 新增参数,提供共享映射
    device: torch.device
):
    """分配原始内存并 reshape 为 KV cache tensor,
    然后根据 shared_layers 将共享层指向目标层的 tensor。"""
    kv_cache_raw_tensors: dict[str, torch.Tensor] = {}
    for kv_cache_tensor in kv_cache_config.kv_cache_tensors:
        tensor = torch.zeros(kv_cache_tensor.size, dtype=torch.int8, device=device)
        kv_cache_raw_tensors[kv_cache_tensor.name] = tensor
​
    kv_caches: dict[str, torch.Tensor] = {}
    for group in kv_cache_config.kv_cache_groups:
        for layer_name in group.layer_names:
            # 每个层分配并 reshape 缓存(共享层也会进入此循环,
            # 但后续映射会被覆盖)
            kv_caches[layer_name] = ... # reshape 逻辑
​
    # 关键步骤:将共享层的缓存指针替换为目标层的缓存。
    # 注意:此处依赖 shared_layers 字典的迭代顺序,
    # 目标层必须先于共享层被处理(当前实现依赖插入顺序)。
    for layer_name, target_layer_name in shared_layers.items():
        kv_caches[layer_name] = kv_caches[target_layer_name]
​
    return kv_caches
vllm/v1/worker/utils.py core-logic

修改 `add_kv_sharing_layers_to_kv_cache_groups` 函数,增加 early return 避免空映射时的无效循环。

# vllm/v1/worker/utils.pydef add_kv_sharing_layers_to_kv_cache_groups(
    shared_kv_cache_layers: dict[str, str],
    kv_cache_groups: list[KVCacheGroupSpec],
    runner_only_attn_layers: set[str] | None = None,
) -> None:
    """将共享层追加到目标层的 KV cache group 中。"""
    # 早期返回:没有共享层时无需修改 groups。
    if not shared_kv_cache_layers:
        return
​
    layer_to_kv_cache_group: dict[str, KVCacheGroupSpec] = {}
    for kv_cache_group in kv_cache_groups:
        for layer_name in kv_cache_group.layer_names:
            layer_to_kv_cache_group[layer_name] = kv_cache_group
​
    for layer_name, target_layer_name in shared_kv_cache_layers.items():
        tgt_kv_cache_group = layer_to_kv_cache_group[target_layer_name]
        tgt_kv_cache_group.layer_names.append(layer_name)
        if runner_only_attn_layers is not None:
            runner_only_attn_layers.add(layer_name)

评论区精华

共享层映射依赖字典顺序的脆弱性 设计

gemini-code-assist[bot] 指出 `_allocate_kv_cache` 中 `for layer_name, target_layer_name in shared_layers.items(): kv_caches[layer_name] = kv_caches[target_layer_name]` 依赖字典的插入顺序,如果目标层在共享层之后处理则引发 KeyError,建议使用显式链解析。

结论:未采纳该建议,最终代码仍依赖 Python 3.7+ 的字典有序性,社区未进一步讨论。 · unresolved

assert 从相等改为子集检查是否过于宽松 正确性

TheEpicDolphin 指出将 `assert layer_names == set(kv_cache_raw_tensors.keys())` 改为 `assert set(kv_cache_raw_tensors.keys()) <= layer_names` 可能忽略缓存未被正确分配的情况。作者 njhill 回复已修复并 push 新 commit。

结论:作者认为在共享场景下允许 extra keys 是合理的,不会掩盖错误。 · 已解决

风险与影响

  1. 顺序依赖未解:若模型配置出现跨越多个组的共享链,_allocate_kv_cache 的映射循环可能因字典顺序产生 KeyError。当前 Python 3.7+ 保证插入顺序,但依赖这个实现细节不够健壮。
  2. 断言放宽set(kv_cache_raw_tensors.keys()) <= layer_names 允许分配了额外 tensor 却不报错,可能掩盖某些 layer 未被正确绑定缓存的问题。
  3. 测试覆盖:API 层面未发现针对共享场景的单元测试,仅提到 e2e 测试 test_kv_sharing_fast_prefill.py(未包含在本次改动的文件列表中)。

影响范围:仅作用于 Model Runner V2,V1 不受影响。功能对用户透明,需模型配置正确设置 kv_sharing_target_layer_name 才能生效。收益:显著降低具有重复 attention 层的模型的内存占用,可能提升并发吞吐。团队协作:需要与其他 MRV2 特性(如 MTP、speculative decoding)配合验证。

潜在顺序依赖问题 assert 放宽可能掩盖异常 缺少独立单元测试覆盖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论