自研模型优化工具链:量化、剪枝、蒸馏与推理加速实战

📅 发布时间:2026/9/29 3:06:58
自研模型优化工具链:量化、剪枝、蒸馏与推理加速实战
1. 我为什么放着现成框架不用偏要自己做Model-Optimizer先交代一下背景。去年下半年我负责的项目把一批计算机视觉模型从测试环境推到生产环境做推理服务模型清一色都是百MB到几百MB级别的CNN和轻量Transformer。部署完第一天就被运维叫过去了——GPU显存吃紧单卡只能塞两个实例线上延迟和压测数据差了一倍用户端能明显感到卡顿。当时的第一反应是上现成的优化工具。TensorRT、ONNX Runtime、OpenVINO这些我都用过量化、算子融合、图优化这些都是现成的看上去只要把模型过一遍就能白捡性能。但真正在项目里跑起来之后我发现自己陷入了一个很尴尬的局面工具每个都很好用却解决不了组合起来用的问题。TensorRT对GPU优化很强但换到CPU部署就得换OpenVINOONNX Runtime支持混合精度却对动态shape的支持让我踩了不少坑更别提剪枝、蒸馏这类训练侧优化几个工具之间根本没有统一的接口优化前后的效果也缺乏可量化的对比体系。来回折腾了快两周我最终决定自己做一个模型优化工具链也就是现在这个Model-Optimizer项目。它的出发点很朴素把量化、剪枝、蒸馏、算子融合这些能力整合成一个可插拔的Pipeline让一条命令从原始模型走到推理优化模型过程中每个环节消耗多少时间、损失多少精度、节省多少显存和延迟全部有记录可查。这篇文章不打算写成一个完整的使用文档而是想把这个项目拆开来讲我是怎么选型的每一项优化技术的底层逻辑是什么代码实现里哪些位置最容易被忽略以及实测下来哪些优化是真香、哪些是鸡肋。如果你也在做模型部署和推理加速或者正打算在训练侧做一些瘦身这篇应该能帮你少走不少弯路。2. 模型优化的四条主线量化、剪枝、蒸馏、图优化到底在优化什么动手写代码之前我花了不少时间把四类主流优化技术背后的机理过了一遍。很多人的误区是觉得优化就是把模型变小实际上这四件事的目标和手段差别非常大选错方向很可能精度掉了、速度还没提上去。2.1 量化不是简单把数字变小而是重新分配数值精度量化在深度学习里最常见的应用就是把FP32权重压缩到INT8。FP32用32位表示一个浮点数INT8只用8位理论上模型体积直接缩到四分之一推理时的矩阵乘法和访存开销也会大幅下降。但量化不是做个类型转换那么简单。FP32的数值范围大约在3.4e-38到3.4e38之间而且分布非常不均匀——深度学习模型里权重和激活值的分布绝大多数集中在0附近呈现一个钟形分布。而INT8只能表示256个离散值。怎么把这256个档位映射到原本浮点数占据的连续空间里就是量化的核心问题。我打了个比方给团队里新来的同事解释FP32记录一个人的身高可以精确到小数点后七八位INT8就像用一把尺子上的256个刻度去量尺子的范围要设计好刻度怎么分布也要设计好。如果尺子刻度的范围定太宽大部分人都会落在同一个刻度上精度损失巨大范围定太窄几个特别高的值直接爆表截断同样损失严重。实际实现里量化参数的标定过程需要训练集上的激活分布作为参考。具体做法是拉一批有代表性的校准数据过一遍模型统计每一层的激活值min/max或者分布直方图然后据此算出scale缩放系数和zero_point零点偏移。scale负责把浮点区间映射到整数区间zero_point用来处理浮点0到整数0的偏移。常见标定策略我对比过三家MinMax最简单直接取观察到的min/max作为边界但容易被离群点带偏Percentile取99.99%分位数对离群点稳健很多实测用得更普遍还有更复杂的KL散度方法通过最小化量化前后分布的信息损失来选阈值。在Model-Optimizer里我默认采用的策略是百分位校准同时会在日志里同时输出MinMax和KL结果方便对比诊断。2.2 剪枝要剪的不是无用权重而是冗余结构剪枝的逻辑跟量化完全不同。量化是压缩每个数字的表示精度剪枝则是直接让一部分权重变为0甚至直接删掉一些神经元或卷积通道。非结构化剪枝是最容易想到的做法训练完之后把绝对值小于某个阈值的权重直接置零。问题也随之而来——稀疏矩阵在CPU和GPU上并不天然加速。除非硬件和底层库针对稀疏计算做了专门优化否则这些0仍然占着存储位置计算时一样会被过一遍结果模型文件小了推理速度没变甚至因为稀疏表示的索引开销变慢。所以我重点做的是结构化剪枝。针对CNN主要就是channel prune通道剪枝针对Transformer主要就是head prune注意力头剪枝。通道剪枝的核心是要解决一个问题怎么判断某一层的哪个通道最不重要判断依据我用了两类。第一类是权重范数L1或L2范数小的通道说明它对输出的贡献整体偏弱第二类是基于激活的统计看这个通道输出的feature map对最终loss的敏感度BatchNorm层的gamma参数是个现成的代理指标——gamma越小说明这个通道的缩放系数越低剪掉它引入的误差相对可控。但这里有个容易踩坑的点不同层的冗余度完全不一样。比如ResNet里shortcut连接存在的层剪枝对梯度流的影响和普通层不同Transformer里Q、K、V三个矩阵的剪枝比例如果不一致attention模式的破坏会很严重。所以Model-Optimizer实现的是全局敏感度感知剪枝先把每层每个通道的敏感度算出来再做全局排序优先剪掉敏感度最低的一批而不是每层按统一比例一刀切。2.3 知识蒸馏教小模型做题思路而不是背答案蒸馏的思想很优雅用一个已经训好的大模型teacher指导一个小模型student训练。但指导这个词其实大有讲究。早期做法是让小模型直接拟合大模型的硬标签输出但这样学到的是大模型的最终结论而不是它的决策过程。真正的蒸馏是用大模型的软输出soft label即各类别上的概率分布作为监督信号。软输出的意义在于它包含了大模型对相似类别的判断信息。比如一张猫和狗都有点像的图片硬标签只会说猫但软概率分布会说猫0.7、狗0.2、其他0.1这个0.2和0.1就是宝贵的暗知识它告诉小模型猫和狗在特征空间里是接近的。蒸馏的损失函数里有个温度参数T它的作用是软化概率分布。原始softmax输出在温度大于1时各类别概率差异会变小让小模型能看清大模型眼中第二、第三选择的信息。温度太低软化效果消失温度太高分布太均匀变成纯噪声。我在实验里通常把T设在3到5之间并且会在训练后期逐步降低到1附近让小模型从模仿思路逐渐过渡到聚焦答案。正因为在逻辑上量化是压缩精度、剪枝是移除结构、蒸馏是压缩知识三者可以天然组合先蒸馏出一个结构更小的student模型再做通道剪枝和量化最后统一走一遍图优化。这也是我design Model-Optimizer时把三类优化放在同一个Pipeline里的核心原因。2.4 算子融合与计算图优化减少的不只是计算量更是访存次数算子融合是我最早在TensorRT上体验到效果的技术。以ConvBNReLU为例BN层在推理时本质上是对卷积输出做一次线性变换这三层可以合并成一个等效的卷积操作。融合后计算量不变但原本需要把中间结果写回显存再读出来做下一次计算的过程被省掉了。这里的瓶颈关键是访存带宽。现代CPU和GPU的计算速度远超内存带宽很多算子不是算不过来而是数据搬不过来。每次层与层之间的数据搬运都消耗显存带宽融合操作让中间结果留在寄存器或L1缓存里继续计算省下的是内存I/O的时间。图优化除了算子融合还包括常量折叠把不依赖输入的子图提前计算好、死节点消除输出不被使用的节点直接删掉、公共子表达式提取这些经典编译优化手段。Model-Optimizer在这块的实现思路是先把模型转成统一的计算图表示再做一遍拓扑排序和模式匹配找到可融合的算子模式最后生成优化后的图。3. Model-Optimizer工具链的架构设计与核心实现框架选型上我没有造轮子。底层用PyTorch做训练侧的剪枝和蒸馏推理侧的量化与图优化基于ONNX Runtime和自定义pass实现。整个工具链对外只暴露一套API加载模型 - 配置优化方案 - 执行Pipeline - 导出优化后模型和报告。3.1 整体架构统一模型表示是衔接一切的枢纽我踩过的最大一个坑就是各个模块拿着不同格式的模型在跑。PyTorch模型转成ONNX再从ONNX转到不同后端图结构变了之后量化节点和剪枝信息就对不上了。所以在项目起步阶段我做了个很关键的决策所有优化模块基于同一个中间表示IR运行PyTorch模型先export成ONNX后续的量化、图优化都在这张计算图上做蒸馏和剪枝的敏感度分析则映射回原始模型结构。有没有过犹豫有。ONNX对动态图的支持一直是个老大难而PyTorch在动态控制流上很灵活。但考虑到目标部署场景是推理服务模型本身是静态图export这个成本是值得的。统一IR带来的收益非常直接剪枝产生的mask和量化产生的scale都可以作为图的附加属性对齐不会再出现张量对不上号的问题。架构上我分成了三层。最底层是模型IO层负责加载和导出PyTorch/ONNX格式模型中间是优化器层包含Quantizer、Pruner、Distiller和GraphOptimizer四个独立组件最上层是Pipeline调度层根据用户配置组合这些优化器并且维护一个全局的metrics记录器。任何一个组件跑完都会往这个记录器里写一条结构化日志包含当前阶段的模型大小、理论计算量FLOPs、平均延迟、精度指标如果有验证集的话。3.2 量化模块百分位校准与混合精度回退量化模块的核心代码如下我简化了工程细节保留了最关键的逻辑class Quantizer: def __init__(self, model, calib_loader, strategypercentile, percentile99.99): self.model model self.calib_loader calib_loader self.strategy strategy self.percentile percentile self.act_scale {} self.act_zero_point {} def collect_activation_stats(self): 在校准数据集上跑一遍收集每层激活的统计分布 def hook_fn(name): def hook(module, input, output): # 对输出张量做统计保留min/max或直方图 act output.detach() self.activation_stats[name].append(act.min().item()) self.activation_stats[name].append(act.max().item()) return hook for name, module in self.model.named_modules(): module.register_forward_hook(hook_fn(name)) with torch.no_grad(): for batch in self.calib_loader: self.model(batch) def compute_scale(self, name, min_val, max_val, bits8): 根据min/max计算scale和zero_point qmin, qmax 0, 2**bits - 1 if self.strategy minmax: scale (max_val - min_val) / (qmax - qmin) zero_point qmin - round(min_val / scale) else: # percentile策略先取分位数再做线性映射 scale (max_val - min_val) / (qmax - qmin) zero_point round(-min_val / scale) return scale, zero_point这段代码里最值得说明的是校准数据的选择。我见过很多团队直接拿验证集去校准这不是不行但要注意验证集的分布和线上真实数据分布如果差异过大量化边界就会偏移。我自己在项目里用了一组从训练集里抽出来的、覆盖各类别和光照条件的样本组合数量大概在500到1000张之间比用整个训练集快得多也比用验证集更贴近真实分布。另一个重要实现是混合精度回退。量化最怕碰到敏感层——有些层比如检测头的最后一层回归分支对数值精度极敏感统一量化成INT8后精度直接崩。Model-Optimizer的策略是跑完量化后自动在GPU上对比量化前后模型在验证集上的输出差异用KL散度衡量如果某个节点输出分布差异超过阈值就把这一层的量化精度回退到FP16甚至FP32。实测下来80%的精度坍塌问题靠这个机制就能挽救回来。3.3 剪枝模块敏感度排序与通道mask传播通道剪枝的一个实现难点在于剪掉某一层的一个通道会连带影响下一层的输入通道数卷积核的维度也要跟着裁剪。所以剪枝必须做mask传播。我实现里的核心数据结构是一个ordered dictkey是层名value是一个mask向量表示这一层哪些通道被保留。剪枝过程分三阶段class ChannelPruner: def __init__(self, model, sensitivity_loader): self.model model self.sensitivity_loader sensitivity_loader def compute_channel_sensitivity(self): 基于BN层gamma和权重范数综合打分 sensitivity {} for name, module in self.model.named_modules(): if isinstance(module, torch.nn.BatchNorm2d): gamma module.weight.detach().abs() # 结合卷积核的L1范数做综合打分 conv_weight_name self._find_conv_before_bn(name) conv_weight self._get_module(conv_weight_name).weight.detach() l1_norm conv_weight.abs().sum(dim(1, 2, 3)) score gamma * l1_norm sensitivity[name] score return sensitivity def generate_global_mask(self, prune_ratio): 全局排序并生成每层的通道mask all_scores [] for name, score in self.sensitivity.items(): all_scores.append((name, score)) # 展平、排序取阈值 flat_scores torch.cat([s.flatten() for _, s in all_scores]) threshold torch.quantile(flat_scores, prune_ratio) masks {} for name, score in all_scores: masks[name] score threshold return masks这里的全局排序是重要的一点。很多剪枝实现是每层剪掉相同比例比如每层都剪掉30%这在模型结构比较均匀的时候可行但ResNet这类有残差结构的模型就不好办了。全局排序相当于把各层的通道放在一个盘子里面比谁更不重要先剪掉最不重要的那批直到达到目标压缩率为止。我在跑CIFAR-10和ImageNet子集实验时全局排序比逐层等比例剪枝在相同压缩率下精度高出3到5个点。剪完之后的mask不能只用在第一层。因为第n层的mask决定了第n层输出哪些通道保留而第n1层卷积核的输入通道必须对应这些保留的通道所以需要把mask沿计算图向后传播层层裁剪卷积核和后续BN层。我专门写了test代码来验证剪枝后每一层的输入输出通道数和预定义的结构完全匹配这块看似简单但实际上最容易出维度配错这种低级错误。3.4 蒸馏模块三部分损失函数的权重博弈蒸馏部分的实现我融合了三部分损失硬标签交叉熵、软标签KL散度、以及特征对齐损失。公式上可以写成L_total alpha * CE(y_pred, y_hard) beta * T^2 * KL(softmax(y_pred/T), softmax(y_teacher/T)) gamma * MSE(feat_student, feat_teacher)温度T的平方乘在KL前面是为了让梯度量级和CE对齐。这个细节很多人会忽略——如果不乘T^2温度高了之后软标签的梯度会因为分布变平而变得特别小蒸馏损失在总损失里就跟消失了一样。特征对齐损失我用了相对朴素的MSE对标的是teacher模型某个中间层和student对应层的feature map。但注意student的通道数可能和teacher不一样毕竟student结构更小所以这一项要么只在teacher和student结构相近时用要么在feature后面加一层1x1卷积来对齐通道数。我后来发现加一个可训练的线性投影层效果比固定对齐好得多因为student学到的最优特征空间本来就不需要严格和teacher一致只需要在一个线性变换下对齐即可。蒸馏在Model-Optimizer里扮演的角色不是独立的优化手段而是压缩前的预处理——先把大模型蒸馏成一个小模型再做剪枝和量化这样后两步面对的模型复杂度更低保留精度的容错空间更大。我在项目里管这个叫三段式瘦身。4. 实测结果精度、显存、吞吐的三角权衡参数说再多不如跑数据。实测环境是单张RTX 3090CPU推理用的是一台Intel Xeon Gold 6330测试模型包括ResNet-50和ViT-Base数据集从CIFAR-10换到ImageNet的1万张测试子集。我拿了一个周末跑了四轮完整实验下面把数据摊开来说。4.1 单独量化INT8的收益与边界先看ResNet-50在GPU上的量化表现。版本模型体积(MB)Top-1准确率(%)GPU平均延迟(ms)显存占用(MB)FP32基线9876.124.821220INT8 (MinMax)2572.351.36810INT8 (Percentile99.99)2575.481.37812INT8 混合精度回退2675.921.44818数据说明了两件事。第一INT8的加速效果极其明显延迟从4.82ms降到1.36ms约3.5倍提速显存从1220MB降到810MB左右。第二校准策略对精度影响巨大——MinMax直接掉了将近4个点Percentile只掉了0.64个点配合混合精度回退之后精度损失控制在0.2个点以内。ViT-Base的结果类似但稍微差一点INT8量化后Top-1从84.2%掉到82.7%即使加了回退也只能回到83.4%。attention里的softmax和LayerNorm对量化特别敏感这点我后面再展开。4.2 单独剪枝结构越规整收益越稳定剪枝实验我固定了压缩比例在30%即剪掉30%的通道ResNet-50和ViT-Base分别看效果。ResNet-50剪掉30%通道后模型体积从98MB降到大约46MBTop-1精度从76.12%掉到74.85%损失约1.3个点。延迟降低反而没想象中明显GPU上从4.82ms降到3.91ms约20%。这个数字很好解释GPU的并行计算能力强通道数的减少未必精确换算成延迟降低很多时候带宽和算子本身的调度开销占比更大。ViT-Base做head prune就更有意思了。我把12个注意力头按敏感度排序剪掉3个Top-1精度损失接近2个点但剪掉2个时精度只掉0.6个点。这说明ViT的注意力头里有的几乎就是冗余的但剪到一定数量后会触发质变。我专门画过一张曲线图精度相对剪头数量是个阶梯式下降找到那个拐点比直接追求多剪几个头有意义得多。剪枝这事我的结论是压缩体积效果明显推理提速幅度得看硬件和后端支持。如果你的目标是模型小、能塞进更多实例剪枝非常合适如果目标是单次推理最快优先考虑量化剪枝作为第二梯队配合使用。4.3 三段式组合蒸馏剪枝量化最终组合实验我用了蒸馏后的ResNet-50变体student通道数为原版的75%再叠加30%结构化剪枝和INT8量化。版本模型体积(MB)Top-1准确率(%)GPU延迟(ms)显存(MB)FP32基线9876.124.821220蒸馏75%通道5575.013.441000蒸馏剪枝30%2673.622.87860蒸馏剪枝INT8772.480.84598这个7MB的模型是个什么概念FP32基线的1/14大小延迟只有原来的17%显存省了一半精度损失约3.6个点。对视觉模型来说如果想在边缘设备上跑实时推理这个体量和速度基本是可接受的。但我要泼一盆冷水组合优化不是三条线简单叠加误差会累积。蒸馏损失1.1个点剪枝损失1.4个点量化损失1.1个点单看都不大但加在一起就超过3.5个点了。所以组合使用的时候一定要盯紧总预算我的经验是单步精度损失控制在1%以内这条线比较稳。4.4 什么情况下优化收益会明显缩水跟几位同行交流后我总结了三类场景优化效果会大打折扣第一类是模型本身已经小得可怜比如只有几MB的轻量模型再压缩空间有限反而可能因为量化边界选择不当导致精度崩盘。第二类是任务对精度极度敏感例如医学影像分割、细粒度分类这类场景下INT8的误差不可接受用FP16量化可能更合适。第三类是动态shape推理ONNX Runtine对动态shape的优化支持本来就不如静态shape算子融合经常熔不到一起去优化收益会打个对折。5. 踩坑实录三个让我折腾到深夜的问题与排查过程做Model-Optimizer的过程中遇到的技术问题不少这里挑三个最有代表性的把完整的排查链路写出来希望帮你直接跳过这些坑。5.1 现象量化后的模型精度从76%跌到48%根因竟然是激活值前分布第一次跑INT8量化校准完精度直接掉到48%比随机猜好不了多少。一开始以为是校准数据太少把校准集从200张加到1000张还是没改善。后来逐个节点对比FP32和INT8模型的输出分布发现大部分层的分布差异很小但有一个位于网络前端的卷积层输出分布严重变形——原本是典型的钟形分布量化后变成了一堆尖峰。这个位置太关键了所有后续层的输入都从这里来误差被一级级放大。再看这层的激活统计它的最大值是一个明显的离群点比其他激活值大了两个数量级。在Percentile校准下这个离群点不影响阈值选择但在MinMax下就把scale拉伸得很大导致大部分正常值被暴力压到同一个量化档位上。修复方案是设计了一个离群点感知校准的改进策略先在全局分布上计算99.99分位数作为初始阈值如果最大值超过阈值的3倍就对这个最大值单独做clip处理不让它参与scale计算。后续我在代码里加入了对每层分布形状的自动记录量化前先做一个离群点检测有问题的层会在日志里单独标注出来不用再一个个节点手动排查。5.2 现象结构化剪枝后模型变小了推理却更慢了有一次跑完剪枝模型体积确实从98MB降到了54MB结果一测推理延迟反而从4.8ms涨到了6.2ms当时人都是懵的。直觉上模型变小了计算量也小了怎么会更慢排查后发现问题在GPU上的AHBAsynchronous High Bandwidth内存布局。剪枝后模型虽然参数少了但通道数不是128的整数倍比如某个卷积层原来的输出通道是256剪完后变成173。GPU处理非对齐的通道数时要填充padding补齐到对齐边界相当于模型小了但每个算子都要在padding后的数据上跑数据搬移的开销比省下的计算量还大。这个问题的通用解法是通道数对齐剪枝时把每层保留的通道数round到硬件对齐阈值GPU上我默认取8的倍数有些后端建议取16或32。Model-Optimizer在mask生成阶段就加入了对齐约束宁可多留几条通道也不产生非对齐的层。这个改动之后剪枝的延迟收益才真正体现出来。说起来这个坑还有个后续CPU上跑的时候对齐的影响没那么大反而剪枝省下的计算量是实打实的。所以我在文档里专门注明了剪枝后的通道对齐在GPU部署时必须开CPU部署可以关掉。5.3 现象算子融合后输出结果漂移但不是bug有一次做完ConvBNReLU融合验证集的精度比融合前掉了0.3个点。原本以为融合是个恒等变换精度不应该有任何变化第一反应是自己实现里某个地方把参数搞错了。逐层对比融合前后每一层的输出发现偏差确实存在量级大约在1e-3左右。后来才明白Conv和BN融合时涉及数值重排原始BN里的除法和缩放是在卷积输出上做的融合后这些操作被并入了卷积权重和偏置。浮点运算的先后顺序变了最终结果的舍入误差就会有略微差异。当模型层数很深时这种微小差异会被逐层累积。这不是实现bug是浮点语义变化带来的固有误差。解决方案有两个方向在融合时保留FP32计算精度误差通常在可接受范围如果精度极度敏感可以把融合后的权重视为新模型做一次轻量的蒸馏校正让模型重新适应新权重分布。5.4 一个方法论优化效果必须有可复现的回归验证上面三个问题能快速定位很大一部分功劳来自Model-Optimizer内置的回归验证机制。每条优化管线跑完后会自动生成一份报告包括优化前后的大小、FLOPs、每层激活分布KL散度、GPU/CPU延迟、显存峰值、精度指标如果有验证流程。整个机制受CI的启发我希望每次改动代码后都能在跑完测试的同时看到优化收益有没有引入新的回退。这套验证机制的关键点不是把每个指标都测出来而是要能自动对比和告警。如果某次优化后精度损失超过阈值可配置默认2%报告里会直接标红并列出分布差异最大的前10个层。排查问题的起点就从全网搜索变成了只看这10个层效率完全是两个档次。6. 工具链的外观体验与一点后续思考现在Model-Optimizer的实际使用感受用一句话概括就是从研究三件套变成了一条命令的事。过去跑一个优化实验要分别准备量化脚本、剪枝脚本、蒸馏脚本数据格式还可能对不上现在只需要写一个配置文件指定模型路径、优化策略组合、目标指标和验证集路径Pipeline会自己决定执行顺序、保存中间产物、输出对比报告。这个设计背后是我现在的核心体会模型优化工具链的价值不在于某一个优化技巧有多深而在于把不同优化手段之间的交互关系理清楚。量化、剪枝、蒸馏单独拎出来任何一个都有成熟的库可以调用但它们组合在一起时误差如何传递、敏感度如何相互影响、执行顺序如何安排这些才是真正需要打磨的地方。对后续方向我比较关注三件事。一是把优化支持范围扩大到更多类型的主干网络比如检测和分割模型的前处理部分目前还很依赖ResNet系列的规律性对更复杂的DETR这类结构支持还不够好二是细粒度量化现在的混合精度回退是层级别的实际上一层的内部不同通道的敏感度也很不一样按通道做精度分配空间会更大三是在工具链上加入专门的模型幻觉检测模块优化后模型即使精度合格偶尔也会在某些输入上产生与原始模型差异性很大的输出这个问题在图像生成模型上尤其值得警惕。最近有一件事让我觉得这个方向特别值得坚持一位做嵌入式部署的同事用了Model-Optimizer把一个人脸识别模型从25MB压到3MB而且精度只掉了0.8个点原计划要买的新推理板卡直接省掉了。听到这个消息时我想到的是这个项目最初那段被各种工具格式折腾的日子。如果你也想做自己的模型优化工具链我的建议是别急着写量化代码和剪枝代码先把你要处理模型的瓶颈搞清楚是显存放不下还是延迟不达标还是两者都要然后针对瓶颈挑一两个优化手段先做实验跑出数据、观察收益再决定要不要集成成一个Pipeline。优化这件事最怕一上来就全上最后精度掉了、速度没提还不知道是哪一步出了问题。