尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

大模型推理优化实战:剪枝、量化与图优化如何榨干GPU算力

大模型推理优化实战:剪枝、量化与图优化如何榨干GPU算力 1. 从一次深夜压测说起我为什么非要撸一个模型优化器上个月给客户交付大模型推荐服务4卡A100部署了个7B模型业务方张口就要500 QPS。结果压测一跑单卡只能扛80 QPS延迟还飙到800ms这数字在场的人都沉默了。换更大的卡预算表拍在桌上甲方财务的脸比会议室白墙还白。加机器机房里那点冗余带宽也是问题。那几天我把推理框架的日志翻了个底朝天问题集中在三个地方显存里常数张量占了两成空间、算子调度碎片化严重、动态shape反复触发重新编译。这套组合拳打下来资源利用率根本拉不上去。那会儿我就确定了方向与其指望下一代硬件不如做一层模型优化器把现有卡上的计算和显存潜力压榨干净。于是就有了 Model-Optimizer —— 这不是我训练模型用的Adam或SGD而是一个针对推理阶段的模型优化工具链。它能做四件事对模型做结构化剪枝、对权重量化压缩、对图结构做算子融合与重排、对推理引擎做运行时自适应调优。这段时间跑下来7B模型在单卡A100上撑到了380 QPSp99延迟压在180ms以内峰值显存降低了四成。这篇文章我打算把从选型到落地的完整路径写清楚包括技术决策的理由、踩过的坑、以及每一类场景下最省力的优化姿势。适合正在搞大模型低成本部署、受限于单卡资源或推理性能的工程团队参考。我不打算写成操作手册式的流水账而是尽量还原我在每个关键岔路口是怎么判断的——因为模型优化这件事方案本身的工程量往往不是瓶颈真正折磨人的是为什么走这条路不走那条路的决策过程。2. 先说清楚边界Model-Optimizer 管不到的那几件事一说到模型优化器不同背景的人上来会先吵一架做训练的说这玩意儿指的是优化算法做部署的说这是推理加速框架做硬件融合的又觉得这是算子编译器。这个工具链的定位必须一开始就立清楚它只针对已经训练完毕、准备上生产的模型文件在推理侧把模型变轻、变快。换个角度理解训练阶段的优化器解决模型能不能收敛推理阶段的优化器解决模型跑得够不够快、装不装得下。Model-Optimizer 干的是后者读入训练产出的权重文件输出一个比原来小而快的推理版本。这决定了它的三个边界也是大家最容易产生误解的地方第一条边界它不会帮你提精度。所有优化策略都以把精度损失压到可接受范围为前提但如果你原模型本身就欠拟合优化器救不了你。我给内部团队定的验收线是优化后模型的准确率或BLEU等指标相对原版不得下滑超过1.5%超过这条线宁可回滚方案重调参数。第二条边界它对训练框架产出的模型格式有要求。目前最省事的是从 ONNX 或 HuggingFace PyTorch 权重入口接入如果手里是 TensorFlow 的 SavedModel 想直接吃进来需要先做一次格式转换这个环节偶尔会引入算子兼容性问题。第三条边界它做的是分布式的单机优化。多机多卡的通信拓扑优化、数据并行切分策略那是部署框架和调度系统的事Model-Optimizer 只负责把单个节点内的计算密度和显存效率做到极限。跨节点的活儿我不拦但你不能指望它同时把 RDMA 网络大包小包问清楚。把这层边界先讲透是因为后面很多优化决策都与责任范围有关。比如我决定不做低比特量化到INT4以下、不做对精度敏感的稀疏化都是因为那些领域超出我当前工具能兜底的质量范围。明确边界之后整条优化管线的验收指标才真正可控谁也不想为一个追求极端的量化方案天天替模型效果背锅。3. 技术路线选择为什么是剪枝 量化 图优化三件套模型优化方向多得很蒸馏、低秩分解、算子融合、编译执行、动态路由……我最终敲定剪枝、量化、图优化这三件套不是觉得别的路线不行而是它们在通用性、压缩比、工程落地难度三个维度上最均衡适合我这种要服务多个业务线的边缘条件团队。3.1 结构化剪枝先把没用的块搬走剪枝不是新概念但结构化剪枝在大模型场景里一直有个尴尬点非结构化剪枝能把参数量砍得非常狠可是稀疏权重在GPU上根本跑不出理论加速因为底层硬件没有针对稀疏矩阵的高效支持。我在本地 RTX 3090 上验证过一批 50% 稀疏度的非结构化剪枝模型实际推理速度不升反降直接被稀疏索引计算的开销反噬。Model-Optimizer 采用的是基于注意力头重要性评估的结构化剪枝。重要性的打分方式不是看权重范数大小而是计算每个注意力头在验证集上的敏感度——具体做法是把目标头置零观察模型输出的扰动幅度扰动越大说明该头对预测越关键。这个方案是受一篇剪枝论文启发的思路论文里用泰勒展开近似这个置零扰动省去逐个跑前向的开销。我试跑之后发现泰勒近似在深层 Transformer 上偏差有点大后来只把打分当作初筛关键层还是做完整置零扰动实验。落到执行层剪枝的粒度是头和前馈网络的神经元分组。7B模型我尝试剪掉 15% 的注意力头和 10% 的FFN神经元跑在业务数据上的效果是困惑度只涨了 0.8%但推理吞吐提升了 22%。代价是原本的稠密矩阵乘法变成了一块块小矩阵排布得好内存连续性才有收益这为后面算子融合埋了个伏笔。3.2 量化压缩从FP16到INT8的收益账我最早用 FP16 跑推理显存占用是压下来了但距离要部署到4卡环境还有不少差距。我们模型权重有 14GBFP16加上 KV Cache 和激活值峰值显存逼近 28GB单卡 A100 的 80GB 容量看着够可一旦并发拉高内存带宽立刻就顶到天花板。所以量化直接上的是 INT8 权重 INT8 激活的 W8A8 方案不是在推理框架里开个开关就完事而是真正把权重和量化系数绑进图结构里。量化本身有两条路Post-Training QuantizationPTQ和 Quantization-Aware TrainingQAT。我选的 PTQ原因很简单——手里没有带标签的业务数据大规模重训的条件QAT 那种需要把模拟量化误差塞进反向传播的做法成本对我来说不现实。PTQ 的核心工作是校准。我从训练数据里抽出 300 条有代表性的样本喂给模型统计每个张量的 min/max 范围然后计算 scale 和 zero_point。这一步最考验人的不是实现而是选多少条样本、从哪个分布抽。太少的话大数值 outlier 把整个量化范围拉宽小数值的有效位全被挤没了太多的话校准时间比推理优化时间还长得不偿失。我的经验是跑 3 轮校准样本观察激活分布是否稳定第 4 轮基本不动了就停。另外一个值得说的细节是只量化权重不量化激活吞吐提升大约 1.5 倍权重和激活一起量化算子就能直接用 INT8 乘加指令吞吐可以到 2.8 倍左右。这个差距来自 GPU 上 INT8 张量核相对 FP16 的吞吐翻倍要是只量化一侧另一侧还得先转成 FP16 再做混合精度运算计算单元空转就浪费了。3.3 图优化算子融合把搬运工辞退量化之后模型里出现了一批 INT8 的量化/反量化算子跑起来很拖沓。比如一个标准的线性层加激活函数原始图里是 MatMul - Add - Cast - Clip四个算子各读各写显存带宽和核心同步的开销非常难看。Model-Optimizer 在这里做的事是图重写。把上面这条子图替换成单个融合算子MatMul 和 Add 合并成带偏置的 GEMMCast 和 Clip 合并进 GEMM 后处理。这个替换逻辑在 ONNX 图层面就可以实现把折叠路径写清楚模型文件里就直接少掉三个节点。跑起来的效果特别直观带宽不浪费了GPU 小核心之间同步事件也少了端到端延迟降了 18% 左右。除了算子融合图优化还有两块主要工作一是常量折叠把位置编码、旋转编码里不依赖输入的部分预先算好以常量形式塞进图里省得加载时反复生成二是节点重排把可以并行执行的多个分支顺序打散让硬件调度器有更多并发机会。这两块都不华丽但攒在一起收益才赶上融合那种一眼就看得见的提升。4. 系统架构与关键实现优化器到底管哪些环节做模型优化器和写普通的CRUD服务完全不是一回事——它的核心是变换和验证两条链路的闭环。我把它设计成了四层架构每层职责单一层与层之间通过标准的数据结构交互。第一层是接入层。输入可以是 PyTorch 权重目录、ONNX 文件或者 HuggingFace 的模型ID。接入层要做的事情是把各种框架的权重统一转成内部中间表示IR这个IR说白了是一张计算图节点是算子边是张量在每个节点上挂着权重和量化参数。转IR的过程需要处理框架差异比如 PyTorch 的 F.linear 和 ONNX 的 Gemm 在细节上就有出入需要对齐到统一的语义。第二层是分析层。这一层不修改图只做三件事计算图遍历、算子统计、敏感度分析。遍历会得出图的深度、分支数、节点类型分布算子统计会给出每个算子的 FLOPs、内存占用、访存比为优化决策提供数据支撑敏感度分析则针对剪枝和量化评估每个张量在多大变化范围内不会破坏模型效果。分析结果以报告形式输出我调优时主要就是看这份报告哪个模块是瓶颈一目了然。第三层是变换层。变换层是核心它包含剪枝引擎、量化引擎、图优化引擎三个子模块。每个引擎都实现了一套重写规则比如量化引擎的规则是把某个张量从FP16替换成INT8并插入量化节点图优化引擎的规则是检测到 MatMul-Add-Cast-Clip 子图时替换成融合算子。规则的执行顺序很关键先剪枝后量化因为剪枝改变张量形状之后量化还有机会重新校准图优化放在最后因为它可以吸收前面变换产生的新融合机会。第四层是验证层。验证层不是简单跑一下输出大小对不对而是做三件事数值一致性检查、精度回归测试、性能Benchmark。数值一致性检查是拿优化前的模型在固定输入上做前向和优化后的输出逐元素比较确定最大相对误差是否在容忍范围精度回归测试是把模型接到离线评测集上跑完整指标性能Benchmark则是把模型部署到推理服务里压测吞吐和延迟。这个架构的优点是分析结果和变换策略解耦优化失败时可以精确回溯到是哪一步变换出了问题。我后来加新优化手段比如算子融合的变体时只需要在变换层加一条规则不用动验证逻辑效率和复用性比把逻辑全塞在脚本里好太多。5. 落地实践第三章剪枝、量化、融合的完整执行顺序理论讲多了容易飘我把一整套流程跑了一遍从拿到原始权重到产出优化后的推理部署包每一步都记录下来了。执行顺序按先宽度减、再深度减、最后磨算子的逻辑来顺序反了容易事倍功半。5.1 第一步用 1% 数据跑敏感度分布我在一个内部文本分类任务上做实验模型是 7B 的 decoder-only 架构。直接拿全部验证集做敏感度分析太贵我抽了 1% 的验证样本大概 200 条。对每一层做一次前向 反向统计注意力头的敏感度。整个过程跑了两小时出的报告里能看到哪些头可以闭眼剪哪些头打死都不能动——后一种往往负责句子的全局依赖剪了就崩。剪枝执行时还遇到一个坑层与层之间的敏感度不是独立的。我把第3层的某头剪了之后第5层同位置头的敏感度会上升。所以我的建议是剪枝要迭代式剪一批验证一批每轮只剪当前敏感度最低的 5%循环到目标剪枝率而不是一次性剪到位。这个做法让我的最终剪枝模型效果稳定多了。5.2 第二步量化校准需要跑三种数据量化前先要解决一个前置问题模型里有些算子的输出范围动态浮动很大比如 LayerNorm 后的值域随 input 长度变化明显。我跑校准集时特意把输入长度分成短64 token、中512 token、长1024 token三组保证 scale 不会被少数长文本带偏。校准过程用的是我前面说过的方案300条样本三组各100条做 3 轮迭代。迭代的目的是让 scale 适应激活值的真实分布不是一次算出 min/max 就完事因为量化后的误差反过来会影响后面层的激活需要反复逼近稳定点。跑了三轮后误差基本收敛第四轮变化小于 1%我就直接用了第三轮的参数。一个容易被忽略的地方是量化后的模型在某些硬件上可能因为 INT8 算子的实现差异导致结果不一致。我在两代 NVIDIA GPUAmpere 和 Ada Lovelace上跑同一个量化模型最大输出误差居然差了近 2%后来排查发现是两代卡对 INT8 乘加的舍入策略有差异。这个结论提醒我量化模型上线前必须在目标硬件环境上重新做数值验证不能在开发机上验证完就自认为万事大吉。5.3 第三步算子融合顺序的小讲究图优化阶段的融合顺序也有讲究。我的经验是先做垂直融合再做水平融合。垂直融合指的是把一条数据依赖链上的算子合并比如 LayerNorm 后接 Attention 的 QKV 投影这个区域访存频繁融合收益最大水平融合指的是把多个独立分支里结构相同的子图合并成批量算子比如多头注意力里的12个头可以分成两批各算6个减少内核启动次数。执行完垂直和水平融合之后我会再做一遍常量折叠。折叠的对象包括旋转位置编码的 cos/sin 表、缩放因子、掩码矩阵。这些常量如果留在图里每次推理都要重新生成纯粹是浪费。把它们变成模型文件里的数据块之后启动加载反而更快。优化完的模型导出成 TensorRT 可以直接加载的 engine 格式另外保留一份 ONNX 格式作为备份——为什么TensorRT engine 绑定具体的 GPU 架构换卡就得重新构建ONNX 那份可以随时拿去别的环境重新优化是救急用的。6. 性能评测从数字里看真实收益我不喜欢只甩结论给出我实测的一组对比数据环境是单卡 A100 80G、CUDA 12.2、TensorRT 8.6模型是 7B llama 架构测试输入长度 512 token、输出长度 128 token。优化阶段显存占用(GB)吞吐(tokens/s)p50延迟(ms)相对原模型延迟提升原始FP16权重27.8820312- 结构化剪枝(15%头10%FFN)23.1104024820% INT8 W8A8量化15.3221011862% 图优化融合14.726809669% 算子重排与内核调优14.328908971%这个趋势很能说明问题剪枝主要省显存是从27.8到23.1吞吐提升不算夸张量化带来的吞吐红利最明显直接翻倍还多图优化在延迟上锦上添花。我提醒大家注意显存数字——14.3GB甚至低于权重的FP16体积14GB说明量化加剪枝把权重压成了7.1GB左右KV Cache 和激活值反而占了剩余空间。精度方面我也测了。文本分类任务 F1 从 0.923 降到 0.915下降了 0.008在 1.5% 的容忍线内。生成任务上困惑度从 8.41 升到 8.77BLEU 掉了0.6。对业务方来说这个幅度是可接受的他们自己的评测集上浮点退化也在可控范围内。值得强调的是这个收益数字高度依赖模型规模和输入长度。7B 模型的收益比 13B 明显因为小模型的计算密度更受制于访存短输入情况下算子启动开销占比更高图优化的贡献就更大长输入情况下 KV Cache 占比高剪枝省显存的效果就更明显。做性能预算时不能照搬我的数字务必在你自己模型上跑一遍Benchmark。7. 踩坑实录三个差点让项目翻车的细节优化思路没问题但在实现里还是有一堆细节坑。挑三个最典型的说一下每个都是我花了至少一天才定位到根本原因的。7.1 第一个坑结构化剪枝让 Attention 的 mask 维度对不上剪掉部分注意力头之后模型的 hidden_size 和 num_heads 都变了。模型实现里通常用固定的 attention_mask 张量形状是 [batch, 1, seq_len, seq_len]这里 seq_len 是序列长度头数和它没关系。真正炸的是 KV Cache 的形状它缓存的是 [batch, num_heads, seq_len, head_dim]num_heads 一变缓存索引全部错位。我第一次剪枝后跑推理直接报 shape mismatch。一开始还以为是加载权重的问题查了半天才反应过来是 KV Cache 的初始化逻辑里写死了原始头数。这个问题的通用解决方式是把 num_heads 做成模型配置参数剪枝后回写配置文件让加载端统一读配置而不是读常量。7.2 第二个坑INT8量化后的算子对某些输入长度会触发慢路径量化做完吞吐确实上去了但压测时发现一个诡异现象输入长度恰好是 256 的倍数时延迟突然翻倍。用 profiler 一查发现量化算子对输入张量的最后一个维度做了 32 字节对齐要求256 的倍数在取整边界上正好触发了一个 fallback 实现走的是 FP16 模拟路径所以慢。解决的办法有两种一种是把输入长度 padding 到对齐边界比如 256 就 padding 到 272另一种是调整算子实现里的对齐宏让它直接走 SIMD 快速路径。我最后选择了前者因为改算子实现需要重新编译 TensorRT 插件而 padding 方案只需要在预处理阶段做一次张量复制成本低得多。7.3 第三个坑图优化里常量折叠把权重重复计算了三遍常量折叠在脚本里跑的时候明明只执行一次可模型加载后我却发现显存里同样的权重出现了三份。查了很久才意识到问题出在序列化格式上优化后的模型文件里存的是折叠后的常量但加载器还会额外执行一次折叠逻辑文件里的原始权重又被读了一遍。解决方式是给模型文件加了个元数据标记写明已经完成折叠加载器检测到标记就跳过折叠步骤。这个坑教会我任何图优化变换都必须和加载器保持同步不能只改优化端不改运行端否则优化结果会被默默抵消。8. 从单模型到服务化优化后的模型上线没那么省心模型优化只是第一步真正上线还有很多最后一公里问题。Model-Optimizer 输出的引擎文件能不能直接服务化取决于推理服务有没有做配套改造。我在 TensorRT 引擎加载时遇到一个体验上的大问题首次加载要 10 秒左右因为 engine 文件里包含优化好的 CUDA kernel 缓存加载时要反序列化和初始化计算流。我做过一个小优化把 engine 加载放到一个独立预热进程里服务启动时通过共享内存把初始化好的 engine 传给工作进程启动时间从 10 秒压到了 2 秒。这个技巧在处理多副本扩容时特别好用。还有一个老生常谈但必须说的问题动态 batch 的动态 shape 处理。TensorRT 支持动态 shape 但我建议你谨慎用。我把输入限制成固定长度512 tokenbatch 支持 1 到 32 的动态范围换来的是引擎在常见 batch 档位上的性能都接近最优。如果你非要支持任意长度输入引擎要为每个可能的 shape 编译优化候选构建时间随候选数量指数增长这在实际项目中不划算。服务化之后还需要盯监控。我上线了三个关键指标每 token 平均延迟、显存波动范围、优化前/后命中量化快速路径的算子比例。最后一个指标很关键一旦发现某个算子的 fast path 占比下降超过 10%说明输入长度分布变了需要重新跑一遍校准才能维持性能。9. 后续演进我会继续在哪些方向打磨Model-Optimizer 当前版本能稳定支撑业务但我知道还有很多空间可以压。后续计划按优先级排列每一步都在当前架构上增量扩展不推翻重来。第一优先是支持更细粒度的量化方案。当前 W8A8 收益已经很好但不同层对量化误差的敏感度差异很大——Attention 的输出层相对敏感FFN 的中间层相对鲁棒。下一步想给每层配不同的量化位宽敏感度高的层用 FP16鲁棒的层压到 INT8整体显存还能再降几个百分点精度几乎不受影响。第二优先是把校准集选择自动化。现在靠人肉从训练集抽样本换业务线就要重新抽。我在探索用梯度影响函数来挑选最具代表性的校准样本目标是让校准集数量和分布都能根据模型自动确定减少人工介入。第三优先是更聪明的融合策略。现在垂直融合和水平融合是靠手工规则枚举出来的融合策略不少但模型结构一变规则就要跟着改。理想状态是让优化器学习不同融合策略在真实硬件上的代价模型自动搜索最优融合方案。这本质上是个组合优化问题前期可以先从贪心搜索做起。有没有第四优先有但说句实话我之前试过把优化后的模型放到 CPU 推理场景收益远不如 GPU 明显。CPU 的瓶颈在内存带宽和指令集支持INT8 的加速比没有 GPU 那么夸张还把模型精度拉低了一截。这个方向我暂时搁置等业务出现明确需求再拾起来。10. 几点经验写给正准备做模型优化的你带 Model-Optimizer 走完从零到落地的全过程我最大的感受是模型优化不是一锤子买卖它是一条和业务场景强绑定的流水线。做过一轮之后每次业务换模型、换硬件、换输入分布都要重新走过一遍校准和验证流程架不住每次都能发现新问题。所以尽早把自动化验证的机制建好比多压榨几个点的性能更值钱。具体给几点建议敏感度分析和剪枝要用迭代法不要在敏感度报告上一次性下重手量化校准的数据尽量覆盖输入长度的多档分布单一长度会让 scale 偏斜算子融合顺序采用垂直 - 水平 - 常量折叠这个顺序能让每一步收益最大化模型优化日志必须完整记录每个变换的参数和验证结果我靠日志定位过好几次部署事故优化后的模型要在目标硬件上重新评一遍精度开发机和生产机的舍入行为不完全一致做模型优化最忌讳的就是拿来主义网上开源了不少优化工具但每个团队的模型结构、数据分布、硬件环境都不同照搬参数大概率达不到预期。把这个过程当成分析 — 变换 — 验证的闭环实验去迭代每一步都用数据说话优化出来的模型才真正扛得住线上压力。如果你也在做类似的事或者读完发现和我有不一样的实战经验欢迎在评论里聊一聊具体的卡点。我收到过不少朋友的反馈说量化校准样本不好选、图优化规则不够灵活这些都是值得拿出来单独写一篇细说的话题看大家的反馈我后续再展开。
返回列表