
LMCache 多模态缓存键生成机制深度解析mm_hash 占位符替换与防交叉污染设计【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache多模态场景下vLLM 为每个图像/视频输入发出完全相同的占位符 token ID若仅依赖 token 序列计算缓存键两幅不同图片配相同文本会生成相同的 KV 缓存键造成跨图像缓存污染。本文基于 LMCache 的设计文档docs/design/integration/vllm/multimodal_cache_keying.md与真实源码完整剖析 LMCache 如何在保持既有 token 哈希接口不变的前提下通过占位符替换placeholder substitution把多模态标识符的全部熵注入 chunk 哈希并给出实现原理、调用链、历史缺陷与迁移路线。读完本文你将掌握多模态 KV 缓存键正确性问题的根因、mm_hash_to_token_values/apply_mm_hashes_to_token_ids两个核心函数的契约与实现细节以及 LMCache 在 in-process 与多进程MP连接器上的完整覆盖范围。问题背景占位符 token 导致的多模态缓存串扰vLLM 的多模态占位符机制在 vLLM 的多模态图像、视频、音频请求中引擎并不直接在 prompt 中放入真实的多模态内容 token而是为每个多模态 item 生成一段长度固定的占位符 token ID。这些占位符对所有 item 是完全相同的数值——也就是说仅凭 token ID 序列引擎无法区分文本 A 图片 1与文本 A 图片 2。对基于 token 序列做 chunk 哈希的缓存系统而言这直接导致一个严重问题跨图像污染cross-image contamination。若同一段文本配不同图片产生完全相同的缓存键后一个请求会命中前一个请求的 KV 条目从而把错误图片的 KV 缓存返回给用户——这是静默的错误命中silent false hit比缓存未命中危害更大。vLLM 的解法 vs LMCache 的约束vLLM 自己的 prefix cache 通过在每个 block 哈希上追加(mm_hash, offset)额外键来解决该问题。但 LMCache 的键派生是token 序列驱动的且哈希流程流经一系列只携带 token ID的接口——包括 lookup RPC、MP connector 元数据、SDK 等。要照搬 vLLM 的做法就必须改造这些接口使其携带额外的键信息代价巨大。LMCache 因而选择了另一条路径在哈希之前把占位符 token ID 替换为由多模态标识符派生的值。这样携带身份信息的伪 token在进入既有 token 哈希管线之前就被注入无需改动任何接口。设计文档明确记录该方案状态为已实现implemented覆盖 in-process connector 与全部 MP connector 变体。核心契约两个函数构成完整替换链路整套机制的契约集中在 lmcache/integration/vllm/utils.py由两个函数协作完成mm_hash_to_token_values(identifier, length)该函数utils.py#L241-L283从完整的多模态标识符派生一段确定性的、长度为length的值序列每个值落在[0, 2**31)区间def mm_hash_to_token_values(identifier: str, length: int) - Tuple[int, ...]: if length 0: raise ValueError(flength must be non-negative, got {length}) # Be defensive: vLLM may pass non-string identifiers. seed hashlib.sha256(str(identifier).encode(utf-8)).digest() values: list[int] [] for counter in range((length _MM_VALUES_PER_DIGEST - 1) // _MM_VALUES_PER_DIGEST): block hashlib.sha256(seed counter.to_bytes(8, byteorderbig)).digest() for i in range(0, len(block), 4): values.append( int.from_bytes(block[i : i 4], byteorderbig) _MM_TOKEN_VALUE_MASK ) return tuple(values[:length])实现要点SHA-256 计数器模式扩展先对标识符做一次 SHA-256 得到seed再以seed countercounter 为 8 字节大端整数逐块哈希每块 32 字节可切出 8 个 4 字节值对应源码常量_MM_VALUES_PER_DIGEST 8最终截取前length个31 位掩码每个值经_MM_TOKEN_VALUE_MASK掩码保留 31 位常量定义见 utils.py#L230-L237保证在有符号 int32——即 token ID 下游可能经过的最窄整数表示——中始终为正避免任何序列化路径上的符号位问题入参宽容identifier被当作不透明字符串处理内容哈希如0xdeadbeef风格与请求级标识符如chatcmpl-a2a48871c4aad192-image-0均可接受且内部先做str(identifier)防御性转换。该函数承诺三个关键性质它们共同构成了防碰撞的安全论证性质含义安全价值全熵Full entropy每个位置获得独立的 31 位值跨越k个占位符 token 的 chunk 携带31*k位的 item 身份信息k≥3 时基本被 64 位 chunk 哈希宽度封顶位置依赖Position dependence偏移i处的值是整个标识符 × i的函数任意两个位置不重复熵随跨度累积在SegmentTokenDatabase这种不链式哈希的路径上能区分同一 item 的首段与后续段前缀稳定Prefix stabilityvalues(x, m)恒为values(x, n)m≤n的前缀保存路径截断、chunk 边界切分产生的部分 prompt与完整 prompt 的哈希结果一致其中位置依赖尤其值得展开设计文档指出在ChunkedTokenDatabase下前缀链本身已携带位置信息此时该性质是熵论证的充分条件但在SegmentTokenDatabase中每个 segment 独立哈希、没有链式前缀可依赖位置依赖就成了区分 item 首 token 与后续 token 的唯一手段。vLLM 的(mm_hash, offset)额外键编码的正是同一对信息——哪个 item加哪个位置。apply_mm_hashes_to_token_ids(token_ids, mm_hashes, mm_positions)该函数utils.py#L286-L324把mm_hash_to_token_values派生的序列原地写回每个占位符跨度def apply_mm_hashes_to_token_ids( token_ids: torch.Tensor, mm_hashes: list[str], mm_positions: list[PlaceholderRange], ) - torch.Tensor: n token_ids.size(0) for hash_str, placeholder in zip(mm_hashes, mm_positions, strictFalse): start, length placeholder.offset, placeholder.length if start n: continue end min(start length, n) values mm_hash_to_token_values(hash_str, end - start) token_ids[start:end] torch.tensor(values, dtypetoken_ids.dtype) return token_ids关键契约与约束原位修改直接改写传入的token_ids张量并返回同一对象绝对偏移语义PlaceholderRange.offset是相对完整 prompt的绝对位置因此token_ids必须是完整 prompt 或其前缀。若传入后缀或中间切片替换会落到错误位置且不抛任何异常——这正是设计文档特别警告的静默失效路径会悄然恢复本机制要消除的跨图像碰撞长度截断跨度超出张量长度时安全截断end min(start length, n)起点越界时直接跳过多 item 支持mm_hashes与mm_positions按位置一一对应可一次性处理多张图片/多个视频片段。31 位取值边界的深层原因[0, 2**31)的取值上限不是随意选择token ID 在下游可能经过多种整数表示与序列化通道ZMQ 协议、MP 元数据、SDK 等其中最窄的是有符号 int32。若派生值超过 2^31-1在窄类型转换中会变成负数或溢出破坏哈希一致性。31 位上限在保留约 21 亿个可用值的同时确保所有场景下取值恒为正——这是熵最大化与表示安全之间的精确平衡。历史缺陷16 位截断的生日悖论事故设计文档的 History 一节记录了一次真实的生产事故这也是本次重构的直接动因原始实现hex_hash_to_int16将标识符压缩到 16 位然后用同一个值填满整个占位符跨度。这意味着两个不同图像只有 16 位哈希相等就会共享全部缓存键按生日悖论birthday bound计算约300 个相同形状的不同的图像就能带来约 50% 的概率出现两幅图像共享全部缓存键——即静默命中并返回错误图像的 KV 缓存。这正是 LMCache issue #3301 描述的问题。mm_hash_to_token_values是对该缺陷的完整修复31 位独立值 位置依赖 前缀稳定三重性质共同把碰撞概率压到每 token 2^-31 量级。升级路径是安全兼容的升级前缓存的多模态条目会变成未命中miss而非碰撞collide——缓存失效但绝不串扰这是缓存系统升级最可接受的行为。回归测试 tests/v1/test_mm_hash_utils.py 专门钉死了这个历史缺陷。test_16bit_truncation_collision_regression用0x1234与0x11234两个在旧实现int(hex, 16) 0xFFFF下都会截断为0x1234的标识符断言新实现产生的序列完全不同且逐位相同位置不超过 1 个。其余测试还覆盖了test_mm_hash_to_token_values_deterministic_and_in_range确定性 取值范围含空字符串标识符test_mm_hash_to_token_values_position_dependent128 长度序列中超过 120 个互异值test_mm_hash_to_token_values_prefix_stable截断跨度0/1/7/8/9/255/299恒为完整序列前缀test_mm_hash_to_token_values_distinct_identifiers_disjoint不同标识符序列逐位相同位置不超过 1test_mm_hash_to_token_values_rejects_negative_length负长度抛ValueErrortest_apply_mm_hashes_fills_span_with_derived_values替换只落在占位符区间其余区域不变test_apply_mm_hashes_truncated_span_matches_full_span_prefix前缀 prompt 的替换结果与完整 prompt 一致test_apply_mm_hashes_out_of_bounds_is_safe越界位置安全跳过test_apply_mm_hashes_multiple_placeholders_and_length_mismatch多占位符与长度不匹配场景。调用链替换发生在哪些路径设计文档 Coverage 一节与源码调用点共同勾勒出完整的覆盖矩阵。apply_mm_hashes_to_token_ids目前被四处调用1. in-process connectorvLLM v1 adapter在 vllm_v1_adapter.py#L366-L376 的保存路径中adapter 先按num_tokens_to_save截取 token 前缀若 tracker 携带mm_hashes则断言mm_positions非空随后应用替换if tracker.mm_hashes: # TODO: Optimize this token_ids torch.tensor(token_ids) assert tracker.mm_positions is not None, ( tracker got mm_hashes but no mm_positions ) apply_mm_hashes_to_token_ids( token_ids, tracker.mm_hashes, tracker.mm_positions ) token_ids token_ids.tolist()同一文件的 vllm_v1_adapter.py#L1414 还有另一处调用对应 load/lookup 路径保证保存与读取两侧使用完全一致的替换逻辑。可以推断mm_positions的来源是 vLLM 请求中的PlaceholderRange元数据由 adapter 在请求进入时提取并挂在 tracker 上。2. 主 MP connectortracker 级注入MP 路径的替换统一收敛在请求状态跟踪器lmcache_mp_metadata.py中在 lmcache_mp_metadata.py#L97-L101tracker 初始化时即提取mm_hashes, mm_positions extract_mm_features(request)并立刻应用替换把结果保存为mm_adjusted_prompt_idsmm_hashes, mm_positions extract_mm_features(request) if mm_hashes and mm_positions: prompt_ids torch.tensor(request.prompt_token_ids) apply_mm_hashes_to_token_ids(prompt_ids, mm_hashes, mm_positions) self.mm_adjusted_prompt_ids prompt_ids.tolist()这种tracker 级单点注入的设计很关键设计文档明确说明lmcache_mp_connector.py中所有携带键的调用——lookup、store、retrieve、锁管理、eager prefetch——都经过 tracker 的get_token_ids()取 token因此只需在 tracker 一处替换全部键路径自动获得多模态身份无需逐调用点修改。3. 版本锁定的 MP connector 副本为兼容历史 vLLM 版本仓库维护了版本锁定的 connector 副本并内嵌了相同的 tracker 级替换逻辑lmcache_mp_connector_0180.py#L229vLLM 0.18.x 线lmcache_mp_connector_0201.py#L223vLLM 0.20.1 线。这使得在这些版本上做 vendored 部署的用户同样获得多模态缓存键保护。单元测试 tests/v1/test_mp_connector_mm_keys.py 专门验证 MP connector 的多模态键行为。尚未覆盖的路径设计文档诚实标注了当前未处理的路径这些路径上的多模态请求仍可能交叉命中需要替换或 MM bypass guardSGLang 与 TRT-LLM 集成vLLM 之外的引擎集成bypass guard之外的部分token 寻址的 SDK / CLI 路径。设计取舍为何选择替换而非 extra_keys 通道设计文档记录了一个被明确评估并否决的替代方案把完整的mm_hash通过TokenDatabase._hash_tokens(extra_keys...)传递——这个通道本就是为承载这类信息保留的且是与 vLLM 语义对齐的正统设计。但它的代价是必须扩展每一个携带 token 的接口lookup ZMQ 协议、MP 元数据、SDK来传递 per-chunk 额外键这是一次波及面极广的协议级改动。替换方案的优势在于以零接口变更达到等价的防碰撞能力一次性修复所有既有替换调用点对图像/视频模型是最小正确性改动。同时设计文档也明确指出extra_keys通道并未废弃它仍然是请求级元数据如 LoRA ID的正确载体——这类元数据不应被烘进 token 序列里。演进路线显式 extra_keys 通道的迁移规划替换方案是当下最小正确改动但设计文档将其定位为过渡方案因为它的适用前提是身份必须有占位符 token 可覆盖。端态设计与 vLLM 一致key hash(tokens, extra_keys)身份通过每个 token 携带接口lookup RPC、MP 元数据、存储键 schema、SDK带外传递届时 mm_hash 替换退化为 extra_keys 的一个生产者并被退役。迁移的触发条件满足其一才动手不提前做被明确列出模态 LoRA 支持Phi-4-multimodalLoRA 影响全部 KV 且没有占位符跨度可替换——身份必须走带外通道cache_salt/ 请求级键分区同样的形状、同样的通道第二个引擎集成希望与 vLLM 语义共享键bypass guard 之外的 SGLang / TRT-LLM。设计文档还给出了四步迁移草图① 扩展CacheEngineKey/_hash_tokens调用点以接受 per-chunkextra_keys② 对 MP 线上协议与存储键 schema 做版本化旧条目 miss、绝不 collide③ 将 mm_hash 重实现为 extra-key 生产者④ 一个废弃周期后移除apply_mm_hashes_to_token_ids。值得特别注意的约束键必须保持引擎/传输无关——严禁通过直接消费 vLLM 的block_hashes走捷径那会把键空间耦合到 vLLM 的 block 大小与版本破坏跨引擎键共享与离线键重算能力。验证体系该机制的验证分两层单元测试tests/v1/test_mm_hash_utils.py 验证三个核心性质全熵、位置依赖、前缀稳定并钉死 16 位碰撞回归tests/v1/test_mp_connector_mm_keys.py 覆盖 MP connector 的多模态键行为验收测试tests/e2e_mm/目录下的真实引擎矩阵覆盖跨图像隔离、碰撞压力、chunk 边界相位、混合流量、多图像、视频模态、抢占重算等场景其中 T3mp_connector场景会针对真实 MP 缓存服务器重跑 T0/T1 核心用例并通过主 MP connector 验证包含 per-path 检测器的负对照negative control——即确认未替换路径确实会交叉命中而替换路径不会。小结多模态缓存键正确性的本质矛盾是占位符 token 抹掉了内容身份而 token 哈希管线只认识 token。LMCache 的答案不是改造管线而是在管线入口前把身份写回 tokenmm_hash_to_token_values用 SHA-256 计数器模式把完整标识符展开为 31 位、位置依赖、前缀稳定的伪 token 序列apply_mm_hashes_to_token_ids将其精确写回占位符跨度。这一设计以最小改动消除了跨图像静默污染为 in-process 与全部 MP 连接器路径提供了统一保护同时为未来迁移到与 vLLM 对齐的显式extra_keys通道保留了清晰路线。对于在 LMCache 上做多模态推理的用户理解这套机制是排查缓存命中异常、评估升级影响的前提。【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考