news 2026/5/26 11:39:44

LLM 推理加速工程实战:从KV Cache 到Continuous Batching,把吞吐拉满但不把延迟搞崩

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM 推理加速工程实战:从KV Cache 到Continuous Batching,把吞吐拉满但不把延迟搞崩

这篇文章写给做过线上推理服务的人:你可能已经把模型跑起来了,也知道“开缓存”“开 batch”“上 vLLM/SGlang”这些词;但真到线上你会发现:

  • 吞吐提升了,P99 延迟炸了;
  • KV cache 开了,显存不够了;
  • batch 大了,排队时间把 decode 省下来的全吃回去;
  • 模型换成更大/更小,系统瓶颈位置完全变了。

我想把这些“工程里真的会遇到的 tradeoff”讲清楚,并给一套可以直接复制的压测+指标口径。

0. 先把问题说清楚:你在优化的到底是什么?

我见过很多团队一上来就说“要提升 QPS”,然后开始堆机器、调 batch、换框架。最后上线一周:

  • 平均延迟看起来还行
  • P95/P99偶发尖刺
  • 用户抱怨“有时候特别慢”
  • 成本居高不下

原因是:LLM 推理服务通常要同时满足三个目标:

  1. 吞吐(Throughput):单位时间能处理多少 token / request
  2. 延迟(Latency):尤其是 TTFT(Time To First Token)和 P99
  3. 成本(Cost):显存/显卡利用率、单位 token 成本

它们是一个三角形:动一个角,另两个角经常会变形。

本文会用统一的指标口径来讲:

  • TTFT:从请求到第一个 token 输出
  • TPOT(Time Per Output Token):每个输出 token 的平均耗时(不含排队)
  • Queue Wait:排队等待调度的时间(batching 的副作用通常在这里)
  • Tokens/s:吞吐(每秒输出 token 数)

1. 推理的两个阶段:Prefill vs Decode(别混着优化)

LLM 推理可以粗暴拆成:

  • Prefill(又叫 prompt 处理):把输入 prompt 喂进去,构建 KV cache
  • Decode(逐 token 生成):每一步用 KV cache 继续生成下一个 token

在工程上这非常关键:

  • Prefill 更像“大矩阵乘法”,吞吐通常高,延迟与 prompt 长度强相关
  • Decode 更像“小步迭代”,每一步计算量小,但要做很多步,且更容易被调度/通信/内存访问拖慢

一个简单但很有效的做法:把压测拆成两类

  • 固定输出长度,扫输入长度(prefill 压测)
  • 固定输入长度,扫输出长度(decode 压测)

你会很快看到瓶颈到底在哪。

2. KV Cache:它不是“开了就快”,而是“开了就占内存”

KV cache 的本质:把 attention 里历史 token 的 Key/Value 存下来,避免每一步重算。

2.1 工程上最常见的坑:显存被 KV 吃光

很多人看到“KV cache 加速”,就默认开到最大。然后出现两类问题:

  • OOM:并发一上来就爆
  • 吞吐下降:为了不 OOM,把 batch/并发压低,整体吞吐反而下降

你需要一个“显存预算”的概念:

显存 = 模型权重 + 激活/临时 buffer + KV cache

而 KV cache 与以下因素线性相关:

  • 并发请求数(或同时 decode 的序列数)
  • 上下文长度(prompt + 已生成 token)
  • 层数、头数、head dim
  • dtype(fp16/bf16/int8 量化方案)

2.2 一个可以直接用的 KV cache 估算脚本(真实代码)

下面这段 Python 代码可以粗估 KV cache 占用(偏保守),你可以拿去给容量评审用。

# kv_estimate.pyfromdataclassesimportdataclass@dataclassclassModelCfg:num_layers:intnum_kv_heads:inthead_dim:intdtype_bytes:int# fp16/bf16 = 2defestimate_kv_bytes(cfg:ModelCfg,batch:int,seq_len:int)->int:# 每层 KV:K 和 V 各一份# shape ~ (batch, num_kv_heads, seq_len, head_dim)per_layer=2*batch*cfg.num_kv_heads*seq_len*cfg.head_dim*cfg.dtype_bytesreturnper_layer*cfg.num_layersdefhuman(n:int)->str:forunitin['B','KB','MB','GB','TB']:ifn<1024:returnf"{n:.2f}{unit}"n/=1024returnf"{n:.2f}PB"if__name__=='__main__':# 以一个常见 7B-13B 量级的配置举例(请按你的模型改)cfg=ModelCfg(num_layers=32,num_kv_heads=32,head_dim=128,dtype_bytes=2)forbatchin[1,4,8,16]:forseqin[512,2048,8192]:kv=estimate_kv_bytes(cfg,batch=batch,seq_len=seq)print(f"batch={batch:>2}seq={seq:>5}-> KV ~{human(kv)}")

你会发现:上下文长度从 2k 到 8k 是 4 倍,并发从 4 到 16 也是 4 倍,叠加就是 16 倍。

这就是为什么“我只是把 max_tokens 调大一点”可能会让线上直接炸。

3. Batching:吞吐的灵丹妙药,也是 P99 的头号杀手

batching 的逻辑很简单:把多个请求合并,让 GPU 一次算更多。

但线上最常见的问题是:

  • 你 batch 越大,排队时间越长
  • 你为了吞吐把 queue 拉长,TTFT 变差,用户感觉“卡”

3.1 Continuous Batching(连续批处理)为什么是关键

传统 batching 是“凑齐一批再算”,会导致等待。

Continuous batching(vLLM、SGLang 等框架里常见)是:

  • GPU 一直在跑
  • 新请求可以插进来
  • 旧请求生成完就退出

它的价值是:在不牺牲太多吞吐的情况下,显著改善 TTFT 和 P99。

3.2 一个最小可用的“队列 + 批处理”模拟(真实代码)

这段代码不是为了精准模拟 GPU,而是让你在白板上解释清楚:为什么 batch 会拖慢 P99。

# batch_queue_sim.pyimportrandomdefsimulate(arrival_rate,service_ms,batch_size,duration_s=10):"""Rough simulation: Poisson arrival + batch service."""now=0.0end=duration_s*1000queue=[]latencies=[]whilenow<end:inter_arrival=random.expovariate(arrival_rate/1000.0)# msnow+=inter_arrival queue.append(now)whilelen(queue)>=batch_size:batch=[queue.pop(0)for_inrange(batch_size)]start=max(now,batch[0])finish=start+service_msfortinbatch:latencies.append(finish-t)now=finishifnotlatencies:returnNonelatencies.sort()p50=latencies[int(0.50*len(latencies))]p95=latencies[int(0.95*len(latencies))]p99=latencies[int(0.99*len(latencies))]returnp50,p95,p99,len(latencies)if__name__=='__main__':random.seed(42)forbsin[1,2,4,8,16]:r=simulate(arrival_rate=50,service_ms=40,batch_size=bs)print(f"batch={bs:>2}->{r}")

你会看到一个趋势:batch 越大,吞吐上去了,但尾延迟会被排队拉长。

工程上真正要做的是:

  • 给 batching 一个最大等待时间(max wait / batching window)
  • 给交互式请求更高优先级(例如 chat vs batch job)

4. 你应该如何压测:别只看 QPS,至少看这 6 个指标

一个可落地的压测方式是:用一个脚本同时输出

  • request/s
  • tokens/s
  • TTFT
  • TPOT
  • P95/P99
  • GPU 利用率(sm%、mem%、显存占用)

4.1 一个可直接跑的压测客户端(Python + httpx)

假设你的服务是一个 OpenAI-compatible 的/v1/chat/completions,支持stream=true

# loadgen.pyimportasyncioimporttimeimportjsonimportstatisticsimporthttpx API_URL="http://127.0.0.1:8000/v1/chat/completions"MODEL="your-model"PROMPT="""你是一个严谨的工程师。请用 3 点总结 continuous batching 的优缺点,并给出一个线上调参建议。"""defnow_ms():returntime.time()*1000asyncdefone(client:httpx.AsyncClient,max_tokens=256):t0=now_ms()ttft=Noneout_tokens=0payload={"model":MODEL,"stream":True,"max_tokens":max_tokens,"messages":[{"role":"user","content":PROMPT}],}asyncwithclient.stream("POST",API_URL,json=payload,timeout=120)asr:r.raise_for_status()asyncforlineinr.aiter_lines():ifnotline:continueifline.startswith("data: "):data=line[len("data: "):]ifdata=="[DONE]":breakobj=json.loads(data)delta=obj["choices"][0]["delta"].get("content")ifdeltaisnotNone:ifttftisNone:ttft=now_ms()-t0# rough token estimate by chars; replace with tokenizer in prodout_tokens+=max(1,len(delta)//4)t1=now_ms()total=t1-t0returnttftortotal,total,out_tokensasyncdefmain(concurrency=10,seconds=30):ttfts,totals,toks=[],[],[]asyncwithhttpx.AsyncClient()asclient:start=time.time()asyncdefworker():whiletime.time()-start<seconds:ttft,total,out=awaitone(client)ttfts.append(ttft)totals.append(total)toks.append(out)awaitasyncio.gather(*[worker()for_inrange(concurrency)])defp(xs,q):xs=sorted(xs)returnxs[int(q*len(xs))]print(f"requests={len(totals)}")print(f"avg_total_ms={statistics.mean(totals):.1f}p95={p(totals,0.95):.1f}p99={p(totals,0.99):.1f}")print(f"avg_ttft_ms ={statistics.mean(ttfts):.1f}p95={p(ttfts,0.95):.1f}p99={p(ttfts,0.99):.1f}")print(f"tokens_total={sum(toks)}tokens/s={sum(toks)/seconds:.1f}")if__name__=='__main__':asyncio.run(main(concurrency=20,seconds=30))

这份脚本的价值在于:它会把 TTFT 单独拉出来,让你看到 batching/排队的真实代价。

5. vLLM / SGLang / TensorRT-LLM:工程选型时我会看什么

这里不做“文档复述”,我只说上线会遇到的点:

5.1 你的瓶颈是算力还是调度?

  • 如果 GPU 算力吃满(SM 利用率高),但 tokens/s 仍不够:考虑量化、算子融合、TensorRT-LLM
  • 如果 SM 利用率不高,但延迟大:多半是调度/queue/IO/CPU 端瓶颈,先把 batching 和服务架构理顺

5.2 KV 管理策略

  • PagedAttention 这类方案能缓解碎片化,但不是免费午餐:会引入额外管理开销
  • 对长上下文,prefix caching / prompt cache(复用系统 prompt / 业务模板)往往比“无脑扩显存”更划算

5.3 多租户/多模型

一个现实问题:线上不是只有一个模型。

  • 多模型共享 GPU:调度更复杂,容易互相干扰
  • 多模型分 GPU:资源更浪费,但稳定

我更倾向的策略是:

  • 交互式主模型独占一组 GPU(保证 P99)
  • 批处理/离线模型用另一组 GPU(吞吐优先)
  • 需要弹性时,再做跨池迁移

6. 线上调优清单(我真正会按这个顺序做)

按优先级:

  1. 先把指标口径打通:TTFT、TPOT、queue wait、tokens/s
  2. 拆 prefill/decode:分别压测,不要用一个平均值糊弄
  3. 给 batching 加上上限:batching window + 最大并发
  4. 做显存预算:权重/kv/buffer,明确最大上下文与最大并发
  5. 把请求分类:交互式 vs 批处理,走不同队列/不同 GPU 池
  6. 再考虑框架/量化升级:否则你可能在错误的瓶颈上花 2 周

7. 结语:优化推理不是“换个框架”,是把系统当系统看

推理加速的本质是:

  • 你在做一个有排队、有调度、有资源竞争的在线系统
  • LLM 只是其中最贵、最显眼的那个组件

当你把 TTFT/TPOT/queue wait 拆开看,把 KV cache 当成显存预算的一部分,把 batching 当成排队系统的一部分,很多“玄学”就会变成可解释、可调参、可复现。

如果你愿意进一步做工程化:

  • 把压测脚本接入 CI,做回归
  • 把线上参数变更纳入变更流程
  • 给 P99 配置 SLO + 自动扩缩容

你会发现:推理性能这件事,不再靠“某个同学经验很强”,而是靠体系。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/26 11:39:30

Mistral大模型实战:从推理部署到LoRA微调的生产级指南

1. 这不是“又一篇大模型教程”&#xff0c;而是一份 Mistral 实战手记&#xff1a;从模型特性到生产级调用的完整链路 你点开这篇内容&#xff0c;大概率不是为了再听一遍“Mistral 是开源的、性能强、上下文长”这种泛泛而谈。我过去一年半里&#xff0c;在三个不同行业的实…

作者头像 李华
网站建设 2026/5/26 11:39:11

鸿蒙4.0内核逆向实战:符号恢复、SVC校验与IPC漏洞分析

1. 这不是教你怎么“黑”鸿蒙&#xff0c;而是告诉你安全团队每天在盯什么2024年底&#xff0c;我参与了一个面向国内头部终端厂商的鸿蒙生态安全支撑项目。任务很明确&#xff1a;不碰应用层沙箱、不越权调用API、不测试用户态App行为——只聚焦在鸿蒙4.0内核态&#xff08;Li…

作者头像 李华
网站建设 2026/5/26 11:39:07

高保真三路音调控制电路:从Baxandall到精密独立调节的工程实践

1. 项目概述&#xff1a;为什么我们需要一个三路音调控制电路&#xff1f;在音频发烧友和DIY爱好者的世界里&#xff0c;音调控制电路一直是个既基础又充满挑战的领域。基础的Baxandall电路已经流行了半个多世纪&#xff0c;它通过简单的负反馈网络实现了高音和低音的调节&…

作者头像 李华
网站建设 2026/5/26 11:39:02

如何打造你的终极数字阅读自由体验?开源阅读鸿蒙版完整指南

如何打造你的终极数字阅读自由体验&#xff1f;开源阅读鸿蒙版完整指南 【免费下载链接】legado-Harmony 开源阅读鸿蒙版仓库 项目地址: https://gitcode.com/gh_mirrors/le/legado-Harmony 你是否曾为寻找一款真正懂你的阅读应用而烦恼&#xff1f;是否厌倦了被固定书源…

作者头像 李华
网站建设 2026/5/26 11:38:48

PlantUML Server终极实战指南:文本驱动UML绘图的完整解决方案

PlantUML Server终极实战指南&#xff1a;文本驱动UML绘图的完整解决方案 【免费下载链接】plantuml-server PlantUML Online Server 项目地址: https://gitcode.com/gh_mirrors/pl/plantuml-server PlantUML Server是一个基于开源PlantUML语言的在线UML绘图工具&#x…

作者头像 李华
网站建设 2026/5/26 11:38:44

自动化治理系统递归失控:一次由自我指涉引发的服务雪崩复盘

1. 项目概述&#xff1a;当治理系统“自己管自己”时发生了什么&#xff1f;几年前&#xff0c;我参与设计并部署了一套用于管理大型分布式微服务集群的自动化治理系统。它的核心愿景很美好&#xff1a;通过预设的策略和规则&#xff0c;让系统能够自动处理服务发现、流量调度、…

作者头像 李华