ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型高效推理与部署实战:从量化到连续批处理的全链路优化

大模型高效推理与部署实战:从量化到连续批处理的全链路优化

我现在看到大模型项目规模化上线,所有人都会遇到同一个奇怪现象:模型训练好了,指标也准,本地推理也很快,一放到服务端真并发就变慢,或者开始反复 OOM。我很少怀疑模型本身,而是先排查推理链路。这篇文章想把我实际部署大模型推理服务的完整思路从头到尾过一遍,重点记录哪些优化真正值得做,哪些其实是看起来唬人、收益有限的花活。标题虽然叫"人工智能:大模型高效推理与部署技术实战",但真正要掌握的核心不是某个现成 API 的参数,而是理解生成过程本身。这篇文章适合做后端、平台、算法工程的开发者,也适合被交办"把模型上线提速"的运维同学。

我拿一个内部的知识问答项目举例。模型是一个 7B 参数的纯文本生成模型,原本用原来训练时代的推理方式直连 GPU 做了简单服务,上线后首字延迟 4 秒多,并发稍微到 20 就出现排队和显存溢出。后来经过量化、显存规划、调度策略调整和压测调参,稳定在几十路并发下运行,首字延迟降到 1 秒以内,而且显存高峰期没有再爆过。整个过程没有用什么高深理论,都是能直接落地的常规手段,下面拆开说。

1. 推理慢在哪:先分清三类瓶颈

想优化推理,先得搞清楚一个请求从进来到底经历了什么。大模型生成过程和传统深度学习模型完全不一样,它不是一个前向就结束,而是先处理整段输入,再一个 token 一个 token 地生成。这两个阶段分别叫预填充和解码,瓶颈性质完全不同,绝大多数调优失误都是因为把它们混在一起谈。

1.1 预填充阶段:算力密集,不是显存密集

预填充阶段是用户输入进来的第一段处理。模型要看到完整的输入序列,然后做并行计算,为每一个输入位置生成中间状态,也就是后面的 KV Cache。这个阶段计算量非常大,尤其是输入很长的时候,大规模矩阵乘跑满 GPU 计算核心,延迟主要由算力决定。

比如一个 7B 模型,输入 512 个 token 的文本,预填充阶段要做的矩阵计算量可能接近生成阶段很多倍。这个阶段通常响应时间在后端统计里体现为首字延迟的一部分。之前很多团队喜欢把所有优化都压在服务框架上,但输入超长时,预填充本身的计算量摆在那里,不压它不行。

1.2 解码阶段:内存带宽才是天花板

等预填充算完,模型进入逐 token 自回归解码。这一步每生成一个新 token,都要把模型全部权重重新读一遍,同时也要反复读写不断增长的 KV Cache。单看一次计算量不大,真正浪费时间的是权重读取。

打个比方:预填充像是整页抄课文,处理器一直在动笔;解码阶段像查字典,虽然每个字都认识,但要不停翻整本词典才能写出下一个字。对于 7B 模型,权重如果用 FP16 存储大约是 14GB,每生成一个 token 都要把这 14GB 从显存搬到计算单元。哪怕显存带宽是 2TB/s 的级别,光搬运权重就要好几个毫秒,这还只是纯搬运时间。所以解码阶段提升空间非常有限,谁能让权重快速复用、谁减少冗余搬运,谁就是赢家。

1.3 两类瓶颈对部署策略的直接启示

从这里就能理解为什么普通 HTTP 请求+单次推理的模式不适合大模型在线服务:它在预填充阶段让 GPU 算得飞快,但一到解码阶段 GPU 算力大量闲置,带宽也喂不饱,整卡利用率非常难看。反过来,如果系统能同时处理多个请求,让一个请求在解码等待带宽的时候另一个请求在利用算力,整体的吞吐就能明显提升。这就是后面要说的批处理和调度的理论基础。

先看一张瓶颈对照表,后面所有优化手段都会围绕这几行来:

阶段主要瓶颈典型现象优先优化方向
预填充显存带宽外的计算能力首字延迟随输入长度快速上升压缩计算量、控制超长输入
解码显存带宽与权重搬运生成速度稳定但受限批处理、权重量化、KV Cache 复用
整体服务显存容量与调度策略并发一高就 OOM 或排队显存规划、连续批处理、超时策略

服务端性能优化,说穿了就是围绕这三行做文章。

2. 量化落地:不是无脑 INT8

好多文章一上来就喊"量化到 INT4",实际动手才发现效果完全看模型和数据集。量化不是把所有权重简单转成低精度格式那么简单,缩位之后的目标格式、缩放因子怎么定、校准集怎么选,每步都会影响最终效果。量化做得好的项目能省一半甚至更多的显存,做差了直接让生成结果胡言乱语。

2.1 权重量化的实测边界

我先从最稳妥的权重量化开始。常见思路是把 FP16 权重缩到 INT8 或 INT4,并且按一组权重共享一个缩放因子。实践中效果比较好的做法是分块缩放,通常每 64 或 128 个参数共享一个缩放因子,精度损失明显低于一整层共用一个缩放因子的粗暴方式。

在我部署的 7B 模型上,INT8 权重量化几乎无损,显存从 14GB 左右压到 7GB 多,生成质量在测试集上差异可以忽略。INT4 则能压到 4GB 左右,但某些专业领域的输出开始出现重复词和事实性错误。所以我的建议是:如果显存不是极度紧张,先用 INT8;真需要 INT4,也要保留原版模型做 A/B 对比,别全量切换。

2.2 激活量化的隐藏难点

只量化权重简单,把激活值也量化才是容易翻车的地方。激活值在不同 token、不同位置上分布差异很大,某些位置会出现离群值,导致量化误差被放大。这就需要准备一个和真实业务接近的校准数据集,尽量覆盖长文本、短文本、多轮对话等场景,然后在模型跑校准数据时统计激活值的分布范围,再据此决定缩放因子。

我自己踩过的一个坑是不做校准直接用一个默认缩放表,上线后发现一个完全和量化无关的领域开始输出乱码。后来重新用真实问答数据校准,问题才消失。如果你没有充足校准数据,就不要强行做激活量化,收益通常没有权重量化高,风险却大得多。

2.3 KV Cache 量化:显存大户不能漏

很多人算显存时只看到模型权重,等跑起来才意识到 KV Cache 增长有多快。KV Cache 是预填充阶段生成的中间矩阵,生成每个 token 都要往里追加数据。它的大小和层数、注意力头数、序列长度、并发数成正比。上下文一旦长,KV Cache 会占掉比权重还多的显存。

KV Cache 也能量化,通常做法是降低它的存储精度,比如从 FP16 降到 INT8,减少一半占用。这一步相对安全,因为 KV Cache 的价值主要存在于注意力计算中,恢复精度空间相对小,所以量化后质量下降通常不明显。但要注意,KV Cache 量化和激活量化类似,也需要考虑按通道或按头的缩放,不能简单一个缩放因子糊上去。

量化选择建议用一张表快速对比:

量化对象主要收益精度风险推荐场景
模型权重直接降权重占用低风险,INT4 需评估显存紧张必做
激活值提升计算效率高风险,需校准数据充分的项目
KV Cache大幅省显存中低风险长上下文服务必须考虑

3. 高效调度的隐藏战场:显存编排与批处理策略

量化把模型变小以后,下一道坎是调度。同一个推理服务同时收多个请求时,GPU 到底怎么分配计算资源和显存,直接影响吞吐和延迟。很多人只知道"加并发",但错误调度方式会让并发越高、效率越低,甚至互相拖累。

3.1 静态批处理的浪费逻辑

早期的一种做法是把多个请求攒成一个大批次,前向计算时一起处理,这就是静态批处理。听起来很合理,但它的明显缺陷在于:一批请求必须一起开始、一起结束。比如一个批次里有 3 个请求,其中一个已经生成完毕,另外两个还在慢慢解码,那这个 GPU 的算力还会留给已经结束的请求位置,形成空洞。

如果用动态批处理,请求按 token 粒度被调度计算,谁有新 token 要生成谁就上计算单元,已经完成的请求立刻让出位置。这就像食堂打饭:静态批处理是十个人一起坐下,有人吃完也不能走,要等最慢的人吃完才一起收桌;动态批处理是吃完一个走一个,新来的立刻坐空位。实际对比下来,动态批处理的吞吐能提升明显,尤其在请求长度差异大的场景。

3.2 连续批处理与逐 token 调度

现在主流推理服务基本都是"连续批处理"思路:服务端维护一个等待队列,GPU 每个计算步骤动态决定这一轮处理哪些请求的下一页 token。这样一来,每个请求都在按自己的节奏推进,短的先结束,长的继续跑。GPU 算力和显存不会因为一个慢请求而整体空转。

连续批处理对服务框架的显存管理提出了更高要求。因为每个请求需要的 KV Cache 是动态增长的,系统得能动态分配显存空间。实际工程里一般会引入类似分页的机制,把显存分成小块,按需分配给不同请求,而不是每个请求预留一整块。这能大幅减少显存浪费,让我在相同显存下扛住了更多并发。

3.3 前缀缓存:多轮对话和重复上下文的救星

知识问答、客服对话这类场景,用户经常基于同一份背景资料反复提问。每次请求都要重新预填充一遍相同的长文本,太浪费算力。前缀缓存就是针对这一点:把输入序列的中间状态按内容哈希缓存起来,如果新请求的前缀和之前的某次请求相同,直接复用缓存的 KV Cache,跳过预填充计算。

这个优化在短对话场景收益一般,但在长文档问答里收益极大。我见过一个项目把平均首字延迟从 3 秒多压到不到 1 秒,几乎全靠前缀缓存命中了相同的系统提示词和用户背景资料。接入成本不高,却是很容易被忽略的"性价比之王"。

4. 部署实战:从量化到服务化的完整链路

前面的原理讲得再多,最终都要落到一条完整的部署流程上。我以某知识问答系统为例,把从模型压缩到服务上线的完整链路走一遍。下面这些步骤都基于我们实际可行的常规方案,不是某个特殊环境专属的魔法。

4.1 第一步:模型压缩与校准准备

先确定量化目标和校准集。项目里输入主要是中长文档和问题,我准备了一组大约几百条样本的小校准集,尽量包含长文档、短问答、多轮上下文三种类型。然后做权重量化,优先 INT8;通过验证集对比,确认生成质量没有明显劣化后再决定是否继续压 INT4。当时显存还够用,所以停在了 INT8。

做这一步时一定要记录每个量化版本在标准测试样本上的输出结果,哪怕是肉眼对比也行。之后如果线上出现质量问题,至少能快速判断是不是压缩引的锅。

4.2 第二步:推理引擎配置

接下来需要在推理引擎里做好几件事。第一是设置最大并发批次,引擎不是越大越好,它受制于显存和输入长度上限,我通常先设一个偏保守的值,再通过压测逐步往上加。第二是开启连续批处理和前缀缓存,这两个默认开关不一定在,要在配置里显式打开。第三是把 KV Cache 分配成预占的显存池,避免运行时反复申请释放显存造成碎片。

模型权重也从"加载即显存常驻"改成类似"内存映射加载"的方式,减少冷启动时间和内存复制。经过这一步,同一个显存卡能容纳的并发请求数量明显上升,原来 20 并发就爆显存,现在能稳定跑更多。

4.3 第三步:服务接口与并发控制

推理引擎就绪之后,还需要一个对外服务层。这里我坚持用流式返回:模型每生成一页 token 就通过网络推给前端,用户看到的首字没变,但体验上感觉生成速度更快。流式响应同时能降低前端等待焦虑,也让后端不用等整个回答生成完才占用网络连接。

服务层要做两个重要控制:一是并发上限,超过引擎承载能力就去排队而不是无脑涌入;二是超时与重试,超时后立刻熔断,避免请求卡住把连接池占满。重试要加次数上限,防止风暴式重试把所有资源打垮。这一步看着简单,线上稳定性却基本靠它兜底。

4.4 第四步:监控与日志

部署完成不等于结束。我至少会盯三个指标:首字延迟、生成速度、显存峰值。这三个指标对应前面说的预填充、解码、显存容量三大瓶颈。服务层还要记录请求输入长度和输出长度分布,辅助判断调度是否需要进一步调优。

监控面板一旦出现"显存持续上涨但不回落",基本就是 KV Cache 没释放或显存碎片在堆积,需要马上处理。这个信号比响应时间变慢更早暴露问题。

5. 压测与调参:用数据说话

部署链路搭好后,绝不能直接上线,先压测。压测不是为了得出一张好看的报表,而是为了找到服务在真实负载下的性能拐点。很多人上线后才发现响应时间像过山车,就是因为没提前测出那个拐点在哪。

5.1 核心指标别只看 QPS

大模型服务的性能指标和常规接口不一样。除了 QPS,更关键的是首字延迟和生成速度,前者决定用户等待第一句话多久,后者决定整体回复速度。业内一般还会看尾部延迟 p99,因为大模型推理天然波动大,平均数好看没意义,最慢的那 1% 请求才是用户体验崩塌的根源。

我压测时固定输入长度和输出长度,分别测不同并发下的表现。低并发时首字延迟通常稳定,高并发时首字延迟会明显上升,因为新请求要等显存和算力让出来。关键是要找到"延迟还能接受"和"吞吐尽量高"的平衡点。

5.2 一次典型的调参过程

我用一种通用的调参流程来说明:先固定并发数为 1,测单请求基准延迟,确定服务质量上限;再把并发逐步抬高,每次倍增并观察延迟和显存;当 p99 延迟开始剧烈上升时,就降回前一档作为线上限速值。

最大批处理大小也是一个重要的旋钮。开太大,长请求会塞满 KV Cache 池,短请求反而进不来;开太小,GPU 算力又有闲置。我习惯从 32 开始,向上向下各测几轮,看哪一档吞吐高且 p99 稳。某些服务里把"动态批处理等待窗口"调大一点,能把同时到达的请求合并成批次,对短请求吞吐有明显帮助,但会让单个请求多等一小段时间,要忍受首字延迟小幅上升。

5.3 压测结果如何反推配置

压测结束后一定要形成一张对照表,记录不同并发下的表现,哪怕只有几行数字,上线后被问"为什么限制并发"时也有据可依。我自己常带一张简化版:

并发数首字延迟 p50首字延迟 p99生成速度显存峰值结论
10.8s1.0s24 token/s8GB基准
161.2s2.5s280 token/s12GB可接受
322.1s5.8s420 token/s15GB延迟超标

这里数字只做示例,但能清楚看出吞吐和延迟之间的跷跷板。线上我通常选择延迟还在可接受范围内的最高吞吐档位,而不是单追求吞吐最高,毕竟用户等不起。

6. 上线后最容易踩的几个坑

最后这部分是最想写的,因为很多坑只有真正上线跑一段时间才会暴露。调参时看着一切正常,流量一进来就原形毕露。这里列几个高频问题,每个我都在实际项目里遇到过。

6.1 显存碎片与长时间运行后的隐忧

服务连续跑几天以后,显存占用会缓慢上升,最后突然 OOM。最常见的元凶是显存碎片。原因在于 KV Cache 和临时计算缓冲区频繁申请、释放,显存被切得七零八落,新的大块申请反而失败。

解决办法不是简单重启,而是在启动时就做显存预分配,把 KV Cache 池、权重区、激活区提前圈好,运行中尽量从池子里取。如果条件允许,周期性低峰时段重新加载模型也能有效清理碎片,但这不是根治手段。监控里出现显存只涨不降,就要怀疑碎片了。

6.2 动态长度与 padding 浪费

输入长度参差不齐时,很多服务会统一 padding 到批次内最长长度。结果就是短请求被拉长计算,浪费算力。如果用的是连续批处理框架,这个问题会好很多,因为不再强制同步批次大小。但如果框架里某些算子仍然要求固定形状,那会在内部悄悄做 padding,性能一样受影响。

排查方法是在压测时故意混入长短差距很大的请求,观察生成的 token 吞吐有没有明显下滑。下滑就说明调度层有强制对齐,有必要升级到支持动态形状的算子。传统框架里这一步是无解的,只能在新框架里解决。

6.3 长尾请求与队头阻塞

在线服务里,一个特别慢的请求会把后续请求都堵住,这就是队头阻塞。大模型里更常见的是某个输出特别长的请求占住了一大批显存,让短请求反复排队。应对思路是给不同类别的请求设不同优先级和超时:普通问答给一个输出长度上限,超了就强行截断;内部高优请求走单独队列。截断操作要温和,最好在自然句子边界截,不要硬切。

6.4 模型热更新与回滚

模型迭代不可避免要替换版本。最忌讳的是直接停掉所有旧模型实例再起新实例,这会让线上出现一段空窗期。稳妥做法是新版本先灰度上线,流量逐步切换,发现问题立刻切回旧版本。前后端协议也要做到兼容,至少保证老请求不会打到新模型上产生格式错误。

我做这些项目的最大体会是:大模型高效推理与部署不是一个"调一个参数就完事"的活计,而是把量化、显存规划、调度策略、压测方法和线上监控串成一条链。每个环节单独看都不复杂,合在一起就能显著改变服务的稳定性。平时我还会保留一份压测脚本和显存趋势截图,团队里出现性能问题争议时,直接把数据贴出来比争论更有说服力。如果你刚接手类似任务,别急着调参,先把首字延迟、生成速度、显存峰值三个指标记录下来,然后从量化开始一步步做,多半很快能看到效果。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进