
简介这是一套基于 TensorRT v8.0 的 YOLOv8 全功能部署方案覆盖目标检测、关键点检测、实例分割与 ByteTrack 目标跟踪适合需要将 YOLOv8 落地到 Jetson 嵌入式设备或 Linux x86_64 服务器的开发者。资源同时提供 Python 和 C 两套 API 实现均采用面向对象设计C 版本可编译为动态链接库供其他项目调用模型转换链路涵盖 .pth→.onnx→.plan(.engine)并基于 CUDA 手写前处理与后处理算子。包内共 295 个文件以 C/CUDA 源码h、cpp、cu、Python 脚本、模型文件onnx/plan、说明文档md/txt及演示图片视频jpg/jpeg/mp4/gif为主整体约 335.89MB便于对照源码学习部署细节。已有 93 人学习浏览适合正在做 TensorRT 加速或边缘端模型部署的开发者参考。1. 从 ONNX 到 engineTensorRT 部署 YOLOv8 的第一步不是写推理代码GPU 上跑 YOLOv8延迟瓶颈往往不在卷积本身而在 PyTorch 的算子调度、shape 推导和显存拷贝。TensorRT 把网络图做算子融合、精度校准和显存池化让推理真正贴着硬件走。标题里的检测、姿势估计姿态、分割、跟踪四个任务共享同一套骨干和特征金字塔所以部署时可以用一个 ONNX 导出流程、一个 engine 构建脚本和一套前后处理框架差别只在输出头的解析。下面按实际部署顺序展开先讲 ONNX 导出与 trtexec 构建 engine再用 Python API 跑通检测和跟踪接着用 C API 补上分割与姿态最后给耗时测量方法和三个高频踩坑点。适合正在把 YOLOv8 从 PyTorch 原型迁到生产环境的工程师也适合在 Jetson 上做低延迟推理的人。版本边界先说清TensorRT 8.x 的 API 在 10.x 已有变更代码按 v8.08.6 的接口写在enqueueV2处标注兼容性。8.x 里按 tensor 名取形状的调用在新版仍然可用迁移成本不高。2. 导出 ONNX 并用 trtexec 构建 engine四任务共用一条流水线2.1 先统一导出格式ultralytics 导出的 ONNX 里到底有什么用 ultralytics 官方命令把 .pt 权重转成 ONNX四类任务同一套写法只改 taskyolo export modelyolov8n.pt taskdetect formatonnx opset12 simplifyTrue dynamicFalse yolo export modelyolov8s-pose.pt taskpose formatonnx opset12 simplifyTrue yolo export modelyolov8s-seg.pt tasksegment formatonnx opset12 simplifyTrue参数说明opset固定 12TensorRT 8.x 的 ONNX parser 对 opset 12 支持最全升到 17 反而容易触发不支持的ReduceL2或ScatterND变体simplifyTrue会调用 onnx-simplifier 折叠掉大量 Reshape / Transpose显著降低后续解析失败的概率dynamicFalse时 batch 固定为 1服务端要多路并发再单独用dynamicTrue导出一次重建 engine不要指望一个 engine 两头吃。导出后我用 Netron 确认输出节点。检测模型只有一个输出张量[1, 84, 8400]84 4 个框坐标 80 个 COCO 类别姿态模型是[1, 56, 8400]56 4 1 个类别分数 17 个关键点 × 3x, y, 可见度分割模型是两个输出[1, 116, 8400]4 80 32 个 mask 系数加[1, 32, 160, 160]的 prototype mask。8400 是 80×80、40×40、20×20 三个尺度展平后的 anchor 总数后处理时所有输出都要先转置成[8400, C]否则索引全部错位。C2f 模块里的 split 和 Concat 在 ONNX 里被展开成几十个小算子这正是 TensorRT 算子融合收益最大的区域。构建日志里网络层数从几百压到几十主要就是 C2f 那一段的贡献。网络结构图熟悉的人看构建报告会非常直观融合后剩下的层基本是 Conv、ElementWise 和少量的 Resize。2.2 用 trtexec 一行命令构建 enginetrtexec 是 TensorRT 自带的可执行文件除 INT8 校准外的多数构建需求它都能覆盖命令如下trtexec --onnxyolov8n.onnx \ --saveEngineyolov8n.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:4x3x640x640参数说明--fp16开启半精度检测和姿态任务的延迟通常降到 FP32 的一半左右--minShapes、--optShapes、--maxShapes只在 ONNX 是动态 batch 时生效分别对应动态 shape 的下限、优化档和上限。如果部署场景固定是单路视频流把三项都写成1x3x640x640engine 按静态 shape 优化省掉运行时 shape 校验的开销。构建产物是序列化二进制日志里出现[W]警告一般不影响结果但出现[E]或Assertion failed时先回头确认 ONNX 的 simplify 是否真正执行再检查输入节点名是不是images。大量构建失败都源于输入名不匹配--verbose日志最后几行会直接指出 parser 卡在哪个节点。注意engine 文件绑定 GPU 架构和 TensorRT 版本换机器、换驱动型号都必须重新构建没有捷径。2.3 Python API 构建 engineFP16 与 INT8 校准器的接入不依赖 trtexec 时用 Python 构建方便把模型版本化和 CI 自动化串起来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(yolov8n.onnx, rb) as f: if not parser.parse(f.read()): for i in range(parser.num_errors): print(parser.get_error(i)) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) config.set_flag(trt.BuilderFlag.FP16) engine builder.build_serialized_network(network, config) with open(yolov8n.engine, wb) as f: f.write(engine)逻辑说明8.x 里build_serialized_network返回序列化字节直接落盘EXPLICIT_BATCH是动态 shape 的前提8.0 起已是默认要求。WORKSPACE池子给 1GB 对 YOLOv8 足够给太小会报Cannot find a valid plan。要切 INT8 时把FP16标志换成INT8并挂一个校准器class EntropyCalibrator(trt.IInt8EntropyCalibrator2): def __init__(self, images, batch_size8): super().__init__() self.batches load_calib_batches(images, batch_size) # list of float32 CHW self.idx 0 self.device_input cuda.mem_alloc(batch_size * 3 * 640 * 640 * 4) def get_batch_size(self): return 8 def get_batch(self, names): if self.idx len(self.batches): return None cuda.memcpy_htod(self.device_input, self.batches[self.idx]) self.idx 1 return [int(self.device_input)] config.set_flag(trt.BuilderFlag.INT8) config.int8_calibrator EntropyCalibrator(calib_images/)get_batch返回的是 device 指针列表TensorRT 在校准阶段会直接在这些内存上跑前向收集激活分布。校准集建议 500 张左右、内容与部署场景接近纯色背景占多会让熵校准失真这个坑在分类任务上不明显在分割上会被 mask 输出放大。2.4 四类任务的输出绑定对照engine 构建完遍历绑定拿名字、形状和精度是排查输出异常的第一步。四类模型最多两三个绑定任务输出绑定数输出形状batch1解析要点检测 detect1[1, 84, 8400]4 坐标 80 类分数姿态 pose1[1, 56, 8400]4 坐标 1 类分数 51 个关键点值分割 segment2[1, 116, 8400]/[1, 32, 160, 160]前者后 32 通道是 mask 系数跟踪复用检测输出同检测跟踪跑在检测结果之上不进 engine表里最容易出错的是分割mask 系数和 prototype mask 是两个分离张量最终 mask 要靠推理后的矩阵乘法加 sigmoid 得到网络本身不输出二值图这也是分割延迟高于检测的根源。姿态的 56 通道里第 4 通道是单类别分数因为 YOLOv8-pose 只检测 person没有 80 类那套。3. Python API 部署检测推理类与 ByteTrack 跟踪的衔接3.1 engine 反序列化与显存绑定Python 侧用 pycuda 管理显存和IRuntime配合最顺。推理类骨架如下import tensorrt as trt import numpy as np import pycuda.driver as cuda class YoloTRT: def __init__(self, engine_path): logger trt.Logger(trt.Logger.WARNING) with open(engine_path, rb) as f: engine_data f.read() runtime trt.Runtime(logger) self.engine runtime.deserialize_cuda_engine(engine_data) self.context self.engine.create_execution_context() self.stream cuda.Stream() self._alloc_buffers() def _alloc_buffers(self): self.hosts, self.devices {}, {} for name in self.engine: shape self.engine.get_tensor_shape(name) size trt.volume(shape) dtype trt.nptype(self.engine.get_tensor_dtype(name)) host cuda.pagelocked_empty(size, dtype) device cuda.mem_alloc(host.nbytes) self.hosts[name], self.devices[name] host, device def __call__(self, blob): cuda.memcpy_htod_async(self.devices[images], blob, self.stream) bindings [int(self.devices[n]) for n in self.engine] self.context.execute_async_v2(bindings, self.stream.handle) outs [n for n in self.engine if not self.engine.binding_is_input(n)] for n in outs: cuda.memcpy_dtoh_async(self.hosts[n], self.devices[n], self.stream) self.stream.synchronize() return [self.hosts[n].copy() for n in outs]逻辑说明pagelocked_empty分配页锁定内存memcpy_htod_async配合 stream 让拷贝和 kernel 执行重叠bindings 列表顺序必须和 engine 的 binding 顺序一致。execute_async_v2是 8.x 的异步入口10.x 改用execute_async_v3后需要传TensorAddressBuffer对象参数形态变化较大。返回的copy()是 host 内存副本多路视频场景建议去掉copy()直接复用页锁定内存省一次拷贝。get_tensor_shape对动态 shape 的 binding 返回 -1那种情况要先按实际输入调用context.set_input_shape再取形状我这套代码默认静态 shape。3.2 letterbox 预处理要把缩放参数带出去YOLOv8 训练时做了 letterbox推理侧不 pad 会导致 mAP 明显下降。我一般这样写def letterbox(img, size640): h, w img.shape[:2] r min(size / h, size / w) new_h, new_w round(h * r), round(w * r) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) canvas np.full((size, size, 3), 114, dtypenp.uint8) top, left (size - new_h) // 2, (size - new_w) // 2 canvas[top:top new_h, left:left new_w] resized blob canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 return np.ascontiguousarray(blob), (r, top, left)参数说明114是 COCO 训练时的 padding 值BGR 转 RGB 用[:, :, ::-1]HWC 转 CHW 用transpose(2, 0, 1)。返回的(r, top, left)必须保留后处理还原坐标全靠它原图坐标 letterbox 坐标 - left/ ry 方向同理。检测框整体偏移的项目十有八九是这里只做了 resize 没做 letterbox或者还原时忘了减 padding。3.3 检测输出解码与 NMS 参数选择def decode_det(output, conf0.25, iou0.45): preds output.reshape(84, 8400).T # [8400, 84] scores preds[:, 4:].max(axis1) keep scores conf if keep.sum() 0: return np.empty((0, 6)) preds, scores preds[keep], scores[keep] cx, cy, w, h preds[:, 0], preds[:, 1], preds[:, 2], preds[:, 3] boxes np.stack([cx - w / 2, cy - h / 2, cx w / 2, cy h / 2], axis1) idx cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), conf, iou) return np.hstack([boxes[idx], scores[idx][:, None]])逻辑说明YOLOv8 输出的是中心点加宽高坐标在 640×640 输入空间里先转 xyxy 再做 NMS。conf取 0.25、iou取 0.45 是 COCO 评测默认值姿态任务画骨架时 conf 可以放到 0.1靠关键点可见度二次过滤分割任务建议 conf 取 0.3 以上因为 mask 矩阵乘法按框数算框越多后处理越慢。NMS 用cv2.dnn.NMSBoxes就够单帧八千个候选在 CPU 上跑也就 12ms不必为此引入 TensorRT 的 NMS 插件。任务建议 conf说明检测0.4过滤抖动框工业场景可以再高姿态0.1关键点靠 vis 分数二次过滤分割0.3控制进入 mask 计算的框数量提示conf 调到 0.1 以下时候选暴涨cv2.dnn.NMSBoxes是 CPU 实现单帧超过 5 万候选耗时翻倍必要时限制每帧最多输出 300 个框。3.4 用 ByteTrack 衔接跟踪跟踪不进 engine它消费检测输出换跟踪算法不用重建模型。我常用 boxmot 库的 BYTETrackerfrom boxmot import BYTETracker tracker BYTETracker() for frame in video_stream: blob, (r, top, left) letterbox(frame) outputs trt_model(blob) dets decode_det(outputs[0]) dets[:, :4] - np.array([left, top, left, top]) # 还原到原图 dets[:, :4] / r tracks tracker.update(dets, frame) for x1, y1, x2, y2, track_id, conf, cls in tracks: draw_track(frame, (int(x1), int(y1), int(x2), int(y2)), int(track_id))参数说明BYTETracker 内部用高、低两个置信度阈值做两次关联高阈值框先建轨、低阈值框补关联遮挡时的 ID 跳变比纯 IoU 匹配稳。update入参是[N, 6]的数组xyxy conf cls坐标必须是原图像素所以要先做坐标还原。检测和跟踪要在同一个 frame 循环里逐帧推进不要在检测外层再缓存 frame否则 ID 会错位。boxmot 不同版本的update签名略有差别装完先打印一次 help 确认入参。4. C API 部署分割 mask 与姿态关键点还原的完整链路4.1 C 里构建与反序列化 engine 的骨架C 侧分两步离线构建用nvonnxparser运行时常加载序列化 engine。构建路径#include NvInfer.h #include NvOnnxParser.h class Logger : public nvinfer1::ILogger { void log(Severity severity, const char* msg) noexcept override { if (severity Severity::kWARNING) std::cerr msg std::endl; } }; Logger gLogger; auto* builder nvinfer1::createInferBuilder(gLogger); const uint32_t flag 1U static_castuint32_t( nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); auto* network builder-createNetworkV2(flag); auto* parser nvinfer1::nvonnxparser::createParser(*network, gLogger); if (!parser-parseFromFile(yolov8n.onnx, 1)) { std::cerr onnx parse failed std::endl; } auto* config builder-createBuilderConfig(); config-setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1U 30); config-setFlag(nvinfer1::BuilderFlag::kFP16); auto* serialized builder-buildSerializedNetwork(*network, *config); // serialized-data() / serialized-size() 直接写文件参数说明parseFromFile的第二个参数是日志级别填 1 对应 WARNINGEXPLICIT_BATCH与 Python 侧一致是动态 shape 的前提。运行时不带构建器依赖反序列化更轻std::ifstream f(yolov8n.engine, std::ios::binary); std::vectorchar data((std::istreambuf_iteratorchar(f)), std::istreambuf_iteratorchar()); auto* runtime nvinfer1::createInferRuntime(gLogger); auto* engine runtime-deserializeCudaEngine(data.data(), data.size()); auto* context engine-createExecutionContext(); std::vectorvoid* buffers(engine-getNbBindings()); for (int i 0; i engine-getNbBindings(); i) { auto dims engine-getBindingDimensions(i); size_t vol 1; for (int j 0; j dims.nbDims; j) vol * dims.d[j]; cudaMalloc(buffers[i], vol * sizeof(float)); }getBindingDimensions对动态 shape 返回 -1需要再调context-getBindingShape(i)拿实际值。统一按float分配时要注意INT8 engine 的 binding 类型是kINT8buffer 要换成int8_t。Windows 上目标机还得装 Visual C Redistributable否则加载 TensorRT 的 dll 时直接报缺库这个和代码逻辑无关但部署时最先出问题的往往是它。4.2 推理主循环enqueueV2 与 stream 的节奏void infer(float* blob, cudaStream_t stream) { cudaMemcpyAsync(buffers[0], blob, inputSize * sizeof(float), cudaMemcpyHostToDevice, stream); context-enqueueV2(buffers.data(), stream, nullptr); for (size_t i 1; i buffers.size(); i) { cudaMemcpyAsync(hostOut[i], buffers[i], outSize[i] * sizeof(float), cudaMemcpyDeviceToHost, stream); } cudaStreamSynchronize(stream); }逻辑说明enqueueV2是 8.x 的异步推理入口第三个参数是依赖事件多流并发时传前一流的cudaEvent做串行化。hostOut必须用cudaMallocHost分配页锁定内存cudaMemcpyAsync才能走 DMA直接new float[]会被迫降级成同步拷贝。10.x 里enqueueV2已废弃改用enqueueV3时要注意绑定方式的变化。两代 API 的对应关系常踩列在这里功能TensorRT 8.xTensorRT 10.x异步推理enqueueV2enqueueV3取 tensor 形状getBindingDimensionsgetTensorShape绑定传参裸指针数组TensorAddressBuffer4.3 分割 mask 计算cublas 矩阵乘法与坐标裁剪分割后处理核心是把 mask 系数和 prototype mask 乘起来。prototype 是[32, 160, 160]先展平成[32, 25600]系数是[N, 32]float alpha 1.0f, beta 0.0f; cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, 25600, N, 32, alpha, protoFlat, 25600, // row-major [32, 25600] coeff, 32, // row-major [N, 32] beta, maskRaw, 25600); // row-major [N, 25600]逻辑说明内存全是 row-majorcublas 按 column-major 解释形状要做对偶变换。maskRaw第 i 行就是第 i 个框的 160×160 mask 展开m25600, nN, k32对应三个维度lda/ldb/ldc分别是 25600、32、25600漏掉任何一个都会得到花屏 mask。乘完对maskRaw逐元素做 sigmoid然后对每个框按 bbox640 空间裁剪再放缩到原图。裁剪和 resize 在 CPU 上做N 小于 50 时开销可忽略超过 50 建议挪进 CUDA kernel。4.4 姿态关键点的还原可见度不是置信度姿态输出[1, 56, 8400]第 4 通道是 person 分数后 51 个值每 3 个一组是某个关键点的 x、y、可见度// preds: [8400, 56]已按 person 分数过滤 for (int i 0; i numDets; i) { for (int j 0; j 17; j) { float kx preds[i][5 j * 3] - left; // 减 letterbox padding float ky preds[i][6 j * 3] - top; float vis preds[i][7 j * 3]; kx / r; ky / r; // 还原到原图尺度 if (vis 0.3f) kx ky 0.f; // 低可见度置零 } }参数说明关键点坐标和框坐标一样在 640 空间减 padding 再除缩放比vis训练时是 0/1 标签推理输出是 sigmoid 连续值画骨架时按 0.3 以上连线。COCO 的 17 个关键点顺序固定从鼻子到右脚踝按官方 skeleton 索引表连线。PyTorch 推理的坐标是相对原图尺度TensorRT 输出是相对输入张量尺寸两者差一个 letterbox 参数混用会导致关键点整体偏移。5. 耗时验证、动态 shape 取舍与 FP16/INT8 精度坑5.1 用 CUDA event 测耗时而不是 CPU 计时cudaEvent_t start, stop; cudaEventCreate(start); cudaEventCreate(stop); cudaEventRecord(start, stream); context-enqueueV2(buffers.data(), stream, nullptr); cudaEventRecord(stop, stream); cudaEventSynchronize(stop); float ms 0.0f; cudaEventElapsedTime(ms, start, stop);cudaEventElapsedTime统计 GPU 实际执行时间不含 host 侧调度等待。用time.time()或cv2.getTickCount()包推理测出来的是含 memcpy 等待的端到端时间在 Jetson 上误差能到一倍。延迟要预热 50 帧后取 P99不要取平均。5.2 动态 shape 与 batch 大小怎么选动态 shape 有代价实际形状偏离optShapes时触发 kernel 重选或 padding单帧延迟比静态 shape 高 5%15%。单路视频用静态1x3x640x640服务端多路并发用动态 batch上限 48超过 8 建议拆多个 context。Orin 上 TensorRT 和 JetPack 版本不匹配时构建直接失败把容器内 tensorrt 降到与 JetPack 配套的版本再重跑 trtexec这个场景在 JetPack 5.x 上最常见。5.3 FP16 精度下跌与 INT8 校准的验证方法FP16 对框回归影响很小但分割 mask 出现锯齿时先确认预处理里 BGR2RGB 和除以 255 都做了。FP16 输入精度低预处理误差会被 mask 的 sigmoid 放大。INT8 对姿态和分割更敏感上线前跑一次对比是唯一靠谱的验收方式import json results {} for eng in [yolov8n_fp16.engine, yolov8n_int8.engine]: m YoloTRT(eng) results[eng] evaluate_on_val(m, val2017.txt) # 返回 mAP50-95 print(json.dumps(results, indent2))对比时用同一套前后处理代码、相同 conf 和 iou 阈值只在 engine 加载处切换测出来的差异才是量化带来的。INT8 相对 FP16 的 AP 下跌超过 2 个点就不要用 INT8关键点和 mask 对量化敏感时折中方案是只对前几层卷积保留 FP16用set_precision逐层指定精度这个粒度控制在 C 和 Python API 里都支持。本文还有配套的精品资源点击获取