☰
大语言模型推理加速实战:显存、量化、KV Cache与调度优化
2026/10/27 14:23:11 网站建设 项目流程

大语言模型的优化与加速推理部署,是这两年被问得最多、也是踩坑最密集的一块。我见过太多团队在 Demo 阶段一切顺利,一上生产就发现单张 80GB 的卡连 20 个并发都扛不住,首字延迟动辄三四秒,月底账单比预算翻了三倍。大部分人第一反应是"加卡"或者"换个更大的机器",但我实际排查下来,八成的问题根本不在硬件,而在于对推理过程的理解不到位——权重该量化到什么程度、KV Cache 占了多少显存、批处理调度是怎么排队的、哪些算子还在用低效实现,这些没有一项算清楚,堆多少卡都是白搭。

这篇内容是我把大语言模型推理加速从算法侧到工程侧的一条完整链路重新捋了一遍,包含显存账本的手算方法、量化的精度边界、KV Cache 的管理技巧、调度策略的取舍逻辑,以及上线后压测和监控的具体做法。适合两类人看:一是正在做模型服务化、被延迟和成本两头夹击的后端和算法同学;二是准备做技术选型、需要判断"哪条优化路线值得投入"的技术负责人。基础概念我会用生活化的类比带过,不会一上来就堆公式,但只要你想抄作业,里面的参数和配置都能直接拿去用。

1. 先算账:大模型推理慢在哪,钱花在哪

优化这件事最怕的是上来就动手。你得先知道瓶颈卡在哪一环,否则很可能花两周时间把一个只占 5% 耗时的环节优化了 30%,整体提升 1.5%,然后怀疑人生。我一般会先做两件事:拆延迟来源,算显存账本。

1.1 一次解码步的延迟由什么决定

大模型的推理分两个阶段,这两个阶段的性能特征完全不同,很多人混为一谈,导致优化方向跑偏。

预填充阶段处理的是你输入的整段 prompt,所有 token 可以并行计算,本质是一次大规模的矩阵乘法。这个阶段的瓶颈在算力, GPU 的计算单元会被打满,显存带宽反倒是次要的。它的耗时决定了你的首字延迟,也就是用户点下发送到看见第一个字的时间。

解码阶段是逐 token 生成的,每生成一个 token 就要跑一次完整的前向。关键在于,这一步每次只处理一个 token,但要把全部权重从显存里读一遍。这就是所谓的"带宽墙"——算力单元大量闲置,时间全花在搬运数据上。举个大白话的比方:预填充像是给一整个班级批改卷子,人多力量大;解码像是每次只改一份卷子,但每次都要把整摞教材从头翻一遍。

这个区别带来的直接后果是:解码阶段的速度上限由显存带宽决定,而不是算力。一个 7B 模型用 FP16 存权重是 14GB,跑在带宽 2TB/s 的卡上,理论最短单步耗时就是 14GB ÷ 2000GB/s ≈ 7ms。折算下来单请求最高大约 140 token/s,这还是理论值,实际打个六七折很正常。你就算换成算力翻倍的卡,只要带宽没翻倍,单请求速度几乎不会变。这个结论我反复验证过,很多人换卡后发现单条请求速度没提升,就是这个原因。

1.2 显存三段式占用:权重、KV Cache、运行时开销

搞清楚延迟,再来看显存。一张卡能被吃满,通常是三部分叠加的结果,我习惯把它们分开算:

组成部分影响因素是否随并发增长常见误区
模型权重参数量 × 精度字节数否,固定以为量化只省权重,忽略 KV
KV Cache层数、KV 头数、头维度、序列长度、并发数是,线性增长按平均长度估,不留峰值余量
运行时开销框架缓冲、临时张量、通信缓冲、CUDA 上下文弱相关完全不算,最后 OOM 找不到原因

权重是最容易算的:参数量乘精度字节数。7B 模型 FP16 是 14GB,INT8 是 7GB,INT4 大约 3.5GB 到 4GB(算上量化组参数和缩放因子会多一点)。

KV Cache 才是真正的隐形杀手,它的公式是:

KV 显存 = 2 × 层数 × KV头数 × 头维度 × 序列长度 × 并发数 × 每元素字节数

注意前面的 2 是因为要同时存 Key 和 Value 两份。这个量随序列长度和并发数线性增长,长上下文场景下它会轻松超过权重本身。

运行时开销最容易被忽略。我见过一个案例,权重加 KV 算下来只用了 62GB,但实际跑起来 74GB 就 OOM 了。多出来的部分是框架预分配的缓冲池、通信库占用的显存、以及最坑的——显存碎片。这部分保守留 10% 到 15% 比较安全。

1.3 手算示例:一张 80GB 卡能扛多少并发

拿一个具体的例子走一遍,后面所有优化决策都建立在这个账本上。

假设模型是 32 层、32 个注意力头、头维度 128、参数量 7B,用 FP16 部署,目标上下文长度 4096,单卡 80GB。

先算每个 token 的 KV 开销:

2 × 32层 × 32头 × 128维 × 2字节 = 524288 字节 ≈ 512KB / token

那么单条 4096 长度的请求需要 4096 × 512KB = 2GB。80GB 的卡,扣掉权重 14GB,再扣 12GB 运行时预留,剩 54GB,理论上能放 27 条。听起来还行?

但这里有个陷阱:你不能按 4096 去规划,因为请求长度是分布的。真实业务里可能 90% 的请求只有 300 token,但剩下 10% 会飙到 3000 以上。如果你按平均值去分配,峰值一来立刻 OOM;如果按最大值分配,平均利用率又低得可怜。这就是为什么后面的分页管理机制那么重要,它解决的正是这个"预留了用不上、不预留又不够"的两难。

这里插一个我踩过的坑:早期做容量规划时,我把max_model_len设成了业务理论上限 8192,结果框架会按这个长度去预留 KV 空间,并发直接掉到个位数。后来把max_model_len压到实际观测到的 P99 长度(大约 5000),并发立刻翻了几倍,长尾请求走单独的实例池。不要用理论上限去做资源配置,要用真实分布的分位数,这是我交了学费才记住的一条。

2. 量化不是万灵药:把精度损失控制在你敢上线的范围内

算完账,最直接的优化手段就是量化——降低权重和激活值的数值精度,换取更小的显存和更高的带宽利用率。这块的坑特别多,因为量化的收益是立竿见影的,但质量损失往往是渐进的、藏在长尾里的,等到用户投诉"模型变笨了"才发现,已经上线两周了。

2.1 权重量化与权重激活量化的分水岭

量化方案按"量化谁"分成两大类,理解这个区别比记住具体算法名重要得多。

仅权重量化(比如权重用 4 位整数、激活保持 16 位浮点)的核心收益是显存和带宽,计算时还需要反量化回高精度。它实现简单、几乎不挑模型架构,而且因为激活没被量化,精度损失通常很小。在解码这种带宽受限的场景下,收益几乎拉满——权重从 14GB 降到 4GB,单步读取时间理论上降到三分之一。

权重和激活都量化(比如 8 位整数对 8 位整数)的额外收益是能用上低精度的矩阵乘法指令,预填充阶段会明显加速。但它对激活值里的离群点非常敏感。大模型的激活分布有个古怪特性:大部分数值很小,但某些通道会冒出比其他通道大几十倍的极端值,直接按全局最大值去量化,小数值全被压成零,模型立刻失智。所以这类方案必须做逐通道缩放或者离群点分离,工程量明显更大。

我的选择逻辑很朴素:如果瓶颈在解码速度、显存吃紧,优先做权重量化,性价比最高;如果预填充占了大头(比如长文档摘要、批量离线任务),再考虑权重激活都量化。

2.2 校准数据决定成败:几个典型翻车现场

后训练量化需要一批校准数据来统计数值范围,这一步看似机械,实际上是最容易翻车的地方。我整理了几个真实遇到过的情况:

  • 校准集领域跑偏。用通用网页语料校准一个医疗问答模型,通用语料里根本没有那些专业术语的分布,量化后模型在专业问题上的准确率掉了一大截,而通用测试集上几乎看不出变化。
  • 校准集太干净。全是规范书写的句子,没有口语、没有错别字、没有超长表格,结果用户随便输入一个带乱码的文档,输出就开始胡言乱语。
  • 样本量走极端。有人图省事只用 16 条样本,统计出来的范围极不稳定,换个随机种子结果都不一样;也有人用上万条,跑了大半天收益微乎其微。经验上 128 到 512 条是比较合适的区间,覆盖主要的输入形态即可。
  • 忽略了系统提示词的影响。很多业务在每条请求前面拼一大段系统提示,这段文本的激活分布和用户输入完全不同。校准集里如果没有包含它,量化后模型对指令的服从度会下降。

提示:校准集必须和真实流量同分布,并且要包含你的系统提示词模板。取一批线上真实日志(脱敏后)比任何公开数据集都管用。

2.3 量化后的质量回归怎么做才有说服力

量化一定会掉点,"掉多少能接受"这个问题必须用量化指标回答,不能靠"感觉还行"。我一般分三层验证:

第一层是困惑度对比,在同一份留出集上跑原始模型和量化模型,看差距。这是个快速筛查,涨幅在 2% 以内基本安全,超过 5% 就要警惕。

第二层是任务级指标。如果业务是信息抽取,就看字段准确率和格式合规率;如果是客服问答,就看人工抽检的满意度;如果是代码生成,就看通过率。这一层才真正反映业务影响,困惑度好看但任务指标崩了的情况我见过不止一次。

第三层是长尾与对抗样本。专门挑那些容易出问题的输入:超长文本、多语言混杂、需要精确数字计算的、需要严格遵循格式的。量化模型在这些地方最容易露馅,比如该输出 JSON 时少个大括号,或者把金额算错一位。

这里有个细节值得说:对比测试一定要固定解码参数。有些人图快用贪心解码做对比,有些人用采样,两边不一致,结果波动比量化本身的影响还大,测了等于没测。

2.4 精度、显存、吞吐的实测对照

下面这组数据来自一个 7B 级别模型在 80GB 卡上的实测,单请求、输入 512、输出 256,数字取的是多轮平均,仅供量级参考:

精度方案权重显存单请求输出速度相对质量(以 FP16 为 100)适用场景
FP16约 14GB基准 1.0x100质量敏感、显存充裕
INT8 仅权重约 7GB约 1.7x99 左右通用业务的首选
INT4 仅权重约 4GB约 2.6x96 到 98显存紧张、并发优先
INT4 高精度混合约 4.5GB约 2.3x98 到 99精度要求较高的降本方案

倒数第二行值得多解释一句:所谓混合,是把少数敏感层(通常是第一层、最后一层和某些归一化层)保留在高精度,其余层量化。代价是显存稍大一点点,但质量损失明显更小。这个做法我在多个项目里用过,是性价比很高的折中。

最后提醒一句:量化对小模型的影响远大于大模型。同一个方案,用在 70B 上可能几乎无损,用在 1.5B 上可能直接崩掉。小模型本身的参数冗余少,容错空间小,做量化时样本量和评估都要更保守。

3. KV Cache 是长上下文服务的真正天花板

前面算账时已经看到,KV Cache 在高并发长上下文场景下会远超权重成为显存大头。这一块的管理水平,往往决定了一个推理服务能不能从"20 并发"跨到"100 并发"。

3.1 一个公式算清 KV Cache 的账单

再强调一次那个公式,因为它值得你抄在便利贴上:

单 token KV 字节数 = 2 × 层数 × KV头数 × 头维度 × 精度字节数 总占用 = 单 token 字节数 × 序列长度 × 并发数

关键在于公式里的KV 头数,不是注意力头数。这就是分组查询注意力机制的用武之地:让多个查询头共享同一组 Key/Value 头。如果把 32 个头的 Key/Value 压缩到 8 组,单 token 的 KV 占用直接降到四分之一。

举个对比:同样是 4096 长度的请求,不共享时单条约 2GB,共享到 8 组后单条约 512MB。这意味着同样的显存能多扛 4 倍的并发。这个改动对模型质量的影响很小(训练时就是这么设计的),但如果不做,你的显存就白白浪费了。

顺着这个思路,还有一个更激进的方案是让所有查询头共享一组 Key/Value,压缩比更高,但质量损失相对明显,一般只在极端受限的场景考虑。

3.2 分页管理与前缀复用带来的并发红利

传统的 KV Cache 分配方式是"按最大长度预分配一整块连续显存"。这带来两个严重问题:一是内部碎片,一个请求实际只用 300 token 却占着 4096 的空间;二是外部碎片,跑久了显存被切得七零八落,明明总量够却分配不出来。

后来主流的做法借鉴了操作系统的虚拟内存思想:把 KV Cache 切成固定大小的块,按需分配,用一张映射表把逻辑位置和物理块对应起来。这样请求实际用多少就占多少,碎片问题基本消失,显存利用率能从三四成提到八九成。这是我认为近两年推理引擎里最有价值的一项工程改进,收益比很多花哨的算法都实在。

在这个基础上还能延伸出前缀复用:如果大量请求共享同一段前缀(比如固定的系统提示、同一份长文档),这段前缀算出来的 KV 可以直接缓存复用,不必每条请求重算。在客服、RAG 问答这类场景里,系统提示动辄上千 token,复用之后首字延迟能降一半以上。

我做过一个粗略对比:一个带 1500 token 系统提示的问答服务,开启前缀复用后,TTFT 的 P50 从 900ms 降到 380ms 左右,效果非常直接。前提是你的请求格式要规整,前缀必须严格一致,哪怕多一个空格都命不中。

3.3 多头共享、KV 量化与淘汰策略的组合

除了分页,还有几个叠加手段,可以根据场景组合:

KV 量化。把 KV Cache 从 16 位降到 8 位,显存直接减半,而且因为 KV 的数值范围相对稳定,精度损失通常比权重量化还小。需要注意的是 KV 量化的缩放因子要按头或者按块统计,全局统计容易翻车。降到 4 位就要谨慎了,长上下文时误差会累积。

分层淘汰。不是所有历史 token 都同等重要。有些实现会把久远的、注意力权重低的 KV 块换出到低速存储,需要时再换回来。这招在超长上下文(几万 token)场景下有用,但引入了额外的数据传输,延迟会有抖动,不适合对稳定性要求极高的在线服务。

滑窗注意力。只保留最近 N 个 token 的 KV,更早的直接丢弃。显存是恒定可控了,但模型会"忘事",适合那种只关注近期上下文的场景,比如连续对话里的闲聊。

这里要提醒一点:这些手段不是叠加越多越好。每叠加一层,系统的可解释性就下降一层,出问题时排查难度成倍上升。我一般建议先上分页和前缀复用这两项通用收益大的,再根据实际瓶颈决定要不要动量化。

3.4 长文档问答场景的一次调参记录

说个具体的。之前做过一个合同审阅场景,输入动辄两三万 token,输出只有几百 token,对首字延迟敏感、对生成速度不敏感。

第一版配置很朴素:max_model_len设成 32768,并发上限设 8,结果显存是够的,但 TTFT 到了 12 秒,完全没法用。排查发现两个问题:

一是预填充阶段没有分块,一次性把三万 token 全塞进去,中间产生的临时激活张量把显存顶爆的边缘反复横跳,框架不得不降低并行度。把预填充切成每块 2048 token 之后,峰值显存下来了,TTFT 降到 5 秒左右。

二是并发数设得太乐观。8 条三万 token 的请求同时跑,虽然显存勉强装得下 KV,但预填充阶段互相抢算力,导致每条都很慢。改成先让长请求排队、限制同时进行的预填充数量后,虽然排队时间增加了,但单条 TTFT 稳定在 3 秒,用户感知反而更好。

最后的配置是:上下文上限 32768,预填充分块 2048,KV 用 8 位量化,开启前缀复用(合同模板部分复用),单实例稳定支撑 6 到 8 个并发,TTFT 的 P99 控制在 4 秒以内。这套参数不一定适合你,但排查思路是可以复用的:先看峰值显存,再看并行度,最后看排队策略。

4. 调度层:吞吐和延迟的跷跷板怎么压才不翻

显存和算力是硬件给的,调度策略则是软件层面能榨出的额外收益。这一层的核心矛盾是:吞吐和延迟天然对立。批得越大,单位时间处理的总量越多,但每条请求的等待时间越长。找到业务能接受的那个平衡点,才是调度要干的事。

4.1 静态批处理为什么会被连续批处理取代

最早的做法是静态批处理:攒够一批请求,一起送进去,等这一批里最长的那个生成完了,整批才返回,然后开始下一批。问题很明显——如果一批里混进一个要生成 2000 token 的请求,其余只要 50 token 的请求就得陪跑,平均延迟被长尾彻底拖垮。而且批里短请求结束后,它们的算力槽位是空着的,利用率很低。

连续批处理的思路是:每生成一个 token 就检查一次,谁结束了谁就退出,腾出的位置立刻塞进排队的请求。这样算力槽位几乎不会空转,吞吐能提升几倍,同时短请求也不用陪跑。

这个改动的收益有多大?我在一个中等规模模型上测过,同样硬件、同样流量分布,静态批的平均吞吐大约是每秒 600 token,连续批能到每秒 1900 token 左右,提升超过三倍。代价是调度逻辑复杂了,而且批内请求长度差异大时,单步耗时会波动。

4.2 分块预填充:让长 prompt 别把短请求堵死

持续批处理解决了生成阶段的排队问题,但预填充阶段还有个大坑:一个三万 token 的请求进来,预填充要占用整块算力好几百毫秒,这期间所有正在解码的请求全部停滞,表现为延迟毛刺。

解决办法是把长 prompt 的预填充切成小块,和生成请求混在一个批次里跑。每一轮里既有几块预填充,也有一批解码,生成请求的延迟抖动被摊平了,长请求的总耗时略微增加但可接受。

这个策略的调参重点在分块大小:太小了调度开销占比高,太大了抖动脉冲还在。我一般从 512 或 1024 起调,观察 TTFT 的 P99 和生成延迟的 P99 是否都在可接受范围内。有一条经验:如果你的业务里长短请求混跑严重,这个开关基本必开,否则长尾请求会导致大量超时,用户侧看起来就是"这个服务时不时卡一下"。

4.3 投机解码的收益边界

投机解码是这几年比较热的一个加速思路,逻辑是:用一个小模型(草稿模型)快速猜出后续几个 token,然后让大模型一次性验证这几个 token,验证通过的直接采纳。因为大模型一次前向可以并行验证多个位置,如果猜中率高,相当于一步生成好几个 token。

实测下来的收益区间大概是 1.5 到 2.5 倍,但这个收益只在低并发时成立。原因在于:投机解码本质是拿算力换带宽,低并发时算力本来就闲着,正好拿来验证;高并发时算力已经吃满,验证多个位置反而拖慢整体。我测过的一个场景是,单请求场景加速 1.9 倍,但并发到 32 时,总吞吐反而下降了 8%。

所以判断标准很清楚:你的服务是低并发高延迟敏感(比如内部工具、代码补全),还是高并发吞吐优先(比如对外 API)。前者适合上,后者别上。

另外还有一类变体不用草稿模型,而是从输入里找重复片段直接匹配,适合有大量引用原文的场景(比如文档问答、代码改写)。这类方案实现简单、没有额外显存开销,猜不中就退回正常解码,属于低风险尝试。

4.4 四个必须盯住的指标:TTFT、TPOT、吞吐、P99

调度调优如果没有指标,就是盲人摸象。我固定看这四个:

指标含义典型目标反映的问题
TTFT首 token 延迟交互场景 1s 内预填充效率、排队长度
TPOT相邻 token 间隔30-50ms解码速度、批大小是否过大
吞吐每秒总 token 数越高越好,但不能牺牲上面两个资源利用效率
P99上述指标的 99 分位TTFT 的 P99 不超过 P50 的 3 倍调度抖动、长尾

特别要强调 P99。平均值好看但 P99 崩掉的服务,用户体验是灾难性的——用户记不住那 95 次顺畅,只记得那 5 次卡到怀疑人生。我见过一个服务平均 TTFT 只有 400ms,但 P99 是 8 秒,投诉率居高不下,最后查出来是长请求把队列堵住了。

调参时的顺序建议是:先定 P99 的容忍边界,再在这个约束下最大化吞吐。反过来做(先追吞吐再看延迟)几乎必然要返工。

5. 算子与编译:把每一微秒的启动开销抠出来

到了这一层,优化对象从"整体策略"下沉到了"单个计算图"。这部分收益不像量化那样立竿见影,但在特定场景下(尤其是小模型、小批次)能带来显著改善,而且往往是压死性能瓶颈的最后一根稻草。

5.1 注意力为什么要分块并做在线 softmax

标准的注意力计算需要先算出完整的注意力分数矩阵,再做 softmax。问题在于,序列长度是 4096 时,这个矩阵是 4096 × 4096,单单存它就要几十 MB(还要乘上头数和批大小),而且中间结果要反复在显存和计算单元之间搬运,带宽全浪费在这上面了。

改进思路是分块计算 + 在线 softmax:把序列切成若干块,每次只加载一块,边算边更新 softmax 的归一化因子和累计结果。这样中间矩阵的规模被限制在块大小以内,大部分计算都在高速缓存里完成,显存读写量大幅下降。

这个优化的收益随序列长度增长而放大。短序列(几百 token)时可能只快 10% 到 20%,长序列(上万 token)时能快两三倍,并且在长上下文下显著降低显存峰值。所以如果你的业务是长文档处理,这一项基本是必选项。

需要注意的一点:分块实现和你的头维度、精度方案要有对应优化版本。有些组合(比如很特殊的头维度、非标准精度)可能没有高度优化的实现,会悄悄回退到普通版本,性能不升反降。上线前一定要实测,别假设"用了就快"。

5.2 算子融合与图捕获在小模型上的价值

深度模型的一步前向会调用几十到上百个小算子:矩阵乘、加偏置、归一化、激活、残差相加……每个算子单独启动一次,都要付一次调度开销,大约几微秒。

对大模型来说这点开销不算什么——单步耗时几十毫秒,几微秒可以忽略。但对小模型就完全不同了:一个 0.5B 的模型,单步耗时可能只有 0.5 毫秒,如果启动 100 个算子,光调度开销就 0.5 毫秒,占了一半。这时候你会看到一个诡异现象:理论算力明明很充裕,实际速度就是上不去。

解决办法有两个。算子融合是把相邻的、可以合并的计算合成一个算子,比如把"矩阵乘 + 加偏置 + 激活"合成一个,减少启动次数。图捕获是把整个前向的执行序列录下来,后续直接回放,把调度开销降到接近零。后者对小批次、固定形状的场景效果极好,但要求输入形状稳定,变长场景需要配合填充或分桶。

这里有个实操经验:图捕获对动态形状敏感,如果你的批大小和序列长度每轮都在变,捕获会频繁失效,反而更慢。比较稳妥的做法是按长度分几个桶(比如 512、1024、2048),每个桶捕获一份,请求进来先分桶再执行。

5.3 多卡并行:张量并行、流水并行怎么选

单卡放不下模型时就得切分,主流两种切法。

张量并行是把每一层的矩阵按列或按行切开,分到不同卡上,每层计算都需要卡间通信。它的优点是单条请求的延迟低(所有卡同时算同一层),缺点是通信频繁,对互联带宽要求高。一般优先在单机内做,跨机做张量并行通信开销会吃掉大部分收益。

流水并行是把不同层分到不同卡上,像流水线一样,第一张卡算完前几层传给下一张。它的通信量小得多(只在层边界传一次),但单条请求延迟高——因为同一时刻只有一张卡在干活,其余在等。要填满流水线得靠多个微批次错峰,适合吞吐优先的离线场景。

我的选择逻辑是:在线低延迟选张量并行,离线大吞吐选流水并行,两者也可以组合使用。但组合之后调优复杂度陡增,除非规模确实大,不然我更倾向于"单机张量并行 + 多副本负载均衡"这个简单结构,运维成本低得多。

还有个容易忽略的点:多卡之后,单卡的显存反而更紧张,因为每张卡都要存一份通信缓冲和框架开销。所以切分之后能省下的显存没有理论值那么多,规划时要留足余量。

5.4 端侧与纯 CPU 部署的另一条路

不是所有场景都有加速卡可用。私有化交付、离线设备、成本极度敏感的场景,经常要求在纯 CPU 上跑。

这条路的核心思路和 GPU 完全不同。GPU 上我们关心的是带宽和并行度,CPU 上要关心的是内存带宽、指令集优化和量化程度。几个关键点:

  • 量化必须更激进。CPU 上通常直接上 4 位甚至更低,因为 CPU 的内存带宽远低于显存带宽,不量化根本跑不动。
  • 用上指令集加速。现代 CPU 有针对低精度整数的向量指令,配合专门优化的矩阵乘实现,能比朴素实现快好几倍。
  • 控制线程数和绑核。线程数开太多会互相抢缓存,实测往往是物理核数(不含超线程)附近最优。绑核能减少上下文切换。
  • 接受现实。CPU 上跑 7B 模型,输出速度通常只有每秒几个到十几个 token,只适合对实时性要求不高的场景,比如离线批处理、文档预处理。

如果一定要在端侧做交互式体验,更现实的做法是用 1B 到 3B 的小模型,或者做成"端侧小模型 + 云端大模型"的分层架构,简单请求端侧搞定,复杂请求再上云。

6. 上线之后的真实战场:压测、监控与一次扩容翻车复盘

前面五节讲的是"怎么优化",这一节讲"怎么验证优化真的有效"。我见过太多团队本地测出来数据漂亮,上线就崩,根本原因是压测方法有问题。

6.1 压测数据要贴近真实分布,否则全是自欺欺人

最常见的错误压测方式是:所有请求都用固定长度(比如输入 512、输出 256),然后逐级加压看吞吐曲线。这么测出来的数字毫无意义,因为它完全避开了真实流量的两大特征——长度分布的长尾和到达时间的突发性。

正确的做法是至少模拟三件事:

第一,输入长度服从真实分布。别用均匀分布,真实流量往往是长尾的:大部分请求很短,少数极长。可以从线上日志里采样,或者用对数正态分布拟合。

第二,输出长度也要分布。输出长度直接决定解码步数,是耗时的主要来源。很多压测忽略了这点,统一设成 100,结果严重低估了负载。

第三,到达模式要有突发。真实流量不是匀速来的,会有尖峰。用泊松到达太平滑了,建议加入突发模式,比如每秒来 5 个持续 10 秒,然后突然来 50 个持续 2 秒,看服务会不会雪崩。

再补充一个细节:压测一定要跑够长的时间。跑 30 秒看到的都是热态数据,跑 30 分钟才能暴露显存碎片、连接泄漏、缓存膨胀这类慢性问题。我一般至少压 1 小时。

6.2 监控大盘上必须有的几个信号

上线后如果没有监控,等于闭着眼睛开车。我固定会配这几类指标:

延迟类:TTFT 和 TPOT 的 P50、P95、P99,按模型和接口维度拆分。光看总体平均值会掩盖单个模型的异常。

资源类:显存使用率(尤其是 KV Cache 的占用比例)、计算单元利用率、显存带宽利用率。这里有个判断技巧:如果计算单元利用率很低但显存带宽很高,说明是带宽瓶颈,该考虑量化了;如果两者都不高,说明是调度或通信卡住了。

队列类:等待队列长度、请求排队时长、被拒绝的请求数。队列长度持续上涨是最早的预警信号,比延迟报警早得多。

业务类:每请求平均 token 数、超时率、报错率。这几个能帮你判断是流量变了还是服务坏了。

告警阈值不要照抄别人的。我的做法是先稳定跑一周,记录各指标的 P99,然后把告警线设在这个基线的 1.5 倍到 2 倍,既能提前发现异常,又不会天天误报。

6.3 复盘:并发从 20 调到 60 之后的全链路排查

说个印象深刻的案例。一个已经稳定运行两个月的服务,业务方要求支持更多并发,运维把并发上限从 20 调到 60,配置文件就改了一个数字。结果上线 20 分钟后开始零星报错,一小时后大面积超时。

排查过程按时间顺序是这样的:

第一步,看现象。报错是显存不足,但监控显示显存使用率只有 78%,远没到 100%。这个矛盾很关键——不是总量不够,是分配不出来。

第二步,查碎片。把 KV Cache 的块分配日志打开,发现大量块处于"已分配但利用率极低"的状态,平均每块只用了不到三成。原因是并发调高后,同时活跃的请求变多,而每个请求的序列长度差异很大,块被切得很碎。

第三步,定位配置。翻配置发现max_model_len还是最初的 8192,而新流量里有一批请求确实接近这个长度。框架在为每个新请求预留块时,会按最坏情况估算,导致短请求也被迫占用大块空间。

第四步,验证假设。把max_model_len降到 6144(实际观测到的 P99.5 长度),同时把并发上限回调到 45,重跑压测。显存峰值下降 11GB,碎片率降到 8% 以下,服务稳定。

第五步,根因归纳。真正的根因不是"并发太高",而是并发和单请求最大长度的乘积超过了显存能承载的 KV 总量。这个乘积才是真正的容量约束,单独调其中一个参数都会出问题。

这件事之后我改了两条规矩:一是容量规划必须算"并发 × 最大长度"这个乘积,不能只看单个维度;二是任何配置变更都要跑一遍完整压测,哪怕只改了一个数字。一个数字引发的故障,修起来往往要一整天。

6.4 灰度与回滚:给推理服务留一条退路

推理服务的变更特别容易出问题,因为它同时涉及模型、配置、代码三方面,任何一个改动都可能引发性能劣化,而且性能劣化往往是渐进的,不会立刻报错,可能表现为"慢了一点",等积累到用户投诉已经过去几天。

我的做法是:任何变更都走灰度。先切 5% 流量到新版本,观察至少两小时,重点对比 TTFT 的 P99、TPOT 的 P99、报错率和平均输出长度(如果输出长度突然变短,可能是模型在提前截断,说明出问题了)。指标平稳再扩到 20%、50%、100%。

回滚通道要提前准备好,而不是出事再想。具体来说:旧版本的镜像和配置要保留、模型权重不要删、路由切换要能在一分钟内完成。最忌讳的是"新版本部署时把旧版本覆盖了",真出问题只能干等着重新构建。

最后分享一个我认为最有用的小习惯:每次变更前,先写下"如果出现什么指标,我就回滚"。这句话逼着你在头脑清醒的时候定好判断标准,而不是在故障现场手忙脚乱地争论要不要回滚。我靠这一条躲过了好几次大事故——有一次新版本上线后 TTFT 只涨了 200ms,看着不严重,但因为提前写了"P99 涨幅超 30% 即回滚",果断切回去,后来查出来是新版量化配置有兼容问题,再晚半天就是全量事故。

这套东西说到底没有什么神奇技巧,就是把账算清楚、把指标盯住、把变更管住这三件事。我见过太多团队在算法层面反复折腾,却连自己服务的显存账本都说不清楚,那种情况下任何优化都是碰运气。先把第 1 节的那张表填满,再决定往哪个方向使劲,通常能省下一半的试错时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询