1. 项目概述:从模型到应用的最后一步
在深度学习的实际落地过程中,我们常常会遇到一个“最后一公里”的难题:实验室里训练好的、在Python环境下跑得飞起的模型,如何变成一个能在生产环境中稳定、高效运行的独立应用?特别是当我们面对的是C++这样的高性能、低延迟、资源受限的部署环境时,这个问题就变得尤为突出。今天要聊的,就是如何将一个经典的图像分类模型——VggNet,通过OnnxRuntime这个强大的推理引擎,部署到C++环境中。
VggNet,这个由牛津大学视觉几何组提出的深度卷积网络,虽然现在看起来参数量大、计算成本高,但其结构清晰、规整,是理解卷积神经网络和模型部署流程的绝佳范例。而OnnxRuntime,作为一个跨平台、高性能的推理引擎,支持包括CPU、GPU在内的多种硬件加速,并且对ONNX模型格式提供了原生支持,成为了连接训练框架(如PyTorch, TensorFlow)与生产环境(如C++服务端、嵌入式设备)的桥梁。
这个项目的核心价值在于,它打通了从模型训练到C++应用落地的完整链路。无论你是想为桌面应用添加一个智能识图功能,还是为服务器后端构建一个高并发的图像分类服务,亦或是在资源受限的边缘设备上运行AI模型,这套技术栈都提供了一个经过验证的、可靠的解决方案。整个过程涉及模型转换、环境搭建、推理代码编写、性能优化等多个环节,每一个环节都有不少细节和“坑”需要留意。接下来,我就结合自己的实操经验,把这套流程掰开揉碎了讲清楚。
2. 核心工具链选型与原理剖析
2.1 为什么是ONNX与OnnxRuntime?
在模型部署的世界里,格式统一和运行效率是两个核心诉求。各大训练框架(PyTorch, TensorFlow, PaddlePaddle)都有自己的模型保存格式和运行时环境,直接互操作非常困难。ONNX(Open Neural Network Exchange)的出现就是为了解决这个问题。它是一个开放的模型表示标准,定义了一套通用的计算图格式和算子集。你可以把PyTorch训练的模型导出为.onnx文件,这个文件就包含了网络的结构、权重以及数据流信息,独立于任何具体的训练框架。
有了标准的中间格式,还需要一个高效的“执行器”来运行它,这就是OnnxRuntime。它不是一个训练框架,而是一个专门为推理优化的运行时。其优势非常明显:
- 高性能:底层针对不同硬件(x86 CPU, ARM CPU, NVIDIA GPU, 等)进行了深度优化,使用了算子融合、内存复用、多线程并行等多种技术,推理速度往往比直接用原训练框架的推理接口要快。
- 跨平台:提供C++, C, C#, Java, Python等多种语言的API,可以在Windows, Linux, macOS以及各种嵌入式系统上运行。
- 轻量级:作为推理引擎,它比完整的训练框架要小巧得多,依赖更少,更适合集成到最终的应用中。
- 扩展性:支持通过“Execution Provider”机制接入更底层的硬件加速库,比如在NVIDIA GPU上可以接入CUDA和TensorRT,在Intel CPU上可以接入OpenVINO,从而获得极致的性能。
选择OnnxRuntime for C++,意味着我们可以在保持高性能的同时,获得C++带来的系统级控制能力、低内存开销以及与现有C++项目无缝集成的便利。
2.2 VggNet模型简析与部署考量
VggNet的核心思想是使用一系列非常小的卷积核(3x3)和池化层(2x2)来构建深度网络。常见的Vgg16和Vgg19分别有16层和19层带参数的层(卷积或全连接)。虽然其结构规整,但全连接层参数巨大,导致模型文件体积庞大(Vgg16的权重文件约500MB+)。
在部署时,我们需要特别关注以下几点:
- 模型简化:原始的VggNet包含用于分类的1000类ImageNet全连接头。在实际应用中,我们可能需要根据自定义数据集修改分类头。部署前,应在训练框架中完成模型结构的最终调整。
- 输入输出规范:ONNX模型对输入输出的张量形状、数据类型有严格定义。对于VggNet,通常输入是
[1, 3, 224, 224]的浮点型张量(Batch, Channels, Height, Width),且数值需要经过归一化(例如,减去均值[0.485, 0.456, 0.406]并除以标准差[0.229, 0.224, 0.225])。输出是[1, 1000]的向量,表示1000个类别的得分(logits)。 - 计算图优化:OnnxRuntime在加载ONNX模型时,会进行一系列图优化,比如将连续的卷积、批归一化、激活函数融合成单个算子,这能显著提升推理速度。我们需要了解这些优化,并在必要时验证其正确性。
2.3 C++环境与依赖管理
在C++侧,我们的主要工作就是调用OnnxRuntime的C++ API来加载模型、准备输入数据、执行推理、解析输出。这要求我们搭建一个合适的C++开发环境。
- 编译器:推荐使用支持C++11及以上标准的编译器,如GCC (>=7), Clang (>=5) 或 MSVC (Visual Studio 2019+)。在Windows上,使用Visual Studio会非常方便;在Linux/macOS上,GCC或Clang是标准选择。
- 构建系统:现代C++项目强烈推荐使用CMake进行构建管理。它可以方便地处理依赖查找、编译选项设置、跨平台编译等问题。我们将使用CMake来定位和链接OnnxRuntime的库文件。
- OnnxRuntime C++库:我们需要从OnnxRuntime的GitHub Release页面下载预编译的库,或者从源码编译。对于大多数用户,直接下载预编译版本更快捷。需要根据你的操作系统(Windows/Linux)、架构(x64/arm64)和是否启用GPU(CUDA)来选择合适的包。下载后,你会得到包含头文件(
include)和库文件(lib)的目录。
注意:务必确保下载的OnnxRuntime库的版本与你导出ONNX模型时使用的运行时/转换器版本兼容。虽然ONNX标准向前兼容,但某些新算子可能需要新版本的运行时支持。
3. 完整部署流程实操详解
3.1 第一步:模型训练与ONNX导出
部署的起点是一个训练好的模型。假设我们使用PyTorch,并且已经有一个训练好的、调整好分类头的VggNet模型(model.pth)。
import torch import torchvision.models as models import onnx import onnxruntime as ort # 1. 加载训练好的PyTorch模型 model = models.vgg16(pretrained=False) # 不使用预训练权重,使用我们自己训练的 num_ftrs = model.classifier[6].in_features model.classifier[6] = torch.nn.Linear(num_ftrs, 10) # 假设我们改为10分类 model.load_state_dict(torch.load('model.pth')) model.eval() # 至关重要!切换到评估模式 # 2. 准备一个示例输入张量(dummy input) # 形状必须和实际推理时一致:[batch_size, channels, height, width] dummy_input = torch.randn(1, 3, 224, 224) # 3. 导出为ONNX格式 # `export_params`: 导出模型权重 # `opset_version`: ONNX算子集版本,建议>=11 # `do_constant_folding`: 进行常量折叠优化 # `input_names`, `output_names`: 指定输入输出节点名称,后续C++代码会用到 torch.onnx.export(model, dummy_input, "vgg16_10cls.onnx", export_params=True, opset_version=11, do_constant_folding=True, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, # 支持动态batch 'output': {0: 'batch_size'}}) print("Model has been converted to ONNX.") # 4. (可选但推荐) 使用onnxruntime验证导出的模型 ort_session = ort.InferenceSession('vgg16_10cls.onnx') outputs = ort_session.run(None, {'input': dummy_input.numpy()}) print("ONNX model runs successfully, output shape:", outputs[0].shape)关键点解析:
model.eval():这将关闭Dropout和BatchNorm层的训练模式,确保推理行为的确定性。- 动态轴:通过
dynamic_axes参数,我们允许输入输出的第0维(batch维度)是动态的。这在处理可变批量大小的请求时非常有用。但请注意,动态形状可能会限制某些图优化的进行。 - 验证:使用OnnxRuntime的Python API快速验证导出的模型能正确运行,这是一个很好的习惯,能提前发现导出问题。
3.2 第二步:C++项目工程结构搭建
一个清晰的工程结构能让后续开发维护更轻松。建议如下:
VggNet_CPP_Deployment/ ├── CMakeLists.txt # 项目根CMake配置文件 ├── src/ │ ├── main.cpp # 主程序入口 │ ├── classifier.h # 分类器类声明 │ └── classifier.cpp # 分类器类实现 ├── include/ # (可选) 存放第三方库头文件 ├── lib/ # (可选) 存放第三方库文件 ├── models/ │ └── vgg16_10cls.onnx # 转换好的ONNX模型 ├── images/ # 测试图片 └── build/ # 编译输出目录(CMake生成)3.3 第三步:编写CMakeLists.txt
这是项目的构建核心。我们需要告诉CMake如何找到OnnxRuntime库。
cmake_minimum_required(VERSION 3.16) project(VggNetOnnxDeployment LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 假设OnnxRuntime库解压到了项目根目录下的 `onnxruntime-linux-x64-1.14.1` (Linux示例) set(ONNXRUNTIME_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/onnxruntime-linux-x64-1.14.1) # 包含头文件目录 include_directories(${ONNXRUNTIME_ROOT_DIR}/include) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) # 包含自己的src目录 # 查找并链接库文件 add_library(onnxruntime SHARED IMPORTED) set_target_properties(onnxruntime PROPERTIES IMPORTED_LOCATION ${ONNXRUNTIME_ROOT_DIR}/lib/libonnxruntime.so.1.14.1 # Linux .so 文件 # 在Windows上可能是:${ONNXRUNTIME_ROOT_DIR}/lib/onnxruntime.dll # 在macOS上可能是:${ONNXRUNTIME_ROOT_DIR}/lib/libonnxruntime.1.14.1.dylib ) # 添加可执行文件 add_executable(vgg_inference src/main.cpp src/classifier.cpp) # 链接库 target_link_libraries(vgg_inference onnxruntime) # 在Windows上,可能需要额外链接其他库,如onnxruntime_providers_cuda等实操心得:不同平台、不同版本的OnnxRuntime库名和路径可能不同。务必根据你下载的包实际情况修改
IMPORTED_LOCATION。一个更稳健的做法是使用find_library,或者将OnnxRuntime的lib目录加入link_directories。
3.4 第四步:实现C++推理类(Classifer)
我们将推理逻辑封装成一个类,提高代码的复用性和可读性。classifier.h:
#ifndef CLASSIFIER_H #define CLASSIFIER_H #include <string> #include <vector> #include <memory> // 前向声明,避免包含OnnxRuntime头文件污染全局 struct OrtApi; struct OrtEnv; struct OrtSession; struct OrtSessionOptions; struct OrtMemoryInfo; struct OrtValue; class Classifier { public: // 构造函数:传入模型路径 explicit Classifier(const std::string& model_path); // 析构函数:释放资源 ~Classifier(); // 禁用拷贝构造和赋值 Classifier(const Classifier&) = delete; Classifier& operator=(const Classifier&) = delete; // 核心推理函数 // 输入:图像数据指针(假设为HWC排列的RGB数据),图像宽高 // 输出:最可能的类别索引及其置信度 std::pair<int, float> infer(const unsigned char* image_data, int width, int height); // 获取模型要求的输入尺寸 std::pair<int, int> get_input_size() const { return {input_height_, input_width_}; } int get_input_channels() const { return input_channels_; } private: // 初始化OnnxRuntime环境 void init_ort_env(); // 预处理图像:调整大小、归一化、转换为CHW张量 std::vector<float> preprocess(const unsigned char* image_data, int width, int height); // 后处理输出:从输出张量中解析出类别和分数 std::pair<int, float> postprocess(const std::vector<float>& output_tensor); // OnnxRuntime相关资源 std::unique_ptr<OrtEnv, decltype(&OrtReleaseEnv)> ort_env_{nullptr, OrtReleaseEnv}; std::unique_ptr<OrtSession, decltype(&OrtReleaseSession)> ort_session_{nullptr, OrtReleaseSession}; std::unique_ptr<OrtSessionOptions, decltype(&OrtReleaseSessionOptions)> session_options_{nullptr, OrtReleaseSessionOptions}; std::unique_ptr<OrtMemoryInfo, decltype(&OrtReleaseMemoryInfo)> memory_info_{nullptr, OrtReleaseMemoryInfo}; // 模型输入输出信息 std::vector<const char*> input_names_; std::vector<const char*> output_names_; std::vector<int64_t> input_shape_; int input_channels_; int input_height_; int input_width_; // 预处理参数(ImageNet标准) const std::vector<float> mean_ = {0.485f, 0.456f, 0.406f}; const std::vector<float> std_ = {0.229f, 0.224f, 0.225f}; }; #endif // CLASSIFIER_Hclassifier.cpp的实现是核心,由于篇幅限制,这里展示关键部分:
#include "classifier.h" #include <onnxruntime_cxx_api.h> // OnnxRuntime C++ API #include <opencv2/opencv.hpp> // 使用OpenCV进行图像处理,需提前安装 #include <algorithm> #include <numeric> #include <stdexcept> // 辅助函数:释放OrtValue struct OrtValueDeleter { void operator()(OrtValue* ptr) const { OrtReleaseValue(ptr); } }; using OrtValuePtr = std::unique_ptr<OrtValue, OrtValueDeleter>; Classifier::Classifier(const std::string& model_path) { // 1. 初始化全局环境(一个进程通常只需一个) Ort::InitApi(); ort_env_ = Ort::Env(ORT_LOGGING_LEVEL_WARNING, "VggNetInference"); // 2. 创建会话选项 Ort::SessionOptions session_options; // 可以在此设置线程数、优化级别、启用CUDA等 // session_options.SetIntraOpNumThreads(4); // session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); // 如果使用CUDA: Ort::ThrowOnError(OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0)); // 3. 加载模型,创建会话 ort_session_ = Ort::Session(ort_env_, model_path.c_str(), session_options); // 4. 获取模型输入输出信息 Ort::AllocatorWithDefaultOptions allocator; size_t num_input_nodes = ort_session_.GetInputCount(); if (num_input_nodes != 1) throw std::runtime_error("模型输入数量不为1"); auto input_name = ort_session_.GetInputName(0, allocator); input_names_ = {input_name}; allocator.Free(input_name); auto input_type_info = ort_session_.GetInputTypeInfo(0); auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo(); input_shape_ = input_tensor_info.GetShape(); // 处理可能的动态维度(-1),这里我们假设固定尺寸或将其固定 for (auto& dim : input_shape_) { if (dim < 0) dim = 1; // 将动态batch维度固定为1 } if (input_shape_.size() != 4) throw std::runtime_error("输入不是4维张量"); // 假设形状为 [N, C, H, W] input_channels_ = input_shape_[1]; input_height_ = input_shape_[2]; input_width_ = input_shape_[3]; // 获取输出信息 size_t num_output_nodes = ort_session_.GetOutputCount(); auto output_name = ort_session_.GetOutputName(0, allocator); output_names_ = {output_name}; allocator.Free(output_name); } std::vector<float> Classifier::preprocess(const unsigned char* image_data, int width, int height) { // 使用OpenCV将原始数据转换为Mat cv::Mat image(height, width, CV_8UC3, const_cast<unsigned char*>(image_data)); // 注意:这里不复制数据 cv::Mat resized, float_image; // 1. 调整尺寸到模型输入大小 cv::resize(image, resized, cv::Size(input_width_, input_height_)); // 2. 将像素值从[0,255]转换为[0.0, 1.0] resized.convertTo(float_image, CV_32FC3, 1.0 / 255.0); // 3. 归一化 (减去均值,除以标准差) // OpenCV的split和merge可以处理,但为了清晰,我们遍历像素 std::vector<float> input_tensor(input_channels_ * input_height_ * input_width_); for (int c = 0; c < input_channels_; ++c) { for (int h = 0; h < input_height_; ++h) { for (int w = 0; w < input_width_; ++w) { cv::Vec3f pixel = float_image.at<cv::Vec3f>(h, w); // 假设模型输入是RGB顺序,且mean/std也是针对RGB float value = pixel[2 - c]; // OpenCV是BGR,需要转换为RGB input_tensor[c * input_height_ * input_width_ + h * input_width_ + w] = (value - mean_[c]) / std_[c]; } } } return input_tensor; } std::pair<int, float> Classifier::infer(const unsigned char* image_data, int width, int height) { // 1. 预处理 std::vector<float> input_tensor = preprocess(image_data, width, height); // 2. 创建输入OrtValue auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); std::vector<int64_t> input_shape = {1, input_channels_, input_height_, input_width_}; Ort::Value input_tensor_ort = Ort::Value::CreateTensor<float>( memory_info, input_tensor.data(), input_tensor.size(), input_shape.data(), input_shape.size() ); // 3. 运行推理 auto output_tensors = ort_session_.Run( Ort::RunOptions{nullptr}, input_names_.data(), &input_tensor_ort, 1, output_names_.data(), 1 ); // 4. 获取输出数据 float* floatarr = output_tensors[0].GetTensorMutableData<float>(); auto output_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape(); size_t output_count = std::accumulate(output_shape.begin(), output_shape.end(), 1, std::multiplies<int64_t>()); // 5. 后处理:找到得分最高的类别 int max_index = std::max_element(floatarr, floatarr + output_count) - floatarr; float max_score = floatarr[max_index]; // 如果需要softmax概率,可以在此计算 // std::vector<float> scores(floatarr, floatarr + output_count); // std::transform(scores.begin(), scores.end(), scores.begin(), [](float v) { return std::exp(v); }); // float sum = std::accumulate(scores.begin(), scores.end(), 0.0f); // max_score = scores[max_index] / sum; return {max_index, max_score}; } Classifier::~Classifier() { // 智能指针会自动释放资源 }3.5 第五步:主程序与测试
main.cpp负责组织一切:
#include "classifier.h" #include <iostream> #include <chrono> int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " <image_path>" << std::endl; return -1; } std::string image_path = argv[1]; std::string model_path = "models/vgg16_10cls.onnx"; try { // 1. 初始化分类器 std::cout << "Loading model from: " << model_path << std::endl; Classifier classifier(model_path); auto [req_height, req_width] = classifier.get_input_size(); std::cout << "Model requires input size: " << req_width << "x" << req_height << std::endl; // 2. 使用OpenCV加载测试图片 cv::Mat image = cv::imread(image_path); if (image.empty()) { std::cerr << "Could not read the image: " << image_path << std::endl; return -1; } // 确保是3通道 if (image.channels() != 3) { cv::cvtColor(image, image, cv::COLOR_GRAY2BGR); } // 3. 执行推理并计时 auto start = std::chrono::high_resolution_clock::now(); auto [class_id, score] = classifier.infer(image.data, image.cols, image.rows); auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); // 4. 输出结果 std::cout << "Inference result:" << std::endl; std::cout << " Class ID: " << class_id << std::endl; std::cout << " Score: " << score << std::endl; std::cout << " Time cost: " << duration.count() << " ms" << std::endl; } catch (const std::exception& e) { std::cerr << "Error occurred: " << e.what() << std::endl; return -1; } return 0; }3.6 第六步:编译与运行
在项目根目录下:
mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j4编译成功后,在build目录下会生成可执行文件vgg_inference。运行它:
./vgg_inference ../images/cat.jpg4. 性能优化与高级配置
4.1 利用多线程与批处理
OnnxRuntime的会话选项(SessionOptions)允许我们配置并行计算策略。
SetIntraOpNumThreads(): 设置算子内部(如矩阵乘)的并行线程数。SetInterOpNumThreads(): 设置并行执行独立算子的线程数(如果模型有并行分支)。- 批处理:如果模型支持动态batch维度(我们在导出时设置了
dynamic_axes),我们可以一次性处理多张图片。这能更充分地利用CPU/GPU的并行计算能力,显著提高吞吐量。你需要修改预处理和输入张量创建逻辑,将多张图片的数据拼接成一个大的张量。
4.2 启用GPU加速(CUDA)
如果你的环境有NVIDIA GPU,启用CUDA可以带来巨大的速度提升。首先,你需要下载带有CUDA支持的OnnxRuntime包(如onnxruntime-linux-x64-gpu-1.14.1.tgz)。然后,在创建会话时添加CUDA执行提供器:
#include <onnxruntime_cxx_api.h> #include <cuda_provider_factory.h> // 需要包含此头文件 Ort::SessionOptions session_options; OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0); // 0表示设备ID // 然后再创建会话 ort_session_ = Ort::Session(env, model_path.c_str(), session_options);注意:使用GPU版本时,确保系统已安装对应版本的CUDA和cuDNN。输入输出数据会在CPU和GPU之间自动传输,但频繁的小数据量传输可能成为瓶颈,批处理能有效缓解这一问题。
4.3 使用TensorRT进一步优化(仅限NVIDIA)
对于NVIDIA平台,OnnxRuntime可以集成TensorRT作为后端执行提供器。TensorRT会对ONNX模型进行更激进的算子融合、精度校准(INT8)、层张量优化等,通常能获得比纯CUDA模式更高的性能。配置相对复杂,需要在会话选项中指定TensorRT提供器,并可能提供额外的配置文件。
4.4 模型量化与精简
VggNet模型较大,可以考虑进行模型量化,将FP32的权重和激活值转换为INT8,这能大幅减少模型体积和内存占用,并提升推理速度,尤其适合边缘设备。量化可以在训练后(Post-Training Quantization, PTQ)或训练中(Quantization-Aware Training, QAT)进行。OnnxRuntime提供了量化工具,可以将FP32的ONNX模型转换为INT8模型。
5. 常见问题排查与调试技巧
在实际部署中,你几乎一定会遇到各种问题。下面是一个快速排查指南:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 加载模型失败 | 1. 模型文件路径错误或损坏。 2. OnnxRuntime库版本与模型不兼容。 3. 模型包含不支持的算子。 | 1. 检查文件路径和权限。用Python的onnx.load()和onnx.checker.check_model()验证ONNX文件。2. 尝试使用不同版本的OnnxRuntime,或重新导出模型时指定更通用的 opset_version。3. 查看OnnxRuntime的错误信息,确认缺失的算子,考虑修改模型结构或自定义算子。 |
| 推理结果错误或NaN | 1. 预处理不一致(颜色通道、归一化参数)。 2. 输入数据布局错误(HWC vs CHW)。 3. 模型本身训练有问题。 | 1.确保C++预处理与训练/验证时的预处理完全一致。这是最常见的问题。仔细核对均值、标准差、缩放尺寸、插值方法。 2. 确认模型期望的输入是 CHW还是HWC。VggNet通常是CHW。3. 用同一张图片,分别用Python(原框架)和C++(OnnxRuntime)推理,对比中间某层的输出,定位差异出现的位置。 |
| 推理速度慢 | 1. 未启用优化或线程数设置不当。 2. 每次推理都重新分配输入输出内存。 3. 预处理耗时过长。 | 1. 在SessionOptions中设置SetGraphOptimizationLevel(ORT_ENABLE_ALL)和合适的线程数。2. 考虑复用输入输出 Ort::Value对象,避免重复分配。3. 对预处理(如图像缩放)进行性能分析,考虑使用更快的库(如libjpeg-turbo)或异步处理。 |
| 内存泄漏 | 1. OnnxRuntime对象未正确释放。 2. 预处理中动态分配的内存未释放。 | 1. 使用RAII(资源获取即初始化)方式管理Ort资源(如示例中的智能指针包装)。 2. 使用 valgrind(Linux) 或Dr. Memory(Windows) 等工具检测内存泄漏。 |
| GPU推理未生效 | 1. 未链接或未正确初始化CUDA提供器。 2. GPU内存不足。 | 1. 检查是否链接了onnxruntime_providers_cuda库,并在代码中调用了AppendExecutionProvider_CUDA。2. 在创建会话后,打印会话信息,确认使用的执行提供器是 CUDAExecutionProvider。3. 监控 nvidia-smi,查看GPU是否被占用以及内存使用情况。 |
调试技巧:
- 启用详细日志:在创建
Ort::Env时,将日志级别设置为ORT_LOGGING_LEVEL_VERBOSE,可以获取OnnxRuntime内部的详细执行信息。 - 使用Netron可视化模型:用Netron(一个网页工具)打开你的
.onnx文件,可以清晰看到整个计算图,确认输入输出节点的名称和形状,这对编写C++代码至关重要。 - 单元测试:为你的
Classifier类编写单元测试,使用固定的输入图片和已知的输出结果,确保代码修改后核心功能依然正确。
整个流程走下来,你会发现,虽然步骤不少,但每一步都有其明确的目的和最佳实践。从Python训练到C++部署,OnnxRuntime提供了一个坚实可靠的桥梁。掌握了这套流程,你部署的将不仅仅是VggNet,而是绝大多数基于ONNX格式的视觉、乃至其他类型的深度学习模型。