ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LMCache GPU KV Cache Layout 单一事实来源:`normalize_kv_and_discover_format` 不变量深度解析

LMCache GPU KV Cache Layout 单一事实来源:`normalize_kv_and_discover_format` 不变量深度解析 LMCache GPU KV Cache Layout 单一事实来源normalize_kv_and_discover_format不变量深度解析【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache本篇技术指南围绕 LMCache 中 GPU KV Cache 布局管理的一条核心设计不变量展开normalize_kv_and_discover_format是唯一解析 KV-cache 布局的地方。文中将完整剖析这一不变量背后的规范类型、kv_format包结构、格式探测与几何描述spec机制、Helper 接口面以及新增一个 KV 格式的完整操作步骤帮助你理解 LMCache 如何在同一套代码库中同时支撑 vLLMflash-attn / flash-infer / MLA / cross-layer、TRT-LLM、SGLang 等多种引擎的 KV Cache 布局并避免下游模块各自猜形状导致的漂移。核心不变量布局解析只有一个入口normalize_kv_and_discover_format是唯一解析 KV-cache 布局的地方。它在返回格式的同时也返回 kv_caches 的规范permute 之后形式因此调用方无需再做一次独立的归一化。其他所有模块都通过lmcache/v1/gpu_connector/utils.py中接受EngineKVFormat参数的 Helper 来查询 KV-cache 信息。这里所说的布局解析包括列表嵌套深度、张量维度顺序、HND 与 NHD、MLA 与 MHA、按层per-layer与跨层cross-layer。这些信息全部编码在EngineKVFormat中下游代码绝不能从原始 shape 重新推导。在源码中这个不变量由 detection.py 的detect_format落实而utils.py中的normalize_kv_and_discover_format只是它的一个薄封装def normalize_kv_and_discover_format( kv_caches: DiscoverableKVCache, serving_engine: EngineType, layout_hints: LayoutHints | None None, ) - tuple[lmcache_native.EngineKVFormat, DiscoverableKVCache]: return detect_format(kv_caches, serving_engine, layout_hints)其完整执行流程为见 detection.py先做引擎无关的连续视图恢复attempt_permute_to_contiguous_view(kv_caches)依据serving_engine查表得到对应的EngineDetector查不到则抛出ValueError由 detector 的discover()把引擎的原始布局重塑为规范形式并识别格式一次返回(engine_kv_format, kv_caches)识别失败返回None同样抛出ValueError。规范类型DiscoverableKVCache所有 KV-cache 值在 LMCache 中都属于以下递归联合类型定义于 types.pyDiscoverableKVCache Union[torch.Tensor, list[DiscoverableKVCache]]实际形态只有三种单个torch.TensorvLLM cross-layer、TRT-LLM扁平的list[torch.Tensor]vLLM per-layer、SGLang MLA嵌套的list[list[torch.Tensor]]SGLang MHA 的[K_list, V_list]。引擎适配器如果递过来其他容器例如 vLLM 的dict[str, Tensor]有责任在调用任何 Helper 之前解包成该形式。也就是说类型转换的责任在边界处adapter而不是散布在消费方。与规范类型配套的还有LayoutHints定义于 types.py这是服务引擎在注册 KV CacheREGISTER_KV_CACHE时传给 LMCache 的提示信息Hint 键含义kv_layout维度物理顺序NHD大多数 vLLM 构建的默认值、HNDVLLM_KV_CACHE_LAYOUTHND、BLHNC/BLNHCvLLM 标准化布局block 最外层num_kv_heads每层 KV head 数TRT-LLM 用它把 4-D pool 张量重塑为规范的 6-D 形式tokens_per_block每个分页块承载的 token 数TRT-LLM 与 SGLang MHA 都会用到kv_list_layoutSGLang 外层 KV 列表组织方式k_v表示扁平注册中包含等长的 K、V 两半head_dim每 head 维度TRT-LLM 使用包结构kv_format/与门面模式布局逻辑全部位于lmcache/v1/gpu_connector/kv_format/目录utils.py中的公开 Helper 只是薄门面facade委托给它内部实现——对调用方来说单一事实来源的接口面保持不变。lmcache/v1/gpu_connector/kv_format/ ├── types.py # DiscoverableKVCache, LayoutHints基础类型 ├── contiguity.py # attempt_permute_to_contiguous_view零拷贝视图恢复 ├── detection.py # detect_format() 编排 ├── specs/ # 几何层 │ ├── base.py # KVFormatSpec ABC shape_desc/concrete_shape 渲染 │ ├── registry.py # 自动发现 spec 文件get_spec/get_spec_class │ └── engine_kv_format.py # 每种格式一个文件 └── detectors/ # 按引擎划分的检测层 ├── base.py # EngineDetector ABC measure_structure() ├── registry.py # 自动发现 detector 文件get_detector └── engine.py # 每种引擎一个文件单个 discover()仓库中实际已存在的 spec 文件覆盖了全部 15 种格式如 nb_nl_two_bs_nh_hs.py、nl_x_nb_bs_hs.py、two_x_nl_x_nbbs_nh_hs.py 等detector 文件则按引擎分为 vllm.py、sglang.py、trtllm.py、atom.py。每个格式一个 spec 类几何 静态事实每个EngineKVFormat恰好对应一个KVFormatSpec子类该子类知道如何对该格式的值进行索引。spec只描述布局——类和文件都以格式成员命名例如nb_nl_two_bs_nh_hs.py中的NB_NL_TWO_BS_NH_HS_Spec是几何编码永远不代表引擎get_spec(kv, fmt)返回一个几何实例get_spec_class(fmt)返回类用于读取静态事实is_mla、is_hnd、is_cross_layer、attention_backends。KVFormatSpec是抽象基类specs/base.py通过abstractmethod强制每个 spec 实现num_layers、num_blocks、block_size、num_heads、hidden_dim、head_size、dtype、data_ptrs等访问器——缺失任何一个第一次get_spec时就会触发TypeError这正是 golden 测试要兜住的。静态事实是类属性声明一次结构形态is_cross_layer/is_kv_list/is_layer_list恰好一个为 True以及is_mla/is_hnd/is_fused_packed/is_two_major/is_pbs_fused修饰符都是 spec 上的布尔类属性默认False只有适用的才声明。消费方通过get_spec_class(fmt)读取——调用点绝不允许重新列出格式如fmt in (A, B, ...)这正是过去同一谓词在torch_ops.py、传输 Helper 和 connector 之间漂移的原因。例如 nl_x_two_nb_bs_nh_hs.py 中就是is_layer_list True。设备内核在 csrc/engine_kv_format.h 中维护自己的一份拷贝test_kv_format_classification.py 将 Python 侧与 C 侧钉在一起并强制结构划分的一致性。后端标签是诊断性的一个EngineKVFormat可能由多个引擎attention 后端组合产生所以每个 spec 在attention_backends元组中列出它们第一项即规范代表。get_attention_backend(fmt)utils 门面返回第一项用于日志。这些标签只用于诊断绝不驱动几何决策并取代了旧的、手工维护的EngineKVFormat → label字典。枚举是唯一身份每个 spec 在类体内声明自己的engine_kv_format注册表由它派生——没有单独的字符串 id没有引擎属性。C 的EngineKVFormat枚举是存在哪些格式的唯一权威。其成员名本身就是布局图例_连接的 token 序列中X标记列表嵌套边界TWO_X_NL_X_NBBS_NH_HS→2 x NL x [PBS, NH, HS]。符号化的shape_desc(fmt)与数值化的concrete_shape(fmt, size)都从该名称渲染而来specs/base.py因此它们既不会与枚举漂移也不会互相漂移。每个格式一个文件在一处自动发现每个 spec 位于自己的specs/engine_kv_format.py以格式命名。specs/registry.py 通过pkgutil.iter_modules导入文件夹中的每个文件按声明的engine_kv_format索引进SPECS表。新增格式 只需丢一个新文件发现逻辑就是registry.py中一段可读的循环没有__init_subclass__没有散落各处的注册。代码库刻意避免继承分类法格式在 ≥5 个正交轴引擎、per-/cross-layer、MLA/MHA、NHD/HND、fused/separate PBS上变化单一继承脊线无法建模而无孤儿类。只在出现具体需求时才加结构。检测是唯一引擎感知的层detect_format先做引擎无关的连续视图恢复然后按EngineType分发到对应的EngineDetector。每个 detectors/engine.py 就是单个discover(kv, hints)它把引擎的原始布局重塑为规范形式并识别格式一步返回(format, kv)detectors/registry.py 用与specs/相同的方式自动发现它们。spec 层永远看不到EngineType。新增引擎 只需丢一个新的 detector 文件。detector 的基类提供measure_list_depth_until_tensor(kv_caches)detectors/base.py返回(list_depth, tensor_ndim, first_tensor)——这是 detector 内部做格式分派的关键依据但它属于kv_format内部实现细节不对外暴露。新增一个 KV 格式的完整步骤按设计文档新增格式只需以下 4 步其他 Python 模块都不应需要修改添加枚举值在 csrc/kv_transfer_types.h所有加速器后端共享的后端无关定义中添加枚举值然后在通用 native pybind 模块中注册——csrc/lmcache_native/pybind.cpp 以及 csrc/sycl/pybind_sycl.cppSYCL/XPU。在引擎的 detector 中添加分支在detectors/engine.py的discover()中以measure_list_depth_until_tensor得到的(list_depth, tensor_ndim)为键分派返回(format, kv)任何基于 hints 的重塑例如 TRT-LLM 的 4-Dview到 6-D必须在 shape 检查之前、同一方法内完成。添加KVFormatSpec子类新建specs/engine_kv_format.py以格式命名声明其engine_kv_format与静态事实——结构标志加上适用的修饰符。registry.py自动发现它无需改其他文件。ABC 使必需的访问器显式化。向 golden 表添加一行在 test_kv_format_specs.py 和 test_kv_format_classification.py 的 golden 表中各加一行并在 test_kv_format_detection.py 中补一个检测用例。设计文档还给出了一个明确的警示信号如果你为了一个新布局而去改kv_layer_groups.py、gpu_context.py或任何KVLayerGroupInfo消费方——这个分支应该放进 spec 里。Helper 接口面utils.py中的每个 Helper 都接受DiscoverableKVCache在布局相关处接受EngineKVFormat。除此之外的任何代码都不得索引原始 shape。发现DiscoveryHelper返回normalize_kv_and_discover_format(kv_caches, engine, layout_hints)tuple[EngineKVFormat, DiscoverableKVCache]—— 唯一的解析器。返回规范permute 到连续的 kv_caches 连同检测出的格式调用方必须使用返回的张量结构进行后续操作。Format → 引擎映射表EngineKVFormat引擎布局结构NB_NL_TWO_BS_NH_HSvLLM cross-layerNHD裸 6-D 张量[NB, NL, 2, BS, NH, HS]NB_NL_TWO_NH_BS_HSTRT-LLM cross-layerHND裸 6-D 张量[NB, NL, 2, NH, BS, HS]NL_X_TWO_NB_BS_NH_HSvLLM flash-attnNHDNL × [2, NB, BS, NH, HS]NL_X_NB_TWO_BS_NH_HSvLLM flash-inferNHDNL × [NB, 2, BS, NH, HS]NL_X_TWO_NB_NH_BS_HSvLLM flash-attnHNDNL × [2, NB, NH, BS, HS]NL_X_NB_TWO_NH_BS_HSvLLM flash-inferHNDNL × [NB, 2, NH, BS, HS]NL_X_NB_BS_HSvLLM MLA—NL × [NB, BS, HS]TWO_X_NL_X_NBBS_NH_HSSGLang MHANHD[K_list, V_list]各自NL × [PBS, NH, HS]TWO_X_NL_X_NB_BS_NH_HSSGLang MHA via MP daemonNHD[K_list, V_list]各自NL × [NB, BS, NH, HS]NL_X_NBBS_ONE_HSSGLang MLA—NL × [PBS, 1, HS]NL_X_NB_NH_BS_TWO_HSvLLM blocks-first fused (CPU)HNDDEPRECATED改用NL_X_NB_NH_BS_CSNL × [NB, NH, BS, 2, HS]从原始[NB, NH, BS, 2·HS]拆分NL_X_NB_BS_NH_TWO_HSvLLM blocks-first fusedNHDDEPRECATED改用NL_X_NB_BS_NH_CSNL × [NB, BS, NH, 2, HS]从原始[NB, BS, NH, 2·HS]拆分NL_X_NB_NH_BS_CSvLLM blocks-first fused (unified KV cache)HNDNL × [NB, NH, BS, CS]原始注册CS content size 2·HSK/V 打包NL_X_NB_BS_NH_CSvLLM blocks-first fused (unified KV cache)NHDNL × [NB, BS, NH, CS]原始注册CS content size 2·HSK/V 打包NL_X_NB_NH_ONE_BS_HSvLLM-RBLN attentionHNDNL × [2, NB, NH, 1, BS, HS]axis 3 恒为 1RBLN attention 后端的硬性要求两个 cross-layer 格式NB_NL_TWO_*共享单一 base 指针内核内部通过shape_desc.nl遍历各层。用get_spec_class(fmt).is_cross_layer做该分派用.is_hnd检测块内 head-major 布局。基于 Hints 的重塑TRT-LLMTRT-LLM 交给 LMCache 的是 4-D pool 张量[NB, NL, 2, num_kv_heads * tokens_per_block * head_dim]HNDK 和 V 在第 2 维交错。normalize_kv_and_discover_format在连续性检查之前使用layout_hints[num_kv_heads | tokens_per_block | head_dim]将它重塑为规范的 6-D 形式。函数还会把1 元素列表包一个 6-D 张量折叠为裸 6-D 张量使检测落在list_depth 0。适配器既可以传 4-D 裸张量也可以传[4-D]函数两者都处理。标量访问器以下访问器全部按EngineKVFormat分派。其中可能按层变化的接受可选参数layer_idx: int 0传入显式索引即可在异构分组heterogeneous groups中做按层查询无需任何中间 Helper。Helper按层说明get_num_layers(kv, fmt)否总层数。get_num_blocks(kv, fmt)否分页块数组级。get_block_size(kv, fmt)否每块 token 数。get_page_buffer_size(kv, fmt)否已废弃见 utils.py 中的lmcache_deprecate注释仅旧的非 MP 进程内 connector 使用。get_tokens_per_layer(kv, fmt)否每层 token 容量。get_elements_per_layer(kv, fmt)否每层元素数非 MLA 含 K 和 V。get_num_heads(kv, fmt, layer_idx0)是每层 KV head 数。get_head_size(kv, fmt, layer_idx0)是每 head 维度。get_hidden_dim_size(kv, fmt, layer_idx0)是隐藏维度num_heads * head_size。get_dtype(kv, fmt, layer_idx0)是张量 dtype。is_mla(fmt)—格式谓词其余静态事实通过get_spec_class(fmt)读取。get_device(kv)—与格式无关下探到任意叶子。此外 utils 门面还提供了get_kv_sizeK/V 轴大小2 表示拆分1 表示 fused与按层格式探测normalize_and_discover_per_layer_formatsutils.py——后者为混合模型如主缓存kv_size2与 key-only MLA index 缓存kv_size1并存逐层报告正确格式。指针与描述符构造Helper返回说明get_group_data_ptrs(kv, fmt, layer_indices)list[int]内核期望顺序的指针数组cross-layer 为[base]忽略layer_indicesSGLang MHA 为[K_0…K_N, V_0…V_N]其余 per-layer 为扁平。与 csrc/cuda/mp_mem_kernels.cu 中的分派一致。指针数组形状是格式的属性——调用方从不问这个格式有没有 per-layer 指针。make_page_buffer_shape_desc(kv, fmt, layer_idx, num_layers_in_group, num_blocks, block_size, block_stride_elems)PageBufferShapeDesc面向内核的形状结构体。block_stride_elems携带每块 dim-0 元素步长传入resolve_block_stride_and_log_layout返回的值使物理块大小不同的分组例如压缩的 DeepSeek V4 indexer 组与 dense 层可以共享同一个 GPU 池。resolve_block_stride_and_log_layoututils.py是KVLayerGroupsManager获取block_stride_elems的唯一入口同时输出一次性布局审计日志。它的规则是block 轴格式_BLOCK_AXIS_FORMATS当前为NL_X_NB_BS_HS、NL_X_NB_BSV_BSS、NL_X_NB_NH_BS_CS、NL_X_NB_BS_NH_CS返回stride(0)作为每块步长大于 tight stride 表示 dim-0 padding如压缩缓存共享 KV 池其他格式返回None并回退 tight stride若检测到 dim-0 padding 则抛出ValueError下游内核无法处理。连续性Helper返回说明attempt_permute_to_contiguous_view(kv)DiscoverableKVCache递归、仅元数据操作。已连续则原样返回无法通过置换恢复slicing、as_strided则抛ValueError。绝不拷贝。遍历整个结构并对每个张量叶子做 permute。由normalize_kv_and_discover_format内部调用对在 discover 流程之外处理张量的调用方GPUConnectorInterface.initialize_kvcaches_ptr、CudaIPCWrapper.__init__仍保持公开。Consumer 代码中的禁区原始 shape 索引属于kv_format层spec 类内部consumer 代码必须通过utils.py门面查询禁止以下任何做法用isinstance(kv_cache, (tuple, list))区分布局重新列出格式来恢复静态事实fmt in (A, B)、fmt NL_X_NB_NH_BS_CS——改从get_spec_class(fmt)读取或把缺失的事实补进 spec索引原始 shapetensor.shape[3]、len(shape) 5推导维度手写列表深度探测while isinstance(x, list): depth 1; x x[0]——不存在也不应该有公开的 depth Helpernormalize_kv_and_discover_format封装了下探下游只需要最终得到的EngineKVFormat用[tensor]包裹张量来适配 Helper 的列表深度预期——访问器直接接受layer_idx手写指针组装[t.data_ptr() for t in kv_caches]——用get_group_data_ptrs手写设备发现kv_caches[0][0].device——用get_device手写连续性修复tensor.contiguous()、.clone()——用拒绝拷贝的attempt_permute_to_contiguous_view编写把kv_caches重写成统一形状再传给 Helper 的canonicalize函数——Helper 已经通过接受EngineKVFormat完成规范化任何确实需要的 reshape/normalize 步骤都住在normalize_kv_and_discover_format内部调用方从那一次调用就能拿回规范形式。主要 Consumer设计文档点名的核心消费方有三个它们的用法印证了不变量kv_layer_groups.py 的KVLayerGroupsManager.__init__用 5 元组(kv_size, num_heads, head_size, block_size, dtype)对层分组使用is_mla、get_num_heads、get_head_size、get_block_size、get_dtype带每层索引。把block_size纳入分组身份使压缩组如物理 slot 更小的 DeepSeek V4 indexer能与非压缩组在同一个GPUCacheContext下共存。每个组通过make_page_buffer_shape_desc构造PageBufferShapeDesc传入resolve_block_stride_and_log_layout解析出的block_stride_elems。真实构造函数是唯一入口——没有测试专用捷径、没有缓存的拓扑字段manager 只暴露kv_layer_groups、num_groups、get_shape_desc。platform/cuda/cache_context.py 的GPUCacheContext在 init 时直接构造 manager把get_shape_desc(group_idx)委托给它通过get_group_data_ptrs组装每个组的 GPU 指针张量。没有并行的shape_descs_/hidden_dim_sizes_状态。gpu_connectors.py 的VLLMPagedMemGPUConnectorV3._initialize_kv_cache_pointers进程内 vLLM 路径调用normalize_kv_and_discover_format一次完成 HND 支持所需的 permute 与格式检测并在首次 store/retrieve 时惰性构造metadata.kv_layer_groups_manager。适配器vllm_v1_adapter.py不参与格式发现——它只在注册时保存self.kv_caches。只有normalize_kv_and_discover_format消费layout_hints。内部调用的attempt_permute_to_contiguous_view从 strides 推断置换不需要 hints。实现注记mypy 与递归联合类型格式分派的原始索引kv_caches.shape[i]、kv_caches[0][j]集中在kv_format层每个specs/format.py在文件级设置# mypy: disable-error-codeunion-attr,call-overloaddetectors/与contiguity.py只用union-attrutils.py门面对剩余的结构化 Helper 保留该指令。engine_kv_format——或者在 spec 中类身份本身——是索引良定义的证明但 mypy 无法让这个证明穿过递归 Union除非逐行加 cast。文件级指令取代了散落的# type: ignore注释其余所有类型检查保持开启。总结LMCache 的 KV Cache 布局设计可以浓缩为一句话让EngineKVFormat成为唯一身份、让 spec 成为唯一几何来源、让 detector 成为唯一引擎感知层、让utils.py门面成为唯一对外接口。这种分层把布局解析这一横切关注点从散落的 consumer 代码中收拢到一个可测试、可扩展的kv_format包内——新增引擎只需加一个 detector 文件新增格式只需加枚举值、detector 分支、spec 文件与 golden 测试四件事而下游代码永远只面向EngineKVFormat编程从根上杜绝了形状猜测带来的漂移。【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进