1. 大模型推理优化的核心命题与整体思路
1.1 推理优化到底在优化什么
很多人第一次接触LLM推理优化,脑子里第一反应是“让模型跑得更快”。这个理解不算错,但太粗糙了。实际做过线上服务的人都知道,推理优化从来不是单一维度的速度问题,它是一组相互拉扯的指标之间的平衡:首Token延迟(TTFT)、每Token输出延迟(TPOT)、吞吐量(Throughput)、显存占用、单位算力成本。你把这几个指标摆在一起看,就会发现它们天然存在矛盾——想让单次请求响应快,就得牺牲并发吞吐;想把吞吐拉满,单用户体感就会变差。
所以我在做任何优化之前,习惯先问一个问题:这个场景到底更在意什么?是在线对话类产品,用户盯着屏幕等第一个字出来,那TTFT就是命门;还是离线批量摘要、数据标注这类任务,用户根本不在场,那吞吐和成本才是核心。这两种场景的优化路径几乎是相反的。在线场景要优先保延迟,批处理(Batching)要谨慎;离线场景可以激进地堆Batch Size,把GPU吃满。
理解这一点之后,后面的所有技术手段才有落脚点。推理优化本质上是在给定硬件预算和SLA约束下,找到计算、显存、带宽三者之间的最优解。大模型推理之所以慢,根子在于它是显存带宽受限(Memory-Bound)而非算力受限的任务。每生成一个Token,都要把整个模型的权重从显存里读一遍,矩阵乘法的计算量相对访存量来说太小了,GPU的算力单元大量时间在等数据搬运。这就是为什么很多优化手段——量化、KV Cache管理、连续批处理——本质上都在做同一件事:减少单位Token的显存访问量,或者让显存访问被更多计算摊薄。
1.2 从“能跑”到“跑得好”的分层优化框架
我把推理优化拆成四个层次,从下往上依次是:模型层、引擎层、服务层、系统层。这个分层不是学术分类,是我自己在项目里排查问题时用的思路,哪一层出问题就往哪一层钻。
模型层是最底层的优化,包括量化(INT8、INT4、FP8)、剪枝、蒸馏、算子融合。这一层动的是模型本身,收益大但风险也大,量化掉点、精度损失都是常见代价。引擎层指的是推理框架的选择和配置,比如vLLM、TensorRT-LLM、SGLang这些,它们内部实现了PagedAttention、连续批处理、投机解码等机制。服务层是请求调度、路由、缓存、限流这些工程问题。系统层则是硬件选型、多卡并行策略、网络拓扑。
新手最容易犯的错是跳过模型层和引擎层,直接在服务层瞎调参数。我见过有人花两周调Batch Size和并发数,结果发现模型本身用的是FP16全精度,换成INT8量化直接吞吐翻倍。所以顺序很重要:先把模型和引擎这两层的地基打牢,再去做服务层的精细调度。
1.3 一个真实的优化目标拆解案例
假设你手上有一个7B参数的模型,部署在单张24GB显存的卡上,要支撑一个内部知识库问答系统,日均请求量几千次,高峰期并发大概20路。这个场景的优化目标可以这样拆:
- 显存:7B模型FP16权重约14GB,加上KV Cache和激活值,24GB卡勉强够用但没余量。所以量化几乎是必选项,INT8能把权重压到7GB左右,留出充足空间给KV Cache。
- 延迟:内部问答场景,TTFT控制在1秒内、TPOT控制在50ms以内,体感就够用了,不需要追求极致。
- 吞吐:20路并发不算高,但要有突发余量,连续批处理能显著提升GPU利用率。
这个拆解过程说明一件事:优化目标必须量化成具体数字,否则你永远不知道什么时候算“优化好了”。我习惯在项目开始就写一张指标基线表,优化前后对比,用数据说话。
2. 模型层优化:量化、精度与算子融合的取舍
2.1 量化为什么是性价比最高的第一刀
量化是我在所有推理优化项目里第一个动手的地方,没有例外。原因很简单:它直接砍掉了显存带宽压力这个最大瓶颈。前面说过,LLM推理是显存带宽受限的,权重从FP16降到INT8,显存访问量直接减半,理论上吞吐就能接近翻倍。而且现代GPU(比如带Tensor Core的架构)对INT8矩阵乘法有专门的加速支持,算力也不是问题。
但量化不是免费的午餐。核心矛盾在于精度损失。FP16到INT8,动态范围从约65504缩到127,模型里那些数值分布跨度大的层(尤其是Attention的某些投影层和FFN的中间激活)很容易溢出或者精度不够。我实测下来,权重量化(Weight-Only Quantization)比激活量化安全得多,因为权重分布相对稳定,而激活值随输入变化剧烈,量化激活往往掉点明显。
常见的量化方案我列个表对比一下,这是我踩过坑之后总结的:
| 方案 | 精度 | 显存节省 | 掉点风险 | 适用场景 |
|---|---|---|---|---|
| FP16 | 基准 | 0 | 无 | 精度敏感、显存充足 |
| INT8(W8A16) | 高 | 约50% | 低 | 通用推荐,首选 |
| INT4(W4A16) | 中 | 约75% | 中 | 显存紧张、可接受轻微掉点 |
| FP8 | 高 | 约50% | 低 | 支持FP8的新硬件 |
| GPTQ/AWQ | 中高 | 约75% | 中低 | 4bit场景的主流选择 |
提示:W8A16表示权重8bit、激活16bit,这是最稳妥的量化配置。W4A16虽然省显存,但在数学推理、代码生成这类任务上掉点会比较明显,选之前一定要在自己的评测集上验证。
2.2 量化实操:从校准到部署的完整链路
量化不是一句“加载INT8模型”就完事的,中间有个校准(Calibration)环节,很多人忽略它,结果量化后模型胡言乱语。校准的本质是:用一批有代表性的数据跑一遍模型,统计每一层激活值的分布范围,据此确定量化的缩放因子(Scale)和零点(Zero Point)。
具体步骤我按实际操作顺序写:
- 准备校准数据集:从你的真实业务数据里采样,一般128到512条就够。关键是分布要贴近线上输入,如果你用通用语料校准,但线上全是专业领域问题,量化误差会很大。我一般会混入一部分领域数据。
- 选择校准算法:常见的有MinMax、Moving Average、Percentile。MinMax简单但对离群值敏感,Percentile(比如取99.9%分位)更鲁棒,是我更常用的。
- 执行量化:以GPTQ为例,它逐层做量化,用Hessian矩阵指导权重更新,尽量补偿量化误差。AWQ则关注那些“重要权重”,对它们保留更高精度。
- 验证精度:这一步绝对不能省。我会准备一个包含50到100条问题的评测集,对比量化前后的输出,看困惑度(Perplexity)和实际任务准确率。困惑度涨了5%以内通常可接受,涨太多就得回退。
# 以GPTQ量化为例的伪代码流程 from transformers import AutoModelForCausalLM, AutoTokenizer from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig quantize_config = BaseQuantizeConfig( bits=4, # 量化位数 group_size=128, # 分组大小,越小精度越高但越慢 desc_act=False, # 是否按激活顺序重排,开启精度略高 ) model = AutoGPTQForCausalLM.from_pretrained( "your-model-path", quantize_config=quantize_config ) tokenizer = AutoTokenizer.from_pretrained("your-model-path") # 校准数据 calib_data = [tokenizer(text) for text in calibration_texts] model.quantize(calib_data) model.save_quantized("your-quantized-model")2.3 算子融合与KV Cache的显存账
除了量化,模型层还有两个常被忽视的优化点:算子融合和KV Cache管理。
算子融合指的是把多个小算子合并成一个大算子,减少Kernel Launch开销和中间结果的显存读写。比如LayerNorm后面接一个线性层,可以融合成一个算子。这个工作通常在推理框架内部完成,但如果你自己写推理代码,手动融合收益很明显。我实测过一个场景,把Attention里的QKV投影融合后,端到端延迟降了约8%。
KV Cache是另一个显存大户。自回归生成时,每生成一个Token都要缓存之前所有Token的Key和Value,显存占用随序列长度线性增长。一个7B模型、32层、隐藏维度4096,序列长度2048,Batch Size为1时,KV Cache大概是:
2(K和V)× 32层 × 2048序列 × 4096维度 × 2字节(FP16) = 2 × 32 × 2048 × 4096 × 2 ≈ 2.1 GBBatch Size到20,就是42GB,直接爆显存。所以KV Cache的量化(KV Cache INT8)和分页管理(PagedAttention)是必做的。PagedAttention把KV Cache切成固定大小的块,像操作系统管理内存页一样按需分配,碎片率大幅降低,这也是vLLM的核心创新之一。
注意:KV Cache量化对精度的影响比权重量化更敏感,因为Key和Value直接参与Attention计算。我建议先做权重INT8,KV Cache保持FP16,如果显存还不够再考虑KV Cache INT8,并且一定要验证长文本场景下的表现。
3. 引擎层优化:推理框架选型与批处理策略
3.1 主流推理框架的选型逻辑
选推理框架这件事,我的原则是看场景、看硬件、看团队,没有银弹。市面上主流的几个框架各有脾气:
- vLLM:PagedAttention和连续批处理的鼻祖,社区活跃,支持模型多,适合快速起步和通用场景。缺点是自定义算子不如TensorRT-LLM灵活。
- TensorRT-LLM:NVIDIA官方出品,算子融合和量化支持最深入,性能天花板高,但编译流程复杂,对模型改动敏感,适合追求极致性能且有工程能力的团队。
- SGLang:主打RadixAttention,对多轮对话、前缀共享场景优化极好,如果你的业务有大量重复前缀(比如固定System Prompt),它能显著提升缓存命中率。
- llama.cpp:CPU和边缘设备友好,量化方案成熟,适合本地部署和资源受限场景。
我一般这样决策:如果团队没有专门的推理工程能力,直接上vLLM,它开箱即用的性能已经能覆盖80%的场景。如果业务对延迟极度敏感、且愿意投入人力做编译优化,再考虑TensorRT-LLM。SGLang在特定场景(多轮对话、Agent)下优势明显,值得单独评估。
3.2 连续批处理:吞吐提升的关键机制
连续批处理(Continuous Batching)是我认为推理引擎里最重要的一个机制,没有之一。传统静态批处理要等一个Batch里所有请求都生成完才能处理下一批,短请求被长请求拖死,GPU利用率很低。连续批处理则是每个生成步都重新组批,一个请求生成完了立刻腾出位置给新请求,GPU几乎不空转。
这个机制带来的吞吐提升是数量级的。我实测过一个7B模型,静态批处理下吞吐约800 tokens/s,换成连续批处理后直接到3000 tokens/s以上。原因在于静态批处理时,Batch里最长的那个请求决定了整体耗时,短请求的算力全浪费了。
但连续批处理也有代价:调度开销和显存碎片。每个生成步都要做一次调度决策,请求多了调度本身也会成为瓶颈。所以框架通常会设置最大Batch Size和最大Token数上限,需要根据硬件调。
3.3 投机解码:用“草稿模型”换延迟
投机解码(Speculative Decoding)是降低延迟的一个巧妙思路。核心思想是:用一个小的草稿模型(Draft Model)快速生成多个候选Token,再用大模型一次性验证这些Token是否正确。因为大模型验证是并行的,比逐个生成快得多,如果草稿模型命中率高,整体延迟就能显著下降。
我实测下来,投机解码在输入输出有强模式的场景(比如代码补全、格式化输出)效果最好,草稿模型命中率能到70%以上,延迟降低30%到50%。但在开放式创作场景,命中率低,反而可能因为验证开销导致延迟上升。
配置上有几个关键参数:
- 草稿模型大小:一般是大模型的1/10到1/5,太小命中率低,太大失去加速意义。
- 投机Token数(Speculative Length):一次生成几个候选,通常4到8个,太多会浪费验证算力。
- 接受阈值:控制验证的严格程度,影响输出质量和速度的平衡。
提示:投机解码不是万能药,一定要在自己的业务数据上测命中率。我见过有人盲目上投机解码,结果因为草稿模型和主模型分布差异大,命中率不到30%,延迟反而涨了。
4. 服务层与系统层优化:调度、缓存与多卡并行
4.1 请求调度与优先级管理
到了服务层,问题就从“模型怎么跑得快”变成了“请求怎么排得合理”。线上流量从来不是均匀的,高峰期和低谷期差好几倍,而且请求的优先级也不一样——付费用户的请求和免费用户的请求,延迟要求可能完全不同。
我的做法是分级队列加动态配额。把请求按优先级分成几档,高优先级队列分配更多GPU时间片,低优先级队列在资源紧张时主动降级(比如降低Batch Size、关闭投机解码)。同时设置一个准入控制,当队列长度超过阈值时直接拒绝或排队,避免雪崩。
这里有个容易被忽视的点:超时设置。LLM生成是流式的,一个请求可能持续几十秒,如果客户端超时时间设得太短,用户看到的是“请求失败”,但服务端还在傻傻地生成,浪费算力。我一般会把服务端超时设得比客户端略长,并且在客户端断开时及时取消服务端的生成任务。
4.2 多级缓存:把重复计算挡在门外
缓存是服务层性价比最高的优化。LLM场景下有两类缓存特别有价值:
前缀缓存(Prefix Caching):很多请求共享相同的前缀,比如固定的System Prompt、知识库的检索结果。把这些前缀的KV Cache缓存下来,后续请求直接复用,能省掉大量重复计算。SGLang的RadixAttention就是干这个的,vLLM也支持Prefix Caching。我实测过一个知识库问答场景,System Prompt加检索上下文占了输入长度的60%,开启前缀缓存后TTFT降低了约40%。
语义缓存(Semantic Cache):把用户问题和对应的回答缓存起来,新问题来了先做语义相似度匹配,如果和缓存里的问题足够相似,直接返回缓存答案。这个对FAQ类场景效果极好,但要注意相似度阈值的设置,设太低会返回错误答案,设太高命中率又上不去。我一般用向量相似度0.95以上才命中,并且对时效性敏感的问题(比如“今天天气”)禁用缓存。
4.3 多卡并行:张量并行与流水线并行的选择
单卡放不下模型时,就得上多卡。两种主流并行方式:
- 张量并行(Tensor Parallelism, TP):把每一层的权重切分到多张卡上,每张卡算一部分,然后通信汇总。优点是延迟低,因为每层计算被分摊了;缺点是通信量大,卡间带宽要求高,一般用NVLink。
- 流水线并行(Pipeline Parallelism, PP):把模型按层切成几段,每段放一张卡,数据像流水线一样流过。优点是通信量小;缺点是会有流水线气泡(Bubble),GPU利用率下降。
我的经验是:单机多卡优先用TP,跨机用PP。因为TP对带宽要求高,跨机网络扛不住;PP通信少,适合跨机。如果模型特别大,两者可以混合使用。另外要注意,TP的度数不是越多越好,一般不超过8,超过后通信开销会吃掉并行收益。
| 并行方式 | 通信量 | 延迟 | 吞吐 | 适用场景 |
|---|---|---|---|---|
| 张量并行TP | 大 | 低 | 中 | 单机多卡、NVLink |
| 流水线并行PP | 小 | 中 | 高 | 跨机、大模型 |
| 数据并行DP | 无 | 低 | 高 | 多副本、高并发 |
5. 常见问题排查与避坑经验实录
5.1 显存溢出(OOM)的排查路径
OOM是推理部署最高频的问题,没有之一。排查思路我总结成一个顺序:
- 先看权重占用:模型加载后显存占了多少?如果权重就快把卡占满了,那必须量化。
- 再看KV Cache:按公式估算最大序列长度和Batch Size下的KV Cache,这是动态增长的部分,最容易爆。
- 然后看激活值和临时缓冲:推理框架会有一些临时显存开销,通常不大但别忽略。
- 最后看碎片:长时间运行后显存碎片会累积,PagedAttention能缓解,但重启服务仍是最简单的解法。
我遇到过一个典型案例:模型权重14GB,卡24GB,看起来够用,但一上并发就OOM。算了一下KV Cache,Batch Size到8、序列2048时就超了。解决办法是权重INT8量化到7GB,KV Cache开INT8,同时限制最大序列长度,问题解决。
5.2 输出质量下降的归因方法
优化之后输出变差,怎么定位是哪个环节的锅?我的方法是逐层回退:
- 先把量化关掉,用FP16跑,如果质量恢复,那就是量化的问题,需要调整量化配置或换方案。
- 如果FP16也差,检查是不是KV Cache量化或投机解码导致的,逐个关闭验证。
- 如果都关了还差,那可能是框架本身的实现问题,或者输入预处理有bug。
这个回退过程虽然笨,但最可靠。我一般会准备一个固定的评测集,每次改动后跑一遍,用数据对比,避免凭感觉判断。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 首Token延迟高 | 输入长、无前缀缓存 | 看输入长度分布 | 开启Prefix Caching、压缩Prompt |
| 吞吐上不去 | Batch Size小、无连续批处理 | 看GPU利用率 | 开连续批处理、调大Batch |
| 显存OOM | 权重+KV Cache超限 | 算显存账 | 量化、限制序列长度、PagedAttention |
| 输出乱码/重复 | 量化掉点、采样参数问题 | 回退FP16验证 | 调整量化、检查temperature/top_p |
| 多卡加速比低 | 通信瓶颈 | 看卡间带宽 | 换TP/PP策略、减少通信 |
| 长文本性能骤降 | KV Cache溢出、注意力退化 | 测不同长度 | KV Cache量化、滑动窗口注意力 |
5.4 几个反直觉的实操心得
最后分享几个我在实际项目里踩坑得来的、和常规认知不太一样的经验。
第一,不是所有场景都值得上量化。如果你的业务对精度极度敏感(比如医疗、法律),而显存又够用,那FP16跑着挺好,别为了省那点显存引入风险。我见过一个团队为了省显存上了4bit量化,结果在专业术语生成上频繁出错,最后回退到FP16,白折腾两周。
第二,Batch Size不是越大越好。大Batch能提升吞吐,但会拉高TTFT和TPOT。在线场景下,Batch Size超过某个点后,用户体感会明显变差。我一般会画一条“吞吐-延迟”曲线,找拐点,而不是无脑拉满。
第三,监控比优化本身更重要。优化是一次性的,但线上流量是变化的。没有完善的监控(TTFT、TPOT、吞吐、显存、GPU利用率、错误率),你根本不知道优化有没有效果,也不知道什么时候该扩容。我习惯在优化前就把监控埋点做好,用数据驱动决策。
第四,别忽视冷启动。模型加载、编译、预热都需要时间,如果服务频繁重启,冷启动开销会吃掉大量资源。我一般会做预热请求,服务启动后先跑几条典型请求把Kernel编译和缓存暖起来,再接入真实流量。
第五,Prompt长度是隐形的成本杀手。很多人只关注模型和框架,却忽略了输入Prompt的长度。输入长度直接决定Prefill阶段的计算量和KV Cache大小。我见过一个RAG系统,检索回来的上下文塞了8000个Token,其中一大半是无关内容,白白浪费算力。精简Prompt、做检索结果重排和截断,往往比调框架参数收益更大。
这些经验没有一条是教科书上写的,都是实际跑线上服务时一点点磨出来的。推理优化这件事,理论框架能帮你建立方向感,但真正的功夫在细节里——在每一次OOM的排查里,在每一条延迟曲线的拐点里,在每一个用户反馈的bad case里。把监控做好,把评测集建好,把回退方案留好,然后大胆试、小心验证,这才是靠谱的做法。