
简介面向算法部署工程师与深度学习开发者的实战项目完整展示如何将 MobileViT 算法经由 TensorRT 优化并部署到 NVIDIA GPU 上。项目以实际可运行为目标覆盖模型转换、自定义网络层插件、精度校准与推理性能验证帮助读者解决从 PyTorch 训练权重到 TensorRT 引擎落地的关键问题。资源包为 zip 压缩格式共 51 个文件大小 64.25MB主要包含 21 个 Python 脚本和 11 个编译生成文件还提供 C 插件源文件cu/h、模型与数据文件npy/tar、说明文档md/txt/pptx等便于按模块梳理学习。目前已有 136 人学习下载适合具备 PyTorch 基础、希望进阶 TensorRT 部署实战的开发者。压缩包内附转换脚本、测试脚本、校准器、演示文稿与说明材料涵盖 ONNX 导出、TensorRT 转换、插件编写、精度比对等环节可让读者动手复现 MobileViT 的完整部署链路理解每一步的优化方法是一份值得算法部署初学者和进阶者反复参考的实战资源。1. 为什么 MobileViT 值得用 TensorRT 重新部署一遍刚把 MobileViT 跑通训练时你可能觉得直接拿 PyTorch 模型做服务就够用了——但延迟和显存会被这条“够用”拖着走。把 MobileViT 用 TensorRT 重构成 engine 之后同样一张 256x256 的图延迟能降到原来的 50% 到 70%。难的不是 TensorRT 本身而是 MobileViT 的混合结构:卷积和 Transformer 块并存意味着 ONNX 导出、动态 shape、LayerNorm 兼容性这些部署阶段的坑你几乎要挨个踩一遍。这篇笔记描述一条从 pt 转 onnx 再转 TensorRT engine、最后完成推理和验证的完整路径适合正在把 MobileViT 塞进服务端或边缘设备的算法与部署工程师。2. 部署前的模型准备:先拆 MobileViT 算子再谈 pt 转 onnx2.1 摸清 MobileViT 的算子底细:哪些层决定部署成败MobileViT 不是纯 CNN。它把标准卷积、深度可分离卷积和轻量 Transformer 块搭在一起后者包含 unfold、reshape、transpose、LayerNorm 和多头自注意力。这些算子在 PyTorch 里跑得顺滑但导出到 ONNX 再由 TensorRT 解析时问题会集中在 transpose 和 reshape 的组合上——一个 permute 在 ONNX 里就是一个 Transpose 节点一个 view 会被拆成多个 Shape、Gather、Reshape 节点。连续的 Transpose/Reshape 是 TensorRT 构建阶段 shape 推断失败最常见的触发点。所以我拿到权重文件后的第一步不是直接跑导出而是把模型定义里所有的.view()、.reshape()、.permute()、.transpose()圈出来心里有张算子地图。尤其要注意 MobileViT block 里那个把 H×W×C 特征图切成 patch 再重排进 attention 的 unfold 操作它在 ONNX 图上会被展开成一系列 transpose 与 reshape 的组合TensorRT 版本对它的支持差异很大。有的第三方实现喜欢把 unfold 写成几个 concat 和 strided slice 的组合这种展开方式会让 ONNX 图非常臃肿构建 engine 也更慢。2.2 从 pt 到 onnx 的导出参数:固定分辨率与动态 batch 的取舍“pt 文件转换 tensorrt”这个诉求本质上是两跳:pt 先转 ONNXONNX 再被 TensorRT 构建成 engine。第一跳错了第二跳必然错所以导出参数别照抄别人的模板。我导出 MobileViT 的常用代码如下:import torch from mobilevit_net import MobileViTS model MobileViTS(num_classes10) ckpt torch.load(mobilevit_s.pt, map_locationcpu) model.load_state_dict(ckpt[model] if model in ckpt else ckpt) model.eval() dummy_input torch.randn(1, 3, 256, 256) torch.onnx.export( model, dummy_input, mobilevit_s.onnx, opset_version17, input_names[images], output_names[logits], dynamic_axes{images: {0: batch}}, do_constant_foldingTrue, )opset_version 我用 17。MobileViT 里有 LayerNorm 和 SiLU/GELUopset 版本低于 13 时导出会产生很多冗余节点TensorRT 解析时容易绕远路。dynamic_axes 只把 batch 维放开分辨率固定理由是 MobileViT 的 attention 序列长度由分辨率决定宽高做成动态会让 TensorRT 需要生成多套 kernel 排列构建时间和显存占用都上去了。如果你的服务端没有多分辨率需求固定分辨率是性价比更高的选择也方便后面 TensorRT 的 optimization profile 配置。这里还有一个容易忽略的点:如果模型里有 dropout 或训练/推理行为不一致的分支导出前要确认model.eval()真的生效了否则导出的 ONNX 里会残留训练相关的算子TensorRT 构建时又多一个变量来源。导出后立刻检查 ONNX 的输入输出名与你的预期一致不然后面写推理脚本时经常对不上。2.3 ONNX 结果对齐验证:避免把导出错误带进 TensorRT导出完成后先用 onnxruntime 做一次结果对齐这一步我从不省:import onnxruntime as ort import numpy as np import torch x torch.randn(1, 3, 256, 256) with torch.no_grad(): ref model(x).numpy() sess ort.InferenceSession(mobilevit_s.onnx, providers[CPUExecutionProvider]) out sess.run([logits], {images: x.numpy()})[0] print(max abs diff:, np.max(np.abs(ref - out)))这里用 CPUExecutionProvider 是为了彻底去掉 CPU/GPU 两套 kernel 实现的差异只看 ONNX 图结构是否忠实地保存了模型行为。diff 在 1e-4 到 1e-3 之间都可接受超过 1e-3 就要重建或者手动改 ONNX 图。常见原因是模型里存在数据类型不统一比如某一层用了 double 精度导出时部分节点被保留为 double而 TensorRT 的算子集并不覆盖高精度计算到构建阶段又会报不支持。2.4 TensorRT 10.x 对 GTX 1070 这类老架构的支持边界搜“tensorrt 版本如果是 10.x 是否支持 gtx1070”的朋友多半是手头只有老显卡又看到 TensorRT 10 文档里满屏的 Hopper/Ada 字样。先说结论:TensorRT 10.x 依然保留对 Pascal 架构GTX 1070 就是的支持能正常构建和运行 MobileViT engine。但老架构的性能表现和现代卡有差距两个现象要有预期。第一GTX 1070 没有 Tensor Core。TensorRT 的 FP16 kernel 在 Pascal 上只能走普通 CUDA core速度提升有限部分 kernel 反而比 FP32 慢。所以老显卡上部署 MobileViT建议先以 FP32 作为基线不要一上来就开 FP16。第二TensorRT 10.x 对 Pascal 的默认优化路径收益不如 Ampere 明显你在 10.x 上构建的 engine 延迟可能和 8.x 相差不大。这种情况下版本选择反而简单:按你当前环境能装的最稳定版本即可。版本兼容性的另一个坑是配套依赖。TensorRT 安装教程里最容易被忽略的是 cuDNN、CUDA runtime 和 TensorRT 的版本匹配很多人安装后第一跑就报 “cuDNN failed to initialize”往往不是 TensorRT 本身的问题而是系统里 cuDNN 版本和 TensorRT 要求的最低值不一致。我是习惯用 Docker 镜像锁版本镜像里固定 CUDA、cuDNN、TensorRT 三个版本换机器不换镜像就不存在环境漂移。3. 构建 TensorRT 引擎:trtexec 先行Python API 跟上3.1 用 trtexec 跑通最小构建:把参数一行行拆明白ONNX 导出验证没问题后先用 trtexec 跑一次。原因很简单trtexec 是 TensorRT 自带的工具暴露构建错误最快而且它默认会做一次 timing 和内存分析能顺手拿到构建后的性能基线。trtexec \ --onnxmobilevit_s.onnx \ --saveEnginemobilevit_s_fp16.engine \ --fp16 \ --minShapesimages:1x3x256x256 \ --optShapesimages:1x3x256x256 \ --maxShapesimages:4x3x256x256 \ --workspace1073741824参数含义分别是:--onnx 输入 ONNX 文件;--saveEngine 把构建好的引擎写入磁盘二进制格式直接拷到推理机上用;--fp16 启用半精度 kernel 搜索;min/opt/maxShapes 定义 batch 维动态范围和 ONNX 里 dynamic_axes 的声明是对应的;--workspace 给 TensorRT 分配 1GB 显存做 tactic 搜索空间单位是字节。第一次构建我建议不开 --fp16。先用 FP32 把整条链路跑通确认 ONNX 和引擎都没问题再回头加 FP16 找性能。因为 MobileViT 的 LayerNorm 在 FP16 下偶尔会精度翻车如果一开始就开半精度很难分清问题是出在算子兼容性还是数值误差上。FP32 构建就是去掉 --fp16其余参数不变。3.2 用 Python API 构建 engine:动态 batch、workspace 与 FP16 开关trtexec 适合验证和批量转换真正做产品集成时我倾向于用 Python API。因为可以把构建逻辑写进自动化流水线也能在构建失败时捕获更完整的错误栈甚至做逐层精度控制。import tensorrt as trt logger trt.Logger(trt.Logger.WARNING) builder trt.Builder(logger) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, logger) with open(mobilevit_s.onnx, rb) as f: if not parser.parse(f.read()): for i in range(parser.num_errors): print(parser.get_error(i)) raise RuntimeError(ONNX parse failed) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) config.set_flag(trt.BuilderFlag.FP16) profile builder.create_optimization_profile() profile.set_shape(images, (1, 3, 256, 256), (1, 3, 256, 256), (4, 3, 256, 256)) config.add_optimization_profile(profile) serialized builder.build_serialized_network(network, config) with open(mobilevit_s.engine, wb) as f: f.write(serialized)EXPLICIT_BATCH 标志是必须的。如果不加网络会被解释成 implicit batch 模式动态 batch 直接不生效。parse 失败时记得遍历 error 列表逐条打印错误信息虽然啰嗦但里面通常已经包含具体是哪个节点不支持的线索。WORKSPACE 先给大一点构建时 TensorRT 会在这些显存里铺开候选 kernel构建完成后运行时的实际显存占用可能远小于 workspace 的值不要用构建时的占用去预估生产显存。FP16 的打开方式就是 config 上加一个 BuilderFlag.FP16。如果还想用 INT8就再加 BuilderFlag.INT8 并挂上校准器。构建时间长是正常的MobileViT 的 Transformer 块结构复杂kernel 搜索空间大第一次构建花几分钟不代表进程卡死日志在 WARNING 级别下安静不代表它没在工作。3.3 序列化 engine 的跨卡失效:部署物料要按机器准备engine 构建好写到磁盘之后有个现象很容易让新人懵:同一个 engine 文件换一台显卡就加载失败。这不是文件损坏。TensorRT 的 engine 序列化格式里包含了 kernel 的二进制、显存分配策略、autotuning 结果全部是针对构建时那张 GPU 和那个 TensorRT 版本选择的。换版本、换显卡、换驱动三者任一改变都可能让 engine 无法加载。所以部署物料要拆分:ONNX 文件是平台中立的可以进代码仓库;engine 文件是机器相关的必须在目标机器上现场构建。我一般的做法是部署包内同时带 ONNX 和 build_engine.py服务启动时检查本地 engine 是否存在且与当前 GPU 匹配不匹配就自动重新构建。这样省去手工搬运 engine 的环节也避免了线上环境被版本不一致问题卡住。动态 shape 下还有一个隐蔽点:构建时 profile 的数量和范围决定了运行时可以传入的 shape 集合。如果构建时只配了一个 profile而运行时实际输入的 batch 落在这个 profile 之外TensorRT 会直接抛异常。这不是模型问题而是 profile 没覆盖到位。3.4 构建日志怎么看:tactic 搜索与显存不足的判别构建过程中日志信息量很大但真正值得看的只有几类。第一类是 “Could not find any implementation” 开头的报错说明某个层在平台上没有可执行 kernel大概率是算子不兼容。第二类是 “Allocation failed” 或 “Out of memory”这是 workspace 给小了先调大再重试。第三类是 “[W] [TRT] ... tactic” 相关的警告通常只是 autotuning 放弃了某些 kernel 选择不影响最终引擎。有一种情况容易被误判:构建成功但速度很慢日志里能看到大量 “Time to generate” 长耗时。这不是死机是 TensorRT 在做 exhaustive tactic search。网络越复杂搜索越慢MobileViT 这种带 attention 的结构尤其明显。如果构建时间已经到 10 分钟以上可以考虑把 builder 的config.set_tactic_sources限制一下只保留当前 GPU 最常用的几个 tactic source能显著缩短搜索时间代价是可能错过几个非常规的最优 kernel。这种优化适合在 CI 环境里做本地第一次构建还是建议跑全量。4. 推理链路搭建:预处理对齐、显存绑定与服务化封装4.1 预处理必须和训练侧完全对齐:一个 /255 引发的血案我见过不少从 PyTorch 直接转过来的部署脚本推理端把 /255 忘了或者用了 BGR 输入结果精度莫名掉几个点最后定位出来是预处理不一致。TensorRT engine 只负责网络计算不负责图像处理预处理做错了engine 再准也没有用。MobileViT 训练时常用的预处理链是:读图 → BGR 转 RGB → resize 到 256×256 → 转 float32 → 除以 255 → 按 ImageNet 的 mean/std 归一化。推理端要做到逻辑一致我会把预处理单独抽成函数:import cv2 import numpy as np def preprocess(image_bgr, size(256, 256), mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)): img cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) img cv2.resize(img, size, interpolationcv2.INTER_LINEAR) img img.astype(np.float32) / 255.0 img (img - np.array(mean, dtypenp.float32)) / np.array(std, dtypenp.float32) img np.transpose(img, (2, 0, 1))[None, ...] return np.ascontiguousarray(img)两个细节要注意。第一resize 的插值方式要和训练时一致。训练代码如果用 TorchVision 的 Resize默认 PIL bilinear而 OpenCV 的 INTER_LINEAR 和 PIL 的 bilinear 在高倍缩放时有一点点差异建议训练和推理都固定用 OpenCV 做预处理这样导出部署时零迁移成本。第二np.ascontiguousarray 不能省。transpose 之后数组内存布局是分段的TensorRT 的 tensor 地址要求连续内存非连续数组传进去会报 “memcpy failed” 之类错误。4.2 TensorRT 10.x 的 I/O 绑定:按 tensor 名字取地址TensorRT 10.x 把 I/O 接口改成以 tensor 名字为核心代码风格和早期版本差别不小。我用 10.x API 时的推理骨架如下:import tensorrt as trt import pycuda.driver as cuda runtime trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open(mobilevit_s.engine, rb) as f: engine runtime.deserialize_cuda_engine(f.read()) context engine.create_execution_context() input_name engine.get_tensor_name(0) output_name engine.get_tensor_name(1) context.set_input_shape(input_name, (1, 3, 256, 256)) d_input cuda.mem_alloc(1 * 3 * 256 * 256 * 4) d_output cuda.mem_alloc(1 * 10 * 4) cuda.memcpy_htod(d_input, input_np) context.execute_v2(bindings[int(d_input), int(d_output)]) cuda.memcpy_dtoh(output_np, d_output)get_tensor_name(0) 和 get_tensor_name(1) 拿的是构建时 ONNX 的 input_names 和 output_names可以直接打印出来确认顺序。set_input_shape 这一步不能省尤其当构建时的 opt shape 不是 (1,3,256,256) 时如果漏了context 内部还是 opt shape 的尺寸输入数据和你的实际 batch 对不上结果就全错了。execute_v2 的 bindings 列表里放的是设备显存指针顺序必须和 engine 里的 tensor 顺序一致。如果你在 bindings 里传的是int(d_input)而不是一个新的变量那每次调用都会重新解释这个指针性能影响可以忽略。真正要注意的是pycuda 的 mem_alloc 返回对象不能离开作用域一旦被 GC 回收显存可能被释放指针悬空推理结果会变成随机数。4.3 推理封装成类:一次性分配显存别在每帧里 mem_alloc推理代码写顺手之后最后会把它包成一个 Python 类业务层只调 predict()。一个现实的难点是显存管理——如果每一帧都在 predict() 里 cuda.mem_alloc 和 cuda.mem_free短时间跑几千张图后显存会碎片化严重时后续帧甚至推不动。class MobileViTEngine: def __init__(self, engine_path, batch_size1): self.runtime trt.Runtime(trt.Logger(trt.Logger.WARNING)) with open(engine_path, rb) as f: self.engine self.runtime.deserialize_cuda_engine(f.read()) self.context self.engine.create_execution_context() self.input_name self.engine.get_tensor_name(0) self.output_name self.engine.get_tensor_name(1) self.batch_size batch_size self._allocate_memory() def _allocate_memory(self): in_shape self.context.get_tensor_shape(self.input_name) in_size trt.volume(in_shape) * 4 out_size trt.volume(self.engine.get_tensor_shape(self.output_name)) * 4 self.d_input cuda.mem_alloc(in_size) self.d_output cuda.mem_alloc(out_size) def predict(self, input_np): self.context.set_input_shape(self.input_name, input_np.shape) cuda.memcpy_htod(self.d_input, input_np) self.context.execute_v2(bindings[int(self.d_input), int(self.d_output)]) output_np cuda.pagelocked_empty(self.batch_size * 10, dtypenp.float32) cuda.memcpy_dtoh(output_np, self.d_output) return output_np.reshape(self.batch_size, 10)类初始化时一次性分配显存predict 里只做拷贝和执行。batch_size 固定也省去了每次计算 shape 的开销。这里输出尺寸写死成 10 是因为 MobileViT 分类头的类别数如果你的任务不是 10 类改成从 engine 读取输出 shape不要硬编码。pagelocked_empty 分配的是锁页内存和 GPU 的 DMA 拷贝效率更高比普通 numpy array 更适合高频推理。4.4 异步推理与 CUDA stream:给延迟再挤一点空间同步执行时execute_v2 会阻塞调用线程直到推理完成。在服务端场景下如果同一时刻有多个请求同步执行会让 GPU 空闲等待 CPU吞吐量上不去。TensorRT 提供了 execute_async_v2允许传入一个 CUDA stream把 kernel 启动和主线程里的图像解码、预处理并行起来。import pycuda.driver as cuda stream cuda.Stream() cuda.memcpy_htod_async(d_input, input_np, streamstream) context.execute_async_v2(bindings[int(d_input), int(d_output)], stream_handlestream.handle) cuda.memcpy_dtoh_async(output_np, d_output, streamstream) stream.synchronize()用异步接口时input_np 和 output_np 所在的内存必须在拷贝完成前保持有效不能在函数返回时就被释放否则数据会被覆盖。这也是为什么我推荐用 pagelocked memory 的原因之一锁页内存在异步 stream 下更稳定。异步模式引入的复杂度主要在生命周期管理如果请求并发量不大、单 batch 推理已经很快同步也没有问题不必为了追求技术新奇而无谓地用异步。5. MobileViT 部署避坑指南:五个踩过才知道的细节5.1 “Layer not found”算子不支持:先拆 LayerNorm 子图现象:trtexec 构建 MobileViT 时日志提示某个节点 “not supported” 或 “Layer not found”构建直接中断。用 TensorRT 10.x 构建第三方实现的 MobileViT 权重时尤其常见。原因:不同实现里 LayerNorm 的导出路径不一样。如果模型里用的是torch.nn.LayerNormONNX 里通常会归一化为ReduceMean、Sub、Pow、Div的组合TensorRT 能识别;但如果实现里自己写了 LayerNorm 的等价逻辑或者用了torch.layer_norm的旧导出方式ONNX 图里可能出现Shape、Gather、Cast连成一串的复杂子图超出了 TensorRT 的原生支持范围。解决:先用 onnx_graphsurgeon 定位出错节点的名字再看是哪个子图引发的。LayerNorm 的通用拆法是把归一化表达式展开成基础算子序列:先按最后一维计算均值再算方差最后 scale shift。这个拆法机械但有效。拆完重新导 ONNX 并回到 2.3 的验证脚本确认输出没变再去构建 engine。如果拆了仍然报错检查 opset 版本尝试提高到 17 或 18 重新导出。5.2 FP16 精度下降:把敏感层留在 FP32现象:FP32 engine 一切正常开 FP16 后分类 top-1 掉 3 到 5 个点或者同一张图前后两次预测的 logits 明显抖动。原因:MobileViT 的 LayerNorm 输入统计量分散FP16 只有 10 位有效尾数计算均值方差时的累计误差会被 attention 里的 scale 乘法放大。尤其当输入通道数较大时数值很容易超出 FP16 的正常范围每层一点误差叠加起来最终 logits 偏离。解决:先尝试只把 LayerNorm 留在 FP32。在 Python API 构建时遍历每一层将 LayerNorm 对应层的精度标记为 FP32其余卷积保持 FP16。TensorRT 10.x 支持逐层精度设置具体做法是遍历 network 内层找到名字包含 layer_norm 的层调用layer.precision trt.float32同时设置layer.set_output_type(index, trt.float32)。如果版本不支持逐层标记就退回全 FP32。MobileViT 本身在 FP32 下也有不错的融合加速不要因为 FP16 数值不稳就否定整个 TensorRT 方案。5.3 GTX 1070 上 TensorRT 10.x 跑不快:老架构别迷信 FP16现象:在 GTX 1070 上用 TensorRT 10.x 构建 MobileViT engineFP16 模式推理延迟比 PyTorch 还慢FP32 模式只是小幅领先。原因:Pascal 架构没有 Tensor Core。TensorRT 的 FP16 kernel 在 Pascal 上只能走普通 CUDA coreautotuning 阶段会花大量时间搜索 FP16 kernel最终选到的 kernel 未必比 FP32 的融合 kernel 快有时候反而更慢。解决:老显卡直接关掉 FP16以 FP32 引擎为性能基线。MobileViT 在 256×256 输入、RTX 卡上 FP32 的延迟能到毫秒级在 GTX 1070 上也能做到 20 到 40ms 的水平已经比 PyTorch 有质的变化。如果确实要压榨极致走 INT8 而不是 FP16但 INT8 校准集要贴合业务数据收益远高于 FP16。在部署汇报里写明 “GTX 1070 使用 FP32 引擎FP16 无 Tensor Core 加速收益” 这句话能省不少后续疑问。5.4 动态 batch 超上限:引擎不会自动扩容现象:用 batch4 以内的 engine生产高峰时不小心传了一个 batch8 的请求报 “out of range” 或 “allocation failed”。原因:TensorRT 的 optimization profile 在运行时是硬约束。maxShape 规定了该 profile 能处理的最大 shape超过即失败。很多人会想当然地认为动态意味着无上限这是误解。解决:要么把 maxShape 扩到 8 或 16注意显存占用也线性上涨;要么在推理服务里做 batch 切分。我的建议是固定 maxShape8按 8 切分排队显存不至于浪费太多又留了余量。如果你服务的峰值不确定先调大 max 做一次压测用真实的数据流量回缩到一个合理值。这个参数建议写进部署配置别在代码里悄悄写死。5.5 engine 跨卡加载失败或输出固定值:两个序列化与绑定问题现象一:开发机 RTX 3090 上构建的 engine拷到生产机 RTX 2080Ti 上加载报 “deserialize failed” 或 “cudaErrorNoKernelImageForDevice”。现象二:推理跑通但输出的 logits 全是一个常数或者每次结果都一样。原因一:engine 文件里存储的 kernel 是针对特定 GPU 架构编译的换架构不能复用。原因二:execute_v2 的 bindings 列表顺序和 engine 的 tensor I/O 顺序不一致TensorRT 无法校验顺序错了就是把输出指针当输入指针用数据完全错位。解决:第一个问题部署流程把 ONNX 文件和构建脚本一起带上目标机器第一次启动时自动执行构建不要搬运 engine 文件跨卡复用。第二个问题打印 engine 的 I/O tensor 名字逐个核对:for i in range(engine.num_io_tensors): name engine.get_tensor_name(i) mode engine.get_tensor_mode(name) print(i, name, mode)bindings 列表就按打印出来的顺序填。这两个问题都属于“装对了但用错”的典型,不是引擎质量问题而是部署流程的设计问题。6. 验证与调优:用 logits 对比和 P95 延迟收尾6.1 用同一张图的 logits 做精度对比:cosine 与 max_abs 双指标部署完成后第一件事不是看 top-1而是直接比较 PyTorch 和 TensorRT 的 logits。只对比 top-1 分类结果容易掩盖数值偏移——有时候预测对了但置信度已经偏了后面做阈值判断时会出问题。def compare_logits(pt_out, trt_out): pt_np pt_out.detach().cpu().numpy().flatten() trt_np trt_out.flatten() cosine np.dot(pt_np, trt_np) / (np.linalg.norm(pt_np) * np.linalg.norm(trt_np) 1e-12) max_abs np.max(np.abs(pt_np - trt_np)) return cosine, max_absFP32 和 PyTorch 的 cosine 应接近 1FP16 在 0.999 以上INT8 在 0.99 附近。低于这个区间优先查预处理和校准集其次是逐层精度。这个脚本建议固化到测试集里每次改动 ONNX 或构建参数后都跑一遍。6.2 延迟测不准等于白测:warmup 与 P95 的正确测量单跑一次推理就记录延迟在服务端是错的。第一次推理要经历 cuDNN 初始化、kernel 加载、显存分配时间会虚高。正确测法是先做 warmup再统计多轮的 p50/p95。def benchmark(predict_fn, sample, warmup20, iterations100): for _ in range(warmup): predict_fn(sample) times [] for _ in range(iterations): t0 time.perf_counter() predict_fn(sample) times.append(time.perf_counter() - t0) print(fp50: {np.percentile(times, 50):.3f} ms, p95: {np.percentile(times, 95):.3f} ms)p50 反映常规负载p95 反映波动上限。测时确保显卡空闲避免其他进程干扰。6.3 最后再碰 INT8:校准集与敏感算子隔离MobileViT 的 INT8 量化不是打开开关就能用的。attention 部分对量化误差敏感校准集如果和业务图分布偏差大精度掉得很快。如果要做 INT8校准器核心要实现 get_batch 和 get_batch_size返回的必须是 float32 的预处理图像批次校准完成后跑一遍 6.1 的对比脚本。如果 cosine 低于 0.99就把 LayerNorm 和 Softmax 层留在 FP32只让卷积走 INT8。这种混合精度不是玄学是 attention 模型量化的标准做法。做到这一步MobileViT 的部署才算真正收尾。最后说个习惯:每次部署完 MobileViT我都会把 ONNX、构建脚本、推理服务、验证脚本这四件套放进同一个 git repo文件名里带 TensorRT 版本号。三个月后你会感谢这个记录因为 engine 的可复现链比 engine 本身更值钱换卡、换驱动时能重建就是安全感。希望这篇能帮到你。本文还有配套的精品资源点击获取