
简介面向计算机视觉算法工程师和深度学习部署开发者实战资源围绕如何将RTMPose人体姿态估计算法经TensorRT优化后在英伟达GPU上实现高效落地解决实时推理场景中速度和吞吐量不足的问题尤其适合需要把模型从研究原型推向工业级应用的个人或团队。包体共16个文件包含C源码与头文件5个cpp、4个h、可直接运行的TensorRT引擎文件、Visual Studio工程配置、Python辅助脚本与说明文档并附有README说明压缩包大小63.6MB目录结构清晰便于按模块对照学习。已有240人学习/下载。内容完整覆盖从RTMPose原理和TensorRT工作机制到模型导出、转换、校准、序列化及优化前后性能对比的实操链路并提供部署环境搭建与实时推理案例其中对比分析部分给出了推理速度、准确率和资源消耗等关键指标。通过该项目可以掌握算法部署全流程学会排查常见问题在监控、体育分析、人机交互等场景中复用这套工程化方法。1. 把 RTMPose 从 PyTorch 搬到 TensorRT这个 zip 包到底要解决什么模型在训练环境里跑通只是开始部署才是真正让人头大的环节。RTMPose 是精度和速度都很能打的人体姿态估计模型但 PyTorch 推理往往单帧要几十毫秒GPU 利用率却上不去。TensorRT 做的事是把 PyTorch 的卷积、拼接、激活重新编排和融合针对你的 NVIDIA 显卡生成专用推理引擎。这份引擎在不同卡上表现不同所以完整的部署链路不是把.pt存下来就完事而是要转成 TensorRT 的.engine再写推理代码接进业务。这篇实战笔记讲的是 RTMPose 在 TensorRT 上的完整落地环境怎么配、pt 文件怎么转 engine、推理代码怎么写、上线后会遇到哪些玄学故障。适合两类人一类是把姿态估计接进实时视频流的算法工程师另一类是卡在“onnx 转 trt 报错”里的部署新手。看完之后你能按照最小步骤在本地跑通 RTMPose并找到后续验证精度和优化性能的切入点。2. 部署前先搞清两件事TensorRT 为什么值、你的显卡能不能跑2.1 RTMPose 选 TensorRT图的不是推理快四倍是生产环境不抖RTMPose 在 MMPose 生态里属于 top-down 结构的姿态估计模型意思是先检测出人体框再针对每一框做关键点回归。很多人会把它和 HRNet 那种直接出整图 heatmap 的模型搞混但 RTMPose 的头部解码其实用的是 SimCC把关键点坐标拆成 x、y 两个方向、各做一次分类。这种设计让模型的收敛速度和精度表现都不错尤其是部署阶段不需要维护一整张高频热图的上采样内存和执行效率都友好得多。TensorRT 相比直接拿 PyTorch 跑核心变化不是某一个算子的速度变快而是整个计算图被重排了。PyTorch 在推理时会一行行 dispatch 内核相邻层之间的显存读写非常频繁TensorRT 会做层融合比如把卷积后面的 batch-norm 和 ReLU 合并成一个 kernel把多个小算子合并成一个大算子减少对全局内存的往返。这种优化在深层的 CNN 上能吃到很多红利。另一个关键是 kernel autotuningTensorRT 会针对你的显卡架构尝试不同的 CUDA 实现按实际延迟挑出最优解。同样的 ONNX 模型在 A100 和 GTX 1070 上构建出的 engine 内部实现几乎完全不同。具体到 RTMPose它的骨干网络来自 RTMDet是一套相对轻量但层数很多的 CNN。层越多TensorRT 融合的空间越大FP16 下的加速比就越明显。此外RTMPose 是 top-down 流程上游人体检测器输出的人体框大小每一帧都在变所以输入 tensor 的宽高是动态的。PyTorch 在这种动态 shape 下会频繁进行内存分配和图重编译TensorRT 允许在构建期定义 minShape、optShape、maxShape 三个维度边界运行时输入在这个范围内变化时不需要重新构建只要重新绑定 shape。这一点天然解决了多人姿态估计中“框大小不一”的痛点。当然 TensorRT 也不是满分方案。构建引擎的过程可能慢到以十分钟计engine 文件只对生成它的 GPU 型号和 TensorRT 版本有效换机器必须重建。如果你要上 INT8 量化还得准备一份有代表性的校验集用来统计每一层的动态范围不然精度滑坡很难排查。所以选不选 TensorRT不是看别人文章里的加速倍数而是看你要不要长期在 NVIDIA GPU 上稳定跑这条链路。如果要那投入是值得的。注意TensorRT 版本和 ONNX 算子的支持范围是有边界的导出 ONNX 时不要用太新的 op set如果后面构建报算子不支持先回头把后处理从图里摘出来。2.2 先查驱动、CUDA 和 TensorRT 版本GTX 1070 这类老卡最容易在这里卡住部署 TensorRT 的第一步是确认你的机器真的能跑别先一头扎进去转模型。老卡翻车的概率远高于新卡尤其是 GTX 1070 这类 Pascal 架构算力只有 6.1FP16 性能远不如图灵卡跑 TensorRT FP16 时偶尔还会遇到算子实现缺失的告警。网上经常有人问“TensorRT 版本如果是 10.x 是否支持 GTX1070”这类问题不能凭一张兼容表拍板因为不仅看 TensorRT还要看驱动和 cuDNN 的版本组合。我的经验是分三步验证环境nvidia-smi nvcc --version dpkg -l | grep -i cudnnnvidia-smi看驱动支持的最高 CUDA 版本nvcc --version看当前环境里 CUDA 编译器版本最后 grep cuDNN 确认 cuDNN 版本。TensorRT 每出一个大版本比如 8.x、9.x、10.x都会在发布说明里给出支持的 CUDA 和 cuDNN 组合以及最低驱动版本。这里要特别留意你慌着把驱动升到最新可能把服务器上别的框架的环境搞坏。解决方案是用 conda 虚拟环境或 Docker 隔离别动系统级 CUDA。安装 TensorRT 的常见做法有两种下官方 tar 包解压或者在 conda 环境里pip install tensorrt。我一般用 tar 包因为它自带trtexec后面转模型调试 shape 都靠这个工具。解压后设置环境变量export LD_LIBRARY_PATH/opt/TensorRT-10.0.1.6/lib:$LD_LIBRARY_PATH export PATH/opt/TensorRT-10.0.1.6/bin:$PATH trtexec --versionLD_LIBRARY_PATH必须指向 TensorRT 目录下的lib文件夹那里有libnvinfer.soPATH要包含bin目录这样trtexec才能直接执行。如果你还想通过 Python 调用 TensorRT那么 pip 安装的 tensorrt 版本必须和 tar 包版本一致否则运行时会出现找不到符号的诡异报错。对于 GTX 1070还有个很实用的小建议不要一上来就开 FP16先用 FP32 把整条链路跑通再考虑换成 FP16否则你会在构建阶段浪费大量时间。环境装完怎么验证能不能用跑一跑trtexec转换一个简单 ONNX 即可。如果看到Unsupported operation报错说明你的 ONNX 图里混入了 TensorRT 不支持的算子先从导出脚本查起不要急着换 TensorRT 版本。这一条建议能帮你少走两天的弯路。3. 把 RTMPose 的 pt 文件转成 TensorRT engine完整流水线3.1 先转 ONNXpytorch 模型导出时最容易在动态轴上翻车TensorRT 不吃 PyTorch 的.pt文件标准做法是先导出成 ONNX再交给 TensorRT 重建计算图。这一步看起来枯燥却是整个部署链路里翻车率最高的一环。RTMPose 在 MMPose 里通常由 backbone、neck 和 head 组成导出时要把模型切成只含前向推理的路径别把后处理函数包进去。习惯上先model.eval()再用torch.onnx.export。import torch from mmpose.apis import init_model model init_model(rtmpose-l.py, rtmpose-l.pth, devicecpu) model.eval() dummy_input torch.randn(1, 3, 256, 192) # RTMPose 常用尺寸 torch.onnx.export( model, dummy_input, rtmpose.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch, 2: height, 3: width}, output: {0: batch}}, opset_version16, do_constant_foldingTrue )代码逻辑说明dummy_input的 shape 必须和训练配置一致。RTMPose 常用 256×192 或 384×288这个值决定后面解码坐标时的缩放系数。dynamic_axes把 batch、height、width 都标为动态这是后续使用 TensorRT 动态 shape 的前提。opset_version选 16 是为了兼容 TensorRT 10 系列的算子解析版本太新可能出现 TensorRT 还不认识的新算子版本太旧又可能损失一些融合机会。output_names固定成output后面所有绑定张量的代码都用这个名字。这里有个关键点很多人忘记eval()就导出。如果在 train 模式下导出BatchNorm 和 Dropout 会打进计算图ONNX 能顺利生成但推理精度波动明显。另一个常见错误是把后处理包进 ONNX。RTMPose 如果输出 SimCC 分类概率那 softmax 解码写在外面因为你可能要在 Python 里根据业务调整逻辑每次都重新导 ONNX 太耗时。导出后建议先用 onnxruntime 验证一遍不要直接上 TensorRTpython -c import onnxruntime as ort, numpy as np; sessort.InferenceSession(rtmpose.onnx); print(sess.run(None, {input: np.random.randn(1,3,256,192).astype(np.float32)})[0].shape)这条命令的含义用 onnxruntime 加载 ONNX 并以随机数跑一次打印输出 shape。输出的第一个维度必须和导出时的 output 设计一致。如果 shape 对不上多半是dynamic_axes没配对或者output_names没有对应模型实际的输出 key。这一步排错成本极低一定要做。3.2 用 trtexec 从 ONNX 生成 enginefp16、workspace 和动态 shape 一起调ONNX 过了 onnxruntime 这道关就可以交给trtexec构建 engine。trtexec是 TensorRT 自带的命令行工具既能验证模型能否构建也能做基准测试还能直接把 engine 保存成文件放到推理代码里用。我日常部署的完整命令一般长这样trtexec \ --onnxrtmpose.onnx \ --saveEnginertmpose_fp16.engine \ --fp16 \ --workspace4096 \ --minShapesinput:1x3x256x192 \ --optShapesinput:8x3x256x192 \ --maxShapesinput:16x3x256x192 \ --shapesinput:8x3x256x192 \ --verbose参数说明--fp16开启 FP16 精度--workspace4096是构建期 TensorRT 可用的显存上限单位 MB。workspace 不是越大越好但太小会减少层融合的机会导致性能下降。动态 shape 三件套--minShapes、--optShapes、--maxShapes分别定义输入的最小、最优、最大维度TensorRT 会针对这三档分别做优化推理时输入落在 min 到 max 范围内都能执行。--shapes是 trtexec 做实际 benchmark 时使用的输入尺寸。--verbose会输出大量日志排查算子和层报错时特别有用。构建结束以后日志里会显示 FP16 和 FP32 的平均时延对比。如果确认精度损失可接受就把这个 engine 文件交给推理模块。要注意 engine 文件是“死固”的它绑定当前 GPU 型号、TensorRT 版本和 CUDA 环境换一台机器必须用同一套构建脚本重新生成。所以项目里不要只存档 engine也要把trtexec命令或导出 ONNX 的脚本一起存档。如果trtexec里出现Unsupported operation或UNSUPPORTED_LAYER问题基本出在 ONNX 图里包含了平台不支持的算子。常见元凶是 GridSample、特定版本的 Resize、或者自定义 op。处理办法是回导出脚本把这些算子换成 TensorRT 支持的组合比如把动态 grid sample 改写成固定网格的采样或者把 ROI Align 放到模型外做。这一步往往是部署项目里最花时间的要有心理准备。3.3 固定 batch 还是动态 batch选错会导致后续推理代码重写构建 engine 之前要先做一个影响后面所有代码的决定固定 batch 还是动态 batch。我的建议是如果业务场景是单路视频流每次只检测一个人那就固定 batch1。固定 batch 的 engine 在推理时不需要调用set_binding_shapeHost 代码里 buffer 的分配和管理也全都是固定尺寸代码量少很多。如果业务一次性要处理多个人体框比如一帧画面里检测出 10 个人需要把这 10 个框裁剪、缩放后一次性喂给姿态模型那就值得用动态 batch。构建期把maxShapes设置成最大并发人数比如 16推理时单次 batch 16能显著压低 mean 延迟。动态 batch 的代价是代码复杂度更高每帧都要根据实际检测结果调用set_binding_shape重新绑定输入输出 buffer。两种选择没有绝对正确只有贴合场景的做法。4. 用 TensorRT 跑 RTMPose 推理最小可用的 Python 推理代码4.1 加载 engine 并创建 context同一个 engine 别重复加载Engine 文件生成以后推理端的工作是把它加载进内存并反复执行。最容易犯的低级错误是每次请求都重新读取 engine 文件并 deserialize重复加载不仅浪费 IO还会让显存出现波动。标准做法是启动时加载一次保存到全局变量后续所有推理复用。这里注意 engine 与 context 的对应关系一个 engine 可以创建出多个 context每个 context 代表一次可执行的推理状态。多线程场景下每个线程或每个请求持有自己的 context互相不抢。这套结构在 Python 里验证通过后后面换 C 接口时整体流程不变只是函数名变成createInferRuntime和executeV2这也是 C 部署 lamestream 的常见路径。import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np engine_path rtmpose_fp16.engine logger trt.Logger(trt.Logger.WARNING) runtime trt.Runtime(logger) with open(engine_path, rb) as f: engine_buf f.read() engine runtime.deserialize_cuda_engine(engine_buf) context engine.create_execution_context() context.set_binding_shape(engine.get_binding_index(input), (1, 3, 256, 192))这段代码的逻辑runtime.deserialize_cuda_engine把 engine 文件反序列化回计算图对象create_execution_context生成可执行上下文set_binding_shape是动态 shape engine 必备的一步不设置的话 binding 维度还是构建时的默认值推理会报错。如果你用固定 batch1 构建理论上可以少调这行但保留无妨代码更健壮。数字上的坑如果 engine 有多个 profile比如你为不同输入尺寸准备了多个 profileset_binding_shape之前要先确定当前激活的 profile。在 trtexec 层面一个例子只有 profile 0如果后续要多 profile代码里用context.set_active_profile切换。这个操作容易漏漏了就继续用旧 profile输入尺寸对不上输出错乱。4.2 单帧推理流程预处理决定成败推理代码本身很短真正决定成败的是预处理。RTMPose 训练时使用固定的归一化参数不同模型可能不一样比如 ImageNet 的 mean/std。部署代码必须与训练配置完全对齐差一个像素值都不行。图像要先从 BGR 转成 RGB 与否取决于训练时数据管的通道顺序MMPose 默认是 BGR 输入。你要做的是确认 checkpoint 的 normalize 参数而不是照着某个博客抄。def preprocess(image_bgr): mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img cv2.resize(image_bgr, (192, 256)) img img.astype(np.float32) / 255.0 img (img - mean) / std img img.transpose(2, 0, 1) # HWC - CHW img np.ascontiguousarray(img[np.newaxis, ...]) return img h_input preprocess(frame) cuda.memcpy_htod(d_input, h_input) context.execute_v2(bindings[int(d_input), int(d_output)]) cuda.memcpy_dtoh(h_output, d_output)预处理代码的逻辑resize 到 192×256除以 255 转 float再用均值标准差做归一化最后转成 CHW 并增加 batch 维度。memcpy_htod把预处理好的数据从内存复制到显存execute_v2开始推理memcpy_dtoh再把结果从显存拷回内存。bindings参数必须传显存指针的整数列表顺序对应 engine 里 binding 的顺序通常是输入在前、输出在后。先做一次memcpy_dtoh顺利拿到结果再谈下一步优化。流程跑通后可以把 memcpy 换为异步的memcpy_htod_async和execute_async_v2具体我们最后一章再补。这里要强调输出 buffer 的 shape要在 pycuda 里分配足够大的显存不然数据拷贝会越界。通过engine.get_binding_shape(output_binding)查询实际输出维度用这个维度去申请数组。4.3 把 RTMPose 输出解码成 17 个关键点坐标拿到 TensorRT 的原始输出下面这一步是要解码成坐标。RTMPose 的 SimCC 头输出通常是两个方向的 logits比如 x 方向为[B, 17, 192]y 方向为[B, 17, 256]。这里不是说输出的就是最终像素坐标而是每个关键点的分类概率分布。要转换成坐标需要沿着分布轴做 softmax再用位置索引做加权期望。def decode_simcc(simcc_x, simcc_y): # simcc_x: (1, 17, 192), simcc_y: (1, 17, 256) def softmax(x): e np.exp(x - x.max(axis-1, keepdimsTrue)) return e / e.sum(axis-1, keepdimsTrue) prob_x softmax(simcc_x[0]) prob_y softmax(simcc_y[0]) xs np.sum(prob_x * np.arange(prob_x.shape[-1]), axis-1) ys np.sum(prob_y * np.arange(prob_y.shape[-1]), axis-1) return np.stack([xs, ys], axis-1) # (17, 2)解码代码的逻辑softmax 前先减去最大值避免 exp 溢出随后用概率加权求和得到期望坐标。这种软 argmax 比直接取最高概率点的效果更平滑、更稳定。如果你导出 ONNX 时已把解码包含进去那输出直接就是 17×2这里只需要做缩放。注意结果坐标是相对输入图像 192×256 的要还原到原图需要记录 resize 之前的缩放因子。网络输出的命名可能因版本而异比如 output 可能是[1, 17, 2]而非两个 SimCC 张量。在写推理代码前先用 onnxruntime 或trtexec打印一遍输出 shape确认你的解码函数拿到的具体是哪一种格式。返回的 17 个点顺序对应 COCO 关键点顺序鼻子、眼睛、耳朵、肩膀、手肘、手腕、髋、膝盖、脚踝等。如果你需要画框或者按上肢、下肢分组连接按这个索引顺序处理就行。5. 部署避坑从环境到推理代码的常见故障排查5.1 现象构建 engine 时显存溢出trtexec在build engine阶段突然报CUDA out of memory有时甚至直接把图形界面或桌面环境搞崩尤其出现在 GTX 1070 这类显存只有 8G 的老显卡上。原因通常有两个一是--maxShapes设置过大TensorRT 会按照最大 shape 规划中间层显存导致内存急涨二是--workspace设置过高让构建器放开手脚消耗显存。解决是先调小--maxShapes比如把最大 batch 从 16 降到 8同时把--workspace降到 2048 或 1024。如果还不行再加一个--builderOptimizationLevel1降低构建期的优化强度。这样生成的 engine 可能在局部层少了些融合但至少能构建成功。5.2 现象构建成功但推理输出全是 nan这种故障最闹心因为问题不在 TensorRT而在于你的预处理。常见原因是图像读进来是uint8你写了img img / 255.0但忘了先astype(np.float32)结果在 numpy 里做的是整数除法所有像素都变成 0 或 1输入分布直接被破坏。另一个常见原因是归一化用的 mean 和 std 与训练配置不一致导致激活值溢出。解决的唯一可靠办法是回到部署代码里打一个img.min()和img.max()同时把模型在纯 PyTorch 下的输出打印出来对比。如果 PyTorch 也是 nan那问题出在权重文件或者模型配置配对错了。5.3 现象动态 batch 推理时输出错乱用动态 batch 时可能会发现输出的关键点坐标乱跳跟真实人体位置对不上。原因通常是你没有在每次推理前重新设置 batch 对应的 binding shape或者h_output数组按固定维度分配但实际输出的 batch 更大或更小。记住context.set_binding_shape是“按调用设置”的必须放在每次execute_v2之前不能只在创建 context 时调一次。解决的标志是打印实际输入输出的shape逐帧核对是否一致。如果你用多人检测的框作为 batch 输入还要小心框的顺序要与输出对应不然坐标和人对不上。5.4 现象老卡上构建速度极慢动辄几十分钟许多人以为 TensorRT 构建慢是模型太大的锅其实更常见的是显卡太老。TensorRT 的 autotuning 会在同一层上尝试几十种 kernel 实现并在实际 GPU 上跑 benchmark 选优这个过程在 Pascal 架构上尤其耗时。GTX 1070 构建一个 RTMPose 模型FP16 加动态 shape跑 30 分钟不算离谱。解决是分两步先用一个很小的--maxShapes构建验证版本确认网络没问题后再用正常 shape 构建或者在构建脚本里加--builderOptimizationLevel1把优化级别降下来构建时间能缩短到五分之一。想快就少优化想极致性能就让机器多跑一会儿这是部署侧需要做取舍的地方。5.5 现象多线程推理时偶发 crash时好时坏算法服务上线后基本都是多线程并发测试单帧时你不会发现一旦用 ThreadPool 并发调用就会偶发 Segfault有时跑几小时才出现一次。原因大概率是多个线程共用同一个context而 context 内部有可变状态不是线程安全的。解决方法是每个线程自己创建并持有独立的 context或者每个请求在进去时重新创建 context。另一个常见原因是 pycuda 的memcpy_dtoh没有等上一次推理完成就读取数据导致显存区域的读写竞争。在做下一次拷贝前调用cuda.Device(0).synchronize()做一次同步或者改用 stream 异步执行并等待问题就能消除。6. 上线前验证与性能取舍一个技巧让你在精度和速度之间站住脚部署完成后最后一步是验证这一步不做好线上没有人帮你背锅。验证至少分两层。第一层是和 PyTorch 做单帧对齐取三张有代表性的图保证背景、光照、遮挡都不同分别跑 PyTorch 和 TensorRT对比 17 个关键点的坐标误差。在 FP16 下误差小于 2 个像素是常见结果大于 5 个像素就要查预处理是否一致或者检查归一化参数有没有对齐。第二层是测真实吞吐。不要只看trtexec输出的平均时延要按你的业务场景模拟比如一段 10 分钟的视频解码每一帧跑检测器再用检测框裁剪后输入姿态模型统计整个 pipeline 的 FPS。这里常常会出现一个反直觉现象姿态模型的单帧推理只用了 2ms而整体 FPS 仍然只有 30因为瓶颈在上游的视频解码和检测器。所以优化不是盯着姿态引擎调是把整个链路的 GPU 利用率打满。给你一个具体技巧用 CUDA Stream 把预处理和推理并行。传统做法是“预处理 - 拷入显存 - 推理 - 拷出”串行改进后在预处理耗时较长时可以让 GPU 同时跑上一个 batch 的推理。在 Python 端用 pycuda 的Stream很容易stream cuda.Stream() cuda.memcpy_htod_async(d_input, h_input, stream) context.execute_async_v2(bindings[int(d_input), int(d_output)], stream_handlestream.handle) cuda.memcpy_dtoh_async(h_output, d_output, stream) stream.synchronize()这段代码的逻辑是把拷贝和推理挂到同一个 stream 上让 GPU 流水线重叠。注意execute_async_v2的 handle 类型是 int所以传stream.handle。如果你只做单帧验证这个技巧不明显但一旦批量请求多起来吞吐提升能达到 20% 到 30%。一个老生常谈的教训是部署代码里的每个改动都要量化验证每次调了 batch size、fp16 开关、输入尺寸之后不要只凭肉眼观察关键点是否“大概对上”我会用脚本自动算一遍坐标误差再放行。这些小习惯会在上线前三周救你一命。希望帮到你。本文还有配套的精品资源点击获取