Prefix Caching 命中率的诊断:块哈希构成与满块边界
在一组 8 卡 **** ****(单卡 64GB HBM,卡间互联 392 GB/s)上,用 **** 的基础软件栈配合 vLLM 起 OpenAI 兼容服务,负载是模板化请求:一条固定的系统提示加一条用户短问题,提示侧数千 token。--enable-prefix-caching 已打开,期望 TTFT 明显下降,实测 TTFT 与未开时接近,命中率统计长期偏低。这类现场的共同点不是开关没生效,而是块哈希没对上:块哈希由父块 hash、本块 token、额外 hash 三段算出,任何一段随请求变化,整条哈希链断裂;三段都稳定时,末尾不满 block_size 的前缀段仍不进入缓存。诊断要按请求看命中块数与比例,只看全局命中率会被少数高复用请求拉高,掩盖多数请求零命中。
块哈希的三段构成
vLLM 的前缀缓存按块复用,块哈希为 hash(tuple[components]),components 分三段:
- 父块 hash:前一块的哈希值,使哈希链随前缀顺序递推。
- 本块 token:本块内 token id 的元组,用于降低碰撞概率。
- 额外 hash:使本块唯一所需的其他值,包括 LoRA 名、多模态输入 hash、prompt embeddings 摘要,以及多租户隔离用的
cache_salt。
三段任意一段变化,该块及其后续块的哈希全部改变。哈希算法由 --prefix-caching-hash-algo 控制,取值为 sha256(默认)、sha256_cbor、xxhash、xxhash_cbor;不同算法序列化方式不同,跨环境复用块要求算法一致。
| 组成段 | 由什么决定 | 何时变化 |
|---|---|---|
| 父块 hash | 前缀序列的递推结果 | 前缀前部任一 token 变化 |
| 本块 token | 本块 token id 元组 | 本块内容变化 |
| 额外 hash | LoRA 名、多模态输入 hash、prompt embeddings 摘要、cache_salt |
任一额外项变化 |
满块条件:前缀长度须为 block_size 整数倍
前缀缓存只缓存满块。参与复用的前缀长度须为 block_size 整数倍,末尾不满一块的部分不计入。
# 字段名示意:共享前缀 10 个 token,block_size = 4
Block 0: [A B C D] 满块,缓存
Block 1: [E F G H] 满块,缓存
Block 2: [I J] 只有 2 个 token,不满块,不缓存
命中结果:2 块 = 8 个 token;尾部 2 个 token 不命中,需要重算
两条请求共享 14 个 token、block_size 为 4 时,只有前 8 个 token(两个满块)命中,第三块只对上 2/4,整块作废。前缀长度与 block_size 越不对齐,尾部浪费越大。
易变项混入哈希
命中率骤降通常来自额外 hash 或 prompt 文本里的易变项。
| 混入项 | 来源 | 后果 |
|---|---|---|
cache_salt |
请求体字段,多租户隔离 | 值注入首块 hash;每请求不同则首块即变,后续块全部不命中 |
| LoRA 名 | 多 adapter 服务 | 同一 prompt 配不同 adapter 算作不同块 |
| 多模态输入 hash | 前端图像处理器生成的 image hash | 图片微变即改变含占位符块的 hash;官方多模态示例中每个含占位符的块都带该 hash |
时间戳 / request id / 随机化 system prompt |
应用层拼进 prompt | 本应相同的块算出不同哈希 |
| 会话拼接顺序变化 | 多轮对话上下文顺序 | 前缀序列改变,哈希链从变化点起断裂 |
cache_salt 的设计意图是隔离:只有约定同一 salt 的请求才能复用块,用于阻挡按延迟差推断缓存内容的时序攻击。它是安全特性,同时也是命中率骤降的常见来源。
block_size 对命中粒度的影响
block_size |
命中粒度 | 代价 |
|---|---|---|
| 小(16) | 命中更细,可复用尾段更长 | 块数多,块表与调度元数据开销上升 |
| 大(32 及以上) | 元数据省 | 颗粒更粗,序列尾部浪费按比例变大,前缀尾部更易对不齐 |
--block-size 受 attention backend 约束,须满足所选后端的块大小要求;默认后端要求为 16 的整数倍。取错值会使目标后端失格,框架回退到另一条 attention 实现。默认值为 16;未显式指定时框架按后端能力选取,实际生效值以启动日志或 Prometheus info 指标 vllm:cache_config_info 的 label 为准。
现象
- 开关已开,TTFT 与未开时接近。
- 全局命中率偏低,或时高时低。
- 命中率被少数高复用请求拉高,多数请求为零;均值掩盖分布。
- 改动 system prompt 的一个字符,命中率整体下台阶。
- 命中成立但内存压力上升,收益被显存占用抵消。
- 命中块数分布两极:少数长前缀请求全部命中,多数短请求零命中,加权后的全局命中率掩盖这一分布。
- 多租户共享一套服务时,命中率随租户切换出现周期性归零。
命中不等于收益
命中成立后收益仍可能为零或被抵消。命中率上升代表 prefill 计算量下降,但若瓶颈不在 prefill(落在 decode 段或主机下发链路),TTFT 与吞吐不变,收益为零。另一种情形是收益被抵消:命中块常驻 KV 池,池子被缓存块占满后,新请求可用的空闲块减少,抢占与重算上升,命中省下的 prefill 计算被新增的抢占与重算抵消。判定时要分清两类:收益为零说明优化没碰到瓶颈,收益被抵消说明收益存在但被新增开销抵消,处置方向不同。
定位步骤
- 确认开关与生效配置:读 Prometheus info 指标
vllm:cache_config_info的 labelenable_prefix_caching、block_size、prefix_caching_hash_algo。没开前缀缓存时该族指标不存在,属「读不到」,不等于「框架不提供」。 - 读全局分子分母:
vllm:prefix_cache_hits_total与vllm:prefix_cache_queries_total,命中率 = hits / queries;两项缺一则不计算命中率。前端口径只取vllm:prompt_tokens_by_source_total{source="local_compute"}与source="local_cache_hit"两项,不含external_kv_transfer,避免把从别处传入的 KV 算成命中。 - 按请求看命中块数与比例:命中块数约为命中 token 数除以
block_size,与该请求总块数相比。看分布而非只看均值,区分「普遍偏低」与「少数请求极低把均值拉低」。 - 逐项核验额外 hash 来源:请求体是否带
cache_salt、是否按 adapter 变化、是否含多模态输入、prompt 文本里是否混入时间戳或request id。 - 核验满块边界:共享前缀长度是否为
block_size整数倍。 - 核验请求顺序:多轮会话的上下文拼接顺序是否稳定。
处置
| 动作 | 作用 | 代价 / 前提 |
|---|---|---|
去掉 prompt 内的易变项(时间戳、request id、随机前缀) |
让前缀稳定,块哈希可复现 | 应用层改动;需确认对业务语义无影响 |
统一 cache_salt |
让同信任组内的请求复用块 | 缩小隔离范围,跨组不再共享 |
调 --block-size |
改变命中粒度与尾部浪费 | 取值受后端约束,同时改变碎片与元数据开销 |
| 复用同一 LoRA adapter | 避免 adapter 混入额外 hash | 切换 adapter 需重载,失去多 adapter 并行 |
| 对齐前缀到满块 | 让尾部前缀进入缓存 | 需在应用层补齐或截断前缀,长度须为 block_size 整数倍 |
固定哈希算法 --prefix-caching-hash-algo |
跨环境命中一致 | sha256 依赖 Python pickle,跨版本不一定可复现;跨环境用需换可复现序列化 |
| 提高前缀复用率(请求模板化、模板放在最前) | 拉高命中块数 | 需应用层重构请求构造方式 |
| 监控命中块的 KV 占用 | 发现缓存挤占空闲块 | 需结合 vllm:kv_cache_usage_perc 一起看 |
记录字段
# 一条 prefix caching 命中率记录(字段名示意)
prefix_cache:
enabled: # --enable-prefix-caching
hash_algo: # --prefix-caching-hash-algo 取值
block_size: # 生效值,取自 vllm:cache_config_info 的 label
hits_total: # vllm:prefix_cache_hits_total
queries_total: # vllm:prefix_cache_queries_total
hit_rate: # hits / queries
per_request:
hit_blocks: # 按请求命中块数
total_blocks: # 该请求总块数
hit_ratio: # 命中块数 / 总块数
extra_hash_sources:
cache_salt_present: # 请求体是否带 cache_salt
lora_id_varies: # adapter 是否按请求变化
multimodal_input: # 是否含多模态输入 hash
volatile_tokens: # 时间戳 / request id / 随机 system prompt
prefix_alignment:
shared_prefix_tokens: # 共享前缀 token 数
mod_block_size: # 前缀长度对 block_size 取模
workload:
request_pattern: # 模板化 / 混合 / 无复用
prompt_reuse_ratio: # 前缀复用率
avg_prompt_len:
perf:
ttft_before:
ttft_after:
memory_peak_before:
memory_peak_after:
小结
前缀缓存命中由块哈希三段与满块边界共同决定。块哈希由父块 hash、本块 token、额外 hash 三段构成;额外 hash 可掺入 cache_salt、LoRA 名、多模态输入 hash、prompt embeddings 摘要,prompt 文本里的时间戳与 request id 同样改变哈希。满块边界要求前缀长度为 block_size 整数倍。诊断顺序是:先读 vllm:prefix_cache_hits_total 与 vllm:prefix_cache_queries_total 得到全局命中率,再按请求看命中块数与比例,最后逐项核验额外 hash 来源与满块对齐。全局命中率高而收益不成立时,落点在这些易变项与对齐关系上。