1. 模型优化器到底在解决什么问题
第一次接触 Model-Optimizer 这个概念,是在一个推荐系统的项目里。当时模型训练完,离线指标 AUC 0.82 看着挺漂亮,一上线推理延迟直接飙到 800ms,QPS 连 50 都扛不住。老板问“能不能压到 100ms 以内”,我盯着那坨 FP32 的权重文件发呆——这就是模型优化器要解决的核心矛盾:模型效果和推理效率之间的拉锯战。
Model-Optimizer 不是一个具体的库或者工具,它是一整套方法论和工具链的集合。你可以把它理解成模型从“实验室状态”到“生产状态”之间的加工流水线。训练出来的模型就像刚从矿山挖出来的原石,能用,但太重、太慢、太占地方。优化器要做的事情就是切割、打磨、压缩,让它变成能塞进生产环境这颗“螺丝孔”里的标准件。
具体来说,它覆盖了这么几个层面的事情。量化是把 FP32 的权重和激活值用 INT8 甚至 INT4 来表示,直接把模型体积砍到原来的四分之一甚至八分之一。剪枝是去掉那些对输出贡献极小的神经元连接,让网络变得稀疏。知识蒸馏是让一个小模型去学大模型的行为,用大模型当老师。算子融合是把多个连续的小算子合并成一个大的计算核,减少 kernel launch 的开销。图优化则是在计算图层面做等价变换,消除冗余计算。
什么人需要关注这个?如果你只是在学校跑跑实验、发发论文,那可能不太需要。但只要你涉及到模型部署——不管是在服务器端、移动端还是嵌入式设备上——Model-Optimizer 就是绕不过去的坎。我见过太多算法工程师模型训得贼好,一到部署就抓瞎,最后只能加机器堆资源,成本高得离谱。
一个基本认知:模型优化不是“锦上添花”,而是“雪中送炭”。在资源受限的场景下,不优化根本跑不起来。
2. 量化:最直接的提速手段
2.1 从 FP32 到 INT8 到底损失了什么
量化的本质是用更少的比特位来表示数值。FP32 有 32 个比特位,其中 1 位符号、8 位指数、23 位尾数,能表示的数值范围大约是 ±3.4×10³⁸,精度能到小数点后 7 位左右。INT8 只有 8 个比特位,表示范围是 -128 到 127,精度就是整数级别。
那为什么量化之后模型还能用?关键在于神经网络对数值精度其实没那么敏感。权重和激活值的分布通常集中在某个范围内,真正需要高精度的极端值很少。量化的核心操作就是找到一个合适的缩放因子(scale)和零点(zero point),把浮点数映射到整数空间。
公式很简单:
real_value = (int_value - zero_point) * scale int_value = round(real_value / scale) + zero_pointscale 决定了量化的粒度,zero_point 保证了浮点零能精确映射到某个整数。对于对称量化,zero_point 就是 0;对于非对称量化,zero_point 需要根据实际数据分布来定。
2.2 训练后量化与量化感知训练的选择
量化分两条路线。训练后量化(PTQ)是拿训练好的模型直接量化,不需要重新训练,速度快、成本低。量化感知训练(QAT)是在训练过程中模拟量化误差,让模型提前适应低精度表示,效果更好但需要重新训练。
我的一般建议是:先试 PTQ,如果精度掉得不多(比如 Top-1 准确率下降在 1% 以内),那就直接用。如果掉得厉害,再考虑 QAT。PTQ 里面又分动态量化和静态量化——动态量化只量化权重,激活值在推理时动态计算 scale;静态量化需要校准数据集提前统计激活值分布,推理时直接用固定的 scale,速度更快。
校准数据集的选择很关键。一般从训练集里随机抽 100-500 个样本就够了,但要保证覆盖各种典型场景。我之前做人脸识别模型量化,校准集里全是正脸,结果侧脸场景精度直接崩了。后来把侧脸、遮挡、不同光照的样本都加进去,问题才解决。
2.3 实操中的精度补偿技巧
量化之后精度下降是常态,关键是怎么补。几个我常用的手段:
逐通道量化:对卷积层的每个输出通道单独计算 scale,而不是整个层共用一个。这样能更好地适应不同通道的数值分布差异,精度损失明显更小。
混合精度:不是所有层都适合 INT8。第一层和最后一层通常对精度更敏感,可以保持 FP16 或 FP32。中间那些计算密集的层用 INT8,兼顾速度和精度。
偏差校正:量化会引入系统性偏差,可以通过分析量化前后激活值的分布差异,对权重做微调来补偿。
下面是一个典型的 PTQ 配置示例:
import torch from torch.quantization import get_default_qconfig, prepare, convert # 选择量化配置 qconfig = get_default_qconfig('fbgemm') # 服务器端用 fbgemm,移动端用 qnnpack # 准备模型 model.eval() model.qconfig = qconfig model_prepared = prepare(model) # 校准 with torch.no_grad(): for data in calibration_loader: model_prepared(data) # 转换为量化模型 model_quantized = convert(model_prepared)这段代码看起来简单,但坑不少。比如prepare之后模型结构会变,插入一些观察者节点,这时候不能直接保存。convert之后模型才是最终形态。还有校准的时候一定要用torch.no_grad(),不然显存直接爆炸。
量化最大的坑:不要等到模型要上线了才做量化。最好在模型设计阶段就考虑量化友好性,比如避免使用对量化敏感的激活函数(如 Swish 在低精度下表现就不太好)。
3. 剪枝:给模型做减法
3.1 结构化剪枝与非结构化剪枝的取舍
剪枝的思路很直观:神经网络里有很多参数其实没什么用,去掉它们对输出影响不大。但怎么去掉、去掉之后怎么存、怎么算,这里面门道很多。
非结构化剪枝是把单个权重置零,理论上能获得很高的稀疏度(90% 以上),但问题是硬件对稀疏矩阵的支持参差不齐。GPU 对稀疏计算的支持有限,实际加速比往往远低于稀疏度。你剪了 90%,可能只快了 20%。
结构化剪枝是直接去掉整个通道、整个头或者整个层。这种剪枝对硬件友好,因为剪完之后还是稠密矩阵,只是维度变小了。加速比更实在,但稀疏度通常做不了太高,一般 30%-50% 就差不多了。
我的经验是:如果目标平台是通用 GPU,优先考虑结构化剪枝;如果有专门的稀疏计算加速器,非结构化剪枝才能发挥价值。移动端基本只能走结构化路线。
3.2 基于重要性的剪枝策略
怎么判断哪些通道重要?常见的有几种方法:
L1/L2 范数:计算每个通道权重的范数,范数小的认为不重要。简单粗暴但有效,是很多论文的 baseline。
BN 层缩放因子:如果网络里有 BatchNorm,可以直接用 BN 的 gamma 参数作为重要性指标。gamma 接近零的通道,说明这个通道的输出被 BN 压得很小,对后续贡献有限。
梯度信息:计算损失对每个通道的梯度,梯度小的说明对最终输出影响小。但需要额外的反向传播,计算成本高。
泰勒展开:用一阶泰勒展开估计去掉某个通道后损失的变化,更精确但计算量更大。
实际操作中,我一般先用 BN gamma 做初筛,再用少量数据做微调验证。下面是一个基于 BN 缩放因子的剪枝流程:
import torch.nn.utils.prune as prune # 收集所有 BN 层的 gamma 值 bn_gammas = [] for module in model.modules(): if isinstance(module, torch.nn.BatchNorm2d): bn_gammas.append(module.weight.abs()) # 全局排序,确定阈值 all_gammas = torch.cat([g.flatten() for g in bn_gammas]) threshold = torch.quantile(all_gammas, 0.3) # 剪掉 30% # 对每个 BN 层生成 mask for module in model.modules(): if isinstance(module, torch.nn.BatchNorm2d): mask = module.weight.abs() > threshold prune.custom_from_mask(module, name='weight', mask=mask)剪完之后一定要做微调,一般用原学习率的十分之一跑几个 epoch,精度基本能恢复回来。
3.3 剪枝后的微调与精度恢复
剪枝最怕的就是“剪完就完”。直接剪掉 50% 的通道,精度不掉个 10% 才怪。微调是必须的,而且有一些技巧。
渐进式剪枝比一次性剪枝效果好很多。比如你想剪 50%,可以分 5 次,每次剪 10%,每次剪完微调一下。这样模型有缓冲时间,精度恢复更平滑。
学习率策略也很关键。微调时用余弦退火或者带热重启的调度器,比固定学习率效果好。我试过用 OneCycle 策略,收敛速度明显更快。
知识蒸馏辅助:剪枝后的模型当学生,原模型当老师,用蒸馏损失辅助微调。这样精度恢复得更充分,尤其在小模型上效果显著。
剪枝的一个反直觉经验:不是剪得越多越好。存在一个“甜点区”,超过这个点精度会断崖式下跌。我一般会画一条精度-稀疏度曲线,找到拐点位置。
4. 知识蒸馏:让小模型学到大模型的精髓
4.1 软标签为什么比硬标签更有效
知识蒸馏的核心思想是:大模型(教师)的输出不只是“正确答案”,还包含了“错误答案有多错”的信息。比如一个手写数字分类任务,教师模型对某个“7”的预测可能是:70% 是 7,20% 是 1,8% 是 9,2% 是其他。这个分布比单纯的“label=7”包含了更多信息——它告诉学生模型,这个样本长得像 1 和 9,但最像 7。
这种“软标签”提供的监督信号更丰富,学生模型能学到类间相似性,泛化能力更强。温度参数 T 就是用来控制软标签平滑程度的:
softmax_with_temperature(z_i) = exp(z_i / T) / sum(exp(z_j / T))T=1 就是普通 softmax,T 越大分布越平滑,类间关系的信息越突出。一般 T 取 3-10 之间,具体看任务。
4.2 蒸馏损失函数的设计与调参
蒸馏的损失函数通常是两部分加权:
total_loss = alpha * distillation_loss + (1 - alpha) * student_lossdistillation_loss 是学生和教师软标签之间的 KL 散度,student_loss 是学生和真实标签之间的交叉熵。alpha 控制两者的平衡,一般取 0.5-0.9 之间。
温度 T 和 alpha 需要联合调。T 太小,软标签退化成硬标签,蒸馏没意义;T 太大,分布太平,学生学不到重点。我的经验是先用 T=4、alpha=0.7 跑一版,看验证集表现再微调。
还有一个细节:蒸馏时教师模型要设成 eval 模式,而且要用torch.no_grad()包住教师的前向传播,不然显存直接翻倍。
4.3 中间层特征蒸馏的进阶玩法
只蒸馏最后输出层有时候不够,尤其是学生和教师结构差异大的时候。这时候可以蒸馏中间层的特征图。
FitNets是最早的方法之一,让学生中间层的特征图去逼近教师的。但两者维度可能不一样,需要加一个回归器(regressor)做映射。
注意力蒸馏是让学生学习教师的注意力图,即特征图的空间激活模式。这个对分类任务特别有效,因为注意力图反映了模型“看哪里”。
关系蒸馏更高级,不是让学生模仿教师的单个输出,而是模仿教师对不同样本之间的关系。比如教师认为样本 A 和 B 很像,学生也要认为它们很像。这种方法对异构蒸馏(教师和学生结构完全不同)特别有用。
# 中间层特征蒸馏示例 class DistillationLoss(nn.Module): def __init__(self, alpha=0.7, temperature=4.0): super().__init__() self.alpha = alpha self.T = temperature self.kl_div = nn.KLDivLoss(reduction='batchmean') self.mse = nn.MSELoss() def forward(self, student_out, teacher_out, labels, student_feat=None, teacher_feat=None): # 软标签蒸馏损失 soft_student = F.log_softmax(student_out / self.T, dim=1) soft_teacher = F.softmax(teacher_out / self.T, dim=1) distill_loss = self.kl_div(soft_student, soft_teacher) * (self.T ** 2) # 硬标签损失 hard_loss = F.cross_entropy(student_out, labels) # 中间层特征损失 feat_loss = 0 if student_feat is not None and teacher_feat is not None: feat_loss = self.mse(student_feat, teacher_feat) return self.alpha * distill_loss + (1 - self.alpha) * hard_loss + 0.1 * feat_loss蒸馏的一个常见误区:教师模型不是越大越好。教师太大,学生学不动,反而效果差。一般教师比学生大 2-5 倍比较合适。
5. 算子融合与图优化:榨干硬件的每一滴性能
5.1 常见的算子融合模式
算子融合是推理引擎层面的优化,不需要改模型结构,但效果立竿见影。最常见的融合模式有几种:
Conv + BN + ReLU:这是最经典的组合。推理时 BN 的参数可以完全折叠进 Conv 的权重里,ReLU 直接接在后面。三个算子变成一个,减少两次内存读写和两次 kernel launch。
Conv + Add + ReLU:残差连接里的常见模式。Add 和 ReLU 可以融合成一个算子。
MatMul + Add:全连接层加偏置,这个融合最简单,但收益也不小。
LayerNorm + MatMul:Transformer 里的常见模式,融合后能减少不少开销。
融合的数学原理其实不复杂。以 Conv+BN 为例:
BN(Conv(x)) = gamma * (Conv(x) - mean) / sqrt(var + eps) + beta = gamma / sqrt(var + eps) * Conv(x) + (beta - gamma * mean / sqrt(var + eps))令W' = W * gamma / sqrt(var + eps),b' = (b - mean) * gamma / sqrt(var + eps) + beta,就得到了融合后的卷积权重和偏置。
5.2 计算图优化的典型手段
除了算子融合,计算图层面还有很多优化空间:
常量折叠:把图中所有能在编译期计算的节点提前算好。比如两个常量相加,直接算出一个常量,不用在运行时再算。
死代码消除:去掉那些对输出没有贡献的节点。比如某个分支的输出没有被用到,整个分支都可以删掉。
公共子表达式消除:如果两个地方计算了相同的表达式,只算一次,结果复用。
内存复用:分析张量的生命周期,让不重叠的张量共享同一块内存。这个对显存受限的场景特别有用。
循环展开:把循环体展开成直线代码,减少循环控制开销。对固定次数的循环效果明显。
这些优化在 TensorRT、TVM、ONNX Runtime 这些推理引擎里都有实现,但不同引擎的优化策略和效果差异很大。我一般会同时试几个引擎,看哪个在实际硬件上跑得最快。
5.3 不同推理引擎的优化效果对比
| 引擎 | 优势场景 | 量化支持 | 算子融合 | 动态形状 | 上手难度 |
|---|---|---|---|---|---|
| TensorRT | NVIDIA GPU | INT8/FP16 | 非常丰富 | 有限支持 | 中等 |
| ONNX Runtime | 跨平台 | INT8 | 较丰富 | 支持好 | 低 |
| TVM | 自定义硬件 | INT8/INT4 | 可定制 | 支持好 | 高 |
| OpenVINO | Intel CPU/GPU | INT8 | 丰富 | 支持好 | 中等 |
| TFLite | 移动端 | INT8 | 一般 | 支持好 | 低 |
选引擎不能只看纸面性能,要结合你的部署环境。NVIDIA GPU 上 TensorRT 基本是最优解,但如果是 ARM 芯片,TFLite 或者 TVM 可能更合适。ONNX Runtime 的优势是通用性好,一套模型能跑在多种硬件上,适合快速验证。
引擎选型的一个实用建议:先用 ONNX Runtime 跑通流程,确认精度没问题,再针对目标硬件做深度优化。不要一上来就死磕 TensorRT,调试成本太高。
6. 实操中的常见问题与排查技巧
6.1 量化后精度暴跌的排查思路
量化后精度暴跌是最常见的问题,排查要按步骤来:
第一步:确认量化配置是否正确。检查 qconfig 是否匹配目标硬件,fbgemm 和 qnnpack 不能混用。检查是否所有该量化的层都量化了,有些自定义层可能没被正确处理。
第二步:检查校准数据。校准集是否覆盖了所有典型场景?数量是否足够?我一般用 200-500 个样本,太少统计不准,太多浪费时间。
第三步:逐层分析。用工具把每层的量化误差打出来,看是哪几层出了问题。通常是第一层、最后一层或者某些特殊结构(如 attention 里的 softmax)对量化敏感。
第四步:尝试混合精度。把敏感层保持 FP32,其他层量化。精度一般能回来不少。
第五步:考虑 QAT。如果 PTQ 怎么调都不行,那就上 QAT。虽然麻烦,但效果确实好。
6.2 剪枝后模型无法收敛的解决方案
剪枝后微调不收敛,通常是这几个原因:
剪枝率太高:一次性剪太多,模型结构被破坏得太厉害。降低剪枝率,或者改成分步剪枝。
学习率太大:剪枝后模型参数已经在一个比较好的位置了,学习率太大会直接跳出去。用原学习率的 1/10 到 1/100。
没有冻结 BN 统计量:剪枝后 BN 的 running mean 和 var 还是旧的,和新的权重不匹配。微调前先跑几百个 batch 更新 BN 统计量,或者直接冻结 BN 层。
数据增强太强:微调阶段数据增强要减弱,让模型专注于恢复精度,而不是学新的不变性。
6.3 部署时的兼容性坑点
模型优化完,部署时还有一堆坑:
算子不支持:某些优化后的算子目标推理引擎不支持。比如 TensorRT 对某些自定义算子的支持就有限。解决方案是查引擎的算子支持列表,或者用插件机制自己实现。
动态形状问题:优化时如果固定了输入形状,部署时遇到不同尺寸的输入就会报错。要么在优化时保留动态维度,要么准备多个优化版本。
精度对齐:优化后的模型和原模型输出有微小差异,如果业务逻辑对精度敏感(比如金融风控),需要做精度对齐验证。
内存对齐:某些硬件对内存对齐有要求,优化后的模型可能不满足。这个一般在引擎层面处理,但偶尔也会遇到。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 量化后精度掉 5%+ | 校准集不具代表性 | 检查校准集分布 | 补充典型样本 |
| 剪枝后不收敛 | 学习率过大 | 观察 loss 曲线 | 降低学习率 |
| 推理速度没提升 | 算子未融合 | profile 算子耗时 | 检查融合规则 |
| 输出结果不一致 | 精度对齐问题 | 逐层对比输出 | 调整量化配置 |
| 显存溢出 | 中间张量未复用 | 分析内存峰值 | 开启内存复用 |
一个血泪教训:优化后的模型一定要做端到端测试,不能只看离线指标。我遇到过离线精度完全一致,但上线后因为输入预处理不一致导致结果全错的情况。
7. 工具链选型与自动化流程搭建
7.1 主流优化工具的特点与适用场景
现在模型优化工具很多,选对了事半功倍:
PyTorch Quantization:PyTorch 官方量化工具,和 PyTorch 生态无缝集成。支持 PTQ 和 QAT,文档齐全。缺点是导出到其他格式时偶尔会有兼容性问题。
NNCF:Intel 出的神经网络压缩框架,支持量化、剪枝、蒸馏。和 OpenVINO 配合很好,在 Intel 硬件上效果最佳。
TensorRT:NVIDIA 的推理优化引擎,在 NVIDIA GPU 上性能无敌。支持 INT8 和 FP16,算子融合非常激进。缺点是绑定 NVIDIA 硬件,调试信息不够透明。
ONNX Runtime:微软的跨平台推理引擎,支持多种硬件后端。量化工具链完善,上手简单。性能不如 TensorRT 极致,但通用性好。
TVM:开源深度学习编译器,支持自定义硬件后端。优化空间大,但学习曲线陡峭,适合有编译原理背景的团队。
我的建议是:如果团队没有专门的推理优化工程师,优先用 ONNX Runtime 或者 PyTorch 自带工具。如果有 NVIDIA GPU 且追求极致性能,上 TensorRT。如果是 Intel 平台,NNCF + OpenVINO 是首选。
7.2 构建自动化优化流水线
手动优化一次两次还行,如果要持续迭代,必须自动化。我一般会搭这么一条流水线:
第一步:模型导出。训练完自动导出 ONNX 格式,同时保存一份 PyTorch 原始模型作为 baseline。
第二步:自动量化。用校准集跑 PTQ,生成量化模型。同时记录精度变化,如果掉太多就触发告警。
第三步:自动剪枝。根据预设的剪枝率生成剪枝模型,自动微调,记录精度恢复情况。
第四步:性能测试。在目标硬件上跑 benchmark,记录延迟、吞吐、显存占用。
第五步:精度验证。在验证集上跑端到端测试,对比优化前后的输出差异。
第六步:生成报告。把精度、性能、模型大小等指标汇总成报告,方便决策。
# 自动化优化流水线伪代码 class ModelOptimizerPipeline: def __init__(self, model, calibration_loader, val_loader): self.model = model self.calibration_loader = calibration_loader self.val_loader = val_loader self.results = {} def run(self): # baseline self.results['baseline'] = self.evaluate(self.model) # 量化 quantized_model = self.quantize(self.model) self.results['quantized'] = self.evaluate(quantized_model) # 剪枝 pruned_model = self.prune(self.model) self.results['pruned'] = self.evaluate(pruned_model) # 量化+剪枝 combined = self.quantize(pruned_model) self.results['combined'] = self.evaluate(combined) # 性能测试 for name, model in self.results.items(): self.benchmark(model, name) return self.generate_report()7.3 优化效果评估指标体系
评估优化效果不能只看一个指标,要综合看:
精度指标:Top-1/Top-5 准确率、mAP、F1 等,根据任务定。关键是和 baseline 对比,看掉了多少。
性能指标:延迟(P50/P99)、吞吐量(QPS)、显存占用、模型体积。延迟要看 P99 而不只是平均,因为长尾延迟对用户体验影响更大。
压缩比:模型体积压缩了多少倍,参数量减少了多少。
加速比:推理速度提升了多少倍,这个要和硬件绑定看。
能效比:每瓦性能,对移动端和边缘设备特别重要。
我一般会画一张雷达图,把各个维度都标出来,直观对比不同优化方案的综合表现。
8. 从项目实战中积累的经验
8.1 一个推荐模型的优化全过程
去年做过一个推荐模型,原始模型 2.3GB,FP32 精度,在 T4 上推理延迟 120ms,QPS 只能到 80。业务要求延迟压到 30ms 以内,QPS 至少 300。
第一步做量化。PTQ 之后精度掉了 0.8%,还能接受。模型体积降到 600MB,延迟降到 45ms,QPS 到 200。还不够。
第二步做算子融合。用 TensorRT 重新编译,Conv+BN+ReLU 全部融合,延迟降到 32ms,QPS 到 280。接近目标了。
第三步做结构化剪枝。剪掉 25% 的通道,微调 10 个 epoch,精度恢复 0.3%。模型体积降到 450MB,延迟降到 26ms,QPS 到 350。达标。
第四步做蒸馏。用原模型当教师,剪枝后的模型当学生,再蒸馏 5 个 epoch,精度又回来 0.2%。最终精度只比 baseline 低 0.3%,延迟 26ms,QPS 350。
整个过程踩了不少坑。比如 TensorRT 对动态 shape 支持不好,推荐模型的序列长度是变化的,最后只能固定长度加 padding。还有剪枝后 BN 统计量没更新,导致微调一直不收敛,后来先跑了几百个 batch 更新 BN 才正常。
8.2 移动端部署的特殊考量
移动端和服务器端完全是两个世界。服务器端可以堆硬件,移动端不行,功耗、内存、算力都是硬约束。
移动端优化有几个特殊点:
功耗优先:不是越快越好,而是要在功耗和性能之间找平衡。有时候降频跑反而更合适,因为能效比更高。
内存带宽瓶颈:移动端 GPU 的内存带宽有限,算子融合减少内存读写比减少计算量更重要。
NCHW vs NHWC:移动端 GPU 通常对 NHWC 格式更友好,转换布局能带来明显加速。
INT8 是标配:移动端基本只能跑 INT8,FP16 都嫌重。量化时必须考虑移动端的特殊算子支持。
模型分片:大模型可以拆成多个小模型,按需加载,减少内存峰值。
我用 TFLite 部署过一个图像分类模型,原始模型 80MB,INT8 量化后 20MB,延迟从 200ms 降到 45ms。关键优化点是用了 TFLite 的 delegate 机制,把能卸载到 GPU 的算子都卸载过去,CPU 只处理剩下的。
8.3 优化与精度的平衡艺术
做了这么多优化项目,最大的体会是:优化不是免费的午餐,每一分性能提升都要用精度来换。关键是要找到业务能接受的平衡点。
我的做法是先和业务方对齐精度底线。比如推荐模型 AUC 不能低于 0.80,图像分类 Top-1 不能低于 92%。有了底线,优化就有目标了。
然后按“量化→融合→剪枝→蒸馏”的顺序逐步上,每做一步测一次精度和性能。如果某一步精度掉太多,就回退或者调整参数。不要一次性把所有手段都用上,出了问题都不知道是哪个环节导致的。
还有一个经验:优化要趁早。不要等模型定型了再优化,最好在模型设计阶段就考虑优化友好性。比如少用对量化敏感的操作,控制模型深度和宽度,这些都能让后续优化更顺利。
最后分享一个我常用的精度-性能权衡表,每次优化完都填一下,直观看到每一步的收益和代价:
| 优化阶段 | 精度变化 | 延迟(ms) | QPS | 模型大小(MB) |
|---|---|---|---|---|
| Baseline | 0 | 120 | 80 | 2300 |
| +PTQ | -0.8% | 45 | 200 | 600 |
| +算子融合 | -0.8% | 32 | 280 | 600 |
| +剪枝 | -1.1% | 26 | 350 | 450 |
| +蒸馏 | -0.3% | 26 | 350 | 450 |
这张表比任何文字描述都直观。每次优化完更新一下,团队里谁都能看懂当前状态和下一步方向。
优化这件事没有终点,硬件在变、模型在变、业务需求也在变。保持对新技术和新工具的敏感度,多动手试,多记录数据,慢慢就积累出自己的方法论了。