大模型推理过程中的性能优化方法
欢迎留言,每条留言都会精选、本人当天回复,文章错误内容也会在回复中更新。本文介绍大模型推理性能的优化方法。包括三方面:服务调度、计算引擎和模型算法。【AI大模型教程】
本文参考:
孙庆虎(故北),公众号:阿里云开发者【万字长文】大模型训练推理和性能优化算法总结和实践
优化大模型推理性能,就是找一种平衡 —— 既要让 GPU 算得快、不闲着,又要少占显存、少浪费带宽,还得兼顾吞吐量和延迟的矛盾。
服务层从调度入手:
Continuous Batching 作为调度员,每生成一个 token 就调整批次,让早结束的请求腾位置、新请求插队,免得 GPU 空转;
流式生成边算边返回,还能中途打断,既让用户等得短,又省了无效计算;
长序列推理则靠 “局部注意力”“状态向量” 等小聪明,把 O (n²) 的计算量砍成 O (n×k),让模型能 “啃” 长文档还不费太多力。
推理引擎层从效率入手:
KV-Cache 像记笔记,存下历史计算结果避免重复劳动;
PagedAttention 学操作系统分页,解决显存碎片问题,让内存用得更透;
APC 则抓 “重复前缀”,比如 100 个用户问同一篇财报,它只算一次前缀的缓存,剩下 99 次直接复用,省了大半算力;
分布式并行(TP+PP)“分工合作”,把大模型拆给多卡干,你算这部分我算那部分,既装下大模型又提速;
算子融合则把零散的计算步骤捆成一团,少读写显存、少启动 Kernel,让数据在 GPU 里跑得更顺。
模型量化层则是 “瘦身术”:
把 32 位浮点数压成 8 位甚至 4 位整数,用点精度损失换显存减半、速度翻倍,接着靠 SmoothQuant、GPTQ 这些技巧,让 “瘦身后” 的模型依然能打。
下面是这三个层面的详细阐述: 一、服务层优化
(一)Continuous Batching
传统静态批处理(比如以前用 Triton 的 Static Batching)最大的浪费是 “token 级空转”—— 比如一批里有个请求只需要 5 个 token,其他要 50 个,那前 45 个 token 生成时,这个请求早就结束了,但 GPU 还得为它保留位置,相当于占着坑不干活。 Continuous Batching 的关键是 “按推理步调度”,每生成一个 token(也就是走一步推理)就重新排一次队:把那些 Early-Finished(早结束)的请求踢出去腾位置,再把新进来的 Late-Joining(晚加入)请求塞进来,这样 GPU 的计算单元每一步都满负荷,没有空档。但这里不是随便塞,得处理好请求长度差异,不然短请求会被长请求堵死,所以现在大多会分 “长度桶”,把差不多长度的请求放一个桶里调度,减少调度出现碎片化(长短不一导致的),比如 100-200token 的放一桶,200-300 的放另一桶,各自调度,互不干扰,既保吞吐量又压延迟。
Continuous Batching适合处理不同长度的prompt,因为大模型推理时,有的请求可能只需要5个token就完成,有的可能需要50个,传统批处理会为了最长的那个请求等很久,而Continuous Batching可以实时调整,让GPU一直在干活,不闲着。 它在每次GPU推理的间隙,也就是"写完一个token的空档",偷偷做点调度工作。系统会实时监控每个请求的进度,一旦发现某个请求提前写完了(Early-Finished),就把它从当前批次中移除,腾出位置给新来的请求。同时,当新请求到达时(Late-Joining),它也能立即插入到当前批次中,不需要等当前批次完成。
(二)流式交互式生成
再看流式交互式生成,流式生成其实是 KV-Cache 在服务层的另一种应用,它不只是 “边生成边反馈” 这么简单。传统非流式是等模型把所有 token 都算完再返回,这里有两个大问题:一是用户等得久,二是 “资源占用长尾”:1个要生成 100 个 token 的请求,得占着 KV-Cache 整整 100 步,期间其他请求进不来。流式生成= 增量解码 + 中途打断,生成一个 token 就返回一个,同时把这个 token 对应的临时计算结果(不是 KV-Cache,KV-Cache 还得留着算下一个)释放掉,更重要的是用户能随时喊停,比如模型生成到第 10 个 token,用户觉得不对,直接终止,后面 90 步的计算就全省了,这才是真的省资源。而且底层必须跟 KV-Cache 联动,因为每一步只算当前 token,得靠 KV-Cache 调出前面的历史信息,不然每一步都得重算前面所有 token,反而更慢。
(三)长序列推理
长序列推理这块,痛点是传统 Transformer 的 Attention 计算是 O (n²),n 是序列长度,n 从 1k 涨到 10k,计算量直接从 1e6 飙到 1e8,显存和算力都扛不住。所以所有长序列优化,本质都是--如何在不丢关键信息的前提下,把 O (n²) 降到 O (n) 或 O (nk)(k 是某个小常数)。 英伟达的 Star Attention,它赌的是 “局部性假设”—— 当前 token 主要跟附近的 token 有关,读文章时,一句话里的词大概率只和前后几句关联,不用跟开头的词扯关系。所以它分两阶段:第一阶段把长序列切成小块,每个块只算自己和前一个块的 Attention,抓 “短期依赖”,这一步计算量是 O (nk)(k 是块大小);第二阶段用全局 Attention 把所有块的结果拼起来,抓 “长期依赖”,但全局计算时只算块级的特征,不是每个 token,所以总量还是可控。比如 k=512,n=10k,计算量直接降 10 倍,还能保住长距离关联。
Star Attention 的两阶段计算:
第一阶段是 “局部注意力”,把长序列切成大小为 k 的块。比如 n=10000,k=512,那大概能切成 10000÷512≈20 个块(取整数方便算)。每个块只关注 “自己内部的 token” 和 “前一个块的 token”(抓短期依赖)。每个块有 k 个 token,前一个块也有 k 个,所以每个 token 需要和 k(自己块)+k(前一个块)=2k 个 token 算注意力。那一个块的计算量就是:k 个 token × 2k 次计算 = 2k²。20 个块的总计算量就是:20 × 2k² = 40k²。代入 k=512,就是 40×512×512≈1048 万次。
第二阶段是 “全局注意力”,抓长期依赖。这时候不细算每个 token 了,而是给每个块提一个 “总结特征”(比如用块内所有 token 的均值或最大值当代表),20 个块就有 20 个总结特征。全局注意力就是这 20 个特征之间互相计算,计算量是 20×20=400 次(和第一阶段比几乎可以忽略)。所以 Star Attention 的总计算量≈第一阶段的 1048 万次 + 第二阶段的 400 次≈1048 万次。传统 Attention 是 1 亿次,1 亿 ÷1048 万≈9.5,差不多 10 倍
壁仞的 “分段 + 状态向量” 更鸡贼,状态向量其实是前一段的 “语义摘要”,就像你读论文记段落大意,不用记每句话,后面段落基于大意算,不用回头算每句话的 Attention。这里的关键是状态向量得小,比如把前一段 1000 个 token 的 Attention 输出压缩成 256 维向量,既省空间又能保留关键信息,不然存状态向量也占显存。
FlexPrefill 的 “动态调整范围” 是 “注意力稀疏化” 的变种,它不是固定块大小,而是靠一个轻量的 “语义分析器” 判断当前内容的 “密度”—— 遇到 “综上所述” 这种总结句,就扩大 Attention 范围,关联前面更多内容;遇到 “例如” 这种举例句,就缩小范围,只关联前面的论点句,这样比固定块更精准,该省的省,该保的保。
“以存换算” 技术:利用显存和磁盘存储,把不常用的数据放到显存外,需要时再调回来。我们先搞清楚 “存什么换什么”,存的是不常用的 KV-Cache 或 Attention 权重,换的是显存空间,但得控制换入换出的延迟,比如把前 1000 个 token 的 KV-Cache 放到 SSD,但不能等要用了才调,得提前预取到内存,再从内存调到显存,让 IO 和计算重叠,不然 GPU 等数据会更慢。
二、推理引擎优化
(一)KV-Cache机制
大模型推理的传统方法每次生成新token都要重新计算所有历史token的键值对,就像每次写新句子都要重新读一遍前面所有内容,这太浪费时间了。
KV-Cache 你可能知道是存 K 和 V,但为什么是 K 和 V,不是 Q?因为 Q 是当前 token 的查询,每次都不一样,没法存;K 是历史 token 的 “键”(特征标识),V 是历史 token 的 “值”(信息内容),这俩是固定的,生成新 token 时只需要用新 Q 去查旧 KV,所以能存。而且 KV-Cache 的存储格式不太一样,一般按注意力头拆分,每个头的 KV 单独存,这样后面做 TP(张量并行)时,能直接把每个头的 KV 切分到不同 GPU,不用跨卡传数据,省通信量。还有动态扩容问题,一开始不知道请求要生成多少 token,不能预分配太大空间(浪费),也不能太小(不够用要扩容,拷贝数据费时间),所以现在都用 “预分配小块 + 动态拼接”,比如每个请求先给 64 个 token 的 KV 块,不够了再追加,内存碎片少,扩容快。
华为的 UCM 技术是 KV-Cache 的升级版,搞 “三级缓存”:热数据(最近用的)放显存,温数据(近期可能用的)放内存,冷数据(很久不用的)放 SSD,还会跟踪每个 KV 块的访问频率,比如 5 分钟没碰过就丢 SSD,1 分钟内碰过就放内存,这样既省显存,又能通过预取减少调数据的延迟,比如在当前计算快用完显存时,提前把下一段需要的 KV 从 SSD 调到内存,不让 GPU 等。
(二)PagedAttention
PagedAttention 是解决 KV-Cache 的 “内存碎片化” 痛点,传统 KV-Cache 要分配连续显存,比如一个请求要 100 个 token,得找一块 100token 的连续空间,要是显存里有 10 个 10token 的空闲块,也没法用,只能等别的块释放。PagedAttention 学操作系统的内存分页,把 KV-Cache 切成固定大小的 “页”(比如每页存 64 个 token 的 KV),一个请求的 KV 不用连续存,分散在多个页里,用 “页表” 记每个页的位置,这样不管空闲页在哪,数量够就能用,彻底解决碎片化。比如一个请求要 100 个 token,拿 2 个页(64+36,第二个页只用 36 个位置)就行,不用管这俩页在显存的哪个角落。而且它还支持 “页共享”,比如两个请求有相同的前缀(比如都用同一段文档当上下文),它们的前缀 KV 页能共享,不用存两份,这就跟后面的 APC 联动上了。淘汰策略也不是简单 LRU,是 “引用计数 + LRU”,比如一个页被 3 个请求共享,引用计数是 3,只有所有请求都不用了,计数归 0,才会被淘汰,不会误删共享页。
(三)自动前缀缓存(APC)
自动前缀缓存 (APC) 解决的是 “前缀重复计算” 的大坑,比如 100 个用户问 “这份 2025 财报的利润是多少”“这份 2025 财报的营收是多少”,前面都带相同的 1000 字财报前缀,传统做法是每个用户都算一遍这 1000 字的 KV-Cache,重复 100 次,纯浪费。APC 的核心是 “前缀特征哈希 + 页共享”,不是直接对 token 哈希,而是对前缀对应的 KV 页特征做哈希(比如把每个 KV 页的特征压缩成哈希值),这样即使前缀略有不同(比如 “2025 财报” 和 “2025 年度财报”),也能匹配到相似 KV 页,提高命中率。而且它能 “动态匹配子集”,比如前缀 A 是 “财报第一章”,前缀 B 是 “财报第一章第一节”,APC 能认出 B 是 A 的子集,直接共享 A 里第一节的 KV 页,不用重算。vLLM 里的 APC 还跟请求队列绑在一起,新请求进来先查哈希表,有匹配的缓存就直接加载,跳过最费算力的 prefill 阶段(计算前缀 KV 的阶段),直接进 decode 阶段(生成回答 token),prefill 阶段本来就占推理算力的 70% 以上,所以 APC 一上,吞吐量直接涨好几倍。另外,APC 得处理 “缓存过期”,比如财报更新了,旧缓存就得删,所以每个缓存块都有 “版本号” 和 “时间戳”,文档一更新,自动淘汰旧版本,避免返回错数据。
(四)分布式推理并行方案
TP和PP这两种分布式推理并行方案其实是解决推理时"人多力量大"但又"别抢着干活"的问题。TP(张量并行)是把一个大蛋糕切成多块,每块由不同的人负责切,但最终要拼成一个完整的蛋糕。具体来说,TP把模型的张量(比如多头注意力中的每个头)分散到多个GPU上,每个GPU只负责一部分计算,比如MHA并行就是把多头注意力的每个头切分到不同GPU,KV缓存也跟着切分。这样,当模型很大时,单个GPU放不下,TP就能把一个大计算任务拆成小块,让多个GPU一起干活,避免了单卡内存不足的问题,而且还能同时处理KV缓存的切分,让推理效率更高。
PP(流水线并行)把整个模型按层切成多个阶段,每个GPU负责一个阶段的计算,就像生产线上的不同工位。比如第一层在GPU1上算,算完后把结果传给GPU2算第二层,GPU1空闲了又能接新任务。这样当请求量大时,GPU1可以继续处理新请求,而GPU2还在处理上一个请求的后续层,大大减少了设备空闲时间。PP最厉害的是它通信量小,因为只传层间的激活值,KV缓存是独立的,不需要跨设备传递,所以能支持更大的batch_size,因为单个GPU只存模型的一部分,节约下来的显存可以存更多KV缓存。
TP 是 “水平切分”,把同一层的模型权重拆到多卡,比如 16 个头的 Attention,用 4 卡 TP,每卡负责 4 个头,解决 “单卡装不下大模型” 的问题。但 TP 有上限,并行度不能超过模型的 “可切分维度”,比如只有 16 个头,最多 16 卡 TP,再多就没法切了,所以适合中小规模并行。PP 是 “垂直切分”,把不同层拆到多卡,比如 100 层模型,用 10 卡 PP,每卡负责 10 层,解决 “层数太多计算慢” 的问题。PP 的关键是减少 “流水线气泡”,刚开始卡 2 要等卡 1 算完才动,有个空窗期,现在都用 “重叠通信”,卡 1 算完一层就把中间结果传给卡 2,不用等 10 层都算完,气泡就小了。TP 和 PP 可以这样混合用:先按 TP 把每层拆成 M 卡,再按 PP 把 M 卡一组的层拆成 N 组,总共 M*N 卡,比如 4 卡 TP+2 卡 PP=8 卡,既能装下大模型(TP 解决权重大小),又能提速(PP 解决层数多)。华为的 “1 卡 1 专家” 是 MoE 模型的并行,每个专家放一卡,TP 切分单个专家的权重,PP 切分 MoE 的层,既用 MoE 的稀疏性(每次只激活部分专家),又解决专家数量多的问题。DistriFusion 是图像生成的并行,把图像块按空间切分,每卡算一个块,减少边缘通信,本质是 TP 的变种。
(五)算子融合和优化
算子融合其实就是让神经网络的计算流程变得更紧凑,你做菜的话,不可能每切一块肉就要洗一次刀,换一次砧板。算子融合就是把切肉、洗刀、换砧板这些步骤合并成一个连贯的动作,直接把肉切好就下锅,不用来回折腾。算子融合不是简单 “合并步骤”,而是抓 “显存带宽瓶颈”。每个算子启动一次 CUDA Kernel 要花几十微秒,两个算子启动两次就多一笔开销,这还是小事。更要命的是没融合时,Add 的输出要写回显存,LayerNorm 再从显存读,一写一读占两次带宽,显存带宽本来就不够,这一堵就慢了。融合后,Add 的输出直接在 GPU 的寄存器(或共享内存)里传给 LayerNorm,不用碰显存,少了两次带宽占用,还省了一次 Kernel 启动开销。但融合得看 “依赖和兼容性”,比如 Add 和 LayerNorm 是前后依赖,还在同一维度操作,才能合。
这种优化不改变计算逻辑,只是把计算流程重组了,只是把我们小组成员的工位合并,不用来回走动。在Transformer这种结构里,这种融合特别有效,因为像Add和LayerNorm这种操作本来就是前后依赖的,完全可以合并。FasterTransformer 把 MultiHeadAttention 的整套流程(QKV 投影→Scaled Dot-Product→多头拼接→线性层)都融合成一个 Kernel,原来要启动 5-6 次 Kernel,现在一次搞定,中间数据全在共享内存里传,显存访问少 80% 以上,速度直接翻倍。
三、模型量化技术
最后是模型量化,做到平衡精度损失和节省资源。大模型的坑在于激活值和权重分布极不均匀,比如激活值里可能有几个 100 的离群值,大部分在 - 1 到 1 之间,普通对称量化会把范围拉到 [-100,100],小值全被量化成 0,失真严重。SmoothQuant 的办法是 “转移误差”,给激活值乘 0.1 缩到 [-10,10],权重乘 10 扩到 [-100,100],让两者量化范围接近,失真变小。LLM.int8 是 “混合精度”,权重用 INT8,激活值里的离群值用 FP16,因为离群值少但影响大,这样既省资源又保精度,比如 99% 的数用 INT8,1% 用 FP16,显存只多一点,精度跟 FP16 差不多。GPTQ 更狠,量化前先微调,把权重切成小块,每块量化后算误差,再调整未量化的权重补偿误差,相当于 “瘦身时补营养”,让模型瘦了还有力气。比如 16B 模型 FP16 要 32GB 显存,INT4 量化后只要 10GB,普通显卡就能跑,batch_size 还能翻倍,速度快 4 倍以上。寒武纪的 vLLM-MLU 用 INT8,是因为 MLU 的 INT8 计算单元比 FP16 多,比如每个核心有 1024 个 INT8 ALU,只有 256 个 FP16 的,不用 INT8 就浪费硬件算力。另外,量化还得防 “溢出”,INT8 范围是 - 128 到 127,计算前得把输入缩到位,不然一溢出结果就错了,现在的量化算子都有动态缩放,就是做这件事情的。