
1. 第七十一期的位置为什么系统学习后半程要重点啃推理优化追过这套 LLM 大模型系统学习指南的朋友应该知道前面的期数一直在做两件事把模型从原理层面拆开再把训练链路跑通。到了第 71 期我打算换一个视角——不再关心模型是怎么训出来的而是关心模型是怎么伺候用户的。说得直白点就是推理优化和部署落地。这件事很多人误以为只是敲几行启动命令等真上手才发现它涉及的显存管理、调度策略、精度取舍每一个单拎出来都够写一篇长文。为什么这个阶段值得单独占一期因为训练和推理虽然共用同一套模型结构但优化目标完全相反。训练时你希望吞吐尽量高、显存尽量被榨干容忍一定程度的延迟波动推理时你面对的是一个个真实用户每个请求都在等你的第一个 token延迟的毛刺会直接变成用户感受到的卡。而且推理成本是持续性的模型每响应一次都在烧钱同样的 GPU 资源能扛多少并发直接决定这个项目能不能长期跑下去。我见过不少团队模型效果调得很好最后死在推理成本压不下来或者并发一高就 OOM非常可惜。这一期适合什么人看如果你已经能独立微调一个中小规模模型但对把它变成稳定服务还没有完整方法论那这篇内容正好补上这块拼图。如果你还处在入门阶段也不要紧我会尽量把底层机制讲成人话你哪怕暂时不实操也能建立起一个正确的全局认知推理优化到底在优化什么为什么同样一块卡别人能服务的用户数比你多好几倍。这一期不求把每个细节讲到极致目标是帮你把推理这条链路的关键节点全部点亮后续再遇到具体问题你知道该往哪个方向查。2. 慢从哪来自回归解码的串行本质与 KV Cache 显存账本2.1 一个 token 一个 token 蹦出来的硬约束先聊一个很多人忽略的事实大模型生成文本的方式本质上是一个一个 token 往外蹦的。假设模型在生成一句话第 10 个 token 的计算必须依赖前面 9 个 token 的结果无法并行。这个特性叫自回归解码是当前绝大多数生成式语言模型的底层逻辑也是推理慢的最根本来源。你可以在任意一次请求里直观感受到这个约束模型从你输入整段提示词到吐出第一个 token这段时间叫首 token 延迟它由一次大规模前向计算决定通常还能接受但之后每生成一个 token都要做一次完整的前向推理token 一个接一个串行产出生成 500 个 token 就意味着至少 500 次串行计算。所以决定用户体验的不只是模型快不快还有一个很实在的指标——每秒能吐多少个 token。有的模型跑得快单次前向计算只需要几十毫秒但生成长文时用户还是觉得慢关键就在这个串行节奏。2.2 KV Cache 的显存账本算给你们看串行带来的另一个问题是浪费。既然每次生成新 token 都要重新看一遍前面的 token那前面这些 token 的中间结果能不能缓存下来复用能。Transformer 架构在做注意力计算时每个 token 都要产生一对 Key 和 Value 向量它们编码了这个 token 在当前位置的上下文信息。如果你不缓存每生成一个 token前面所有 token 的 K、V 都要重算一遍浪费大量算力缓存下来每次只计算新 token 的 K、V然后和缓存拼接这就是 KV Cache 的基本思想。显存消耗有多大我给你算一笔实际账。假设一个 70 亿参数规模的模型常见配置是 32 层 Transformer、隐藏维度 4096模型权重用 FP16 存储。每缓存一个 token 的 KV需要的显存大约是层数 × 2K 和 V 各一份× 隐藏维度 × 每个数值的字节数也就是 32 × 2 × 4096 × 2 字节约等于 0.5 MB。听起来不多但上下文越长越惊人2048 token 的上下文KV Cache 就要吃掉约 1 GB如果把上下文加到 8192光缓存就是 4 GB。你以为显卡主要被模型权重占着实际上上下文一长KV Cache 的分量一点都不小。这就是为什么同样一个模型部署时有人在 context length 上抠得非常紧——不是不想支持长文本是显存不支持。2.3 把显存账本补齐权重、激活值与缓存三方分账很多人在显存规划上只盯着模型权重这是一个非常容易踩的误区。完整的显存开销要分三块看权重、激活值、KV Cache。以那个 70 亿参数的模型为例FP16 权重约 14 GB70 亿参数 × 2 字节看起来是大头但实际服务中激活值和 KV Cache 会在峰值时刻反超。激活值是指前向计算过程中每一层产生的中间张量它和 batch size 直接相关batch 越大越吃显存KV Cache 则和总上下文长度直接相关。三者在同一张卡上争抢资源任何一个失控都会触发 OOM。理解了这三块账再回头看那些推理框架做的事情思路就清楚了它们无非是在省权重显存量化、省 KV 显存缓存管理和显存复用、省激活值显存梯度检查点思路的推理版本以及控制 batch 调度这几个方向上做文章。后面几节的内容全是围绕这个账本展开的。3. 服务化的两次架构升级动态批处理与 KV Cache 分页管理3.1 静态批处理为什么喂不饱 GPU单请求串行已经是瓶颈但只处理单请求GPU 的利用率其实很低。因为一次前向计算里矩阵运算的大头是批量矩阵乘一个请求的矩阵乘远没有塞满计算单元。很自然的想法是多个请求同时进来拼成一个 batch 一起算。这就是批处理也叫 batching。早期的推理服务大多用静态批处理攒够 N 个请求一起前向计算等这批全部生成完再放下一批进来。这看起来合理但有一个致命问题——每个请求的长度和生成速度不一样。有的请求上下文短几秒就生成完了有的请求要求生成 800 个 token得跑几十秒。静态批处理要等最慢的那个请求彻底结束整个 batch 才能释放。你去看 GPU 利用率的监控曲线会发现大量时间片被浪费在等待掉队者上吞吐根本拉不起来。3.2 Continuous Batching把一整批拆成流水线后来社区的主流做法变成连续批处理思路和工厂流水线很像不再要求一批请求同时进、同时出而是每个请求独立推进它每生成一个 token 就算完成一步某个请求生成完了立刻把这个位置腾出来让排队的新请求补进去参与下一轮计算。这样 GPU 每一轮前向都在为当前活跃的请求集合服务而不是为一个固定 batch 服务整体利用率大幅提升。用一句话总结两种方式的差异静态批处理是等全班交卷才下课连续批处理是谁交卷谁走新同学随时进教室。同样是 100 个请求静态批处理的完成时间是所有请求生成时长之和的某种最长路径连续批处理则能把吞吐拉高一个量级。这也是后来几乎所有主流推理服务框架都把连续批处理作为默认能力的原因。3.3 给 KV Cache 做虚拟内存分页管理连续批处理解决的是利用率问题但它也让显存管理变得更复杂。每个请求的 KV Cache 大小是动态增长的因为 token 是一个一个往外蹦的。如果部署时给每个请求预先分配最大可能长度的 KV 空间显存碎片和浪费会非常严重如果动态分配又面临高频内存分配和释放的开销以及碎片化问题。我最早看到工程上真正把这个问题解决干净是某个开源推理框架引入的分页注意力机制灵感直白得让人拍大腿——它把操作系统虚拟内存的分页思想搬到了 KV Cache 上把连续的 KV Cache 逻辑空间切成固定大小的块类似内存页物理显存上也按块分配逻辑上相邻的块在物理上可以散落各处通过一张块表来映射。这样每个请求只在真正需要时才申请新的块解决了动态增长的问题多个请求如果共享相同的前缀上下文还能直接共用同一批块省下一大块显存。这就像一个酒店房间不同客人入住时间不同、行李多少不同你不可能给每个客人预租一整栋楼而是按需给钥匙、按需加床。这套设计对长上下文的帮助尤其明显。之前 KV Cache 需要一次性预留足够空间很多请求实际用不到那么多白白浪费分页之后显存是按实际生成量逐步消耗的同样的显存能容纳的并发请求数量明显上升。3.4 Prefill 与 Decode 分离让不同阶段各跑各的批处理优化到这一步还有一个调度维度值得讲请求的生命周期其实可以分为两个阶段。第一个阶段是 Prefill也就是读取你输入的一大段提示词并计算它们的 KV 值特点是计算密集、显存开销大、延迟敏感第二个阶段是 Decode也就是逐 token 生成特点是访存密集、单步计算量小。这两个阶段的计算特征完全不同混在一起调度会因为互相拖累导致吞吐下降。现在主流方案里常见的一种做法是把 Prefill 和 Decode 拆到不同的调度策略甚至不同的计算单元上去处理Prefill 阶段用较大的 batch 做长矩阵运算把计算单元打满Decode 阶段用小步快跑的调度方式优先保证每个请求都能及时产出 token。实际测下来这种拆分能让首 token 延迟和后缘 token 生成速率同时受益属于那种原理讲了谁都懂但做了收益显著的工程优化。你在学习推理框架源码时重点看调度器是怎么把请求从 Prefill 队列挪到 Decode 队列的这一块的逻辑吃透了你对整个服务化架构的理解就成型了一半。4. 量化选型精度、吞吐、成本的三角权衡实践4.1 各家量化方案到底在干什么权重显存往往是大模型部署里最直观的支出项于是量化成了最热门的优化手段。量化的本质是用更少的数据位去表示权重数值FP16 用 16 位INT8 用 8 位INT4 用 4 位。位数越低显存占用越小计算也能更快但精度损失风险越高。现在主流方案大致分几类学习的时候不要只记名字要理解它们在解决什么问题。一种思路是训练后量化直接拿已经训练好的模型权重做转换重点在于怎么把浮点数值映射到低比特范围同时把误差控制在最小。最常见的是按组做缩放和偏移把一个矩阵切成若干小组每组独立计算一个缩放因子把这一组的数值塞进低比特范围。组切得越小精度越高但额外开销也越大128 和 64 是实践里最常见的两种组大小。另一种思路是看数据说话的量化不是对所有权重一视同仁而是先跑一批真实数据统计哪些权重通道对激活值影响大对这些关键通道保留更高精度对不敏感的通道大胆压到低比特。这种方案在不少场景里效果比均匀量化好因为语言模型的权重重要性本来就不是均匀分布的。另外还有一类思路是直接训练时就考虑低比特约束但成本高、链路复杂中小团队很少走这条路。4.2 我的四组对照实验怎么设计量化好不好不能只看一张 benchmark 表。我当时做选型时设计了一个简单的四组对照实验这个思路建议你也复用选用同一个 70 亿参数的模型固定同一批测试数据集分别在 FP16、INT8、按组 INT4、以及基于激活感知的 INT4 这四种配置下测了三件事——显存占用、推理吞吐、以及一段有明确事实性答案的任务上的回答准确率。结果符合预期也很有参考价值FP16 是基线INT8 显存几乎减半准确率下降几乎看不出来属于无脑可上的配置按组 INT4 显存进一步压到三分之一左右吞吐提升明显但在长文本生成和复杂推理任务上开始出现可感知的变差基于激活感知的 INT4 在某些任务上能追平 INT8 的水平但实现和调试复杂度更高对部署环境也有额外要求。这里我特别想说一点如果你只是在做内部演示或者短文本问答INT4 的精度损失通常可以接受如果你的场景涉及复杂的多步推理、数学计算或者严谨的事实抽取那么省下来的显存可能抵不上回答质量的下降量化前一定要做任务级验证而不是只信跑分。4.3 什么场景建议放弃量化聊到这里我必须泼一盆冷水不是所有场景都适合量化至少有三类情况我建议你保持 FP16。第一类是模型本身已经很接近能力边界你依赖的就是它那一点推理上限任何精度损失都可能让输出从勉强能用变成不可用。第二类是显存瓶颈其实不在权重而在 KV Cache 的长上下文场景——这种情况下你压权重的收益远不如去优化缓存管理来得直接。第三类是团队没有足够的评测条件无法确认精度损失到底有多大那就不应该在没有度量体系的情况下贸然上线一个精度未知的版本。另外提一个经常被忽略的新方向KV Cache 本身也可以量化。前面算过长上下文下缓存显存是大头现在有一些方案把 KV 从 FP16 压到低比特效果据说在可接受范围。我还没有在生产环境全面铺开但从账面上看这是比权重量化更有潜力的方向因为上下文越长收益越大。建议你把这块也放进学习清单等到方案成熟度上来直接用现成的经验。5. 上线前必须补的评测闭环指标分层与回归集维护5.1 从离线指标到在线体验中间隔着一层体感这也是我见过最多团队翻车的地方。模型部署上线了监控面板显示 GPU 利用率正常、平均延迟也合格但是用户反馈依然说变笨了回答变短了。问题出在哪出在评测指标选错了。推理服务的评测至少分成两层一层是系统性能指标另一层是回答质量指标两层都要管但管法完全不同。系统性能指标里最核心的三个是首 token 延迟、单 token 生成间隔、以及整体吞吐。首 token 延迟直接决定用户点了发送之后多久看到第一个字一般要求在几百毫秒到一两秒之间单 token 生成间隔决定输出节奏太慢会让人感觉在挤牙膏吞吐决定你的硬件成本能支撑多大并发。这三个指标在压测工具里都能测但要注意平均延迟会掩盖长尾问题我习惯上更关注 P95 甚至 P99 分位——那些最慢的请求往往才是用户真正会投诉的请求。质量指标的难点在于它是非确定性的。同样的输入两次调用结果可能不同所以不能只靠一个样例判断好坏需要一套相对稳定的评估方法。比较务实的做法是两类结合能自动判定的任务用自动化评测比如选择题、事实抽取、代码能否通过测试用例不能自动判定的任务就建立人工抽样打分规范定期抽一批线上请求按清晰度、忠实度、完整性几个维度打分。把这两个维度整理成一张表每次改动之后跑一遍你才能知道优化到底有没有用。5.2 回归集怎么维护才不骗自己评测最怕的不是没有指标而是指标失真。我见过某个项目回归测试集里全是训练阶段见过的题目模型答得漂亮上线后一遇真实问题立刻露馅。维护回归集的核心原则是要防泄漏和贴近线上。具体来说我给自己的项目定了三条规矩第一回归集里至少要包含一定比例的线上真实脱敏请求按周轮换第二每一类典型场景要有固定数量的钉子户样例专门盯易错点第三新增模型版本时禁止把新版模型生成过的优质回答再塞回测试集——那等于开卷考试。维护回归集的另一个关键点是版本管理。测试集本身要纳入版本管理每次评测记录模型版本、推理配置、量化参数、测试集版本四者缺一不可。否则你根本说不清某个指标变化是因为模型更新了还是因为换了量化档位还是因为测试集被人改了。这看起来像工程管理常识但在小团队里真的很容易被忽略最后排查问题时只能靠拍脑袋。5.3 评测结果反推优化方向的具体套路评测不只是为了判断能不能上线它还能帮你定位瓶颈在哪。我有一个固定的排查顺序先看 P99 首 token 延迟如果偏高大概率是 Prefill 阶段调度或显存的问题再看单 token 生成间隔如果偏高大概率是 Decode 阶段的访存带宽或者量化后计算瓶颈如果整体吞吐上不去去看并发度和 batch 调度如果回答质量下降回头查量化档位和上下文管理。这套顺序帮我避开过很多无效优化比如某次吞吐不达标我先去调量化参数折腾两天没效果后来发现是批处理调度没开启一个简单配置就解决了。线上和离线指标之间还有一个常用技巧把线上延迟指标和线上的回答长度做关联分析。如果发现低延迟请求的回答普遍偏短往往是因为服务在负载高时悄悄截断输出或者触发了某种应急机制。这种问题光看平均延迟看不出来但用户体验已经很差了。评测体系的成熟度往往就体现在这种别人想不到要查的地方上。6. 三次线上事故式的踩坑复盘碎片、假稳与带宽瓶颈6.1 事故一OOM 不是显存不够是碎片太多第一次遇到服务频繁 OOM 时我第一反应是显存不够连夜下单加卡。结果加上卡之后问题依旧才开始怀疑方向错了。后来把显存监控打开发现峰值显存远没到上限但分配请求却频繁失败典型的显存碎片化症状——大量小块空闲显存彼此不连续凑不出一个大的连续块。这个坑在长上下文高并发场景下特别容易踩每个请求的 KV Cache 动态增长频繁分配和释放很快就把显存切得稀碎。解决思路当时有两个方向一是换用带显存池和分页管理的推理框架让缓存分配走框架自己的复用机制二是限制单请求的最大上下文长度从源头减少极端分配。我把两个方向都做了碎片问题基本消失。这个复盘给我的教训是看到 OOM先看监控确认是总量不足还是分配失败这两个问题的解法完全不同。6.2 事故二压测 10 分钟看起来很稳是假象第二次事故是上线后第三天才爆发的。压测阶段我用固定的 prompt 长度、固定并发压了 10 分钟指标一片漂亮于是在完全信任的情况下上了生产。结果运行到第三天延迟开始周期性飙高最后直接把服务打挂。复盘时候查到一个关键变量实际线上的 prompt 长度分布是高度不均匀的长文本请求会在 Prefill 阶段抢占大量显存和算力导致后续请求集体排队。我压测时犯的错误是用平均长度 固定并发模拟完全没覆盖这种长尾冲击。后来我把压测方案改成三个阶段短文本高并发、长文本中并发、长短混合的随机波动并发每个阶段至少跑 30 分钟以上并且关注延迟的时序曲线而不是平均值。这套方案后来帮我提前发现了至少两个类似的稳定性隐患。要记住压测最怕的就是测了但没测到点上固定长度、固定并发、短时长的压测结果保守一定没坏处。6.3 事故三单机指标正常多机部署后吞吐反而下降第三次事故发生在想把服务从单机扩到多机的时候。单机一切正常多机部署后总吞吐没有线性增长反而在某些请求下变慢了。查了很久才定位到瓶颈多机之间的通信开销超过了并行计算带来的收益。大模型推理的多机并行本质是让多个设备分担一个矩阵运算每算一步都要同步一次中间结果同步频率非常高而跨节点的通信带宽远低于设备内部互联于是通信成了瓶颈。这次事故让我学会了在部署前先做一个简单评估如果模型规模在单机显存能容纳的范围内优先单机多卡甚至单卡部署不要为了听起来更高级而强行上多机如果确实需要多机尽量用模型分片而不是流水线的方式把通信量压低同时在压测里专门设计跨节点场景。这个经验现在回想起来非常简单但在当时它让我意识到部署架构跟着硬件带宽走这个朴素原则的重要性。说到最后我还是想强调那个最开始的观察跑通模型只是起点把模型跑稳、跑省、跑得可度量才是一个完整项目的真正分水岭。第 71 期讲的这条路从编码瓶颈到显存账本从动态调度到量化取舍再到评测闭环和故障复盘每一步都是在补从模型到服务之间缺失的那一环。如果你正在学这套系统指南我建议你学完这一期后亲手把自己手头某个模型的延迟指标测一遍把 KV Cache 的显存账也算一遍——这些动作比看十篇文章都管用。