1. 这不是“一键压缩”工具,而是模型交付链路上的手术刀
“Model-Optimizer”这个词最近在工程团队的 Slack 频道里出现频率陡增——但它既不是某个新发布的开源库名,也不是某家大厂刚推出的 SaaS 服务。它是我给一组自研脚本和配置模板起的内部代号,一套专为模型交付阶段做精准减负的轻量级工具链。关键词里没填内容,恰恰说明它还没被包装成产品;热搜词“Model-Optimizer”背后,其实是大量算法工程师在模型上线前夜反复重跑、改参数、删层、调精度时发出的真实叹息。
我第一次把它拎出来用,是在一个边缘端语音唤醒模型的交付现场。客户要求:在算力仅 2TOPS 的国产 NPU 上,把 42MB 的 ONNX 模型压到 15MB 以内,同时唤醒准确率下降不能超过 0.8%。当时团队惯用做法是扔进 TensorRT 自动优化器——结果导出模型体积只减了 12%,推理延迟反而涨了 7%,因为 TRT 在无约束条件下默认启用了 float16 精度+层融合+kernel auto-tuning,而我们的 NPU 对某些 fused kernel 的支持存在兼容性缺陷。我们不是缺工具,是缺可控、可解释、可回溯的优化决策权。
Model-Optimizer 的核心定位,就是把“模型优化”这件事,从黑盒调参行为,拉回到工程可控范畴:它不替代 TensorRT / ONNX Runtime / TVM,而是站在它们之上,做三件事——明确告诉优化器“你要保留什么”,清晰定义“你能牺牲什么”,并全程记录“你改了哪一行 IR”。它不追求绝对最小体积或最高吞吐,而是让每一次体积缩减都对应一条可验证的业务代价声明。比如,把某组 Conv-BN-ReLU 合并后,会自动生成一份 diff 报告:该操作移除了 3 个中间 tensor 分配,节省显存 1.2MB,但导致量化敏感度上升 0.3dB(通过注入噪声模拟实测),是否接受?由模型负责人签字确认。
它适合三类人:一是部署工程师,需要在资源受限设备上稳定交付;二是算法同学,不愿为上线妥协精度却苦于无法精准控制剪枝粒度;三是技术负责人,要对模型交付质量建立可审计的基线。如果你还在用“试下 quantize,不行就 prune,再不行就换 backbone”这种三板斧式流程,那 Model-Optimizer 不是锦上添花,而是止损刚需。
提示:它不解决模型训练阶段的问题。不会帮你调 learning rate,不提供新的 loss function,也不做架构搜索。它的战场非常明确——模型已训好、权重已固化、ONNX/TFLite 已导出,下一步怎么安全、高效、可追溯地交到硬件手上。
2. 四层结构:为什么不用现成工具而要自己搭这套链路
市面上所有“模型优化器”类产品,几乎都卡在同一个结构性矛盾上:越易用,越不可控;越可控,越难用。TensorRT GUI 版点几下就能出 engine,但你永远不知道它偷偷 fusion 了哪几个 op;ONNX Simplifier 能删冗余节点,但删掉的是 dead code 还是影响 batch norm 统计的 placeholder,全凭运气;TVM 的 AutoScheduler 算力强悍,但 tuning log 动辄上 GB,想复现某次最优编译结果?得靠 sha256 校验加人工翻日志。
Model-Optimizer 的设计哲学,是把优化过程拆解为四个正交、可插拔、可审计的层级,每一层只解决一类问题,且彼此之间有明确契约:
2.1 第一层:IR Clean —— 清理中间态污染,而非删除节点
绝大多数模型体积膨胀,根源不在权重本身,而在导出过程引入的“中间态污染”。典型如 PyTorch → ONNX 导出时,torch.nn.BatchNorm2d会被展开为BatchNormalization+Mul+Add三组 op,即使训练时已 fold BN,ONNX 图里仍残留大量 scalar multiply/add;又如nn.Sequential嵌套过深导致图中出现冗余Identity和Reshape节点;再如动态 shape 推导失败后插入的ConstantOfShape占用数 MB 显存。
Model-Optimizer 的 IR Clean 层不依赖通用图遍历算法,而是基于导出框架指纹识别 + 模式匹配白名单。它先读取.onnx文件的producer_name和producer_version,确认是 PyTorch 2.0.1 + onnx 1.14 导出,然后加载预置的 clean rule set:
- 对
BatchNormalization后紧跟Mul/Add且 scale/bias 为常量的子图,执行 BN fold; - 删除
Identity输入输出 shape 完全一致、且非图输入/输出节点的实例; - 将
ConstantOfShape替换为Constant(若 shape 可静态推导); - 对
Resizeop,若 mode=nearest 且 scales=[1.0,1.0,2.0,2.0],则替换为Upsample并标记align_corners=False。
关键在于:每条 rule 都附带pre-check和post-verify。pre-check 判断当前 subgraph 是否满足 fold 条件(如 Mul 的 weight 是否 scalar);post-verify 则用随机 input 在原始图和 clean 后图上跑 forward,比对 output tensor 的 max-abs-diff ≤ 1e-6。不通过?跳过该 rule,记录 warning 日志,不强行执行。
实测效果:某 ResNet-18 ONNX 模型(38MB)经 IR Clean 后体积降至 31MB,推理耗时下降 9%,且所有 layer-wise output diff ≤ 1e-7。这不是 magic,是把导出环节的“脏活”显式化、自动化、可验证。
2.2 第二层:Precision Gate —— 精度不是标量,是分层契约
量化不是“把 float32 全换成 int8”就完事。真实场景中,不同 layer 对精度损失的容忍度天差地别:Conv 的 weight 量化误差可通过 calibration 补偿,但 Softmax 的 input 量化若引入 >0.1 的 offset,会导致指数运算溢出,整个 logits 彻底失真;Embedding lookup table 若用 int8 存储,index 映射错误会直接引发 OOV crash。
Model-Optimizer 的 Precision Gate 层,强制要求用户定义layer-level precision contract,格式为 JSON:
{ "default": "int8", "exceptions": [ { "op_type": "Softmax", "input_quant": "fp16", "output_quant": "fp16" }, { "op_type": "Gather", "input_quant": "int32", "weight_quant": "int8" }, { "name_pattern": ".*backbone\\.layer4.*", "weight_quant": "int16" } ] }它不提供“自动选择最佳 bit-width”功能,因为不存在全局最优——只有针对具体硬件 backend 的 trade-off 决策。当配置传入后,工具链会:
- 遍历 ONNX graph,按 name_pattern 和 op_type 匹配 target nodes;
- 对每个 node,生成对应的 quantization annotation(QDQ node 或 TensorRT int8 config);
- 自动插入 verification stubs:在 Softmax 前插入 fake-quant op,其 scale/zero_point 由 calibration 数据统计得出,但 forward 时仍走 fp16 path,仅记录量化后 tensor 的 min/max,用于后续校准;
- 输出一份
precision_contract_report.md,列出所有被 override 的 layer、其原始 dtype、contract dtype、以及 calibration 时 observed 的 dynamic range。
这个设计带来的最大收益,是把“量化是否 safe”的判断,从部署后测试,提前到配置阶段。某次我们发现Gatherop 的 index input 被误设为 int8,contract report 里立刻标红警告:“index tensor range [-128,127] 超出实际 vocab_size=30000”,避免了上线后批量 crash。
2.3 第三层:Structure Trim —— 剪枝不是删通道,是删计算路径
传统 channel pruning 工具(如 torch-pruning)依赖 weight L1-norm 或 feature map activation,但这类方法在 transformer 类模型上失效明显:attention head 的 weight norm 差异极小,而 activation 又高度稀疏且动态变化。更本质的问题是,它们优化目标是“减少参数量”,而工程交付真正关心的是“减少内存带宽占用”和“减少 MAC 计算次数”。
Model-Optimizer 的 Structure Trim 层,采用computation path analysis思路。它不看 weight,而是构建模型的dataflow graph,识别三类可 trim 路径:
- Redundant Path:同一 tensor 被多个 consumer 使用,但其中部分 consumer 的 output 在后续被 discard(如 training-only dropout mask);
- Bottleneck Path:某 layer output shape 为 [1, 128, 1, 1],但下游所有 consumer 都只取第 0 个 channel,其余 127 个 channel 的计算纯属浪费;
- Fused Path:
Conv + ReLU + BN已被 IR Clean 层 fold,但图中仍残留未连接的 BN bias add node,属于 dead code。
Trim 过程分两步:
- Static Analysis:用 ONNX shape inference + value propagation,标记所有 dead tensor 和 redundant op;
- Dynamic Profiling:在 calibration dataset 上运行 profiler(基于 onnxruntime 的 custom pass),记录每个 node 的 input/output tensor size、memory footprint、compute cycles(通过 hardware counter API 获取)。
最终生成trim_plan.json,例如:
{ "node_to_remove": ["encoder.layer.3.attention.dropout_mask"], "channel_to_prune": { "encoder.layer.2.output_proj": [16, 32, 48] }, "op_fusion_suggestion": ["encoder.layer.1.attn.q_proj", "encoder.layer.1.attn.k_proj", "encoder.layer.1.attn.v_proj"] }注意:它不直接修改模型,而是输出 plan。执行需人工 review,因为op_fusion_suggestion可能触发硬件 backend 的 unsupported pattern。某次我们建议 fuse QKV proj,但目标 NPU 的 compiler 不支持 multi-input matmul,plan 被驳回,转而采用channel_to_prune方案,同样达成 18% 带宽节省。
2.4 第四层:Backend Align —— 不是适配硬件,是暴露硬件真相
所有优化最终要落地到特定 backend:TensorRT、ONNX Runtime、TVM、或某家芯片厂商的 proprietary runtime。但各 backend 对同一 ONNX op 的实现差异极大。比如GatherND:TensorRT 7.2 用 CUDA kernel 实现,latency 稳定;ONNX Runtime CPU 版本用 loop 实现,batch size > 1 时性能断崖下跌;而某国产 NPU 的 driver 甚至不支持该 op,必须 fallback 到 host CPU。
Model-Optimizer 的 Backend Align 层,核心是一个backend capability database(YAML 格式),收录各版本 backend 对 ONNX opset 15 中 127 个 op 的支持状态、推荐实现方式、已知 bug 及 workaround。例如:
tensorrt: version: "8.6.1" ops: GatherND: support: true impl: "cuda_kernel" notes: "requires input indices to be int32, int64 not supported" Resize: support: false workaround: "replace with Upsample + Cast"当用户指定 target backend 后,Align 层会:
- 扫描模型中所有 op,比对 capability db;
- 对 unsupported op,提供 rewrite rule(如 Resize → Upsample);
- 对 suboptimal op,给出 performance warning(如
Loopop 在 TRT 中 latency 是Scan的 3.2x); - 生成 backend-specific optimization hint:例如对 TensorRT,自动添加
--fp16 --strict-type-constraints参数建议;对 ONNX Runtime,提示开启--enable_mem_pattern。
这层的价值,在于把硬件限制从“部署时才发现的坑”,变成“设计时就规避的常识”。我们曾用此层提前发现某模型中的ScatterElementsop 在目标 NPU 上无硬件加速,改用ScatterND+ index remapping,将推理耗时从 120ms 降至 28ms。
3. 实操全流程:从 ONNX 到可交付包的七步手把手
Model-Optimizer 不是命令行工具,而是一套config-driven workflow。它不提供model-optimize --input model.onnx --output opt.onnx这种魔法命令,因为真正的优化决策,必须发生在 human-in-the-loop 环节。以下是我们在某智能座舱 DMS 模型(YOLOv5s 改版,输入 640x480 RGB,输出 5 个人脸 bbox + 128D embedding)上的完整实操流程,全程耗时 3.2 小时,最终模型体积从 54MB → 18.3MB,NPU 推理耗时从 41ms → 22ms,mAP@0.5 下降 0.3%。
3.1 Step 0:环境准备与模型诊断(25 分钟)
首先确认基础环境:Python 3.10,onnx==1.15.0,onnxruntime-gpu==1.16.0,torch==2.1.0。关键动作不是装包,而是跑诊断脚本:
python diagnose.py --model dms_yolov5s.onnx \ --calib_dir ./calib_data \ --target_backend tensorrt_861 \ --hardware nvidia_xavierdiagnose.py会输出三份报告:
model_summary.md:总 node 数、op type 分布、最大 tensor size(发现Conv_123output 为 [1, 256, 80, 60],占显存 4.8MB,是瓶颈);calibration_stats.json:各 layer input/output 的 min/max(发现Softmax_201input range 达 [-12.8, +15.3],需 fp16 保精度);backend_compatibility.csv:列出所有 unsupported op(发现NonMaxSuppression在 Xavier 上需 plugin,但我们的 firmware 版本不支持,必须替换)。
注意:这一步必须做。跳过诊断直接优化,等于蒙眼开车。我们曾因忽略
NonMaxSuppression兼容性,在烧录后才发现 NMS 结果全为空,返工 2 天。
3.2 Step 1:IR Clean 配置与执行(18 分钟)
基于model_summary.md,编辑ir_clean_config.yaml:
producer_fingerprint: - pytorch: "2.0.1" - onnx: "1.14" rules: - name: "bn_fold" enabled: true target_ops: ["BatchNormalization", "Mul", "Add"] - name: "identity_remove" enabled: true threshold: 0.99 # only remove if input/output shape identical in 99% cases - name: "resize_to_upsample" enabled: true mode: "nearest"执行:
python ir_clean.py --config ir_clean_config.yaml \ --input dms_yolov5s.onnx \ --output dms_clean.onnx验证:用onnx.checker.check_model()确认图合法;用onnxruntime.InferenceSession跑 100 张 calib 图,diff ≤ 1e-6。体积降至 47MB,Conv_123output tensor 减少 1.2MB(BN fold 移除了 3 个中间 buffer)。
3.3 Step 2:Precision Contract 设计(42 分钟)
这是最耗脑力的环节。打开precision_contract_template.json,逐层分析:
Softmax层:input range [-12.8, +15.3],int8 无法覆盖,设为 fp16;Gather层(用于 embedding lookup):index input 必须 int32,weight 可 int8;- backbone 中所有
Conv:weight int8,input/output int8(calibration 显示 dynamic range 紧凑); - head 部分
Conv(bbox regression):weight int16,因 weight distribution 偏态严重,int8 误差超标。
保存为precision_contract.json,执行:
python precision_gate.py --model dms_clean.onnx \ --contract precision_contract.json \ --calib_dir ./calib_data \ --output dms_quantized.onnx工具自动插入 QDQ node,并输出precision_contract_report.md。重点检查Gatherindex tensor:report 显示 observed range [0, 29999],contract 要求 int32,完全匹配。体积降至 32MB。
3.4 Step 3:Structure Trim Plan 生成与评审(35 分钟)
运行 profiler:
python structure_trim.py --model dms_quantized.onnx \ --calib_dir ./calib_data \ --backend tensorrt_861 \ --output trim_plan.jsontrim_plan.json关键项:
node_to_remove:["Detect.head.dfl.conv.weight", "Detect.head.dfl.conv.bias"]—— DFL 模块的 conv 是 training-only,inference 时恒为 identity;channel_to_prune:"backbone.model.10.conv2"→ prune channels [64, 128, 192](profiler 显示这些 channel output std < 0.001);op_fusion_suggestion:["backbone.model.8.conv1", "backbone.model.8.conv2"]—— 但 capability db 显示 TRT 8.6.1 不支持 conv+conv fuse,故放弃。
人工 review 后,执行 trim:
python apply_trim.py --model dms_quantized.onnx \ --plan trim_plan.json \ --output dms_trimmed.onnx体积降至 26MB,profiler 显示 memory bandwidth 下降 22%。
3.5 Step 4:Backend Align 与 Op Rewrite(28 分钟)
根据backend_compatibility.csv,发现NonMaxSuppression不支持。查阅 capability db,workaround 是TopK+Gather+ScatterND组合。编辑backend_align_config.yaml:
target_backend: "tensorrt_861" rewrite_rules: - op_type: "NonMaxSuppression" replacement: "nms_custom_plugin" # our internal plugin condition: "num_classes == 1" # DMS only detects face, so class-agnostic执行:
python backend_align.py --model dms_trimmed.onnx \ --config backend_align_config.yaml \ --output dms_aligned.onnx工具自动插入 plugin node,并验证 plugin signature。体积微增至 26.2MB(plugin metadata 开销),但确保 NMS 正确性。
3.6 Step 5:TRT Engine 编译与验证(45 分钟)
这才是真正见真章的环节。用 Model-Optimizer 生成的 TRT config:
trtexec --onnx=dms_aligned.onnx \ --saveEngine=dms.engine \ --fp16 \ --workspace=2048 \ --timingCacheFile=timing.cache \ --minShapes=input:1x3x480x640 \ --optShapes=input:4x3x480x640 \ --maxShapes=input:8x3x480x640 \ --plugins=./libnms_plugin.so关键参数解读:
--fp16:Precision Gate 已保证 fp16 layer 的 safety,此处启用;--workspace=2048:基于diagnose.py报告的最大 tensor size(4.8MB)× 2.2 得出;--timingCacheFile:复用历史 tuning cache,避免每次重搜;--plugins:加载我们自研的 NMS plugin。
编译成功后,用trtexec --loadEngine=dms.engine --dumpProfile输出 layer-wise timing。发现backbone.model.10耗时异常高(18ms),远超其他 conv(平均 2ms)。检查发现是 channel prune 后,该 layer input channel 数变为 256→192,但 TRT kernel 未 hit 最优配置。解决方案:在trtexec命令中添加--tacticSources=-CUBLAS,-CUBLAS_LT,+CUDNN,强制使用 cuDNN kernel,耗时降至 3.1ms。
3.7 Step 6:交付包打包与 QA(27 分钟)
最终交付不是单个 engine 文件,而是一个可审计的 package:
dms_delivery_v1.2/ ├── model/ │ ├── dms.engine # TRT engine │ ├── dms.onnx # aligned ONNX (for debug) │ └── dms.calib_cache # calibration cache ├── config/ │ ├── ir_clean_config.yaml │ ├── precision_contract.json │ ├── trim_plan.json │ └── backend_align_config.yaml ├── report/ │ ├── model_summary.md │ ├── precision_contract_report.md │ ├── trim_verification_report.md │ └── trt_profile.csv └── README.md # 包含所有 contract 的 human-readable summaryQA 流程:
- 在 target Xavier 设备上跑
trtexec --loadEngine=dms.engine --iterations=1000,确认 avg latency ≤ 22ms; - 用 1000 张 real scene 图跑 end-to-end,mAP@0.5 = 0.782(baseline 0.785),符合 ≤0.3% 降幅要求;
- 检查
README.md中的 contract summary 是否与 config 文件一致(防止文档与代码脱节)。
至此,交付包签字归档。整个流程中,没有一步是“黑盒运行”,每一步都有可验证的输入、可审计的输出、可回溯的决策依据。
4. 那些没写进文档,但踩过才懂的硬核经验
Model-Optimizer 的代码可以开源,但有些经验,只存在于深夜 debug 的 terminal history 和 coffee stain 斑驳的 notebook 里。这些不是文档里的“注意事项”,而是决定项目成败的隐性知识。
4.1 Calibration 数据集的质量,比算法本身更重要
我们曾为一个 OCR 模型优化,calibration dataset 用的是 synthetically generated text images(字体、背景、模糊程度都可控)。IR Clean + Precision Gate 后,engine 在 synthetic data 上 accuracy 100%,但上线后 real photo 识别率暴跌 40%。根因是:synthetic data 的 contrast 极高,Convlayer 的 activation range 非常 tight([0.1, 0.9]),int8 quantization 误差小;real photo contrast 低,activation range 扩展到 [0.01, 0.99],int8 的 256 个 level 无法精细刻画,大量信息丢失。
解决方案:calibration dataset 必须包含 real-world corner cases。我们后来建立了一套标准:
- 至少 30% 的图片来自产线抓拍(非 studio 拍摄);
- 包含 5% 的 low-light / motion-blur / extreme-angle 样本;
- 每类样本标注其 degradation type(如
blur_medium,noise_high),并在 precision contract 中为这些样本单独定义 calibration policy。
提示:不要相信“1000 张 random image”。用
ffmpeg -i input.mp4 -vf "select=gt(scene\,0.4)" -vsync vfr scene_%03d.jpg抽帧,比 random 更有效捕捉真实场景变化。
4.2 “体积减半”是个危险指标,要盯住 bandwidth 和 latency 的三角关系
很多团队 KPI 是“模型体积压缩 50%”。但体积(MB)和推理性能(ms)没有直接线性关系。某次我们把一个模型从 40MB 压到 18MB,体积减 55%,但 latency 反而涨了 15%。profiler 显示:体积下降主要来自 weight pruning,但 pruned 后的 sparse weight matrix 导致 GPU memory access pattern 变得不规则,cache miss rate 从 12% 升至 38%,带宽利用率暴跌。
正确指标是bandwidth-bound ratio:用nsys profile抓取 kernel 的l1tex__t_sectors_op_read.sum和sms__sass_average_data_bytes_per_sector_mem_shared_op_read,计算实际带宽占用 / 理论峰值。Model-Optimizer 的diagnose.py会输出这个 ratio,>0.7 才算 bandwidth-efficient。低于此值,宁可体积大一点,也要保证 dense memory access。
4.3 Backend Align 的“workaround”不是兜底,是技术债预警
capability db 里的workaround字段,不是解决方案,而是 red flag。比如Resize → Upsample的 workaround,看似简单,但 Upsample 在 TRT 中有额外的Castop 插入,可能引入精度损失;Gather → ScatterND的 workaround,需要手动 remap index,一旦 vocab size 变更,remap logic 就失效。
我们的实践是:所有 workaround 必须关联 Jira ticket,指派 backend team owner,设定 3 个月解决 deadline。Model-Optimizer 的 deliverable 里,backend_compatibility.csv会标出哪些 op 是 workaround,README.md中明确写:“此模型依赖 workaround X,预计 Y 版本 firmware 解决,当前版本需人工 verify”。这逼着硬件 team 主动升级,而不是让算法 team 永远背锅。
4.4 最大的坑,永远在“优化之外”
Model-Optimizer 解决的是模型交付链路的技术问题,但最大的交付风险,往往来自流程外。某次客户验收,我们交付了完美优化的 engine,但客户 side 的 driver 版本是旧的,不支持 TRT 8.6.1 的新 plugin ABI。折腾 2 天才发现,driver 更新需客户 IT 部门审批,周期 2 周。
从此,Model-Optimizer workflow 强制增加pre-delivery checklist:
- ✅ 目标设备 driver version ≥ required_version(从 capability db 读取);
- ✅ OS kernel version 兼容(某些 NPU driver 需 kernel ≥ 5.10);
- ✅ CUDA/cuDNN 版本匹配(TRT 8.6.1 需 CUDA 11.8);
- ✅ network stack:若模型含 http fetch(如 online embedding update),确认 firewall 白名单。
这个 checklist 不是 Model-Optimizer 的代码,而是交付 SOP 的一部分。它提醒我们:交付的终点,不是 engine 文件生成,而是它在客户真实环境中 stable run 的第一帧 output。
5. 它不是终点,而是模型交付工业化的新起点
Model-Optimizer 这个名字,最初只是我们团队 Slack 频道里一句自嘲:“又要 optimizer,又要 model,干脆叫 Model-Optimizer 吧”。没想到,它慢慢长成了支撑 17 个边缘 AI 项目交付的基础设施。它没有惊人的算法创新,没有 fancy 的论文引用,它的价值,藏在那些被避免的线上事故里,藏在客户签收单上“性能达标”的签字里,藏在算法工程师终于能准时下班的傍晚里。
它证明了一件事:在 AI 工程化落地的深水区,最稀缺的不是 SOTA 模型,而是能把“不确定的 research”转化为“确定的 engineering”的系统性能力。这种能力,不靠单点工具,而靠可拆解、可审计、可传承的流程设计。
所以,它不会停止进化。下一阶段,我们正在做三件事:
- 加入 hardware-aware NAS:不是搜索新架构,而是基于 target NPU 的 micro-benchmark 数据(如 conv throughput vs. channel count),为 backbone layer 自动生成最优 channel width,让 Structure Trim 从“人工 prune”升级为“auto-config”;
- 构建 model lineage tracking:每次 IR Clean / Precision Gate / Trim 的 commit hash,自动关联到 git repo 的 model training commit,实现从 training code → optimized engine 的全链路 traceability;
- 开发 visual audit dashboard:上传 ONNX,自动生成 interactive graph,点击任意 node,显示其:IR Clean 是否处理、Precision Contract 约束、Trim status、Backend support status、historical latency trend。让“优化决策”从 terminal log,变成可视化 story。
Model-Optimizer 的终极目标,不是成为另一个开源项目,而是让“模型优化”这个词,从一个充满玄学的动词,变成一个有明确定义、可测量、可管理的工程活动。就像当年 CI/CD 把“发布”从手工操作变成 pipeline,我们想做的,是把“模型交付”从艺术,变成科学。
最后分享一个小技巧:每次优化前,先用onnx.shape_inference.infer_shapes()跑一遍模型,再用onnx.checker.check_model()验证。这两行代码,能提前拦截 60% 的“模型导出 bug”,省下无数 debug 时间。它不酷,但很实在——这大概就是 Model-Optimizer 的全部精神:不炫技,只解决问题;不承诺完美,只确保可控。