在深度学习模型部署的工程实践中,我们经常面临一个关键问题:如何将训练好的模型高效地集成到实际应用中?以YOLOv5目标检测模型为例,虽然Python生态提供了便捷的训练和推理接口,但在生产环境中,我们往往需要更高的执行效率和更低的资源占用。这就是C++结合TensorRT运行时发挥价值的地方。
通过将YOLOv5推理功能封装为C++动态库(.so文件),我们可以获得以下优势:
我在实际项目中测量发现,同样的YOLOv5模型,使用C++ TensorRT实现相比原生PyTorch Python实现,推理速度提升3-5倍,显存占用减少40%。这对于需要处理高并发请求的服务端应用尤为重要。
项目的核心架构分为三个层次:
plaintext复制┌───────────────────────┐
│ Python调用端 │
└──────────┬────────────┘
│ ctypes调用
┌──────────▼────────────┐
│ C接口封装层 (yolo.h) │
└──────────┬────────────┘
│ 内部调用
┌──────────▼────────────┐
│ 核心逻辑层 (yolo.cpp) │
├───────────────────────┤
│ • 模型初始化 │
│ • 图像预处理 │
│ • TensorRT推理 │
│ • 后处理 │
└──────────┬────────────┘
│ 依赖
┌──────────▼────────────┐
│ CUDA + TensorRT运行时 │
└───────────────────────┘
动态库接口设计遵循以下原则:
extern "C"避免C++名称修饰(name mangling)__attribute__((visibility("default")))确保符号可见cpp复制// 示例:典型的导出函数声明
extern "C" {
__attribute__((visibility("default")))
bool yolo_init(const char* engine_path);
__attribute__((visibility("default")))
void yolo_infer(const unsigned char* input, int height, int width,
float** output, int* output_len);
}
跨语言调用时的内存管理需要特别注意:
cpp复制// 输出内存分配示例
void yolo_infer(..., float** output, int* output_len) {
*output = new float[OUTPUT_SIZE]; // 在堆上分配
*output_len = OUTPUT_SIZE;
// ...填充数据...
}
// 配套的释放函数
void yolo_free_result(float* output) {
delete[] output; // 释放堆内存
}
TensorRT引擎初始化是推理流程的基础,主要步骤包括:
nvinfer1::IRuntimecpp复制bool yolo_init(const char* engine_path) {
// 1. 加载引擎文件
auto engine_data = load_file(engine_path);
// 2. 创建运行时
nvinfer1::IRuntime* runtime = nvinfer1::createInferRuntime(logger);
// 3. 反序列化引擎
engine = runtime->deserializeCudaEngine(engine_data.data(), engine_data.size());
// 4. 创建执行上下文
execution_context = engine->createExecutionContext();
// 5. 创建CUDA流
cudaStreamCreate(&stream);
return engine && execution_context && stream;
}
关键注意事项:
- 引擎文件路径应为绝对路径,避免相对路径导致的加载失败
- 反序列化后的引擎对象需要在整个生命周期保持有效
- 每个执行上下文(ExecutionContext)不是线程安全的,需要根据需要创建多个
YOLOv5的预处理包含两个关键操作:
cpp复制void LetterBox(const cv::Mat& image, cv::Mat& outImage) {
// 计算缩放比例
float r = min(INPUT_WIDTH / (float)image.cols,
INPUT_HEIGHT / (float)image.rows);
// 计算填充尺寸
int new_un_pad[2] = {
(int)round(image.cols * r),
(int)round(image.rows * r)
};
// 执行缩放
cv::resize(image, outImage, Size(new_un_pad[0], new_un_pad[1]));
// 添加边框
int dw = INPUT_WIDTH - new_un_pad[0];
int dh = INPUT_HEIGHT - new_un_pad[1];
cv::copyMakeBorder(outImage, outImage,
dh/2, dh - dh/2,
dw/2, dw - dw/2,
cv::BORDER_CONSTANT,
cv::Scalar(114, 114, 114));
}
预处理性能优化技巧:
cuda::resize)完整的推理流程包含以下步骤:
executeV2启动推理cpp复制void yolo_infer(const unsigned char* input, ...) {
// 1. 分配主机和设备内存
float *input_host, *output_host;
cudaMallocHost(&input_host, input_size);
cudaMalloc(&input_device, input_size);
// 2. 执行预处理
pre_process(image, input_host);
// 3. 主机到设备拷贝
cudaMemcpyAsync(input_device, input_host,
input_size, cudaMemcpyHostToDevice, stream);
// 4. 设置绑定
void* bindings[] = {input_device, output_device};
// 5. 执行推理
context->executeV2(bindings);
// 6. 设备到主机拷贝
cudaMemcpyAsync(output_host, output_device,
output_size, cudaMemcpyDeviceToHost, stream);
// 7. 同步流
cudaStreamSynchronize(stream);
}
性能关键点:
- 使用
cudaMemcpyAsync实现异步传输- 复用CUDA流减少同步开销
- 批处理(batch)推理可显著提高吞吐量
YOLOv5的后处理主要包括:
cpp复制std::vector<float> post_process(float* output_data) {
vector<Rect> boxes;
vector<float> scores;
// 1. 解析输出
for (int i = 0; i < num_boxes; ++i) {
float* box_ptr = output_data + i * box_size;
float confidence = box_ptr[4];
if (confidence < confidence_threshold)
continue;
// 提取框坐标
float x = box_ptr[0], y = box_ptr[1];
float w = box_ptr[2], h = box_ptr[3];
boxes.emplace_back(x-w/2, y-h/2, w, h);
scores.push_back(confidence);
}
// 2. 执行NMS
vector<int> indices;
cv::dnn::NMSBoxes(boxes, scores, confidence_thresh, nms_thresh, indices);
// 3. 返回最终结果
vector<float> final_results;
for (int idx : indices) {
auto& box = boxes[idx];
final_results.push_back(box.x);
final_results.push_back(box.y);
final_results.push_back(box.x + box.width);
final_results.push_back(box.y + box.height);
}
return final_results;
}
后处理优化建议:
Python通过ctypes调用C++动态库的关键步骤:
python复制import ctypes
# 1. 加载动态库
lib = ctypes.CDLL("/path/to/libtrt_infer.so")
# 2. 定义函数原型
lib.yolo_init.argtypes = [ctypes.c_char_p]
lib.yolo_init.restype = ctypes.c_bool
lib.yolo_infer.argtypes = [
ctypes.POINTER(ctypes.c_ubyte), # 输入图像数据
ctypes.c_int, ctypes.c_int, # 图像高度和宽度
ctypes.POINTER(ctypes.POINTER(ctypes.c_float)), # 输出指针
ctypes.POINTER(ctypes.c_int) # 输出长度
]
Python与C++间的内存交互需要特别注意:
python复制def infer(image):
# 确保输入是连续的内存块
if not image.flags['C_CONTIGUOUS']:
image = np.ascontiguousarray(image)
# 准备输出参数
output_ptr = ctypes.POINTER(ctypes.c_float)()
output_len = ctypes.c_int()
# 调用推理
lib.yolo_infer(
image.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)),
image.shape[0], image.shape[1],
ctypes.byref(output_ptr), ctypes.byref(output_len)
)
# 拷贝结果并释放内存
try:
output = np.ctypeslib.as_array(output_ptr, shape=(output_len.value,))
return output.copy()
finally:
lib.yolo_free_result(output_ptr)
python复制from concurrent.futures import ThreadPoolExecutor
class InferBatch:
def __init__(self, lib_path, batch_size=4):
self.lib = ctypes.CDLL(lib_path)
self.pool = ThreadPoolExecutor(max_workers=batch_size)
def infer_async(self, image):
return self.pool.submit(self._infer_single, image)
def _infer_single(self, image):
# 实际的推理实现...
pass
完整的CMake配置需要处理以下依赖:
cmake复制cmake_minimum_required(VERSION 3.12)
project(trt_inference)
# 设置C++标准
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找CUDA
find_package(CUDA REQUIRED)
include_directories(${CUDA_INCLUDE_DIRS})
# 查找OpenCV
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
# 手动指定TensorRT路径
set(TENSORRT_DIR "/path/to/TensorRT")
include_directories(${TENSORRT_DIR}/include)
link_directories(${TENSORRT_DIR}/lib)
# 创建动态库
add_library(trt_infer SHARED
src/common.cpp
src/yolo.cpp
)
# 链接依赖库
target_link_libraries(trt_infer
PRIVATE
${CUDA_LIBRARIES}
${OpenCV_LIBS}
nvinfer
nvinfer_plugin
)
Linux:
-fPIC编译选项.so文件的运行时路径(RPATH)Windows:
__declspec(dllexport)替代__attribute__.dllMacOS:
.dylib依赖打包:
ldd检查动态库依赖版本兼容:
性能调优:
trtexec工具分析性能瓶颈问题现象:引擎初始化失败
排查步骤:
bash复制# 检查动态库依赖
ldd libtrt_infer.so
# 验证CUDA版本
nvcc --version
# 检查TensorRT版本
dpkg -l | grep tensorrt
典型性能瓶颈:
优化方案:
cpp复制// 异步传输示例
cudaMemcpyAsync(dev_ptr, host_ptr, size, cudaMemcpyHostToDevice, stream);
// 可以立即执行其他CPU工作
do_cpu_work();
// 需要结果时再同步
cudaStreamSynchronize(stream);
常见内存错误:
解决方案:
cpp复制// RAII包装示例
class ManagedArray {
public:
ManagedArray(size_t size) : ptr(new float[size]) {}
~ManagedArray() { delete[] ptr; }
float* get() { return ptr; }
private:
float* ptr;
};
通过抽象接口实现多模型加载:
cpp复制class ModelBase {
public:
virtual bool init(const char* model_path) = 0;
virtual void infer(const void* input, void* output) = 0;
virtual ~ModelBase() = default;
};
class YoloModel : public ModelBase {
// 具体实现...
};
// 工厂函数
extern "C" ModelBase* create_model(const char* type);
修改接口支持批量推理:
cpp复制void yolo_infer_batch(
const unsigned char** inputs, // 输入数组
const int* heights, // 各图像高度数组
const int* widths, // 各图像宽度数组
int batch_size, // 批大小
float** outputs, // 输出数组
int* output_lens // 各输出长度数组
);
使用pybind11创建更友好的Python接口:
cpp复制#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
PYBIND11_MODULE(trt_infer, m) {
m.def("init", &yolo_init);
m.def("infer", [](py::array_t<uint8_t> input) {
// 自动类型转换和处理
});
}
在实际项目中,我发现这套架构不仅能用于YOLOv5,经过适当调整后也成功应用于其他CV模型如ResNet、EfficientDet等的部署。关键是要保持接口的通用性和内存管理的清晰性。