KV Cache 复用与批处理的真实收益边界

举报
上云 发表于 2026/11/06 09:30:02 2026/11/06
【摘要】 前缀缓存、连续批处理、量化三项优化的收益都不是单调的。用固定压测口径拆开命中率、调度甜点与精度损失三条曲线,给出不同流量形态下该开什么、什么时候反而更贵,以及净收益的判定公式。

KV Cache 前缀复用、连续批处理、量化,几乎已经成了大模型推理降本路上的三件必备手段。但这三项优化的收益曲线都不单调:命中率、batch 大小、量化位宽各自都有一个“划算区间”,越出区间之后成本会掉头往上走。下面用一套固定口径的压测,把这三个区间分别划出来,并给出可直接用来判断“该不该开”的公式。

一、统一评定标准,否则三条曲线不可比

降本讨论最容易出问题的地方是评定标准漂移:有人比较 QPS,有人比较单请求延迟,有人看 GPU 利用率。这些指标之间可以互相牺牲,因此必须先把“成本”固定成单一可比的数字。

本文采用 ¥/1M 达标输出 token:只有满足 SLO 的请求才计入分子,延迟不达标的吞吐不算数。

单位成本 = 实例小时单价 × 1M / (达标输出吞吐 tok/s × 3600)
有效成本 = 单位成本 / (1 - 重试率)

压测台固定以下六个变量,任何一组只改一个:

变量 取值
硬件 单机 8 卡同型号,禁跨机,固定功耗档
模型 7B / 13B dense,max_len 32k
输入长度 短 128~512 / 中 2k~4k / 长 16k~32k 三档
输出长度 固定 256,另跑一档 1024
并发 1 / 4 / 16 / 48 / 96 / 192 阶梯加压
SLO TTFT P95 ≤ 1.5s(长输入 3s),TPOT P95 ≤ 80ms

采集窗口为预热 3 分钟后取 10 分钟稳态,只保留 P95 而不是均值,因为均值会把尾部长请求的影响掩盖掉。压测骨架用固定到达率、流式读取首 token 计 TTFT:

import asyncio, time

SLO_TTFT, SLO_TPOT = 1500, 80

async def one(session, url, req):
    t0 = time.perf_counter()
    async with session.post(url, json=req) as r:
        ttft, n = None, 0
        async for line in r.content:          # 流式 chunk
            if not line.strip():
                continue
            if ttft is None:
                ttft = (time.perf_counter() - t0) * 1000
            n += 1
        total = (time.perf_counter() - t0) * 1000
    tpot = (total - (ttft or total)) / max(n - 1, 1)
    return ttft or total, tpot, n

def report(rows, wall_s, card_hour_cny=60.0):
    ok = [r for r in rows if r[0] <= SLO_TTFT and r[1] <= SLO_TPOT]
    tps = sum(r[2] for r in ok) / wall_s        # 只算达标请求
    return {"pass_rate": len(ok) / len(rows),
            "eff_tps": round(tps, 1),
            "cny_per_1m": round(card_hour_cny * 1e6 / (tps * 3600), 2)}

下文出现的数字均来自这套台架,更换硬件或模型后绝对值会变,看形状不看绝对值。

二、前缀缓存:命中率的统计口径决定结论,收益窗口在 30%~80%

前缀缓存只在 block 边界上复用,常见的 block 是 16 个 token,因此前缀末尾不足一个 block 的部分永远要重算。这决定了它有两个必须先讲清的统计口径问题:

  1. 统计口径按请求还是按 token。用“命中请求数 / 总请求数”在长 prompt 场景会严重高估收益,必须用 token 级:命中率 = 复用 prefill token / 总 prefill token。同一个系统两种算法能差 3 倍。
  2. 收益要乘以 prefill 占比。命中省掉的只是 prefill,如果输出很长、decode 主导,prefill 归零也救不了端到端吞吐。
hit_rate     = hits_total / max(queries_total, 1)      # token 级
prefill_share = prefill_ms / (prefill_ms + decode_ms)
speedup ≈ 1 / (1 - hit_rate * prefill_share)

按这个式子,收益可以分成三段:

  • < 25%:基本打平。缓存块在未命中时仍然占着显存直到被驱逐,块管理本身也有开销,净收益在噪声范围内。
  • 30%~80%:近似线性。实测 prefill 时间下降与 token 级命中率基本 1:1,端到端吞吐提升 ≈ 命中率 × prefill 占比。
  • > 85%:边际递减。剩下的 prefill 是必算的 suffix,且缓存块逼近 budget 后驱逐频繁,会出现 cache thrash,命中率抖动。
流量形态 前缀特征 token 命中率 prefill 占比 端到端吞吐 结论
Agent / 客服多轮 1~3 种,约 3k token 的统一 system + 工具描述 85%~95% 55% +70%~90% 必开
代码补全 每文件前缀不同,共享段短 15%~25% 70% +5%~10%,但并发上限降一档 谨慎,需与 max-num-seqs 联调
开放域长文档问答 每请求文档不同 < 5% 80% 负收益 关

判定规则:前缀长度 ≥ 512 token、前缀种类数在几十个量级、实例并发 ≥ 8,三条同时满足才开。多租户场景如果每家一份 system prompt,命中率会被请求数量稀释,先按租户拆池再谈。

三、连续批处理:最优点在“TPOT 刚好达标”处,不在最大 batch

decode 阶段是 memory-bound:每生成 1 个 token 都要把权重和全部 KV 读一遍。batch 变大时权重读取被摊薄,吞吐近似线性上升;但每个 token 的计算量也在涨,TPOT 同步劣化。两条线交叉的地方才是经济的批大小。

并发 达标输出吞吐 tok/s TTFT P95 TPOT P95 ¥/1M 达标 token 判定
4 180 0.35s 32ms 92.6 资源闲置
16 1650 0.6s 41ms 10.1 偏保守
48 3400 0.9s 62ms 4.9 甜点
96 4100 1.6s ✗ 95ms ✗ 名义 4.1,达标后 6.4 越过 SLO
192 4300 3.4s ✗ 150ms ✗ — 排队主导

关键读法在第 4 行:名义吞吐还在涨,但 SLO 达标率跌破阈值,计入口径后成本反而比并发 48 更贵。这就是“批量越大越省钱”这句话的反例。

具体调法是反推而不是试错:把 TPOT P95 打到 SLO 的 80%(即 64ms)对应的 max-num-seqs 作为在线池起点。

vllm serve $MODEL \
  --max-num-seqs 64 \
  --max-num-batched-tokens 8192 \
  --enable-chunked-prefill \
  --enable-prefix-caching

两个坑要提前躲:

  • 长短混跑会有队头阻塞。chunked prefill 把长 prefill 切片插进 decode 之间,TTFT 变平滑,代价是 TPOT 抖动上升。给 chunk 上限设个 2k 左右的硬顶,否则长请求会持续挤占 decode 时间片。
  • 在线池和离线池必须分开。离线批处理没有 SLO 约束,可以把 batch 拉到显存极限,吞吐比在线最优点再高 30%~50%,但 P99 是秒级。两类流量混在一个实例里,两边的甜点都会被对方毁掉。

四、量化:省的是显存并换取并发,但别拿困惑度验收

量化有两条路径,收益来源完全不同:

  • 权重量化(W8A8 / W4A16):权重显存下降,decode 阶段的权重读取带宽下降,直接改善 TPOT;同时腾出的显存可以多放 KV block,抬高并发上限。
  • KV Cache 量化(FP8 / INT8):只影响 KV 显存,本质上是在换最大并发和最大上下文长度,对单请求 TPOT 改善有限,但误差在长上下文里会累积。
方案 权重显存 KV 显存 相对吞吐 结构化输出合规率 16k 召回 结论
BF16 基线 1.0x 1.0x 1.00 99.2% 96% 基线
W8A8 0.5x 1.0x 1.55x 99.1% 96% 默认选它
W8A8 + FP8 KV 0.5x 0.5x 1.80x 98.7% 93% 高并发短上下文可用
W4A16 (AWQ) 0.28x 1.0x 2.10x 97.4% 90% 先做任务级回归
W4 + INT8 KV 0.28x 0.5x 2.40x 95.1% 84% 仅离线 / 低风险链路

真正致命的误区是用困惑度当验收指标。困惑度只涨 2% 看起来无害,但工具调用的 JSON 解析失败率可能从 0.8% 涨到 4.9%——这类损失在困惑度上看不出来。

也别把损失夸大。单次失败重试只增加约 5% 期望成本,但线上链路通常是“重试 → 仍失败 → 降级到更大模型或人工兜底”,单请求成本是 3~5 倍,5% 的失败率折算下来是 +10%~20% 的期望成本,同时以 P95 变差的形式还给用户。吞吐 2.1 倍的收益经此折损,实际落在 1.6 倍附近——仍然划算,但绝没有账面那么漂亮。

验收只有一条路:用同一批真实请求做 shadow 双跑,比对三项业务指标——结构化输出解析成功率、长上下文召回、拒答率。任一项劣化超过 1 个百分点就退回上一档。

五、三条曲线叠加:哪些流量形态真的省钱

单项最优不等于组合最优。前缀缓存吃显存、批处理吃延迟预算、量化吃精度余量,三者共用同一个资源池。

流量形态 前缀缓存 连续批处理 量化 主导成本项
Agent / 客服(高 QPS、长共享前缀) 强开 开到 SLO 甜点 W8A8 + FP8 KV prefill 与 decode 均衡
内部工具(< 1 QPS、多租户前缀分散) 关 小 batch,不等批 仅 W8A8 显存闲置,优化空间在缩容
离线批量(抽取、数据合成) 开,同模板批量提交 拉到显存极限 W4 可试 纯吞吐
长上下文 RAG(每请求文档不同) 关,或只缓存指令段 chunked prefill 忌 KV 量化 prefill

三种“开了反而更贵”的典型情形:

  1. 低 QPS + 高前缀基数:缓存块长期占着显存不被复用,KV block 池缩小,并发上限掉一档;而低 QPS 本来并发就用不满,等于纯粹白占,极端配置下还会因 block 分配失败触发重算让延迟更差。
  2. 并发不足时把 batch 上限开大:调度器为了凑批引入等待,TTFT 涨了吞吐没涨,纯粹是负优化。
  3. 精度敏感任务上激进量化:质量损失不会凭空消失,它会以重试和降级的形式回到账单,同时以 P95 的形式回到用户。

统一的判定式:

净收益 = 单位请求成本下降额
       - Δ失败率 × 单次失败额外成本
       - 运维复杂度折算
仅当 净收益 > 0 且 SLO 达标率未下降 时才上线

落地建议

  1. 先建评定标准再谈优化:固定硬件、模型、长度分布与 SLO,把 ¥/1M 达标输出 token 作为唯一对比指标;不达标的吞吐不进分子,否则所有结论都会被“平均延迟”带偏。
  2. 前缀缓存按 token 级命中率做开关决策:单实例连续观测 24 小时,命中率低于 25% 直接关;高于 60% 且 prefill 占比超过 40% 才值得为它单独切池。不要按“命中请求数”上报这个数。
  3. 批处理参数用反推不用试错:先测 TPOT 曲线,取 P95 打到 SLO 80% 对应的并发作为 max-num-seqs 起点;在线池与离线池分开部署,混跑会让两边的甜点同时消失。
  4. 量化以业务指标验收:结构化输出解析成功率、长上下文召回、拒答率三项,任一劣化超过 1 个百分点就退回上一档;困惑度只能作为初筛,不能作为上线依据。
  5. 把重试成本计入账单:每周用一批真实请求做 shadow 双跑(新配置 vs 当前基线),用 成本 / (1 - 重试率) 复核,避免优化只优化了名义成本。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。