前阵子我把手头一个基于13B开源模型的对话服务完整做了一遍推理优化,结果相当夸张:整机吞吐从每秒一百多个token,硬生生拉到了五千多,差不多就是标题里说的"50倍"。先别急着高兴,这里有个容易混淆的点——变快50倍的是同一块GPU上能同时服务的请求数(系统吞吐),不是用户感知的单次回答速度。单条对话的生成速度其实只翻了3倍左右,但整个服务的承载能力直接跨了一个数量级。
正是这一步,让很多原本只在内部Demo里跑跑的场景,真正变成了敢对外的业务功能。这篇文章把整个优化思路、实测数据、以及中间踩过的坑完整记录下来,给同样在部署大模型推理服务、被成本和并发卡住的朋友一个可以直接参考的路线图。
1. 性能瓶颈先定位:事故现场里最容易被忽略的两个数字
1.1 显存快满了,算力却在摸鱼
事情的开头不太体面。当时我接手的服务跑在一块40GB显存的服务器显卡上,模型是13B参数量的开源底座,权重用FP16存,光参数就要26GB。第一次做联调测试,10个测试用户同时进来,GPU直接亮了OOM的报错红灯。可更诡异的是,报错之前我看了一眼监控,算力利用率只有不到8%——显存都快撑爆了,算力却几乎在睡觉。
拆开看原因其实特别简单:大模型生成回复是自回归的,一个token一个token往外蹦。每蹦一个token,推理框架都要把模型全部参数从显存里读一遍。26GB的参数,以这块卡大约1.6~2TB/s的显存带宽来算,一条对话的理论上限也就每秒七八十个token;实际跑起来因为框架开销和调度损耗,我这边测到的是每秒28个token左右。
这个阶段本质上是显存带宽在卡脖子,不是算力。打个比方:仓库管理员接到一个订单,里面只要一根针,但他每次取货都必须在仓库里从头走到尾,把整座仓库的货柜全部过一遍才能拿到那根针。货柜数量(参数体积)不变,运货通道(显存带宽)不变,你订单再多,他也快不起来。当时最典型的症状就是:算力利用率低得可怜,显存却永远紧巴巴的,并发一往上加就爆内存。
1.2 预填充算得快,解码才是拖后腿的
大模型推理其实分两个阶段。第一阶段叫预填充,用户输入的prompt一次性喂进来,模型可以并行计算,这个阶段是"算力密集型",快得很,几百个字的输入通常半秒内就出结果。第二阶段叫解码,模型开始逐个生成回复的token,每生成一个,都要重新读一遍全部参数。这个阶段是"带宽密集型",想快也快不起来。很多刚接触推理优化的朋友会把注意力全放在如何加速计算上,结果发现瓶颈根本不在计算。
还有个被很多人忽略的隐形内存消耗——KV缓存。模型每处理一个token,都要把中间计算出来的Key和Value矩阵缓存下来,供后面所有token做注意力计算时复用。以我用的这个13B模型估算,每个token的KV缓存要占掉几百KB的显存。一个长度几千token的上下文,单条会话的KV缓存就奔着两三个GB去了。再乘上并行路数,KV缓存和模型权重在显存里互相抢地盘,这就是为什么并发一上来就OOM。好在现在不少模型在注意力头设计上做了分组共享,实际缓存占用已经比早期架构小了不少,但绝对量依然不容小觑。
KV缓存这一步,我在优化前几乎是放任不管的。结果就是:批处理大小只能压在4路左右,再多就爆显存。这等于把系统吞吐的天花板死死摁在了每秒一百来个token。后面所有的优化,本质上都是围绕一个目标——把"每生成一个token要搬运的数据量"压下来,同时把"一次搬运能服务多少条请求"提上去。
2. 四条主线的取舍:为什么能叠出50倍
2.1 量化:把每次搬运的货物体积砍到四分之一
第一刀砍的是参数体积。FP16存权重是16bit,换成常见的4bit权重量化,26GB瞬间变成6.5GB。这一步对解码速度的提升几乎是立竿见影的,因为解码阶段卡的是带宽,参数体积缩到四分之一,意味着同样通道里一次能多搬三倍的货。实测下来,单条对话的生成速度从每秒28个token拉到了90个token左右。
很多人担心量化伤精度,这个担心是合理的。我当时先做了个快速验证:拿一批和线上业务分布接近的文本做校准集,把量化后的模型在测试集上跑了一遍,质量和原版差距很小,个别长文本的连贯性略有下降,但对问答场景基本无感。遇到代码、数学这类"一步错步步错"的任务,就需要谨慎一些,后面我会单独讲踩过的坑。简单说,量化是用可控的精度折损换性能,方向正确,但落地时得有验证闭环。
2.2 连续批处理:一次权重读取服务64个请求
模型变小之后,显存腾出一大块,但这只解决了"能开多大batch"的一半问题,另一半是调度方式。很多早期实现用的是静态批处理:攒够一批请求才开始跑,跑完一批再处理下一批。问题是每条请求的长度和生成速度不一样,只要有一条卡在原地慢慢生成,同batch里所有人都得陪着等,GPU利用率自然上不去。
换成连续批处理(社区里通常叫动态批处理)之后,调度器不再等人齐:哪条序列生成完了,腾出来的位置立刻插进新请求。这样每一轮权重读取,服务的就是满编的64路请求,而不是把算力浪费在空位上。解码阶段本来就是"读一次权重、产出一批token",并发从4路提到64路,整机吞吐直接翻了十几倍。这里的关键认知是:批处理不只提高了效率,它让每一次昂贵的权重搬运都同时为更多请求服务,是解码场景下性价比极高的手段。
2.3 页式KV缓存:把碎片化的显存捡回来
batch开大之后,下一个拦路虎是KV缓存。前面算过,KV缓存占得比权重还凶,而且它是动态增长的——请求越长,缓存越多。如果按最大长度预留显存,大部分空间在大多数时候都是浪费的;如果按需分配,请求一个接一个地来,内存碎片又会让你眼睁睁看着显存还剩不少,却塞不进新的序列。
解决办法是像操作系统管内存一样管KV缓存:把缓存切成固定大小的小块(常见的是16个token一块),逻辑上连续的Key和Value,物理上可以散落在内存各处,用完就回收,新请求随时补位。这套页式管理让我印象最深的倒不是吞吐,而是显存利用率:之前统计下来有将近四成的KV显存是浪费掉的,改完之后这个数字掉到了个位数。省下来的显存,又变成了一批能同时跑的并发。显存这个东西,真到高并发时每一GB都值钱。
2.4 前缀缓存与投机解码:锦上添花的两招
等上面三板斧都落地,吞吐已经接近目标了。剩下两招是按场景选的加分项。前缀缓存适用于所有带固定系统提示词的场景——客服、AI助手、批处理任务几乎都有。模型每次处理请求都要重新计算一遍相同的系统提示词,纯属重复劳动。把相同前缀对应的KV缓存直接存起来复用,请求进来时只需要计算用户新增的那一段输入,预填充阶段的耗时能省掉一大半。
投机解码则是冲着单条延迟去的:用一个小模型先"草拟"出未来几个token,再交给大模型一次性验证。因为多个候选token共享同一次权重读取,如果小模型猜得准,等于是花一次带宽的钱,走了好几步路。这条对生成长度长、小模型命中率高的场景很管用,但也不是万能,后面踩坑部分再细说。总之这两招都属于"锦上添花",不要指望它们在最基础的数据搬运层面带来质变。
3. 实测数据:50倍是怎么算出来的,别被数字骗了
3.1 统一口径:TTFT、TPOT、吞吐、并发,一个都不能少
聊推理性能最怕口径不一致。我把优化前后的完整指标贴在下面,所有数据都是同一台机器、同一个模型、同一批压测脚本测出来的。先解释两个关键概念:TTFT是用户发完问题到看到第一个token的时间,TPOT是生成每个token的平均耗时,这两个决定"体感快不快";系统吞吐和最大并发决定"能接多少生意",这才是标题里50倍的来源。
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 单流TPOT(每token耗时) | 约35ms | 约11ms | 约3.2倍 |
| 单流解码速度 | 约28 token/s | 约90 token/s | 约3.2倍 |
| 最大并发路数 | 4 | 64 | 16倍 |
| 整机吞吐 | 约112 token/s | 约5700 token/s | 约50倍 |
| GPU算力利用率 | 约8% | 约85% | - |
| TTFT(512字输入) | 约800ms | 约350ms | 约2.3倍 |
看懂这张表,你就理解为什么我说"50倍"要加引号:单条请求的体感速度只快了3倍,但并发能力宽了16倍,两个一乘,整机吞吐自然就是50倍上下的量级。这不是偷换概念,对业务来说,"同样一块卡能扛住多少并发、每天能处理多少请求"才是最值钱的数字。有时候我们太关注单次响应快不快,反而忽略了服务整体能扛多大压力。
3.2 什么样的场景能拿到接近50倍,什么样拿不到
50倍不是白送的,它高度依赖场景。我压测下来,收益最大的是三类:多用户同时在线的客服和问答、超长输出(比如总结报告、长文生成)、以及带长固定系统提示词的业务(提示词可缓存)。这类场景吃满了并发提升和缓存红利,有的甚至能跑到接近60倍。
反过来,如果是低并发的个人工具、单次输出很短的代码补全,或者请求之间毫无公共前缀的零散调用,那优化主要吃到的是量化带来的3倍左右的延迟红利,吞吐上不去。想再压延迟,就得靠投机解码这类手段。所以拿到某个"50倍"的结论别直接抄,拿自己的场景压一遍才是正事。性能优化的第一步永远是定义清楚你的场景到底吃吞吐还是吃延迟。
4. 算力门槛降下来之后,应用场景怎么跟着扩
4.1 对外服务:客服对话从5路并发跨到50路
优化前这个服务只敢给内部测试用,理由是"并发一高就崩"。指标提升之后,我做的第一件事是把它接进了客服渠道的模拟项目。客服场景的特点是并发波动大、单次对话轮次多、系统提示词固定。连续批处理的好处在这里体现得最充分:没有人需要排队等到天荒地老,TPOT降到11毫秒左右,用户基本感觉不到延迟,而同一块显卡能同时撑住的对话路数,从5路级别直接跨到了50路级别。
成本账也很好算。以前为了撑住预期的峰值并发,我得预算两三块卡,还要留富余;现在一块卡跑到85%利用率还稳得很,硬件采购直接砍一半。对中小团队来说,这个变化是"用得起"和"用不起"的分界线。业务方看到压测数据之后,原本排期的十几个新功能一下子全部放行了——算力一够,产品想象力就跟着放开了。
4.2 离线批量任务:一夜跑完的活,现在按小时算
批量任务和在线服务不一样,它没有延迟要求,只看总吞吐。我有段时间接了个合同审核的小工具,要把上千份文档逐段做信息抽取和分类。优化之前这个活要跑一整晚,中间还得盯着怕OOM;优化之后同样的数据量几小时就跑完了。
原因也很直白:批量任务可以把并发开到极限,页式KV缓存把显存利用率顶上去之后,64路甚至更高的并发都能稳定扛住。而且批量请求的输入长度固定,前缀缓存几乎把同一套抽取模板的重复计算彻底抹掉了。这种场景下,50倍的吞吐提升就是字面意义上的"时间缩短50倍",体验相当直观。后来我把同样的思路用在了日志分类和数据清洗上,每轮迭代的实验周期从"隔天看结果"变成了"当天下午出报告",对研发节奏的改善非常明显。
4.3 私有化部署:单卡也能跑的轻量方案
量化带来的另一个好礼物是部署形态的变化。原来13B模型FP16要26GB显存,小公司想私有化部署,得专门买大显存显卡;INT4量化之后,6.5GB的参数加上KV缓存,现代消费级显卡就能跑得动。我后来在朋友那边搭过一个完全离线的内部知识库问答,用的就是单卡轻量方案,不依赖任何云端服务,数据也不用出内网。当时整个过程从买卡到上线也就花了一个周末,这在量化之前是不可想象的。
代价是并发和大卡上没法比,但对几十个人的内部使用场景完全够用。那条路线让我意识到,推理优化带来的不只是同一套系统变快,更是让类ChatGPT能力第一次出现在预算有限、设备普通的团队里。硬件门槛降下来之后,技术的普适性才会真正释放。
5. 踩坑记录:调优不是把手段全堆上,而是按顺序来
5.1 无脑全量INT4的第一步翻车
优化不是越激进的方案越好。我最初图省事,直接把所有层都压到4bit量化,跑到代码生成任务上立刻露馅:生成的代码结构性错误明显变多,缩进和括号配对这类"细节敏感"能力退化得很厉害。后来改成混合精度——绝大多数层用4bit,少数对精度敏感的层保持8bit或16bit——问题才缓解。
还有个教训是必须收集贴合业务的校准集。一开始我随手拿通用语料做校准,结果业务数据上的表现忽好忽坏。换成从线上日志里抽一批真实请求做校准之后,稳定性明显好多了。量化模型的精度验证,永远要用自己的数据说话,别人的经验只能当参考起点。
5.2 长上下文把KV缓存吃满,连续批处理反而卡死
有段时间为了支持"长文档问答",我把上下文窗口拉得很长。结果很尴尬:页式KV缓存虽然省显存,但长上下文的绝对消耗是省不掉的,几万token上下文的单条会话就吃掉好几个GB。并发一高,KV缓存直接把显存塞满,调度器想插新请求也没地方放,整机吞吐反而比短上下文时掉了一大截。
后来我按场景做了限制:在线客服控制在中等上下文长度,长文档任务单独拆成"先分段摘要、再全局问答"的流程。KV缓存的优化是帮你"压缩浪费",不是帮你"无中生有",该做的容量规划还是得做。这件事让我养成了习惯:每次调完一个优化项,都要重新画一遍显存账本。
5.3 投机解码在短请求上倒亏
投机解码刚上的时候,我测出来平均延迟反而变高了。排查发现原因不复杂——短请求总共就生成十几个token,小模型的草拟还没跑出优势呢,大模型验证一波就结束了;更麻烦的是小模型对某些领域词汇命中率很低,提议的token被大模型频繁否决,等于白白多跑几轮。后来我把投机解码改成按请求级开关:生成长度超过一定阈值、且小模型置信度高的请求才启用,短请求老老实实走普通路径,整体延迟才回到正常水平。
这个坑说明了一个通用道理:任何加速手段都不是免费的,它有自己的适用边界。用之前先问一句"这个手段解决的是我的瓶颈吗",能省掉大量调试时间。
5.4 我的推荐调优次序(按收益和成本排序)
最后把次序总结一下,我实际的落地顺序是这样的:
- 先立监控和压测口径。TTFT、TPOT、吞吐、并发、显存占用全部记录在案,后面每一步优化都有对照。
- 做权重量化,把参数体积压下来,这是收益最高、改动最小的一步。
- 上连续批处理,把并发路数提上去,吃满量化腾出来的显存。
- 做页式KV缓存,把碎片回收,进一步推高并发。
- 有固定系统提示词的场景加前缀缓存,长输出场景再评估投机解码。
一定不要一开始就全上。每一项手段都会引入自己的副作用,一次只动一处,对比压测数据,才能搞清楚到底是哪个改进在起作用,出了问题也知道去哪里回滚。我见过不少人一上来就堆满全套优化,最后性能没上去,反而连问题出在哪都找不到。
最后说点个人体会。这次优化做下来,最值钱的不是我学的那些加速技巧,而是把"以吞吐为中心"的思维立起来了。很多人一提大模型性能就盯着"单次回答快不快",但真正决定业务能不能跑起来的,往往是那块显卡每天能吐多少token。如果你也在搞推理服务,我建议先别急着照搬别人的"50倍",花一两天把监控和压测口径搭好,然后从量化开始,一步一步拿数据说话。等你自己压出那个倍数的时候,你对这套系统的理解,会比任何教程都扎实。