缓存命中率 96%,成本降到原来的三成左右。如果负责过大模型推理服务,看到这句话,第一反应应该是:这不是一个普通的“性能优化指标”,而是一条直接写在预算上的数字。
很多团队在优化 LLM 服务时,习惯盯 GPU 利用率、吞吐量、平均延迟,这些指标当然重要,但容易忽略一个更前置、更致命的问题:大量 GPU 算力在重复计算已经算过的内容。同一个 system prompt、同一份知识库上下文、同一条用户前缀,被不同请求反复 prefill。这部分的算力浪费,不会直接显示在账单上,却会实打实推高成本。
缓存命中率低,本质上是 KV Cache 没有被充分利用。要解决这个问题,需要先回答三件事:命中率怎么测、命中率怎么提升、命中率变化怎么被监控告警。本文以昇腾 NPU 环境的 vLLM-ascend 为例,结合 Prometheus + Grafana,讲清楚缓存命中率从指标暴露、采集可视化到成本估算的完整链路,并解释为什么命中率到了 96%,成本可以出现数倍级别的下降。
文章会涉及部署命令、指标配置、PromQL 查询、告警规则和常见排错,适合正在做 LLM 推理服务、希望把成本监控落到实处的开发和运维同学。
1. 缓存命中率为什么是 LLM 推理的“成本命门”
先看一个容易忽略的现象:LLM 推理的成本分布并不是均匀的。请求处理分为 prefill 和 decode 两个阶段,prefill 阶段需要并行计算整段输入 prompt 的注意力矩阵,计算量随输入长度线性增长,是整个推理过程中计算密度最高的部分。多个请求如果包含相同前缀,这部分计算会在每个请求里被重复执行。
传统缓存解决的是“数据读取”问题,比如 Redis 命中后不用查数据库。而 LLM 的 prefix cache 解决的是“计算复用”问题:如果新请求的前缀 token 和之前某次请求完全一致,那么这段前缀的 KV Cache 可以直接从显存中复用,不需要重新 prefill。缓存命中率越高,需要重复计算的输入占比就越低,GPU 算力越能花在真正的新内容上。
这就解释了为什么命中率会和成本强相关。假设一条生产链路中有大量长上下文请求,比如客服知识库、代码助手、Agent 多轮任务,这些请求的共同前缀可能占到输入 token 的 70% 到 90%。如果命中率只有 30%,意味着大部分前缀都在重复烧钱;如果把命中率提升到 96%,重复计算部分被压缩到极低比例,单位请求成本自然大幅下降。
更要命的是,缓存命中率还是一个“会突然恶化”的指标。近期社区里就有团队反馈,模型客户端升级到新版本之后,请求中的 prompt 模板、工具描述或 system prompt 发生变化,缓存命中率一夜之间从 90% 跌到 60%。指标没有监控,这种问题可能要等到账单出来才发现。
所以缓存命中率不是“锦上添花”的调优项,而是 LLM 服务的成本命门。它需要被采集、被可视化、被告警,尤其在高并发、长上下文的实际业务里。
2. KV Cache、前缀缓存与 vLLM-ascend
2.1 KV Cache 是什么
Transformer 模型在生成每个 token 时,需要计算当前 token 与之前所有 token 的注意力。为了避免每次生成都重新计算历史 token 的 K、V 矩阵,推理框架会把已经生成的 token 对应的 K、V 矩阵缓存在显存中,这就是 KV Cache。
KV Cache 有一个特点:它和输入内容强相关。两个请求如果前缀不同,KV Cache 就无法共享;前缀相同,则理论上可以复用。
2.2 Automatic Prefix Caching 做了什么
vLLM 引入的 Automatic Prefix Caching(自动前缀缓存)把 KV Cache 按块管理,并对每个块的内容做哈希。新请求进入时,框架会从第一个 token 开始匹配已有缓存块,尽可能复用已经计算过的前缀,只对未命中的部分执行 prefill。
要实现高命中率,前提是请求之间的前缀足够“整齐”。如果每个请求动态拼接了时间戳、随机 ID 或者可变的 system prompt,那么前缀缓存会全部失效。这也是模型/客户端升级后命中率大跌的常见原因。
2.3 vLLM-ascend 是什么
vLLM-ascend 是 vLLM 在昇腾 NPU 上的适配实现。昇腾环境无法直接使用原版 vLLM 的 CUDA 内核,需要经过 vLLM-ascend 兼容层来调度 Attention、KV Cache 和推理算子。从使用角度讲,vLLM-ascend 仍然遵循 vLLM 的主体接口和指标体系,因此 Prometheus 采集方案可以和 vLLM 基本对齐,只是在部署和部分参数上存在差异。
2.4 缓存命中率的两种口径
需要区分两种命中率:
| 口径 | 含义 | 对成本的影响 |
|---|---|---|
| token 级命中率 | 被缓存复用的 token 数占总输入 token 数的比例 | 直接决定 prefill 计算量下降幅度 |
| 请求级命中率 | 有多少请求在 prefix 上命中了缓存 | 反映业务前缀的整齐程度,需结合 token 级一起看 |
监控时优先看 token 级命中率,因为它和成本换算的关系最直接。请求级命中率则更偏向业务层,用于判断是否需要调整 prompt 规范。
3. 环境准备与 vLLM-ascend 启动
本文以昇腾 910B 系列 NPU 为例,操作系统为 Linux,版本信息请以你实际项目的兼容矩阵为准,这里重点演示通用思路。
3.1 基础环境
需要准备以下部分:
- 昇腾 NPU 服务器,例如 Atlas 800/900 训练服务器或推理服务器。
- 已安装对应版本的 CANN toolkit,并完成驱动、固件检查。
- Python 3.9 及以上版本,建议使用独立的虚拟环境。
- vLLM-ascend 以及对应版本的 vLLM。
安装命令示例:
# 创建虚拟环境 python3 -m venv /opt/venvs/vllm-ascend source /opt/venvs/vllm-ascend/bin/activate # 安装 vllm-ascend # 注意:请先查看官方 README 中的版本对照表,选择与 CANN 版本匹配的版本 pip install vllm-ascend安装完成后,可以通过python -c "import vllm; print(vllm.__version__)"确认 vLLM 已可正常导入。
3.2 启动 vLLM-ascend 服务
vLLM-ascend 的常见启动方式是通过环境变量VLLM_USE_VLLM_ASCEND=1激活昇腾后端,然后使用 vLLM 的 OpenAI 兼容服务入口。下面是一个带前缀缓存和指标端口的启动示例:
VLLM_USE_VLLM_ASCEND=1 vllm serve /data/models/Qwen2.5-14B-Instruct \ --served-model-name qwen14b \ --max-model-len 32768 \ --gpu-memory-utilization 0.90 \ --enable-prefix-caching \ --metrics-port 8001 \ --host 0.0.0.0 \ --port 8000参数说明:
--enable-prefix-caching:开启前缀缓存。如果当前版本默认开启,则此参数可省略;如果版本不识别该参数,请通过vllm serve --help确认实际参数名。--metrics-port 8001:Metrics 指标端口。该端口用于 Prometheus 采集,--port 8000是 API 服务端口。--gpu-memory-utilization 0.90:允许框架使用 90% 的显存,其中一部分会分配给 KV Cache pool。KV Cache 池越大,缓存越不容易被淘汰。
启动成功后,可以先确认服务内部是否正常暴露了缓存指标。用下面命令查看:
curl http://127.0.0.1:8001/metrics | grep -i cache | head -30如果你看到的输出里有cache_hit、prefix_cache之类的指标名,说明指标已正常暴露。不同版本、不同适配层的指标名会有差异,后续 PromQL 请以这一步的实际输出为准。
4. 暴露指标:确认 /metrics 里的缓存命中率
在配置监控之前,先搞清楚当前版本的指标命名是最重要的一步。vLLM 社区版本中,缓存相关指标可能叫vllm_cache_hit_rate、vllm_cache_hit_tokens_total、vllm_cache_miss_tokens_total、vllm:prefix_cache_hit_rate等,不同版本前后缀不一致,vLLM-ascend 可能还会叠加自己的命名方式。
下面给出一个完整的确认流程:
# 查看全部 cache 相关指标名 curl -s http://127.0.0.1:8001/metrics | grep -i "cache" | awk '{print $1}' | sort -u假设输出包含类似下面的名称:
vllm_cache_hit_rate vllm_cache_hit_tokens_total vllm_cache_miss_tokens_total vllm_cache_len_total那么后面可以这样理解:
vllm_cache_hit_rate:当前采集周期内的缓存命中率,通常是一个 Gauge,可以直接绘图。vllm_cache_hit_tokens_total:累计命中 token 数,Counter,适合做趋势和速率计算。vllm_cache_miss_tokens_total:累计未命中 token 数,Counter。vllm_cache_len_total:当前缓存块的规模。
如果指标是 Counter 类型,而不是直接的命中率,可以用 PromQL 计算:
sum(rate(vllm_cache_hit_tokens_total[5m])) / (sum(rate(vllm_cache_hit_tokens_total[5m])) + sum(rate(vllm_cache_miss_tokens_total[5m])))如果指标本身就是 Gauge,比如vllm_cache_hit_rate,直接查询即可:
avg(vllm_cache_hit_rate)这里必须强调:写 PromQL 之前,一定先用curl确认每个指标的真实名称和类型。监控配置写错了,面板上只会一直显示空数据或 0,这种问题在实操里非常常见。
5. 部署 Prometheus 采集 vLLM-ascend 指标
Prometheus 通过 HTTP 周期性拉取/metrics指标。下面用一台独立的监控服务器或容器来部署 Prometheus,目标是把昇腾节点上的 vLLM-ascend 指标采集进来。
5.1 Prometheus 安装
可以直接通过官方二进制或 Docker 安装。这里以 Docker 方式为例:
docker run -d \ --name prometheus \ --restart unless-stopped \ -p 9090:9090 \ -v /etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \ prom/prometheus5.2 编辑采集配置
创建/etc/prometheus/prometheus.yml,写入 vLLM-ascend 节点的抓取配置:
# 文件路径:/etc/prometheus/prometheus.yml global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: "vllm-ascend" metrics_path: /metrics static_configs: - targets: - "10.0.0.11:8001" # 节点1 - "10.0.0.12:8001" # 节点2 labels: service: vllm backend: ascend配置要点:
scrape_interval建议设置为 10 到 15 秒,既能及时发现命中率下跌,又不会给推理节点增加明显的采集开销。- target 地址必须是 vLLM-ascend 服务实际绑定的 IP 和端口。如果启动时
--host 127.0.0.1,Prometheus 从外部无法抓取,应该绑定到内网可达地址。 - 通过
labels给不同节点打上标签,方便在 Grafana 中按实例过滤。
5.3 验证采集是否成功
Prometheus 启动后,访问http://<prometheus_ip>:9090/targets,可以看到vllm-ascend这个 job 的状态。如果 State 是UP,说明采集链路已经打通。
也可以直接在 Prometheus 的 Graph 页面执行一句最简单的查询:
up{job="vllm-ascend"}正常情况下会返回1,表示目标在线。
6. Grafana 可视化与告警
采集到指标只是第一步。没有可视化时,命中率下降仍然是隐性问题;没有告警时,问题只能靠人肉发现。下面在 Grafana 中把缓存命中率变成面板和告警规则。
6.1 添加 Prometheus 数据源
在 Grafana 中进入 Configuration -> Data Sources -> Add data source,选择 Prometheus,URL 填写http://<prometheus_ip>:9090,点击 Save & Test 完成。
6.2 创建缓存命中率面板
新建 Dashboard,添加一个 Panel,查询语句根据第 4 节确认到的指标名选择:
# 如果指标是 Gauge avg(vllm_cache_hit_rate{service="vllm"}) # 如果指标是 Counter,需要按速率计算 sum(rate(vllm_cache_hit_tokens_total{service="vllm"}[5m])) / (sum(rate(vllm_cache_hit_tokens_total{service="vllm"}[5m])) + sum(rate(vllm_cache_miss_tokens_total{service="vllm"}[5m])))Panel 建议配置:
- Unit 选择
percent,即 0-100 的百分比。 - 如果指标是 0-1 的小数,可以在 Transform 中乘以 100,或直接在查询中乘以 100。
- 最好在同一张图里叠加不同实例的命中率,用 legend 按
instance维度区分。
另外建议增加一个“命中 token 速率”面板:
sum by (instance) (rate(vllm_cache_hit_tokens_total[5m]))它可以直观看出命中量是否在上升。
6.3 配置命中率告警规则
可以有两种做法:在 Grafana Panel 的 Alert 页里配置,或者在 Prometheus 的 rules 文件中配置。推荐使用 Prometheus rules,便于版本管理。
创建告警规则文件/etc/prometheus/rules/llm_cache.yml:
# 文件路径:/etc/prometheus/rules/llm_cache.yml groups: - name: vllm_ascend_cache_alerts rules: - alert: LLMPrefixCacheHitRateLow expr: avg(vllm_cache_hit_rate{service="vllm"}) < 0.8 for: 15m labels: severity: warning annotations: summary: "vLLM-ascend 缓存命中率低于 80%" description: "当前缓存命中率 {{ $value }},请检查 prompt 模板和最近的模型/客户端升级。"然后在 Prometheus 主配置中引入该文件:
rule_files: - /etc/prometheus/rules/llm_cache.yml改动后执行curl -X POST http://<prometheus_ip>:9090/-/reload热加载配置,或重启 Prometheus。注意告警表达式里的指标名要和实际情况一致,否则告警不会触发。
6.4 告警阈值的设定思路
阈值不建议拍脑袋定。可以先跑一周,观察不同业务时段的命中率分布,再把告警阈值设在正常值的下降点附近。比如正常命中率稳定在 90% 以上,阈值可以设在 80%,并持续 15 分钟确认,避免短时间波动误报。
7. 用成本模型解释 96% 命中率与 3.2 倍成本
标题里的“成本降 3.2 倍”并不是一个硬性的性能测试结论,而是一个在长上下文、高命中业务里接近真实情况的估算。它可以被简化为一个成本模型。
LLM 推理成本大致可以分为两部分:
- prefill 计算成本,与输入 token 量强相关。
- decode 生成成本、调度开销、显存占用等其他成本。
假设 prefill 占整体计算成本的 70% 到 75%,缓存命中率是 h,那么整体计算成本比例可以近似为:
综合成本比例 = (1 - h) × prefill成本占比 + 非prefill成本占比当 h = 96%,prefill 占比为 72% 时:
综合成本比例 = 0.04 × 0.72 + 0.28 = 0.3088 ≈ 30.9%也就是说,综合成本约为原来的 31%,按行业常见的“成本降幅”口径,大约相当于“成本降了 3.2 倍”。
下面这张表可以直观看出不同命中率下的成本比例:
| 缓存命中率 | 需要重新计算的 prefill 比例 | prefill 占成本 70% 时的综合成本比例 | 行业习惯说法 |
|---|---|---|---|
| 0% | 100% | 100% | 基线 |
| 50% | 50% | 65% | 约降 1.5 倍 |
| 80% | 20% | 44% | 约降 2.3 倍 |
| 96% | 4% | 28% - 33% | 约降 3 倍以上 |
注意,这是简化模型,真实场景里还要考虑请求长度分布、batch 大小、缓存淘汰策略、显存池配置等因素。但它给出了一个非常重要的判断:缓存命中率每提升一截,都会直接折算成 GPU 算力成本的下降。
这也是为什么值得把命中率接进 Prometheus + Grafana 的原因:指标一旦可视化,就可以在业务大促前、模型升级前,先评估命中率对成本的潜在影响,而不是事后看账单。
8. 常见问题与排查思路
下面整理了一些监控 vLLM-ascend 缓存命中率时常见的问题,以及对应的排查方法。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 命中率突然从 90% 跌到 60% | 模型或客户端升级后,system prompt、工具描述、prompt 模板发生变化 | 对比升级前后请求日志中的原始 prompt,检查前缀是否变化 | 固定 prompt 模板,先灰度升级,异常时回滚 |
| 命中率一直为 0 | 未开启 prefix caching,或请求前缀里有时间戳、随机 ID | 查看启动参数和一次完整请求的报文 | 启用前缀缓存,对 prompt 做规范化处理 |
| Prometheus targets 显示 DOWN | 指标端口未绑定对端可达 IP,或防火墙拦截 | 用 curl 从 Prometheus 所在机器访问 /metrics | 启动时绑定内网 IP,开放访问策略 |
| Grafana 面板无数据 | PromQL 指标名写错,或 label 不匹配 | 在 Prometheus 查询页面手动执行表达式,检查返回结果 | 按 curl /metrics 的实际指标名替换 |
| 缓存命中率低但显存持续增长 | KV Cache pool 太小,缓存块被频繁淘汰 | 查看 KV Cache 利用率指标和平均请求并发 | 提高 gpu-memory-utilization,或降低服务并发 |
| 多副本负载均衡后命中率整体下降 | 请求被分散到不同实例,各实例缓存无法共享 | 观察不同 instance 的命中率分布 | 按业务/用户做一致性路由,或引入分布式 KV Cache 方案 |
| 指标名在文档和实际环境对不上 | vLLM 和 vLLM-ascend 版本差异较大 | 直接抓取当前 /metrics 的指标名 | 后续 PromQL 与告警全部以实际指标名为准 |
8.1 命中率下降排查顺序
遇到命中率下降,建议按下面的顺序快速定位:
- 看最近是否有代码、模型、客户端或者 prompt 模板变更。
- 对比变更前后同一批请求的完整输入前缀。
- 确认是否存在动态内容混进了公共前缀。
- 看 Prometheus 面板,确认下降是瞬间发生的还是逐步发生的。
- 如果是逐步下降,优先怀疑缓存块被淘汰,检查 KV Cache 池大小和并发数。
- 如果只有某一台实例下降,检查该实例负载均衡策略和缓存状态。
9. 最佳实践与工程建议
9.1 把公共前缀做成固定常量
要让缓存命中率稳定在 90% 以上,最重要的工程手段不是调框架参数,而是控制 prompt 的“整齐度”。把 system prompt、工具描述、知识库背景、角色设定等静态内容放在 prefix 的最前面,并且作为固定常量管理;把动态内容放到 prompt 尾部。
如果动态内容必须出现在中间,缓存会变得很难命中。此时可以考虑对 prompt 做分段:静态前缀 + 动态知识检索 + 用户问题,并让框架识别出静态前缀。
9.2 升级前后先看命中率
模型版本升级、vLLM-ascend 版本升级、客户端 SDK 升级,都可能影响缓存命中率。原因是升级后 tokenizer、tool 描述甚至 system prompt 模板可能变化。建议把“缓存命中率对比”纳入发布检查清单,升级前记录基线,升级后灰度观察,不达标就回滚。
9.3 监控指标不要只看命中率
命中率需要和以下指标一起看,才能定位问题:
- TTFT(首次 token 延迟):命中率高时,TTFT 应明显下降。
- 平均吞吐量和 GPU 利用率:命中率提升后,应看到单位时间处理请求数上升。
- KV Cache 利用率:用于判断缓存块是否充足。
- prefill 和 decode 的计算量分布:确认成本结构是否发生变化。
9.4 指标端口的访问安全
Prometheus 抓取指标时,会暴露内部运行数据。不要把/metrics端口直接映射到公网。实测中更稳妥的做法是:
- 指标端口绑定到内网地址。
- Prometheus 与 vLLM-ascend 通过内网通信。
- 如果跨网络,使用认证代理或公司内部的指标网关。
- Grafana 同样建议做访问控制,避免面板和数据源被未授权访问。
9.5 告警要能驱动行动
告警阈值只是一个开始。出现命中率告警后,需要让值班人员知道下一步做什么。最佳实践是在告警描述里写清楚:
- 命中率当前值。
- 最近一次模型或配置变更时间。
- 可能受影响的业务线。
- 需要联系谁。
没有操作指引的告警,最后会被习惯性忽略。
9.6 定期做成本复盘
缓存命中率不是一个“配完就完了”的指标。建议每两周或每个迭代周期做一次复盘:命中率均值、峰值、低点分别是什么,业务变化对命中率的影响,以及命中率变化折算出来的成本变化。这样监控体系和成本优化才能形成闭环,而不是停留在看板好看。
末尾:一个最容易踩的坑
最后提醒一个实际项目里最容易踩的坑:不要在一个请求里把所有内容拼成一大段动态文本。有人把用户 IP、当前时间、trace ID、session ID 全拼进 system prompt 或 prompt 前缀,导致前缀缓存全局失效,命中率无限接近 0。这类问题在代码 review 阶段很难发现,但一旦接上 Prometheus 监控,命中率曲线会立刻暴露出来。
所以,给缓存命中率配上监控,不只是为了看一个好看的百分比,更是为了让成本问题在代码变更时第一时间暴露。部署 prometheus + grafana 收集 vllm-ascend 的缓存命中率指标,本质上是把 LLM 推理成本从“事后账单”变成“实时反馈”,这比优化一个具体参数有意义得多。