简介:面向算法部署工程师与深度学习开发者的实战项目,完整展示如何将 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 转 onnx
2.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 先转 ONNX,ONNX 再被 TensorRT 构建成 engine。第一跳错了,第二跳必然错,所以导出参数别照抄别人的模板。我导出 MobileViT 的常用代码如下:
import torch from mobilevit_net import MobileViTS model = MobileViTS(num_classes=10) ckpt = torch.load("mobilevit_s.pt", map_location="cpu") 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_version=17, input_names=["images"], output_names=["logits"], dynamic_axes={"images": {0: "batch"}}, do_constant_folding=True, )opset_version 我用 17。MobileViT 里有 LayerNorm 和 SiLU/GELU,opset 版本低于 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 \ --onnx=mobilevit_s.onnx \ --saveEngine=mobilevit_s_fp16.engine \ --fp16 \ --minShapes=images:1x3x256x256 \ --optShapes=images:1x3x256x256 \ --maxShapes=images:4x3x256x256 \ --workspace=1073741824参数含义分别是:--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, interpolation=cv2.INTER_LINEAR) img = img.astype(np.float32) / 255.0 img = (img - np.array(mean, dtype=np.float32)) / np.array(std, dtype=np.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_size=1): 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, dtype=np.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, stream=stream) context.execute_async_v2(bindings=[int(d_input), int(d_output)], stream_handle=stream.handle) cuda.memcpy_dtoh_async(output_np, d_output, stream=stream) 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.LayerNorm,ONNX 里通常会归一化为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 engine,FP16 模式推理延迟比 PyTorch 还慢,FP32 模式只是小幅领先。
原因:Pascal 架构没有 Tensor Core。TensorRT 的 FP16 kernel 在 Pascal 上只能走普通 CUDA core,autotuning 阶段会花大量时间搜索 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 超上限:引擎不会自动扩容
现象:用 batch=4 以内的 engine,生产高峰时不小心传了一个 batch=8 的请求,报 “out of range” 或 “allocation failed”。
原因:TensorRT 的 optimization profile 在运行时是硬约束。maxShape 规定了该 profile 能处理的最大 shape,超过即失败。很多人会想当然地认为动态意味着无上限,这是误解。
解决:要么把 maxShape 扩到 8 或 16,注意显存占用也线性上涨;要么在推理服务里做 batch 切分。我的建议是固定 maxShape=8,按 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 应接近 1,FP16 在 0.999 以上,INT8 在 0.99 附近。低于这个区间,优先查预处理和校准集,其次是逐层精度。这个脚本建议固化到测试集里,每次改动 ONNX 或构建参数后都跑一遍。
6.2 延迟测不准等于白测:warmup 与 P95 的正确测量
单跑一次推理就记录延迟,在服务端是错的。第一次推理要经历 cuDNN 初始化、kernel 加载、显存分配,时间会虚高。正确测法是先做 warmup,再统计多轮的 p50/p95。
def benchmark(predict_fn, sample, warmup=20, iterations=100): 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(f"p50: {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 本身更值钱,换卡、换驱动时,能重建就是安全感。希望这篇能帮到你。
本文还有配套的精品资源,点击获取