ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习推理引擎详解:TensorRT与ONNX Runtime的部署优化实战

深度学习推理引擎详解:TensorRT与ONNX Runtime的部署优化实战 1. 为什么我同时用 TensorRT 和 ONNX Runtime先聊一个很多刚接触推理部署的朋友经常问我的问题模型训练出来之后到底用哪个推理引擎跑我入行这几年前后经手过的部署项目少说也有几十个从服务端的 GPU 高并发推理到端侧的边缘盒子、嵌入式设备甚至单片机级别的 AI 推理真正长期留在生产环境里的翻来覆去就两个TensorRT 和 ONNX Runtime。如果你和我一样平时用 PyTorch 训练模型、用 ONNX 做中间格式、最后落到不同硬件平台上部署那么这两个框架大概率是你绕不开的。先说结论TensorRT 是英伟达 GPU 上的性能天花板ONNX Runtime 是跨平台、跨硬件的万能胶水。它们俩不是二选一的关系而是不同阶段、不同场景下的组合拳。ONNX Runtime 干的是通用的活。它由微软开源支持 CPU、GPU、NPU、甚至浏览器里跑WebAssembly模型格式统一走 ONNX。你在 PyTorch 里torch.onnx.export导出一个模型ONNX Runtime 大概率能直接加载跑起来不需要针对特定硬件做专门优化。它的优势是省心、兼容性极好适合快速验证、中小并发场景、或者需要跨平台分发的产品。TensorRT 干的是极致的活。它只能在英伟达 GPU 上跑但它会把你的模型做深度优化——层融合、精度校准INT8/FP16、显存复用、内核自动调优。同样一个 YOLO 模型在 5070 显卡上ONNX Runtime 可能跑 3msTensorRT 能压到 1.5ms 甚至更低。在高并发、低延迟、高吞吐的线上推理场景里这个差距就是实打实的成本差距。所以我会建议你按这个思路选型场景推荐方案原因快速原型验证ONNX Runtime不用额外配置装个 pip 包就能跑CPU 推理 / 边缘设备ONNX Runtime / NCNN跨平台友好ARM、x86 都能用英伟达 GPU 高并发服务TensorRT吞吐和延迟双优线上性价比最高混合部署GPU CPUONNX Runtime TensorRT EP同一套代码按设备切换执行后端落地上车 / 单片机专用推理引擎如 rp2350 用 TFLite Micro / 自定义算子资源极受限需要最轻量的方案这篇文章我会从为什么需要推理引擎讲起拆解 TensorRT 和 ONNX Runtime 的内部工作逻辑再用实际项目含 YOLO 转 TensorRT、PP-OCR 走 ONNX、人物抠图 rmbg-2.0 的 Java 调用带你走一遍完整的推理部署流程最后把我在生产环境中踩过的坑整理成排查手册。2. 推理引擎到底在优化什么2.1 训练框架为什么不适合直接部署很多人问过一个很天真的问题我 PyTorch 模型训练得好好的为什么不能直接把.pt文件丢到生产环境去推理答案是——能但很亏。PyTorch 是一个动态图框架它的计算图是在每次前向传播时动态构建的。这意味着每次推理Python 解释器都要参与执行、动态地搭建算子调用链、检查张量形状、处理自动微分逻辑。这些机制在训练阶段非常灵活但在推理阶段全部是额外开销。举个例子你训练时每个 batch 的输入尺寸可能不同动态 shapePyTorch 会为每一次输入都重新编排一次执行计划。到了部署阶段大部分场景的输入尺寸是固定的比如 Resize 到 640x640你还让框架每帧都重新做一遍动态编排这就像你每天上班走同一条路但导航每次都要重新规划一遍路线——纯属浪费。推理引擎做的事情本质上就是把训练时灵活的动态图固化成部署时高效的静态图并在固化过程中做大量等价变换让计算更快、显存占用更少。2.2 ONNX Runtime 的执行机制ONNX Runtime 的工作方式可以用一句话概括加载 ONNX 模型文件将其转换为内部的计算图表示然后由 Execution Provider简称 EP在具体硬件上执行。模型的 ONNX 文件本身是协议定义它就是一张计算图节点是算子Conv、Relu、MatMul 等边是张量Tensor。ONNX Runtime 拿到这张图之后会做这几件事第一图优化。ONNX Runtime 的GraphOptimizationLevel分三档ORT_DISABLE_ALL不做优化、ORT_ENABLE_BASIC基础优化比如去掉无用节点、ORT_ENABLE_EXTENDED更多常量折叠、算子融合。实测下来光是把图优化等级开到 EXTENDEDYOLOv8 的推理速度就能提升 10%~20%这些优化完全透明你不需要改任何模型代码。第二执行后端分发。ONNX Runtime 默认在 CPU 上执行但当你安装了对应的 EP 之后它会自动把合适的算子分发到 GPUCUDA EP、TensorRTTensorRT EP、OpenVINOOpenVINO EP上执行。例如pip install onnxruntime-gpu后你在代码里设置providers[TensorrtExecutionProvider, CUDAExecutionProvider]ONNX Runtime 会优先尝试把算子交给 TensorRT 执行剩下的落到 CUDA再剩下的回落 CPU。这就是我们常说的异构执行。第三内存规划。推理引擎会提前分析整个计算图的张量生命周期哪些中间张量可以复用同一块显存它会提前算好。ONNX Runtime 在这个方面做得很成熟开箱即用你几乎不需要手动管理显存。2.3 TensorRT 的深度编译原理TensorRT 和 ONNX Runtime 最大的不同在于它不仅仅是执行引擎更是一个编译器。当你把一个 ONNX 模型丢给 TensorRT 时它的工作流程是这样的解析模型将 ONNX 的计算图解析为 TensorRT 内部的 Network 表示。构建引擎Engine基于当前 GPU 型号、显存、算子支持情况生成了一份针对这台 GPU 专属的优化执行计划。序列化保存把这个优化后的执行计划保存为.engine文件部署时直接加载跳过编译环节。这个构建引擎的阶段就是 TensorRT 拉开差距的核心。它会做算子融合、精度选择、内核调优、显存复用、甚至针对你的具体输入 shape 做专门的 kernel 选择。举个例子Conv BN ReLU 这三个算子在常规框架里是三个独立 kernel中间张量需要写显存、读显存各一次。TensorRT 会把它们融合成一个 kernel中间张量直接留在寄存器或共享内存里不再经过显存读写。这个融合对于计算密集型的 CNN 模型能带来 30%~50% 的加速绝对不是玄学。TensorRT 还提供动态 shape支持。通过Dynamic Shape机制你可以给输入指定一个范围比如min_shape(1,3,640,640)、opt_shape(8,3,640,640)、max_shape(32,3,640,640)引擎会在构建时针对常见尺寸做特化同时保证非标准尺寸也能推理。这个特性在做检测、分割任务时非常实用因为不同来源的图片分辨率五花八门。2.4 精度到底会不会丢这是所有用 TensorRT 的人最关心的问题FP16 和 INT8 到底会不会让模型变笨。先说 FP16半精度。FP16 把 FP32 的 32 位浮点数压到 16 位1 位符号 5 位指数 10 位尾数。大部分神经网络的权重和激活值FP16 的表示范围约 6e-5 到 6e4是够用的。我在实测中YOLOv8 FP16 和 FP32 的 mAP 差距通常在 0.1% 以内视觉任务几乎可以忽略。但有个别对数值敏感的任务比如超高精度 OCR 或者稠密回归任务会掉点 1% 左右这时候你需要用 TensorRT 的 Layer-wise 精度控制对敏感层保留 FP32。再说 INT8这个需要格外小心。INT8 量化不是简单地把权重截断成 -128~127而是需要校准的过程。TensorRT 会把一个校准数据集通常取几百张训练集或验证集图片送入模型统计每一层激活值的分布然后计算出一个最优的缩放因子让 FP32 的值域映射到 INT8 的整数域。这个校准如果做不好——比如图片数量太少、分布不具代表性——模型的精度可能明显下降。我自己用 INT8 量化 YOLO 系列的经验是跑偏光场景、白天场景效果很好一遇到夜晚、强逆光、雨雾天气检测率明显下降。所以如果目标场景光线复杂或者你对精度要求高建议要求稳FP16 就够了只有当延迟确实压不下来、或者显存紧张到必须省显存再考虑 INT8。3. YOLO 模型从 PyTorch 到 TensorRT 完整落地3.1 环境准备与安装避坑聊完理论直接上一个完整的实操案例。我用目前最流行的目标检测模型 YOLO 系列以 YOLOv12 为例来演示整个链路是PyTorch 训练 → 导出 ONNX → 转 TensorRT Engine → C 或 Python 加载推理。先说环境。我的这台测试机显卡是 RTX 5070这个卡值得单独说两句——它是 Blackwell 架构对应的 CUDA 版本要求比上一代 Ada 架构更高。如果你照着网上的老教程装 TensorRT 8.x大概率会在加载 engine 的时候报错因为TensorRT 版本必须与 CUDA 版本严格匹配。我这边能跑通的版本组合是组件推荐版本说明CUDA12.6Blackwell 架构必须要 CUDA 12.6 起步cuDNN9.x配套 CUDA 12.xTensorRT10.3推荐 10.x 版本算子支持和图优化明显改进PyTorch2.4导出 ONNX 用新版本对动态 shape 支持更好onnx1.16配合 PyTorch 2.4 使用安装 TensorRT 我给两个方案二选一方案 A推荐新手pip 安装pip install tensorrt10.3.0装完之后运行python -c import tensorrt as trt; print(trt.__version__)能输出版本号就说明基础库没问题。注意pip 安装的 TensorRT 默认带 Python 绑定但不包含trtexec命令行工具要这个工具的还得用 deb 包或 tar 包。方案 B完整工具链tar 包安装从英伟达官网下载 TensorRT 的 tar 包解压后配置环境变量export LD_LIBRARY_PATH/path/to/TensorRT/lib:$LD_LIBRARY_PATH export PATH/path/to/TensorRT/bin:$PATH这个方案的好处是自带trtexec这是一个非常好用的命令行工具可以快速跑一个 ONNX 模型、测试引擎性能、甚至生成 engine 文件适合调试。3.2 导出 ONNX 时最容易踩的坑环境准备好之后第一步是把训练好的 PyTorch 模型导出为 ONNX。这里有一个大多数教程不会提醒你的细节导出时一定要把模型切到 eval 模式并且固定 batch 维度或明确指定动态轴。下面是我在 YOLOv12 上验证过的导出脚本import torch model torch.load(yolov12.pt, map_locationcpu) # 实际加载方式按你的训练代码调整 model.eval() dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, yolov12.onnx, opset_version17, input_names[images], output_names[outputs], dynamic_axes{ images: {0: batch_size}, outputs: {0: batch_size}, }, )几个值得注意的细节第一个是 opset_version。我建议至少 17TensorRT 10.x 对 opset 17 的支持度最好太老的操作集会触发 TensorRT 的不支持算子导致转换失败。如果你的 PyTorch 版本比较旧导出时也可以选 opset 14但别低于 13。第二个是 dynamic_axes 的设置。对目标检测来说通常只有 batch 维度需要动态化高和宽用固定 640 更有利于 TensorRT 的极致优化。如果你希望输入分辨率也动态TensorRT 也是支持的但要在后续构建引擎时提供OptProfile否则会按默认 shape 构建跑非标准尺寸会报错。第三个是切片操作。YOLO 系列的 Backbone 经常用slice操作比如对特征图做x[..., ::2, ::2]这类操作导出的 ONNX 节点在不同版本 PyTorch 下表现不一样。如果你的 ONNX 转 TensorRT 时遇到Slice算子不支持优先升级 PyTorch、onnx、TensorRT 这几个组件的版本多半能解决。导出完成后建议先用onnx.checker.check_model验证一下 ONNX 文件的完整性import onnx model onnx.load(yolov12.onnx) onnx.checker.check_model(model) print(onnx.helper.printable_graph(model))这一步能拦截掉 80% 的模型有问题但推理时才发现的隐患。3.3 trtexec 一行命令生成 EngineONNX 文件没问题之后最快的方式是直接用 trtexec 转换trtexec --onnxyolov12.onnx \ --saveEngineyolov12.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:8x3x640x640 \ --maxShapesimages:32x3x640x640命令行参数说明--fp16开启 FP16 推理。这个开关通常能带来接近 40% 的加速而精度损耗很小。--minShapes/--optShapes/--maxShapes定义动态 shape 的范围。optShape是你预期的常见 batch 大小TensorRT 会为这个尺寸做最高规格的内核调优。如果你是固定 shape这三个参数可以不写。--saveEngine将编译产物保存为 engine 文件后续直接加载用。trtexec 跑完之后终端会输出一堆日志重点看最后几行的性能报告[I] Performance summary [I] Throughput: 2148.7 qps [I] Latency: min 0.42 ms, max 1.28 ms, mean 0.58 ms这个数字就是纯 GPU 推理时延不含预处理和后处理。如果你的项目对延迟敏感比如实时视频流建议用--avgRuns100多跑几轮取平均值避免单次波动造成误判。3.4 C 加载 Engine 的完整代码工程上我更喜欢用 C 做推理服务因为显存管理更精细、更容易做资源池、也没有 Python 的 GIL 锁。TensorRT 的 C API 虽然写着复杂但核心代码其实就那么几段。#include fstream #include iostream #include vector #include cuda_runtime_api.h #include NvInfer.h using namespace nvinfer1; // 读取引擎文件 std::vectorchar loadEngineFile(const std::string path) { std::ifstream file(path, std::ios::binary | std::ios::ate); std::streamsize size file.tellg(); file.seekg(0, std::ios::beg); std::vectorchar buffer(size); file.read(buffer.data(), size); return buffer; } int main() { // 初始化 auto runtime std::unique_ptrIRuntime(createInferRuntime(logger)); auto engineData loadEngineFile(yolov12.engine); auto engine std::unique_ptrIExecutionContext( runtime-deserializeCudaEngine(engineData.data(), engineData.size())); // 绑定输入输出 auto context engine-createExecutionContext(); const char* inputName images; const char* outputName outputs; // 分配显存 void* buffers[2]; cudaMalloc(buffers[0], batchSize * 3 * 640 * 640 * sizeof(float)); cudaMalloc(buffers[1], maxOutputSize * sizeof(float)); // 设置动态 shape可选 context-setInputShape(inputName, Dims4{1, 3, 640, 640}); // 输入数据拷贝到显存 cudaMemcpy(buffers[0], inputData, batchSize * 3 * 640 * 640 * sizeof(float), cudaMemcpyHostToDevice); // 执行推理 bool success context-enqueueV2(buffers, stream, nullptr); // 结果拷回内存 cudaMemcpy(outputData, buffers[1], maxOutputSize * sizeof(float), cudaMemcpyDeviceToHost); cudaStreamSynchronize(stream); // 后处理解码 YOLO 输出的 bbox、score、class return 0; }有两个隐藏的大坑我必须提醒你坑一engine 文件是硬件绑定的。TensorRT 生成的 engine 文件针对的是你当前的 GPU 型号、CUDA 版本、TensorRT 版本换了任何一项加载都会报错。所以如果你的生产环境有多台不同型号的 GPU要分别生成 engine 文件不能通用。你可以在代码里通过getNbDevices/getDeviceName给不同型号的 GPU 分发不同的 engine 文件或者干脆在服务器首次启动时动态生成 engine 并缓存。坑二createExecutionContext 的线程安全问题。在 C 多线程推理场景下IExecutionContext不是线程安全的每个线程需要独立创建自己的 context。但IRuntime和ICudaEngine是线程安全的可以多个线程共享同一个引擎对象各自createExecutionContext。我在早期实现里图省事所有线程共用一个 context结果并发一上来就 crash排查了半天才发现是这个问题。后来改为每线程独立 context问题彻底消失。如果你的服务是单线程推理可能撞不上这个坑但一旦计划上高并发提前把 context 池做好免得后患。3.5 Python 快速验证流程如果你只是想在本地验证一下 TensorRT 的加速效果不想写 CPython 也有非常简洁的流程。TensorRT 10.x 之后新增了tensorrtPython 包的一等公民 API把很多繁琐的步骤封装掉了import tensorrt as trt import numpy as np import pycuda.driver as cuda import pycuda.autoinit logger trt.Logger(trt.Logger.WARNING) runtime trt.Runtime(logger) # 加载 engine with open(yolov12.engine, rb) as f: engine_data f.read() engine runtime.deserialize_cuda_engine(engine_data) context engine.create_execution_context() # 绑定内存 input_shape (1, 3, 640, 640) output_shape (1, 84, 8400) # YOLOv12 输出格式 d_input cuda.mem_alloc(np.prod(input_shape) * np.float32().itemsize) d_output cuda.mem_alloc(np.prod(output_shape) * np.float32().itemsize) # 预处理 import cv2 img cv2.imread(dog.jpg) img cv2.resize(img, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR - RGB img np.ascontiguousarray(img, dtypenp.float32) / 255.0 img np.expand_dims(img, axis0) # 推理 stream cuda.Stream() cuda.memcpy_htod_async(d_input, img, stream) context.execute_async_v2(bindings[int(d_input), int(d_output)], stream_handlestream.handle) cuda.memcpy_dtoh_async(output_data, d_output, stream) stream.synchronize() # 后处理输出 # 解析 output_data进行 NMS 等操作注意 Python 版本里的pycuda依赖pip 安装后如果 import 报错一般是 CUDA 工具链没配好。建议先单独装好 CUDA Toolkit 再装 pycuda省得折腾。4. ONNX Runtime 部署实战从 Java 调用到 OCR 抠图4.1 CPU 场景别忽视基础优化ONNX Runtime 最常被用的场景其实是 CPU 推理。很多公司的线上服务跑在纯 CPU 环境没有 GPU这时候优化手段就用上了。首先多线程参数必须调节。默认情况下SetIntraOpNumThreads和SetInterOpNumThreads可能不会自动适配你的 CPU 核数。我习惯把intra_op设为 CPU 物理核数的一半到全部inter_op设为 1~2。这两个参数一个控制单个节点内部的并行度一个控制图中不同节点之间的流水并行度设错了会导致 CPU 上下文切换开销远大于计算收益。import onnxruntime as ort sess_options ort.SessionOptions() sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_EXTENDED sess_options.intra_op_num_threads 8 # 根据你的 CPU 核数调整 sess_options.inter_op_num_threads 1 sess_options.enable_cpu_mem_arena True session ort.InferenceSession( ppocrv6.onnx, sess_optionssess_options, providers[CPUExecutionProvider], )还有一个容易被忽视的选项是enable_cpu_mem_arena。这个开关默认是 True它的作用是让 ONNX Runtime 在 CPU 上做类似显存复用的内存分配优化减少 malloc/free 的次数。在延迟敏感场景打开之后效果立竿见影。4.2 Java 调用 ONNX Runtime 做人物抠图现在很多业务是 Java 写的但 AI 推理模型大多是 Python 生态。怎么在 Java 服务里优雅地调起 ONNX 推理ONNX Runtime 官方提供了 Java API可以直接用。一个我最近做得较多的场景是使用 rmbg-2.0一个轻量级人物抠图模型在 Java 服务中做人像分割。整个流程的关键在于把一个视觉模型的图像预处理、推理、后处理完整地翻译成 Java 代码。ONNX 模型的好处就在这里模型的输入输出格式是标准化的只要你读懂模型的输入输出要求用什么语言调它都行跟语言无关。import ai.onnxruntime.*; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.nio.FloatBuffer; public class RmbgOnnx { public static void main(String[] args) throws Exception { OrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession session env.createSession(rmbg-2.0.onnx, new OrtSession.SessionOptions()); // 读取图片并拿到输入 shape TensorInfo inputInfo (TensorInfo) session.getInputInfo().get(input); long[] inputShape inputInfo.getShape(); // [1, 3, 1024, 1024] int h (int) inputShape[2]; int w (int) inputShape[3]; BufferedImage img ImageIO.read(new File(portrait.jpg)); img resize(img, w, h); float[] pixels toRgbNormalized(img); // RGB 转 float归一化到 [0,1] // 包装成 OnnxTensor float[][][][] inputArray new float[1][3][h][w]; for (int c 0; c 3; c) { for (int y 0; y h; y) { for (int x 0; x w; x) { inputArray[0][c][y][x] pixels[(y * w x) * 3 c]; } } } OnnxTensor tensor OnnxTensor.createTensor(env, inputArray); // 推理 OrtSession.Result results session.run(java.util.Map.of(input, tensor)); float[][][] output (float[][][]) results.get(0).getValue(); // output 是 [1, 1, H, W] 的 maskfloat 值表示前景概率 // 保存 mask输出透明 PNG saveMask(output[0][0], mask.png); } }Java API 也有几个实用细节第一个用OrtSession.Result获取输出的类型转换。ONNX 模型输出可能是多维数组Java 侧会根据动态维度在运行时展开成float[][][]或float[][][][]。你最好先打印一下results.get(0).getInfo()确认实际 shape再写解析代码别想当然。第二个OrtSession.SessionOptions可以设置优化级别和执行后端。如果你的 Java 服务同时接 GPU可以设置addCUDA()需引入对应的 onnxruntime-gpu 包。不过 Java 生态里 ONNX Runtime GPU EP 对 TensorRT 的支持不如 Python 端完善性能也不如直接上 TensorRT 的 C API所以我通常只在开发环境这么用。第三个服务部署时注意内存占用。Java 堆外内存是 ONNX Runtime 主要吃掉的资源如果模型较大rmbg-2.0 约 160MB多个线程同时创建 session 会快速吃满内存。建议在 Spring 这类框架里把OrtSession做成单例多个线程共用同一个 session 实例去runONNX Runtime 内部对并发执行有良好支持这样既省内存又避免频繁创建 session 的开销。4.3 PP-OCRv6 的 ONNX 推理细节除了抠图OCR 是另一个非常高频的落地场景。PP-OCR 系列我前后跑过 v3、v4、v5最近也在关注 v6它的 ONNX 推理有几个独特的坑检测模型和识别模型要分开跑。PP-OCR 的 pipeline 是检测模型定位文本区域文本框坐标→ 透视变换裁剪出每个文本行 → 识别模型对裁剪后的文本行做文字识别 → 方向分类器判断文字方向如果需要。这三个模型独立推理中间结果互相需要坐标变换和图像裁剪操作。把这些环节串起来的时候最常出问题的就是把检测框的坐标映射回原图时忘记考虑原图缩放比例。# 检测结果 boxes 的坐标是相对于输入尺寸的 # 假设模型输入尺寸 960x960原图是 1920x1080 scale_x original_w / 960 scale_y original_h / 960 # 正确的做法是每个坐标分量分别乘对应方向的缩放很多人在这个环节图省事统一用一个scale导致长宽比不同时文本框位置全偏了。记住横纵两个方向的系数必须分开算。第二个坑是识别模型的输入高度通常不是固定的。PP-OCR 识别模型输入是(3, 48, w)宽w是动态的根据检测框裁剪出来的文本行实际宽度而定。所以转换为 ONNX 时识别模型的宽度维度必须是动态的。而批大小建议固定为 1因为每条文本行的宽度不同无法组成 batch。强行走 batch 推理要么 padding 导致精度下降要么需要写自定义的 batch 处理逻辑。4.4 高并发推理服务怎么设计如果要把 ONNX 推理接入到线上 API 服务单纯会调一个session.run远远不够。生产环境要考虑吞吐、排队、超时、资源隔离。我常用的一套方案是这样的用消息队列解耦图片上传和推理异步处理前端拿到任务已接收的响应推理结果通过回调和轮询获取。这样能挡住突发流量避免推理服务被打垮。搞一个线程池专门做推理不要和业务线程混用。给推理线程池设置独立的队列长度上限超过就拒绝新任务而不是无脑堆积导致内存溢出。对输入的图片大小做好限制和预处理ONNX Runtime 并没有内置图像解码逻辑你传给它的必须已经是张量或字节流。图片解码JPEG/PNG 解码非常吃 CPU建议在入口处限流或者用缩略图来降低解码开销。如果你用 Java 写服务这个模式很清晰ExecutorService inferencePool Executors.newFixedThreadPool(4); // 推理线程池 Semaphore semaphore new Semaphore(10); // 限流最多同时 10 个推理请求 CompletableFuture.supplyAsync(() - { semaphore.acquire(); try { // 调用 ONNX Runtime 推理 return doInference(imageBytes); } finally { semaphore.release(); } }, inferencePool);这样做的好处是推理服务不会因为请求过多而崩溃超时任务可以被优雅丢弃线程池不会泄漏。4.5 ONNX Runtime 跟 NCNN 怎么选热词里有人提到 ONNX Runtime / NCNN 的对比。NCNN 是腾讯开源的针对移动端和嵌入式优化的前向推理框架。如果你只做 CPU 移动端部署NCNN 通常比 ONNX Runtime 轻量低端 ARM 处理器的优化也更激进。如果你的部署平台是 iOS 和 AndroidNCNN 有天然的预处理和后处理的 API集成起来更直观。但 NCNN 也有明显短板算子支持范围比 ONNX 生态窄模型里遇到不支持的算子要自己开发或改网络结构。相比之下ONNX Runtime 的算子覆盖面大得多而且有各种 EP 可以插拔。我的选择逻辑很简单能用 ONNX Runtime 搞定就不额外引入 NCNN除非目标设备性能孱弱到必须用 NCNN 才能达到实时要求。在 rp2350 这种单片机级别的 AI 推理场景NCNN 也不是最优选这类平台资源极其有限更适合 TFLite Micro 或者自己裁剪推理内核。5. 模型服务化从单独推理到 sglang 这类推理框架5.1 为什么大型模型会用 sglang serve热词里出现了 sglang serve 启动推理服务这里也顺便说说。sglang 是面向大语言模型LLM以及多模态模型的高性能推理服务框架跟 TensorRT、ONNX Runtime 不是同一层级的东西。TensorRT 和 ONNX Runtime 主要解决单个模型的算子级优化而 sglang 解决的是大规模 LLM 服务如何高并发调度的问题。如果你要部署的是亿级参数的大模型或者要在多 GPU 上跑一个很大的模型那简单的 TensorRT 推理已经不够用了。你需要的是连续批处理Continuous Batching、KV Cache 管理、Prefix Caching、张量并行等一系列高级调度策略。这些正是 sglang、vLLM 这类推理服务框架的看家本领。不过并不是所有模型都需要上 sglang。如果你的任务是 YOLO 目标检测、OCR、抠图这类视觉模型sglang 帮不上太大忙反过来如果你要部署 GPT 类模型TensorRT-LLM 或者 sglang 这类服务框架是必须的因为它们的连续批处理机制对 LLM 的吞吐提升是指数级的。5.2 我理解中的推理体系四种形态综合刚才说的这些我个人把推理部署划分为四种形态大家可以根据自己项目的复杂程度对号入座单模型工具形态模型训完导出 ONNX 或 Engine用一个脚本或简单 API 做推理。适合离线测试、小流量内部工具部署成本最低。服务化部署形态推理逻辑封装成 HTTP/gRPC 服务加上批处理、排队、线程池和优雅退出机制。适合线上 API、面向 C 端的功能模块。高性能框架形态引入 TensorRT-LLM、sglang、vLLM 等重型框架做多卡并行、KV Cache 管理、PagedAttention 等大规模优化。适合 LLM 服务、超大模型。端侧推理形态根据硬件选择 NCNN、TFLite Micro、自带 NPU 的 SDK 等把模型量化到 INT8/INT4 甚至二进制部署到嵌入式设备、手机、网关。适合边缘计算场景。四种形态之间没有绝对的好坏核心是匹配你的算力、流量、延迟和成本四项约束。6. 推理部署中常见问题与排查6.1 推理结果不对、输出全乱码怎么办这是我处理最多的一类问题。推理引擎没有报错跑出来的结果却完全不对通常不是引擎的问题而是输入数据预处理和后处理解析出了问题。优先排查这几个点图像通道顺序PyTorch 训练时用的是 RGBOpenCV 读取默认是 BGR。忘了转换模型输出满天飞。归一化参数训练时是除以 255 还是减均值再除方差推理时要保持严格一致。输入尺度训练时 Resize 到 640x640 还是保持原始比例做 letterbox如果做 letterbox后处理时要根据 pad 和缩放比把坐标映射回原图。输出解码YOLO 系列的输出是未解码的原始张量比如[1, 84, 8400]需要你做坐标解码和 NMS。如果你的后处理代码是从别人的项目里抄的确认输出格式跟你这份模型一致。不同版本的 YOLO 输出排列方式可能差很多。6.2 转换报错ONNX 转 TensorRT 遇到不支持的算子TensorRT 虽然算子覆盖面广但遇到某些 ONNX 算子不支持或者版本不匹配转换就会失败。我的排查顺序换高度版本 TensorRT。新版本几乎每个迭代都会增加算子支持。简化 ONNX 图。ONNX 模型里常常有一些训练阶段才需要的节点比如 BatchNorm 的 momentum 相关节点用onnx-simplifier清理一遍再转python -m onnxsim yolov12.onnx yolov12_sim.onnx把不支持算子的 Layer 用plugin实现。这个难度较高一般用不到。大多数情况下简化模型 升级版本就能解决。6.3 显存占用过高或者 OOMTensorRT 在构建和推理时的显存占用跟构建时的maxShapes直接挂钩。如果你给动态 shape 定义了很大的maxShapeTensorRT 会为这么大的输入预分配显存导致显存占用虚高。解决方法是把maxShape设置成你实际会用到的最大的输入不要随意放一个很大的数字。另外多个 model context 共享同一个 engine 时也需要留意显存增长。每个IExecutionContext都有自己的激活显存量context 创建多了累计起来也很可观。我的做法是在服务启动时统计显存余量根据余量动态控制 context 池大小。6.4 关于模型推理时的 conf 参数热词里有一个特别典型的入门问题模型训练出来之后那个推理用的 conf 参数是什么这个 confconfidence threshold置信度阈值不是模型训练出来的参数而是推理阶段的一个过滤阈值。模型输出的每个检测框都会带一个置信度分数比如 0.92、0.45、0.30这个分数表示模型认为该框内目标的可信程度。conf 设得越高保留的检测框越少漏检率升高但误检率降低conf 设得越低保留的框越多召回率升高但误检也增加。YOLO 系列默认推荐 conf0.25、NMS IoU0.45但这只是保守的起点。实际场景中目标很小、被遮挡严重把 conf 降到 0.1~0.15 才能找回漏检的目标。误检容忍度低、宁可漏检不可错检把 conf 提到 0.5~0.6。同一个画面里目标密集需要适当调高 NMS IoU比如 0.5~0.6来让重叠的检测框被更多抑制减少重复框。建议你在正式上线前用验证集跑一个conf vs 精确率/召回率的曲线找到最适合业务场景的平衡点。没有哪个 conf 是万能的它本质上是业务风险偏好的映射。7. 我的最后一个经验每次带新人做部署我都跟他们说一句话能跑和跑得快之间隔着一整个工程化的距离。ONNX Runtime 让你一周内上线一个推理功能TensorRT 让你一个月内把性能做到极致但真正决定一个推理系统稳不稳的往往是那些不起眼的细节——线程池多大、队列多长、超时怎么处理、显存怎么复用、模型文件怎么在 GPU 型号间做分发、量化后精度掉了怎么办。这些没有哪个框架能替你解决只能靠实打实用手摸、用数据说话。我做推理部署这几年最大的体会是所谓的全解析其实没有尽头技术栈会变硬件会迭代但分析和排查的思路是相通的——先弄清楚瓶颈在哪儿再选择合适的工具去解决它。最后再分享一个小技巧不管是 ONNX Runtime 还是 TensorRT每次版本升级前先把当前版本跑通的所有模型、参数、代码版本记录在一个文档里。我因为在升级 TensorRT 时没记版本组合踩过的坑至少多花了一星期。好记性不如烂笔头部署这行尤其如此。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进