
1. 这不是“跑个脚本”那么简单Phase A · Step 2 的真实分量“Phase A · Step 2预训练权重与 Pipeline 验证准备”——光看这个标题很多人第一反应是“哦就是下载个模型、跑个验证脚本嘛十几分钟搞定。”我刚入行那会儿也这么想直到在某次边缘端部署项目里卡在这个环节整整三天。不是代码报错而是模型推理结果完全对不上标注框不是显存溢出而是 ONNX 模型导出后尺寸暴涨 47%导致芯片加载失败更不是 pipeline 脚本语法写错而是 ATC 工具转换时 silently 丢掉了某个关键的 post-processing 层而日志里连 warning 都没打出来。后来复盘才发现Phase A · Step 2 根本不是流程图上一个带编号的方块它是整个 AI 工程落地的“压力测试点”它同时检验你对模型结构的理解深度、对工具链兼容边界的掌握精度、对硬件约束的敬畏程度以及对“验证”二字的真实定义——不是“能跑就行”而是“跑得准、跑得稳、跑得省、跑得可复现”。这个阶段的核心关键词YOLO26、预训练权重、Pipeline、ONNX、ATC每一个都不是孤立存在。YOLO26 是骨架但它的结构图里藏着大量非标准算子比如自研的 dynamic anchor fusion layer直接导出 ONNX 就会触发 unsupported op 报错预训练权重不是拿来即用的黑盒它的参数命名空间、归一化方式、输入通道顺序必须和后续 pipeline 的数据预处理逻辑严丝合缝Pipeline 不是 Python 脚本集合它是一条从原始图像到最终结构化输出的确定性流水线其中每个 stage 的输入/输出 tensor shape、dtype、layoutNCHW/NHWC都必须像齿轮咬合一样精确ONNX 是中间表示但它不是万能胶水不同 opset 版本对 YOLO26 中的 slicegather 组合支持度差异巨大ATC 是华为昇腾的编译器它把 ONNX 编译成 .om 模型但它的量化策略、算子融合规则、内存对齐要求会反向决定你前期 ONNX 导出时就必须预留的“编译友好接口”。所以这一步的本质是把学术模型research model转化为工业级可部署资产deployable asset的临界点。它不产出新功能但决定了后续所有环节的成败底限。适合谁不是只懂 PyTorch 训练的算法同学也不是只会调用 SDK 的应用开发而是需要横跨模型、框架、编译器、硬件四层的“AI 系统工程师”。如果你正被 yolo26 github ncnn、yolo26 tr转ncnn的bin和param、.onnx量化int8 这类搜索词困扰说明你已经站在这个临界点上了。2. 整体设计思路为什么必须拆解为“权重”与“Pipeline”两个验证轴2.1 为什么不能“一步到位”——工程落地的“信任建立”逻辑很多团队试图跳过 Phase A · Step 2直接进入模型推理或端侧部署。结果往往是训练时 mAP 58.3部署后只有 41.2或者 batch1 时准确batch4 时 bbox 全乱又或者 CPU 上跑得飞快换到昇腾 NPU 上 latency 翻倍。根源在于他们把“模型能跑”当成了“模型可信”。而 Phase A · Step 2 的核心设计思想就是强制拆解信任链条进行“双轨验证”。预训练权重验证轨目标是确认“模型本体”的完整性与一致性。它回答的问题是“这个 .pth 或 .pt 文件在脱离原始训练环境后是否仍能复现其宣称的数学行为”这包括权重数值精度FP32/FP16、参数绑定关系如 BN 层的 running_mean/run_var 是否冻结、以及最关键的——前向计算路径的等价性。我们不会在这里做任何推理而是通过构建最小闭环加载权重 → 构建 dummy input → 执行 forward → 保存 output tensor → 与原始训练环境下的 reference output 进行逐元素比对允许微小数值误差如 1e-5。这一步必须在 PyTorch 原生环境中完成且 reference output 必须来自同一 commit hash 的训练代码否则一切验证都是空中楼阁。Pipeline 验证轨目标是确认“数据流”的鲁棒性与可移植性。它回答的问题是“当图像从摄像头/文件系统流入经过 resize、normalize、permute 等一系列变换后送入模型的 tensor是否与训练时的分布和格式完全一致”这里的关键陷阱是“隐式依赖”。例如YOLO26 的官方 pipeline 可能默认使用 OpenCV 的 BGR 顺序读图而你的部署 pipeline 用 PIL 读图是 RGB如果不加 color conversion模型看到的就是全错的特征。再比如训练时用的是torchvision.transforms.Resize(640)而部署 pipeline 用的是cv2.resize后者默认插值是 INTER_LINEAR前者是 BILINEAR虽然名字相似但底层实现有细微差异累积起来就会影响小目标检测。因此Pipeline 验证必须包含完整的、可复现的数据预处理链并且要生成一份“pipeline signature”记录每个 stage 的输入/输出 shape、dtype、mean/std 值、color space、interpolation method 等元信息作为后续所有环节的基准。这两轨必须独立验证、交叉校验。只有当权重轨证明模型“没变”Pipeline 轨证明输入“没歪”二者叠加才能保证最终推理结果的可靠性。这是工程思维对学术思维的根本性修正学术追求最优解工程追求可验证的确定性。2.2 工具链选型背后的硬约束为什么是 ONNX ATC而不是其他组合当前热词中频繁出现的yolo26 github ncnn、tts onnx、c# pipeline反映出开发者在工具链上的广泛探索。但 Phase A · Step 2 的官方路径锁定 ONNX ATC绝非偶然而是由三个硬性约束共同决定的硬件生态锁定昇腾系列芯片如 Ascend 310P的官方 SDKCANN明确将 ONNX 作为唯一支持的第三方模型导入格式。这意味着无论你用 PyTorch、TensorFlow 还是 PaddlePaddle 训练最终都必须经过 ONNX 这个“海关”。而 ATCAscend Tensor Compiler是 CANN 生态内唯一的、经过华为深度优化的模型编译器。选择其他路径如直接用 ncnn 加载 PyTorch 模型虽然技术上可行但会失去昇腾芯片的硬件加速特性如 INT8 量化、算子融合、内存优化性能差距可达 3-5 倍。这不是“好不好用”的问题而是“能不能用”的问题。版本兼容性黑洞ONNX 的 opset 版本演进极快而 YOLO26 这类新模型大量使用了 opset 15 的高级算子如NonMaxSuppression的动态 top_k 输入、Slice的负索引。我们实测发现用 PyTorch 1.13 导出的 opset15 模型在 ONNX Runtime 1.15 上能跑但在 ATC 6.3.RC1 上会报Unsupported op: Slice。原因在于 ATC 对 ONNX spec 的支持是滞后且选择性的。因此Phase A · Step 2 的 ONNX 导出必须严格指定opset_version14并手动重写 YOLO26 中的几个“高危”算子如将动态 top_k 改为固定值将负索引 slice 拆分为正索引 concat。这看起来是倒退实则是为了换取 ATC 的稳定支持。那些搜索yolo26改进的同学往往忽略了这种“向下兼容”的工程代价。量化感知的前置要求.onnx量化int8不是部署阶段才做的事而是 Phase A · Step 2 就必须介入的环节。因为 ATC 的 INT8 量化不是黑盒它需要你在 ONNX 模型中预先插入QuantizeLinear和DequantizeLinear节点形成一个“量化感知图”QAT Graph。如果等到 pipeline 验证完再做量化你会发现很多算子如Softmax后接ArgMax无法被 ATC 正确量化导致精度崩塌。所以我们的流程是先用 PyTorch QAT 工具如 torch.quantization对模型进行伪量化训练导出时保留量化节点再用 ATC 的--precision_modeallow_mix_precision参数进行混合精度编译。这解释了为什么热词里pytorch转onnx和.onnx量化int8总是成对出现——它们是同一枚硬币的两面。综上ONNX ATC 的组合不是“最好”的而是“最稳”的。它用一定的灵活性牺牲如 opset 降级、手动算子改写换取了硬件兼容性、工具链成熟度和量化路径的确定性。这是工业级 AI 项目必须接受的现实。3. 核心细节解析预训练权重验证的实操要点与避坑指南3.1 权重验证的“黄金三步法”加载、前向、比对预训练权重验证看似简单但细节决定成败。我们采用一套标准化的“黄金三步法”确保每一步都可审计、可复现。第一步加载与环境隔离import torch import torch.nn as nn from yolo26.models import YOLO26 # 注意必须使用与训练时完全相同的源码路径 # 关键强制设置随机种子禁用 cudnn 非确定性 torch.manual_seed(42) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False # 加载权重必须指定 map_locationcpu避免 GPU 设备号污染 model YOLO26() state_dict torch.load(yolo26_coco_pretrained.pth, map_locationcpu) model.load_state_dict(state_dict, strictTrue) # strictTrue 是底线 model.eval() # 必须设为 eval 模式否则 BN 层会更新 running stats提示strictTrue是生死线。如果训练时用了ignore_missing_keysTrue而你这里设为False会立刻暴露权重缺失或冗余问题。很多“模型跑不通”的根源就是训练时悄悄忽略了某些 head 层的权重而验证时却期望它们存在。第二步构造 Dummy Input 与 Reference Output# 构造 dummy inputshape 必须与训练时完全一致 # YOLO26 官方训练分辨率是 640x640batch1channel3 dummy_input torch.randn(1, 3, 640, 640) # FP32 # 但注意YOLO26 的输入预处理包含 normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225]) # 所以 reference output 应该基于 normalized input 计算 dummy_input_normalized (dummy_input - torch.tensor([0.485,0.456,0.406]).view(1,3,1,1)) / torch.tensor([0.229,0.224,0.225]).view(1,3,1,1) # 获取 reference output必须在原始训练环境中运行一次并保存 # 这里假设 reference_output.pt 是从训练服务器上 copy 过来的 reference_output torch.load(reference_output.pt) # 形状[1, 84, 80, 80] for head1, [1, 84, 40, 40] for head2...注意dummy input 的构造必须模拟真实 pipeline 的输出。很多团队直接用torch.randn结果发现验证通过但实际部署时因输入分布不同而失效。正确做法是用一张真实的 COCO 图像走一遍完整的训练 pipelineresize→normalize→permute然后保存其 tensor作为验证的 input。这样reference output 就是“真实场景下的 ground truth”。第三步前向计算与逐元素比对with torch.no_grad(): output model(dummy_input_normalized) # 比对策略分层比对而非整体 flatten for i, (out, ref) in enumerate(zip(output, reference_output)): # 计算最大绝对误差 MAE 和相对误差 MRE mae torch.max(torch.abs(out - ref)) mre torch.max(torch.abs(out - ref) / (torch.abs(ref) 1e-8)) print(fHead {i} MAE: {mae.item():.6f}, MRE: {mre.item():.6f}) # 硬性阈值MAE 1e-5 且 MRE 1e-3 才算通过 if mae 1e-5 or mre 1e-3: raise RuntimeError(fHead {i} validation failed!)实操心得不要只看平均误差。YOLO26 的输出包含多个 detection head每个 head 的数值范围差异巨大cls prob 在 0~1bbox offset 在 -10~10。所以必须分层比对并设置不同的容忍阈值。我们曾遇到过 cls head 误差合格但 reg head 误差超标 10 倍的情况原因是训练时 reg head 的 loss weight 设置不当导致权重数值本身就不够稳定。3.2 YOLO26 权重的特殊陷阱BN 层、Anchor、Head 结构YOLO26 的权重文件里埋着几个“经典雷区”不提前识别就会在验证时引爆。BN 层的“双重身份”陷阱YOLO26 的 backbone如 CSPDarknet大量使用 BatchNorm2d。在训练时BN 层有两个状态trainingTrue时它用 batch 统计trainingFalse时它用running_mean和running_var。但问题在于很多开源权重文件如deim 的 coco 预训练权重在保存时没有正确冻结 BN 层的num_batches_tracked。这会导致在 PyTorch 1.12 中num_batches_tracked为 0 时eval()模式下 BN 会 fallback 到trainingTrue行为造成输出不稳定。解决方案加载权重后手动遍历所有 BN 层强制设置bn.num_batches_tracked torch.tensor(10000)一个足够大的数并确保bn.training False。Anchor 的“隐式编码”陷阱YOLO26 的 anchor 是 hard-coded 在模型代码里的如models/yolo26.py中的self.anchors [[10,13], [16,30], ...]而不是存储在权重文件中。这意味着如果你修改了代码中的 anchor但没重新训练直接加载旧权重模型会用新 anchor 解码旧权重结果 bbox 全错。验证时必须检查权重文件对应的 anchor 配置文件通常是data/coco.yaml并与代码中的self.anchors数组逐项比对。我们写了一个小脚本自动提取权重文件的 git commit hash然后去对应仓库 commit 中读取 anchor 定义确保 100% 一致。Head 结构的“输出顺序”陷阱YOLO26 的 detection head 输出是一个 tuple顺序是(pred_cls, pred_bbox, pred_obj)。但有些魔改版本如yolo26手部检测模型会调整顺序甚至合并输出。验证时如果reference_output.pt是按(cls, bbox, obj)保存的而你的模型输出是(bbox, cls, obj)那么即使数值完全正确比对也会失败。解决方案在模型 forward 函数末尾强制添加一个assert检查输出长度和形状def forward(self, x): # ... model computation ... outputs (pred_cls, pred_bbox, pred_obj) assert len(outputs) 3, YOLO26 head must output exactly 3 tensors assert outputs[0].shape[1] 80, cls head channel must be 80 (COCO classes) return outputs4. Pipeline 验证准备从 Python 脚本到可部署流水线的蜕变4.1 Pipeline 脚本的“原子化”重构为什么pipeline脚本语法必须重写热词中反复出现的pipeline脚本语法指向一个普遍痛点原始训练 pipeline 是为研究效率设计的而部署 pipeline 是为工程可靠性设计的。二者语法差异巨大不能简单复用。原始训练 pipelinePyTorch Lightning示例# train_pipeline.py - 优雅但不可控 transform transforms.Compose([ transforms.Resize((640, 640)), transforms.ToTensor(), # 自动归一化到 [0,1] 并 permute transforms.Normalize(mean[0.485,0.456,0.406], std[0.229,0.224,0.225]) ])问题在于transforms.Resize的插值方法未显式声明默认 BILINEAR而 OpenCV 的cv2.resize默认是 INTER_LINEAR二者不等价。transforms.ToTensor()是一个黑盒函数它内部做了PIL.Image → numpy → torch.Tensor的转换且PIL读图是 RGBcv2是 BGR混用必错。transforms.Normalize的 mean/std 是硬编码无法在 C 部署时复用。因此Phase A · Step 2 的 Pipeline 验证准备第一步就是“原子化”重构将所有变换拆解为显式的、可审计的、语言无关的步骤。重构后的部署 pipelinePython为后续 C 移植铺路import cv2 import numpy as np def deploy_preprocess(image_path: str) - np.ndarray: 部署端预处理流水线完全对标训练 pipeline 输入BGR 格式图像路径cv2.imread 默认 输出FP32 numpy array, shape(1,3,640,640), range[0,1] # Step 1: 读图 - 显式声明 BGR img_bgr cv2.imread(image_path) if img_bgr is None: raise ValueError(fFailed to load image: {image_path}) # Step 2: Resize - 显式声明插值方法 # 训练时用的是 torchvision.transforms.Resize其底层是 PIL.Image.resize(modebilinear) # cv2.INTER_LINEAR 与 PIL bilinear 最接近误差 0.1% img_resized cv2.resize(img_bgr, (640, 640), interpolationcv2.INTER_LINEAR) # Step 3: BGR to RGB - 强制颜色空间转换 img_rgb cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB) # Step 4: HWC to CHW Normalize - 显式计算无黑盒 img_chw img_rgb.transpose(2, 0, 1).astype(np.float32) / 255.0 # [0,1] range mean np.array([0.485, 0.456, 0.406]).reshape(3, 1, 1) std np.array([0.229, 0.224, 0.225]).reshape(3, 1, 1) img_norm (img_chw - mean) / std # Step 5: Add batch dim img_tensor np.expand_dims(img_norm, axis0) # (1,3,640,640) return img_tensor # 验证用同一张图跑原始训练 pipeline 和 deploy pipeline比对输出 tensor # 必须做到 max(abs(diff)) 1e-4实操心得这个重构过程本质上是在“翻译”学术代码为工程代码。每一行都必须有据可依要么来自论文附录要么来自训练代码的 debug 日志。我们曾花两天时间用pdb逐行跟踪torchvision.transforms的源码确认Resize的插值算法就是为了这一行cv2.INTER_LINEAR的合法性。4.2 ONNX 导出的“手术刀级”操作绕过 YOLO26 的算子地雷YOLO26 的 PyTorch 代码中有几个“著名”的 ONNX 不友好算子必须手动外科手术。地雷一DynamicNonMaxSuppressionYOLO26 的后处理使用了torchvision.ops.nms它支持动态max_output参数。但 ONNX opset 14 不支持动态输入ATC 也不认。解决方案在导出前将nms替换为一个固定max_output100的版本并在模型 forward 中硬编码# models/yolo26.py def forward(self, x): # ... get raw outputs ... # 替换原 nms 调用 boxes self._decode_boxes(pred_bbox) # decode to xyxy format scores pred_cls.max(dim1)[0] * pred_obj.squeeze() # 使用固定 max_output 的 nms keep torch.ops.torchvision.nms(boxes, scores, iou_threshold0.45, max_output100) return pred_cls[keep], pred_bbox[keep], pred_obj[keep] # 导出时必须指定 input_signature 包含 fixed max_output torch.onnx.export( model, dummy_input, yolo26_fixed_nms.onnx, opset_version14, input_names[input], output_names[cls, bbox, obj], dynamic_axes{input: {0: batch}, cls: {0: detections}, bbox: {0: detections}, obj: {0: detections}}, )地雷二torch.where的布尔索引YOLO26 的 anchor matching 使用了torch.where(mask)这在 ONNX 中会生成复杂的NonZeroGather组合ATC 编译时常失败。解决方案用torch.nonzero替代并确保返回值是 2D tensor# 错误写法 indices torch.where(mask) # 返回 tuple of 1D tensors # 正确写法 indices torch.nonzero(mask, as_tupleFalse) # 返回 2D tensor, shape(N, 2) if indices.numel() 0: return torch.empty(0, 4), torch.empty(0, 80), torch.empty(0, 1)地雷三torch.split的负维度YOLO26 的 head 分离用了x.split([80, 4, 1], dim1)其中dim1是正向的但某些版本 PyTorch 会将其优化为负维度dim-3ONNX 不支持。解决方案显式使用正维度并在导出时用--dynamic_axes声明# 在 forward 中 cls_out, reg_out, obj_out pred.split([80, 4, 1], dim1) # dim1, not -3 # 导出时 dynamic_axes { input: {0: batch}, cls: {0: batch, 1: classes, 2: grid_h, 3: grid_w}, reg: {0: batch, 1: bbox, 2: grid_h, 3: grid_w}, obj: {0: batch, 1: obj, 2: grid_h, 3: grid_w}, }4.3 ATC 编译的“参数炼金术”从 .onnx 到 .om 的关键配置ATC 编译不是“一键生成”而是一场精细的参数调优。以下是我们经过上百次实验总结出的“黄金参数组合”。基础命令与必选参数atc \ --modelyolo26_fixed_nms.onnx \ --framework5 \ # 5ONNX --outputyolo26_om \ --soc_versionAscend310P \ --input_shapeinput:1,3,640,640 \ --logerror--framework5ONNX 的固定 code不能错。--soc_versionAscend310P必须与目标硬件完全一致写成Ascend310会编译失败。--input_shape必须与 ONNX 模型中的 input name 完全匹配区分大小写且 shape 必须是静态的不能用-1。量化参数INT8 的“三件套”--precision_modeallow_mix_precision \ --insert_op_confinsert_op.conf \ --input_fp16_nodesinput--precision_modeallow_mix_precision允许部分算子保持 FP16避免精度损失。YOLO26 的Softmax和Sigmoid必须 FP16否则分类概率全为 0。--insert_op_conf指向一个配置文件定义哪些算子需要量化。内容如下[op_nameSoftmax] quant_typefp16 [op_nameSigmoid] quant_typefp16 [op_nameConv] quant_typeint8--input_fp16_nodes告诉 ATC输入 tensor 是 FP16 格式因为 ONNX 模型是 FP32但 ATC 会在编译时自动 cast 到 FP16 输入。性能优化参数让 .om “跑得更快”--enable_small_channel1 \ --fusion_switch_filefusion_switch.cfg \ --optypelist_for_implmodeConv2D;MatMul--enable_small_channel1针对 YOLO26 中大量小 channel如 32, 64的卷积开启此开关可提升 15% 吞吐。--fusion_switch_file一个开关文件禁用某些激进的融合如ConvBNReLU融合因为 YOLO26 的 BN 层有特殊处理强行融合会导致精度下降。--optypelist_for_implmode指定哪些算子启用高性能实现模式Conv2D和MatMul是 YOLO26 的主要耗时算子。注意事项ATC 编译日志是唯一真相。必须开启--logdebug运行一次检查日志中是否有INFO级别的Fusion applied或Quantization applied提示。如果日志里全是WARNING: No fusion pattern matched说明你的模型结构或参数设置有问题必须回溯 ONNX 导出环节。5. 常见问题与排查技巧实录踩过的坑都给你标好了5.1 权重验证失败的“Top 5”原因与速查表问题现象根本原因排查步骤解决方案RuntimeError: Error(s) in loading state_dictstrictTrue下权重 key 与模型定义不匹配1.print(model.state_dict().keys())2.print(torch.load(weights.pth).keys())3. 用set(keys1) - set(keys2)找缺失项检查模型代码是否与训练 commit 一致或用strictFalse加载但必须手动load_state_dict修复缺失层MAE 1e-5且MRE正常某些层如 Conv bias数值为 0导致abs(out-ref)/abs(ref)除零1.torch.isinf(mre).any()2.torch.isnan(mre).any()3. 单独检查 bias 层输出在比对时对ref加1e-8防止除零或单独屏蔽 bias 层比对outputshape 与reference_output不符模型 forward 返回了额外的 debug 信息如 feature map1.print(type(output))2.print(len(output))3.print([o.shape for o in output])修改模型 forward确保只返回(cls, bbox, obj)三元组或在保存reference_output时只保存这三者eval()模式下 BN 输出波动num_batches_tracked为 0导致 fallback 到 training 行为1.for name, m in model.named_modules(): if isinstance(m, nn.BatchNorm2d): print(name, m.num_batches_tracked)加载权重后遍历所有 BN 层m.num_batches_tracked torch.tensor(10000)dummy_input与reference_output生成环境不一致reference_output是用torchvision.transforms生成的而dummy_input是torch.randn1. 用同一张图分别用两种 pipeline 处理2.torch.max(torch.abs(pipeline1_out - pipeline2_out))放弃torch.randn用真实图像生成reference_output5.2 Pipeline 验证的“隐形杀手”数据类型与内存布局Pipeline 验证失败80% 的原因是数据类型dtype和内存布局layout的细微不一致。dtype 陷阱uint8vsfloat32训练 pipeline 的ToTensor()输出是torch.float32范围[0,1]。OpenCV 的cv2.imread()输出是np.uint8范围[0,255]。如果你在cv2.imread后直接astype(np.float32)会得到[0,255]而不是[0,1]。必须除以255.0。更隐蔽的是cv2.resize对uint8和float32的插值算法不同。解决方案始终在resize后再做astype和/255.0。layout 陷阱NHWCvsNCHWTensorFlow 默认 layout 是NHWCbatch, height, width, channel。PyTorch 默认是NCHWbatch, channel, height, width。YOLO26 是 PyTorch 模型要求NCHW。如果你的 pipeline 输出是NHWC如某些 C 库必须在送入模型前transpose(0,3,1,2)。验证方法打印img_tensor.shape必须是(1,3,640,640)而不是(1,640,640,3)。内存连续性陷阱contiguous()torch.permute()或numpy.transpose()会产生 non-contiguous tensor。ONNX 导出时non-contiguous tensor 会被自动contiguous()但 ATC 编译时可能出错。解决方案在 pipeline 末尾强制img_tensor img_tensor.contiguous()。5.3 ONNX/ATC 链路的“幽灵错误”没有报错但结果全错这类问题最折磨人因为 ATC 编译成功.om 模型也能加载但推理结果完全不对。症状cls输出全为 0bbox输出为 nan原因ATC 的--precision_modeallow_mix_precision开关未启用导致Softmax被错误量化为 INT8。排查用netron打开 .om 模型搜索Softmax节点看其quant_type是否为int8。如果是说明insert_op.conf未生效。解决确认insert_op.conf路径正确且文件权限为644或在 ATC 命令中加入--enable_scope_fusion_ops1。症状bbox坐标全部偏移且 scale 固定原因ONNX 导出时--input_shape的batch维度写成了-1ATC 会用默认 batch1 编译但实际推理时 batch4导致reshape算子计算错误。排查用atc --help查看--input_shape格式必须是name:N,C,H,W不能有-1。解决重新导出 ONNX用--dynamic_axes声明动态 batch但--input_shape必须填具体值如1,3,640,640。症状ATC 编译耗时超过 30 分钟且内存占用飙升原因ONNX 模型中存在大量ConstantOfShape或Loop算子ATC 编译器会陷入无限优化循环。排查用onnx.shape_inference.infer_shapes()对 ONNX 模型做 shape 推