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 分三段:

三段任意一段变化,该块及其后续块的哈希全部改变。哈希算法由 --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 为准。

现象

命中不等于收益

命中成立后收益仍可能为零或被抵消。命中率上升代表 prefill 计算量下降,但若瓶颈不在 prefill(落在 decode 段或主机下发链路),TTFT 与吞吐不变,收益为零。另一种情形是收益被抵消:命中块常驻 KV 池,池子被缓存块占满后,新请求可用的空闲块减少,抢占与重算上升,命中省下的 prefill 计算被新增的抢占与重算抵消。判定时要分清两类:收益为零说明优化没碰到瓶颈,收益被抵消说明收益存在但被新增开销抵消,处置方向不同。

定位步骤

  1. 确认开关与生效配置:读 Prometheus info 指标 vllm:cache_config_info 的 label enable_prefix_caching、block_size、prefix_caching_hash_algo。没开前缀缓存时该族指标不存在,属「读不到」,不等于「框架不提供」。
  2. 读全局分子分母: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 算成命中。
  3. 按请求看命中块数与比例:命中块数约为命中 token 数除以 block_size,与该请求总块数相比。看分布而非只看均值,区分「普遍偏低」与「少数请求极低把均值拉低」。
  4. 逐项核验额外 hash 来源:请求体是否带 cache_salt、是否按 adapter 变化、是否含多模态输入、prompt 文本里是否混入时间戳或 request id。
  5. 核验满块边界:共享前缀长度是否为 block_size 整数倍。
  6. 核验请求顺序:多轮会话的上下文拼接顺序是否稳定。

处置

动作 作用 代价 / 前提
去掉 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 来源与满块对齐。全局命中率高而收益不成立时,落点在这些易变项与对齐关系上。