☰
大模型推理性能优化:从算力访存到量化融合的五个排查角度
2026/10/8 5:14:21 网站建设 项目流程

做模型推理部署这几年,我听最多的一句话是:模型跑起来没想象中那么快。显卡标称算力明明高得吓人,可实际单次推理的耗时就是压不下去。有个项目让我印象很深——7B 参数的大模型在 A100 上做自回归生成,单 token 的实测耗时换算下来只有理论算力的零头,当时团队差点直接去申请新硬件。后来沿着性能优化的思路排查完才发现,问题根本不在显卡,而在我们对推理链路里的瓶颈判断错了方向。

这类困惑其实很常见。模型推理的性能优化和训练调优完全是两套逻辑:训练时关心吞吐,推理时还要同时照顾延迟、显存、批处理策略、前后处理。很多人上来就是"加个更好的 GPU""把并发拉高",但如果不清楚推理为什么慢、慢在哪个环节,这些操作往往是把成本花在了刀背上。这篇就把我踩过的坑按五个角度重新梳理一遍:算力、访存、模型、算子、系统。每个角度回答一个核心问题——推理是不是真的缺算力、数据搬运占了多少时间、模型本身能不能变得更轻、算子有多少浪费、调度和显存还能榨出多少。对刚接触推理部署的人,这是一条从定性到定量的排查路径;对有经验的人,希望里面有些数据和思路还能再帮到你。

1. 角度一:算力视角——显卡算力很高,推理却跑不动

1.1 理论算力是一张没法兑现的支票

先看一组数据。以 A100 为例,FP16 稠密算力大约 312 TFLOPS,RTX 4090 也能到 165 TFLOPS 左右。放到 7B 模型上算一笔账:生成一个 token,前馈需要的乘加运算量大约在 14 GFLOPs 级别(按 2 倍参数量估算)。如果真能贴着算力峰值跑,4090 上单个 token 也就 0.1 毫秒左右,换算成生成速度差不多每秒上万 token。可现实中 7B 模型在 4090 上单机部署,decode 速度普遍只有每秒三四十个 token,差了足足两三个数量级。这说明什么?理论上限当然存在,但能不能摸到,取决于你的计算模式有没有让 GPU 真正忙起来。

我见到的常见误解,就是把"算力大"等同于"推理快"。算力只是峰值,利用率才是实际值。推理场景里利用率低到个位数百分比的例子比比皆是,这时候问题就不该再归到算力头上。换卡、加机器这类粗暴方案,恰恰是在这个问题上投入产出比最低的一类。

1.2 自回归解码为什么喂不饱 GPU

大模型推理里两个阶段的行为完全不同。prefill 阶段一次处理成百上千个 token,算的是大矩阵乘法 GEMM,Tensor Core 能发挥威力;decode 阶段每步只有一个 token,矩阵退化成了矩阵向量乘 GEMV,计算密度断崖式下降。

这里面有两个结构性原因。第一,自回归生成天生有依赖链——第 t 个 token 必须等前 t-1 个生成完才能开始,计算上无法像训练那样大尺度并行。第二,GEMV 这类操作本身就不适合 Tensor Core 的设计目标,Tensor Core 喜欢足够大的矩阵去做分块乘加,向量乘法的分块利用率低得可怜。所以即使你把 batch 设成 1 来保证低延迟,GPU 的大半算力就是闲着,"算力高"这张支票在 decode 阶段根本兑现不了。

还有一个常被忽略的小头:kernel launch 开销。一个 7B 模型一次 decode 要经过几十上百个算子,每个算子在 GPU 上启动都有微秒级固定成本,叠加起来在延迟敏感场景里同样不能忽视。算子数量越碎,这个占比越明显,这也是后面算子融合和 CUDA Graph 能起作用的原因。

1.3 先用 profiling 确认算力是不是瓶颈

既然算力不一定是真瓶颈,动手优化前最重要的动作是先 profile。我一般用两层思路:先拿算子级耗时数据,再用总体指标交叉验证。

import torch from torch.profiler import profile, ProfilerActivity with profile( activities=[ProfilerActivity.CUDA, ProfilerActivity.CPU], record_shapes=True ) as prof: for _ in range(10): out = model(input_ids) # 模拟一次推理 print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=20))

如果 GPU utilization 看着不高但算子时间分布很散,那大概率是访存或 launch 瓶颈;如果排到前面的都是大 GEMM 而且耗时接近理论值,才需要往算力耗尽的方向想。性能优化最忌讳凭空猜测,几分钟的 profile 往往比一小时讨论更能定位问题。这一步确认之后,再往下面四个角度走就不会白费力气。

2. 角度二:访存视角——真正拖着后腿的是搬数据

2.1 厨师和传菜员的比喻

把 GPU 比作一间厨房,算力是厨师的刀工,内存带宽则是传菜员的速度。菜谱再复杂,如果传菜员一次只能端一盘菜,出菜速度就被卡在传菜环节。推理也一样,很多模型卡的不是"算不动",而是"数据搬不动"。判断一个算子到底是算力瓶颈还是带宽瓶颈,有个简单指标:运算强度 arithmetic intensity,等于计算量除以数据搬运量。运算强度低于某个临界点(算力峰值除以带宽)就是访存瓶颈,高于临界点才是计算瓶颈。

GPU 的这个临界点通常很高。拿 A100 举例,312 TFLOPS 除以约 2 TB/s 的 HBM 带宽,临界点在 156 FLOPs/Bytes 附近。也就是说,每从显存里读 1 个字节,至少要配套 156 次浮点运算,算力才不会被饿着。现实里绝大多数推理算子根本达不到这个比例。

2.2 用 7B 模型算一笔清清楚楚的账

访存瓶颈在 decode 阶段有多严重,算给自己看就明白了。7B 模型用 FP16 存储,光权重就是 14GB。生成每个 token 时,即便只算一次前向,也要把这 14GB 至少读一遍(实际上还有中间激活和 KV cache 的读写)。A100 的带宽大约 2TB/s,14GB 除以 2TB/s 等于 7 毫秒——这是理论上的最低时间。也就是说,decode 速度上限约为每秒 140 个 token,居然和很多项目实测的百 token 级速度吻合得很好。这个吻合本身就是证据:decode 阶段你已经贴着了访存天花板,再怎么堆算力都没用。

把同样的账套在量化模型上更直观。换成 INT8 权重后字节数减半,理论上限直接翻倍。很多人不理解为什么量化后速度几乎线性提升,原因就在这里——它减的不是算力压力,而是带宽压力。移动端性能优化也遵循同一逻辑:端侧算力本来就弱,内存带宽和拷贝开销更是兵家必争之地,减少数据搬运永远是性价比最高的优化手段之一。

2.3 同一模型的两个阶段,优化方向完全相反

用 roofline 模型去看会更清楚。prefill 阶段一次算大批量 token,运算强度能冲到几百 FLOPs/Bytes 以上,跨过了临界点,属于典型计算瓶颈,优化方向是提高矩阵乘效率、扩大 batch、用好 Tensor Core;decode 阶段运算强度经常连 10 都不到,是标准带宽瓶颈,优化方向是减少字节——量化、权重缓存、算子融合、避免中间张量来回读写。

这解释了为什么没有一个万能优化能让两个阶段同时达到最佳。实际做工程时,要把 prefill 和 decode 当成两个独立问题分别调优,再在引擎层面做好两者的资源分配。这点和写 Oracle SQL 优化很像——同样的查询,数据量不同、索引选择不同,执行计划完全两样;推理同理,阶段不同、批次不同,瓶颈就不在同一个地方。

2.4 省带宽的三板斧

围绕访存做优化,我总结三件最有效的事:

  • 权重减肥。量化到 INT8/INT4,直接从源头减少每次搬运的字节数。
  • 减少中间张量。能让数据在寄存器或 shared memory 里多待一阵就别写回显存,这正是算子融合的核心价值,后面单独展开。
  • 提高复用率。同一个权重尽量被多个计算步骤同时用到,避免反复从 HBM 读同一份数据。

顺带说一句,Julia 这类语言做数值计算时反复强调的"内存分配是性能杀手",和推理里的访存优化本质上是同一件事——分配、拷贝、搬运的成本往往比计算本身贵得多。不同领域最终得出了同一个结论,只是名字不一样。

3. 角度三:模型视角——推理变快,从模型本身下手

3.1 量化:性价比最高的倍速器

模型压缩的第一梯队,我永远推荐量化。核心思想很简单:推理不需要那么高的数值精度。从 FP32 换成 FP16,权重体积直接减半,带宽压力减半;再从 FP16 压到 INT8,又减半。对 memory-bound 的 decode 阶段来说,量化多少倍,速度就能接近翻多少倍。

量化有两条路线。训练后量化 PTQ 最省事,拿一份代表性数据过一遍模型,统计激活和权重的分布,确定缩放因子,几行代码就能完成;量化感知训练 QAT 在训练时模拟量化误差,效果更好,但需要重新训练,成本高。LLM 场景里还有 GPTQ、AWQ 这类专门针对大模型设计的量化算法,核心都是在处理激活值里"异常大数"导致精度崩坏的问题——输入里有些维度数值明显偏大,简单均匀量化会把多数小数值挤成一团浆糊,SmoothQuant 的思路就是把这部分数值压力转嫁到权重上再量化。

有个重要提醒:不要拿通用指标一概而论。做量化验证要用目标任务的真实数据,而不是只看几个公开基准数字。有些模型在通用 benchmark 上掉点不多,但在你业务的长尾样本上会明显变差。量化上线前一定要做业务侧回归,这个环节不能省。

3.2 剪枝:要剪就剪结构化的

剪枝听起来很美好——把不重要的连接删掉,模型变小变快。但实际落地时坑很多。非结构化剪枝把权重矩阵剪得千疮百孔,变成不规则稀疏矩阵,通用 GPU 跑稀疏矩阵并不比稠密快,专门写稀疏 kernel 又成本高昂,通常只在特殊硬件上才划算。

真正对推理有直接收益的是结构化剪枝:按 channel、按 head、按层去剪。剪完整个维度都消失,矩阵还是规则形状,FLOPs 和访存量一并下降。代价是需要微调甚至重新训练来恢复精度。我的经验是,剪枝适合那些明显有冗余的模型,比如超大参数量但任务简单的中小型场景;结构本身已经很精炼的模型,剪枝空间往往很有限,别在这上面花太多时间。

3.3 蒸馏:把变小这一步放到训练期

蒸馏是另一种思路——不剪现有模型,而是用一个大的 teacher 教出一个小 student。小模型从头训练就朝着 teacher 的分布对齐,最终参数量更小、精度却比直接训练同样大小的模型要好。LLM 领域里 12B 蒸馏到 7B,甚至 70B 蒸馏到 13B 都是常见操作。蒸馏一旦完成,后续的部署成本全链路下降,属于"最值得的早期投资"。

蒸馏和量化通常还能叠加:先蒸馏缩小模型,再量化缩减字节,两项收益大体是乘性关系。把这两步都做完,同一份吞吐预算里能塞进的服务量可能差好几倍。如果你的项目还在选型或训练阶段,我建议优先把蒸馏纳入规划,而不是等部署时再亡羊补牢。

3.4 结构重参数化:训练推理解耦

有一类优化在模型结构上做得更巧妙——训练时用复杂结构保证精度,推理时把结构等价变换成简单算子。最典型的是 RepVGG,训练时用多分支(3×3、1×1、恒等)提升表示能力,推理时把分支融合成单个 3×3 卷积。数学上完全等价,推理结构却简单一个量级,速度和显存都受益。我把这四种手段的关键差异整理成一张表:

手段对推理速度的影响精度风险主要成本我推荐的时机
量化高,尤其 decode 阶段中低,需业务回归低,PTQ 一天内可完成第一优先,几乎所有部署场景
结构化剪枝中高,视稀疏度中需微调或重训模型冗余明显的场景
蒸馏高(模型变小)低,但需训练资源重新训练新项目从零开始训练时
结构重参数化中低训练结构改造卷积类模型,训练流程可控时

实际项目里我很少孤立使用某一种,通常是量化打底、蒸馏解决精度、剪枝和重参数化看情况补充。

4. 角度四:算子视角——把细碎的算子拧成一股绳

4.1 算子越碎,浪费越多

一个推理模型在框架里会被拆成几十上百个算子,每个算子执行完,中间结果要写回显存,下一个算子再从显存读出来。这就像把一道菜拆成切、炒、装盘三个步骤,每步都换一个厨房、重新生火备菜,时间全耗在交接上。

浪费主要有两块。一是数据搬运:中间张量在 HBM 里写一次读一次,decode 阶段带宽本来就紧张,绕不开的搬运全是纯开销。二是 kernel launch:每个算子在 GPU 上启动都要开销,CPU 侧连续启动几十个 kernel,累积的延迟在低延迟场景尤其刺眼。算子融合的思路,就是把这些细碎算子合并成一个算子,让它内部的中间数据尽量留在寄存器或 shared memory 里,不落回显存。

4.2 Conv+BN+ReLU 融合的数学账

最经典的融合是 Conv+BN+ReLU。BN 在推理时是一个纯线性变换:输出等于输入乘一个系数再加一个偏移。卷积也是线性运算,所以可以先把 BN 的系数合并进卷积极或者偏置里,再和 ReLU 拼成一个算子,数学上完全等价。

假设卷积输出为 y,BN 参数为 scale 和 bias,融合后的新权重就是原权重乘以 scale,新偏置等于 (原偏置 - 均值) × scale + bias。推理引擎(比如 TensorRT 或 ONNX Runtime)会自动做这类变换,你不需要手写。但理解了原理,你就能明白为什么有的模型在 ONNX 导出时把 BN 留在图里会导致引擎优化失效——导出前的算子规范化很重要,这也算是我踩过的一个隐蔽坑。

4.3 FlashAttention 如何靠内核重写翻盘

如果说算子融合是把已有算子合并,FlashAttention 就是更激进的一种:重写算子本身,改变它的数据流。

标准 attention 要计算 S = QK^T,然后 softmax,再乘 V。问题在于中间那个 N×N 的注意力矩阵会被完整写进显存,序列越长,这个矩阵越大,读写开销呈平方增长。FlashAttention 的做法是分块计算,每个小块的 softmax 用"online softmax"技巧,不依赖完整矩阵,最终把对 HBM 的访问量从 O(N²) 降到 O(N)。它没有减少 FLOPs(甚至略多),但因为把访存从显存挪到了片上,实测速度却能快好几倍。

这对做服务的人来说是个心态上的启发:不要总以为优化就是砍计算量,很多时候只是把数据搬动的路径改得更聪明。后来的 FlashDecoding、各种 MQA/GQA 变体,也都是围绕"让带宽和数据流程更合理"在做文章。

4.4 数据布局:NHWC 还是 NCHW,不是小事

同样一份数据,在内存里的排列方式不同,读写效率差很多。NCHW 是深度学习框架的默认布局,图像通道是连续的,对早期按通道处理的方式友好;但 NHWC 布局在卷积里能更好地利用 Tensor Core 和 SIMD,因为相邻像素的数据在内存里也是连续的,存取和向量化都更高效。

很多推理引擎在内部会自动把图优化到 NHWC 再跑,你在框架层看到的数据布局未必是真正的执行布局。端侧 NPU 和移动端把布局问题放得更大,不同厂商的异构单元对布局要求各不相同,布局转换本身也是开销。手游性能优化里经常提到的"减少纹理拷贝""统一内存格式",跟这里其实是同一个道理。遇到性能瓶颈时,值得检查一下模型在目标设备上到底按什么布局在跑,这往往是隐藏成本。

5. 角度五:系统视角——引擎、调度与显存的艺术

5.1 推理引擎在默默替你做哪些事

把同一个 ONNX 模型直接丢进框架跑,和丢进 TensorRT 跑,耗时经常差一倍甚至更多。差别在哪?TensorRT 这类引擎在加载时会做几件事:图层融合(把 Conv+BN+ReLU 这类模式合并掉)、kernel 自动选择(同一个算子有多种 GPU kernel,实测哪个快用哪个)、显存规划(提前计算生命周期,复用显存块)、动态 shape 适配等。

ONNX Runtime 也有自己的图优化 pass,比如常量折叠、无效算子消除。选引擎不要盲目迷信"最强",要看你的硬件、框架生态和部署形态。自建服务用 TensorRT 或 vLLM 居多,轻量级服务端和端侧可能更适合 ONNX Runtime 或 TNN、MNN 一类的移动端引擎。引擎层的收益不需要你改一行模型代码,属于"躺着赚"的部分,前提是模型导出得当,尤其是算子没有畸形写法。

5.2 从 static batching 到 continuous batching

很多人在推理服务里做性能优化,第一个想到的词是 batch。对 GPU 而言,batch 越大矩阵越肥,算力利用率越高,prefill 阶段尤其如此。但动态 batching 有个直接代价:把一个请求拼进当前批次,就要等这个批次整体跑完,单个请求的延迟被拉长。

vLLM 这类方案里引入的 continuous batching 是更聪明的做法。它不再按"一个批次必须同时开始同时结束"来调度,而是以请求为粒度动态调度:某个请求的生成长度到了,就把它从当前批里摘出去,再插入新请求。GPU 一直在跑有效计算,空闲的算术资源和显存都被填得更满。一句话总结:传统批次是整桌一起开饭,continuous batching 是快吃完的可以先走、新客人随时上桌,餐厅翻台率自然上去。

5.3 KV cache 和 PagedAttention

大模型推理的显存大头,除了权重,就是 KV cache。prefill 阶段算出的 key、value 要缓存在显存里给后续 decode 复用,每个请求的 KV cache 大小随序列长度动态变化。传统做法是按最大可能长度一次性预留连续块,结果一是碎片化严重,二是预留多用少,显存利用率很低。

PagedAttention 把 KV cache 切成固定大小的页,按需分配,用类似操作系统虚拟内存的机制管理,碎片问题大大缓解,同样显存下能塞进更多并发请求。这本质上是把系统设计里的经典方案搬进了推理引擎,和数据库里按页管理缓冲池的思路同源。理解了这点,你在给服务配置显存预算时会更有底,而不是拍脑袋定 max_seq_len。

5.4 前后处理、流水线与 CPU-GPU 同步

推理耗时不只等于模型计算时间。图像预处理、文本 tokenize、结果后处理,这些 CPU 工作如果和 GPU 计算串行执行,每一轮都会空转。正确的姿势是流水线 overlap:CPU 处理第 N+1 个请求的预处理时,GPU 正在跑第 N 个请求的推理。CUDA Graph 也能把一串 kernel 启动打包成一次提交,减少 CPU 和 GPU 之间的同步次数,这对小模型收益尤其明显,一次同步的开销占比太高。

手游和移动端性能优化同样讲 overlap——渲染管线里 CPU 提交和 GPU 渲染错开、UI 逻辑和资源加载并发,背后的原理完全一致。性能优化做到最后,拼的都是系统级的资源编排能力,不是单点技巧。

6. 收尾:我自己沉淀下来的优化顺序与教训

6.1 一套可以照抄的排查顺序

接到一个推理性能需求,我基本按这个顺序走,你可以直接当 check list 用:

  1. 先 profile 定性。用 torch.profiler 或 nsys 拿到数据,判断核心瓶颈是算力、带宽还是 launch 开销。/这一步最容易被跳过,但恰恰决定了后面所有动作的方向。
  2. 做低垂果实。量化、开引擎自带优化、确认数据布局没问题,这些改动小、风险低、收益立竿见影。
  3. 再动模型结构。蒸馏、剪枝这类手段周期长,适合方向确认后再投入,避免白做。
  4. 最后才动调度和显存。连续批处理、KV cache 管理、CUDA Graph 这些是在前面的基础上放大收益,前提是前面的瓶颈已经处理过。

每一轮只改一个变量,用真实压测数据说话。我见过太多人一次性上三个优化,结果出问题时根本不知道是谁引入的。

6.2 一个真实项目:从延迟超标到不换硬件达标

最后讲个实际案例。有个 7B 模型项目,上线前延迟就是不达标。最初的讨论方向是换卡或者加节点,预算不小。我没急着定,先跑了个 profile,发现 decode 阶段 GPU 利用率不到百分之二十,但带宽指标已经接近打满——典型的访存瓶颈。于是直接做了两件事:INT8 量化、配合引擎层的算子级优化,延迟掉了差不多一半。后来又用 continuous batching 把吞吐提了一截,整个过程中原来的硬件什么都没换。

这个项目的教训有两点。第一,瓶颈判断错了,再贵的方案都是白搭;第二,量化和算子融合这类"土办法"在多数场景里依然是最划算的选择。模型推理的性能优化,拼的从来不是堆资源,而是找准瓶颈之后用合适的手段逐一击破。希望这五个角度能帮你少走点我当年绕过的弯路。

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

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

立即咨询