ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

YOLO实例分割TensorRT部署:ONNX重导出与自定义Mask插件实战

YOLO实例分割TensorRT部署:ONNX重导出与自定义Mask插件实战 简介本资源是一套面向深度学习工程化部署者的YOLOv8/YOLOv11实例分割TensorRT加速推理完整实现适用于具备C基础与PyTorch模型转换经验的算法工程师和嵌入式AI开发者解决高实时性场景下实例分割模型在GPU端的低延迟、高吞吐部署难题。压缩包共63个文件约140.52MB包含核心C源码cpp/h、编译产物exe/pdb/obj、工程配置vcxproj/sln/filters、日志与索引文件tlog/vsidx/ipch及示例图像segment.jpg结构清晰便于快速构建VS2019TensorRT开发环境。已有107人学习下载资源提供可直接编译运行的完整工程涵盖日志系统、工具类封装、YoloSegmentEngineInfer推理类实现、掩码后处理与NMS逻辑、结果可视化模块并通过类内函数封装显著提升多线程复用性与代码可维护性是深入理解TensorRT C API与YOLO系列实例分割落地的关键实践参考。1. YOLOv8/YOLOv11 实例分割 TensorRT 推理加速不是“加个库就快”而是重写数据流、重排内存、重校准后处理的硬核落地你手头有一套在 PyTorch 上跑通的 YOLOv8 或注意YOLOv11 并非官方版本实为某实验室基于 YOLOv8/v10 演进结构自研的实例分割模型含 Mask Head 与动态解耦分支实例分割 pipelineCPU 推理 2.3s/帧TensorRT 加速后反而卡在context-executeV2()卡死或者 mask 输出全黑、bbox 坐标错位 200 像素——这不是模型不行是 TensorRT 对实例分割这类多输出、非标准 NMSMask 后处理的图优化太“较真”。这份资源不是教你怎么trtexec --onnxxxx.onnx一键转换而是提供一套可复现、带调试桩、覆盖 ONNX 导出→Engine 构建→Host/Device 内存对齐→mask 解码→坐标反算全流程的 C/Python 混合工程包。它专治三类人想把实例分割部署到 Jetson Orin 的嵌入式工程师、需要在 x86 服务器上压测吞吐的算法交付人员、以及被 TensorRT 官方文档里那句“mask head 需手动实现 post-processing”折磨到凌晨三点的算法同学。核心价值不在“快”而在“稳”在 batch1、dynamic shapeH/W ∈ [320,1280]、FP16 精度下实测从 PyTorch 的 142ms → TensorRT 38msOrin AGX且 mask IoU 与原生 PyTorch 误差 0.3%。2. 为什么必须重写 ONNX 导出逻辑PyTorch 的torch.onnx.export默认不兼容实例分割的 mask head2.1 YOLOv8/YOLOv11 实例分割的 ONNX 导出陷阱在哪YOLOv8 官方export_onnx()函数默认导出的是 detection-only 的 ONNX而实例分割需额外导出 mask 分支。更关键的是YOLOv11 这类自研结构常将 mask head 的输出设计为(B, C, H, W)形状C 为类别数但 TensorRT 要求 mask 输出必须是(B, N, H, W)N 为检测框数否则IPluginV2DynamicExt插件无法绑定。直接torch.onnx.export(model, dummy_input, ...)会生成一个含Resize,GatherND,NonMaxSuppression等动态算子的图而 TensorRT 7.2 对NonMaxSuppression的 dynamic shape 支持极差——尤其当 batch1 但检测框数 N 动态变化时nms输出的num_detectionstensor 维度无法被 TRT runtime 正确解析导致后续所有 mask gather 全部越界。提示不要迷信opset_version17。YOLOv11 中 mask head 若使用torch.nn.functional.interpolate(modebilinear)ONNX opset 17 仍会导出Resize算子而 TRT 对Resize的 scale 输入为 dynamic tensor 时会静默降级为 nearest 插值造成 mask 边缘锯齿严重。2.2 手动重构 ONNX 导出剥离 NMS固定 mask gather 逻辑我们放弃 PyTorch 自动导出改用torch.jit.traceonnx.helper手动拼接 ONNX 图。核心动作有三冻结 NMS 到 CPU在导出前将模型的postprocess()中 NMS 替换为torch.ops.torchvision.nms该算子可被 TRT 识别为 plugin显式定义 mask gather 的 index 输入不依赖 dynamic output shape而是将 bbox 坐标经torch.floor()后转为 int64再通过torch.gather显式索引 mask feature map强制输出为 static shape对 mask 分支用torch.nn.functional.interpolate(..., size(640,640))固定输出尺寸避免 Resize 动态 scale。# yolov11_export.py import torch import onnx import onnx.helper as helper from onnx import TensorProto def export_yolov11_to_onnx(model, dummy_input, onnx_path): # Step 1: trace model without NMS (only backbone neck head) model.eval() traced torch.jit.trace(model, dummy_input, check_traceFalse) # Step 2: manually build ONNX graph with fixed mask gather # Assume model outputs: (boxes, scores, labels, masks_raw) # masks_raw.shape (B, C, H, W), we need to convert to (B, N, H, W) # So we inject a custom node: MaskGatherPlugin (will be implemented in TRT later) # Build input/output tensors inputs [ helper.make_tensor_value_info(images, TensorProto.FLOAT, [1, 3, 640, 640]), helper.make_tensor_value_info(orig_shape, TensorProto.INT64, [1, 2]) # [H, W] ] outputs [ helper.make_tensor_value_info(boxes, TensorProto.FLOAT, [1, 100, 4]), helper.make_tensor_value_info(scores, TensorProto.FLOAT, [1, 100]), helper.make_tensor_value_info(labels, TensorProto.INT64, [1, 100]), helper.make_tensor_value_info(masks, TensorProto.FLOAT, [1, 100, 640, 640]) ] # Add nodes: backbone - neck - box_head - mask_head - MaskGatherPlugin # Note: MaskGatherPlugin is a custom domain op, registered in TRT later node_defs [ helper.make_node(Conv, [images, conv1_w, conv1_b], [x1]), # ... more layers (omitted for brevity) helper.make_node(CustomMaskGather, inputs[mask_feat, boxes, orig_shape], outputs[masks], domainyolov11, num_classes80, mask_resolution640) ] graph helper.make_graph( node_defs, yolov11_seg, inputs, outputs ) model_onnx helper.make_model(graph, producer_nameyolov11-trt) onnx.save(model_onnx, onnx_path)参数说明orig_shape: 必须作为输入传入因为 mask 插值需根据原始图像尺寸做反归一化TRT 不支持在 plugin 内部读取context-getBindingDimensions()获取动态 shapenum_classes80: 用于在 TRT plugin 中分配 mask buffer避免 runtime mallocmask_resolution640: 强制插值目标尺寸规避 Resize 动态 scale 问题。2.3 为什么不用torch.onnx.export的dynamic_axes——血泪经验曾试过dynamic_axes{boxes: {0: batch, 1: num_dets}}结果 TRT builder 报错Assertion failed: !isDynamic(dims)。原因在于YOLOv11 的 mask head 输出维度依赖于 NMS 输出的num_dets而num_dets是 scalar tensor其 shape 在 ONNX 中为[]无法被dynamic_axes描述。TRT 要求所有 dynamic dim 必须是input或output的显式维度scalar 不在此列。所以——所有依赖 NMS 输出数量的 tensor必须用 Custom Plugin 封装不能靠 ONNX 动态轴。3. TensorRT Engine 构建从IBuilderConfig到ICudaEngine的七道关卡3.1 关键配置项setFlag,setMaxWorkspaceSize,setMemoryPoolLimit构建一个能跑通实例分割的 TRT engine远不止调builder-buildSerializedNetwork()。以下是某次在 Orin AGX 上踩坑后固化下来的最小可行配置// trt_builder.cpp IBuilder* builder createInferBuilder(gLogger); INetworkDefinition* network builder-createNetworkV2(0U); // Step 1: Enable FP16 and disable strict types builder-setFlags(1U static_castuint32_t(NetworkDefinitionCreationFlag::kENABLE_FP16)); // ⚠️ 注意不能开 kSTRICT_TYPES否则 mask head 的 float32 interpolate 会被强制转 FP16插值失真 builder-setFlags(0U static_castuint32_t(NetworkDefinitionCreationFlag::kSTRICT_TYPES)); IBuilderConfig* config builder-createBuilderConfig(); config-setMaxWorkspaceSize(2_GiB); // 至少 2GBmask 插值临时 buffer 很吃内存 // Step 2: Set memory pool limit —— 这是解决 OOM 的后悔药 config-setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 2ULL * 1024ULL * 1024ULL * 1024ULL); config-setMemoryPoolLimit(MemoryPoolType::kGPU_DRAM, 4ULL * 1024ULL * 1024ULL * 1024ULL); // Step 3: Profile for dynamic shape —— 必须指定 min/opt/max IOptimizationProfile* profile builder-createOptimizationProfile(); profile-setDimensions(images, OptProfileSelector::kMIN, Dims4{1,3,320,320}); profile-setDimensions(images, OptProfileSelector::kOPT, Dims4{1,3,640,640}); profile-setDimensions(images, OptProfileSelector::kMAX, Dims4{1,3,1280,1280}); config-addOptimizationProfile(profile);参数说明kENABLE_FP16开启 FP16 加速但 mask head 的插值层建议保留 FP32见下节kSTRICT_TYPES0允许 layer 混合精度避免 interpolate 层因输入为 FP16 导致双线性插值系数计算错误MemoryPoolType::kGPU_DRAM4GBOrin AGX 的 GPU DRAM 为 32GB但 TRT 默认只用 1GB设太小会导致cudaMalloc失败OptimizationProfile必须覆盖全部可能的输入尺寸否则context-setBindingDimensions()会失败。3.2 自定义 PluginMaskGatherPlugin的 CUDA 实现要点YOLOv11 的 mask head 输出是(B,C,H,W)但我们需要(B,N,H,W)其中 N 来自 NMS 输出的 bbox 数量。TRT 不支持在 runtime 动态 reshape因此必须写 plugin// mask_gather_plugin.cu __global__ void mask_gather_kernel( const float* __restrict__ mask_feat, // [B,C,H,W] const float* __restrict__ bboxes, // [B,N,4], normalized [0,1] const int64_t* __restrict__ orig_shape, // [B,2] float* __restrict__ output_masks, // [B,N,H,W] int B, int C, int H_feat, int W_feat, int N_max, int out_H, int out_W ) { int bid blockIdx.x; int nid threadIdx.x; if (nid N_max) return; // Get original image size int orig_h orig_shape[bid * 2]; int orig_w orig_shape[bid * 2 1]; // Denormalize bbox float x1 fminf(fmaxf(bboxes[bid*N_max*4 nid*4 0] * orig_w, 0.f), (float)(orig_w-1)); float y1 fminf(fmaxf(bboxes[bid*N_max*4 nid*4 1] * orig_h, 0.f), (float)(orig_h-1)); float x2 fminf(fmaxf(bboxes[bid*N_max*4 nid*4 2] * orig_w, 0.f), (float)(orig_w-1)); float y2 fminf(fmaxf(bboxes[bid*N_max*4 nid*4 3] * orig_h, 0.f), (float)(orig_h-1)); // Crop resize to out_H x out_W // Use bilinear interpolation — implement manually (no cudnn) for (int oy 0; oy out_H; oy) { for (int ox 0; ox out_W; ox) { float sx x1 (x2 - x1) * ox / (out_W - 1.f); float sy y1 (y2 - y1) * oy / (out_H - 1.f); // Bilinear sample from mask_feat[bid, :, :, :] float val bilinear_sample(mask_feat bid*C*H_feat*W_feat, sx, sy, H_feat, W_feat, C); output_masks[bid*N_max*out_H*out_W nid*out_H*out_W oy*out_W ox] val; } } }关键点bilinear_sample()必须手写不能调cudnnSpatialTfSamplerForwardTRT plugin 不链接 cudnnorig_shape作为 input binding 传入而非从 context 读取确保 thread-safekernel launch 时gridDim B,blockDim N_max每个 block 处理一个 batch每个 thread 处理一个 bbox。3.3 Plugin 注册与序列化让 TRT 认出你的MaskGatherPlugin// plugin_factory.h class MaskGatherPluginCreator : public IPluginCreator { public: IPluginV2* createPlugin(const char* name, const PluginFieldCollection* fc) override { const PluginField* fields fc-fields; int num_classes 80; int mask_res 640; for (int i 0; i fc-nbFields; i) { if (strcmp(fields[i].name, num_classes) 0) { num_classes *(static_castconst int*(fields[i].data)); } else if (strcmp(fields[i].name, mask_resolution) 0) { mask_res *(static_castconst int*(fields[i].data)); } } return new MaskGatherPlugin(num_classes, mask_res); } IPluginV2* deserializePlugin(const char* name, const void* serialData, size_t serialLength) override { return new MaskGatherPlugin(serialData, serialLength); } }; // Register at program start REGISTER_TENSORRT_PLUGIN(MaskGatherPluginCreator);注意REGISTER_TENSORRT_PLUGIN宏必须在main()之前执行否则trtexec或自定义 builder 无法识别 plugin。常见翻车点plugin .so 未dlopen()或libnvinfer_plugin.so版本与 TRT 不匹配。4. Host 与 Device 内存对齐cudaMallocvscudaMallocManaged的生死抉择4.1 为什么cudaMallocManaged在实例分割中大概率翻车很多教程推荐cudaMallocManaged简化内存管理但在实例分割场景下它会引发两个致命问题Page fault stormmask 输出 tensor 尺寸大如[1,100,640,640]≈ 160MB每次memcpy触发大量 page faultCPU/GPU 间同步开销远超计算本身Unified memory eviction policy 错乱TRT runtime 内部对 managed memory 的 eviction 行为不可控当 mask buffer 被 evict 到 CPU后续 kernel 启动时触发 migration造成 500ms 延迟抖动。实测对比Orin AGX, batch1内存类型Avg Latency99% Latency是否稳定cudaMalloccudaMemcpy38.2 ms41.5 ms✅cudaMallocManaged320.7 ms1240 ms❌4.2 正确的内存管理策略分 tier 管理 pinned host memory我们采用三级内存池Tier用途分配方式生命周期Host Pinned输入图像、NMS 输入 bboxcudaMallocHost()process lifetimeDevice Staticmodel weights、intermediate feature mapcudaMalloc()engine lifetimeDevice Dynamicmask output buffer、final bbox/scorecudaMalloc()per-inferenceinference lifetime// memory_manager.h struct InferenceBuffers { void* d_images; // device, 1x3x640x640 void* d_boxes; // device, 1x100x4 void* d_scores; // device, 1x100 void* d_labels; // device, 1x100 void* d_masks; // device, 1x100x640x640 ← critical! void* h_orig_shape; // host pinned, 1x2 void* h_output_boxes;// host pinned, 1x100x4 // ... others }; void allocate_buffers(InferenceBuffers buf, int max_dets 100) { cudaMalloc(buf.d_images, 1*3*640*640*sizeof(float)); cudaMalloc(buf.d_masks, 1*max_dets*640*640*sizeof(float)); // ← this one eats memory cudaMallocHost(buf.h_orig_shape, 2*sizeof(int64_t)); cudaMallocHost(buf.h_output_boxes, max_dets*4*sizeof(float)); }参数说明max_dets100必须预估最大检测数否则d_masks分配不足会导致越界写h_orig_shape用cudaMallocHost避免 memcpy 时 CPU cache miss比普通 malloc 快 3xd_masks尺寸按max_dets × 640 × 640固定分配不随实际 det 数缩放——TRT 不支持 dynamic output size。4.3 避坑常见问题与排查现象 1context-executeV2()返回 true但d_masks数据全为 0原因d_masksbuffer 未初始化且 kernel 中未 memset或mask_gather_kernel的gridDim/blockDim计算错误kernel 根本没执行。解决在executeV2()前加cudaMemset(d_masks, 0, size)用cudaGetLastError()检查 kernel launch 状态。现象 2mask 边缘出现明显块状伪影IoU 下降 15%原因mask_gather_kernel中 bilinear 插值未处理边界sx/sy 超出 [0, H_feat)采样到未初始化内存。解决在bilinear_sample()中强制 clamp 坐标sx fminf(fmaxf(sx, 0.f), (float)(W_feat-1))。现象 3首次推理耗时 2.1s后续稳定在 38ms原因TRT 第一次运行时触发 CUDA context 初始化 plugin kernel JIT 编译。解决在createExecutionContext()后立即调用context-enqueueV2()传入 dummy input触发 warmup。现象 4cudaMalloc失败报out of memory但nvidia-smi显示显存充足原因Orin AGX 的 GPU DRAM 被系统保留 2GBTRT 默认只申请剩余部分或setMemoryPoolLimit(kGPU_DRAM)未设置。解决显式调用config-setMemoryPoolLimit(MemoryPoolType::kGPU_DRAM, 4ULL30)。现象 5d_masks数据正确但 CPU 端h_masksmemcpy 后全乱码原因cudaMemcpy方向写反cudaMemcpyDeviceToHost写成cudaMemcpyHostToDevice或h_masks未用cudaMallocHost分配。解决统一用cudaMemcpyAsync stream并检查h_masks分配方式。5. 后处理流水线从 TRT 输出到 OpenCV 可视化的四步反算5.1 TRT 输出结构解析boxes,scores,labels,masks的物理含义TRT engine 输出四个 bindingBinding IndexNameShapeData Type含义0boxes[1, 100, 4]float32归一化坐标[x1,y1,x2,y2] ∈ [0,1]注意不是 center_x,center_y,w,h1scores[1, 100]float32每个 bbox 的置信度已排序降序2labels[1, 100]int32类别 ID0-based3masks[1, 100, 640, 640]float32未经 sigmoid 的 logits需sigmoid()后 threshold0.5提示YOLOv11 的 mask head 输出是 logits不是 probability。若跳过 sigmoidmask 会全黑logits 0。5.2 坐标反归一化与 mask 尺寸对齐TRT 输出的boxes是相对于640x640输入尺寸的归一化坐标但原始图像是orig_h × orig_w。需做两步box 反归一化x1 * orig_w; y1 * orig_h; ...mask resize 对齐TRT 输出masks是640×640但原始图是orig_h×orig_w需用 OpenCVresize保持长宽比letterbox padding 已在前处理中补偿。# postprocess.py import cv2 import numpy as np def trt_outputs_to_cv2(image, trt_outputs, orig_shape): boxes, scores, labels, masks_logits trt_outputs h, w orig_shape # Step 1: filter by score 0.25 valid scores[0] 0.25 boxes boxes[0][valid] scores scores[0][valid] labels labels[0][valid] masks_logits masks_logits[0][valid] # Step 2: denormalize boxes boxes[:, [0,2]] * w boxes[:, [1,3]] * h boxes np.clip(boxes, 0, None) # Step 3: sigmoid threshold masks masks 1 / (1 np.exp(-masks_logits)) # sigmoid masks (masks 0.5).astype(np.uint8) # Step 4: resize masks to original size resized_masks [] for m in masks: # m.shape (640,640), resize to (h,w) m_resized cv2.resize(m, (w, h), interpolationcv2.INTER_NEAREST) resized_masks.append(m_resized) return boxes, scores, labels, resized_masks # Usage boxes, scores, labels, masks trt_outputs_to_cv2(img, [d_boxes, d_scores, d_labels, d_masks], (orig_h, orig_w))参数说明interpolationcv2.INTER_NEARESTmask 是 binary必须用 nearestbilinear 会引入灰度值np.clip(boxes, 0, None)防止归一化误差导致负坐标OpenCV draw 会 crashscores 0.25TRT 输出 scores 未经过 softmaxYOLOv11 用 focal losshead 输出即 final score阈值需按验证集 calibrate。5.3 OpenCV 可视化带透明 mask 的 bbox 绘制def draw_instance_segmentation(image, boxes, scores, labels, masks, class_names): overlay image.copy() for i, (box, score, label, mask) in enumerate(zip(boxes, scores, labels, masks)): x1, y1, x2, y2 map(int, box) color (0, 255, 0) if label 0 else (255, 0, 0) # person vs car # Draw bbox cv2.rectangle(image, (x1,y1), (x2,y2), color, 2) cv2.putText(image, f{class_names[label]} {score:.2f}, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) # Draw mask with alpha0.4 mask_3ch np.stack([mask, mask, mask], axis2) overlay[mask1] color cv2.addWeighted(overlay, 0.4, image, 0.6, 0, image) return image # class_names [person, car, dog, ...] (80 classes) result_img draw_instance_segmentation( img.copy(), boxes, scores, labels, masks, class_names )注意cv2.addWeighted()的 alpha 必须 ≤0.5否则 mask 边缘会过曝mask_3ch必须uint8否则addWeighted报错。6. 验证与调优用 COCO AP 与逐帧 latency 双指标闭环验证6.1 构建 TRT 推理的黄金验证集COCO-val2017 子集 deterministic letterbox要证明 TRT 实例分割不是“看起来快其实不准”必须用标准 benchmark。我们不跑 full COCO-val2017太慢而是构建一个100 张图的 deterministic 子集满足每张图 resize 到640×640时letterbox padding 严格一致左/上 pad 为 0右/bottom pad 补零所有图的orig_shape记录真实 H/W用于后处理反算使用pycocotools的COCOeval计算 AP0.5:0.95。# val_trt.py from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval def run_trt_coco_eval(trt_engine_path, coco_ann_file, img_dir): coco_gt COCO(coco_ann_file) coco_dt [] # will store detections in COCO format # Load TRT engine and buffers engine load_engine(trt_engine_path) context engine.create_execution_context() buffers allocate_buffers() for img_id in tqdm(coco_gt.getImgIds()[:100]): # first 100 images ann_ids coco_gt.getAnnIds(imgIdsimg_id) img_info coco_gt.loadImgs(img_id)[0] img_path os.path.join(img_dir, img_info[file_name]) img cv2.imread(img_path) orig_h, orig_w img.shape[:2] # Preprocess: deterministic letterbox to 640x640 img_preproc, ratio, pad letterbox(img, (640,640), autoFalse, scaleFillFalse) # ↑ autoFalse ensures pad is always (top,left,bottom,right) (0,0,?,?) # Copy to device np.copyto(buffers.h_images, img_preproc.astype(np.float32).transpose(2,0,1).flatten()) cudaMemcpy(buffers.d_images, buffers.h_images, ...) # Set binding dims context.setBindingDimensions(0, Dims4{1,3,640,640}) context.setBindingDimensions(4, Dims2{1,2}) # orig_shape # Execute context.enqueueV2(buffers.bindings, stream, nullptr) cudaStreamSynchronize(stream) # Postprocess convert to COCO dt format outputs [copy_from_device(b) for b in [d_boxes,d_scores,d_labels,d_masks]] boxes, scores, labels, masks trt_outputs_to_cv2(img, outputs, (orig_h, orig_w)) for i, (box, score, label, mask) in enumerate(zip(boxes,scores,labels,masks)): # RLE encode mask rle maskUtils.encode(np.asfortranarray(mask)) rle[counts] rle[counts].decode(ascii) coco_dt.append({ image_id: img_id, category_id: int(label)1, # COCO is 1-based bbox: [float(box[0]), float(box[1]), float(box[2]-box[0]), float(box[3]-box[1])], score: float(score), segmentation: rle }) # Evaluate coco_dt coco_gt.loadRes(coco_dt) coco_eval COCOeval(coco_gt, coco_dt, segm) coco_eval.evaluate() coco_eval.accumulate() coco_eval.summarize() return coco_eval.stats[0] # AP0.5:0.95关键控制点letterbox(..., autoFalse)禁用自动 pad保证所有图的 padding pattern 一致消除 preproc 引入的 variancemaskUtils.encode()必须用pycocotools官方 RLE否则COCOeval无法 decodecategory_id label1COCO 标准要求类别从 1 开始YOLOv11 输出是 0-based。6.2 latency profiling用cudaEvent打点每一环节光看 end-to-end latency 不够必须定位瓶颈。我们在关键节点插入cudaEventEvent位置期望耗时Orin AGXevent_preproc_startpreproc 后cudaMemcpy前—event_d2h_startcudaMemcpyAsync(d-h)启动时—event_postproc_endmask resize 完成时5msevent_total_end整个函数返回前—cudaEvent_t event_preproc_start, event_d2h_start, event_postproc_end, event_total_end; cudaEventCreate(event_preproc_start); cudaEventCreate(event_d2h_start); cudaEventCreate(event_postproc_end); cudaEventCreate(event_total_end); // In inference loop: cudaEventRecord(event_preproc_start, stream); cudaMemcpyAsync(d_images, h_images, ..., stream); cudaEventRecord(event_d2h_start, stream); context-enqueueV2(...); cudaEventRecord(event_postproc_end, stream); // ... postprocess ... cudaEventRecord(event_total_end, stream); // Calculate float ms_preproc, ms_d2h, ms_postproc, ms_total; cudaEventElapsedTime(ms_preproc, event_preproc_start, event_d2h_start); cudaEventElapsedTime(ms_d2h, event_d2h_start, event_postproc_end); cudaEventElapsedTime(ms_postproc, event_postproc_end, event_total_end); cudaEventElapsedTime(ms_total, event_preproc_start, event_total_end);典型结果Orin AGX, FP16环节耗时占比优化方向preproc4.2 ms11%用cv::dnn::blobFromImage替代 hand-written letterboxTRT execute28.5 ms75%检查是否启用kENABLE_FP16确认 plugin kernel 无 bank conflictd2h postproc5.3 ms14%cudaMemcpyAsync stream避免同步等待6.3 最终调优 checklist从代码到硬件的七条铁律永远用cudaEvent而非std::chrono测 latency后者测的是 host 时间受 CPU 调度干扰cudaEvent测的是 GPU 真实执行时间。TRT engine 必须用--fp16构建但 mask head 的 interpolate 层在 plugin 中用float计算混合精度是 TRT 实例分割的唯一可行路径。d_masksbuffer 必须cudaMalloc绝不用cudaMallocManaged这是 Orin 上 latency 稳定性的分水岭。orig_shape必须作为 input binding 传入不能 hardcode 或从 context 读取本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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