Postman便携版:构建零污染的API测试工作流
2026/5/4 20:15:07
voidinference(){// ------------------------------ 1. 准备模型并加载 ----------------------------TRTLogger logger;autoengine_data=load_file("engine.trtmodel");// 执行推理前,需要创建一个推理的runtime接口实例。与builer一样,runtime需要logger:nvinfer1::IRuntime*runtime=nvinfer1::createInferRuntime(logger);// 将模型从读取到engine_data中,则可以对其进行反序列化以获得enginenvinfer1::ICudaEngine*engine=runtime->deserializeCudaEngine(engine_data.data(),engine_data.size());if(engine==nullptr){printf("Deserialize cuda engine failed.\n");runtime->destroy();return;}nvinfer1::IExecutionContext*execution_context=engine->createExecutionContext();cudaStream_t stream=nullptr;// 创建CUDA流,以确定这个batch的推理是独立的cudaStreamCreate(&stream);/* Network definition: image | linear (fully connected) input = 3, output = 2, bias = True w=[[1.0, 2.0, 0.5], [0.1, 0.2, 0.5]], b=[0.3, 0.8] | sigmoid | prob */// ------------------------------ 2. 准备好要推理的数据并搬运到GPU ----------------------------floatinput_data_host[]={1,2,3};float*input_data_device=nullptr;floatoutput_data_host[2];float*output_data_device=nullptr;cudaMalloc(&input_data_device,sizeof(input_data_host));cudaMalloc(&output_data_device,sizeof(output_data_host));cudaMemcpyAsync(input_data_device,input_data_host,sizeof(input_data_host),cudaMemcpyHostToDevice,stream);// 用一个指针数组指定input和output在gpu中的指针。float*bindings[]={input_data_device,output_data_device};// ------------------------------ 3. 推理并将结果搬运回CPU ----------------------------boolsuccess=execution_context->enqueueV2((void**)bindings,stream,nullptr);cudaMemcpyAsync(output_data_host,output_data_device,sizeof(output_data_host),cudaMemcpyDeviceToHost,stream);cudaStreamSynchronize(stream);printf("output_data_host = %f, %f\n",output_data_host[0],output_data_host[1]);// ------------------------------ 4. 释放内存 ----------------------------printf("Clean memory\n");cudaStreamDestroy(stream);execution_context->destroy();engine->destroy();runtime->destroy();// ------------------------------ 5. 手动推理进行验证 ----------------------------constintnum_input=3;constintnum_output=2;floatlayer1_weight_values[]={1.0,2.0,0.5,0.1,0.2,0.5};floatlayer1_bias_values[]={0.3,0.8};printf("手动验证计算结果:\n");for(intio=0;io<num_output;++io){floatoutput_host=layer1_bias_values[io];for(intii=0;ii<num_input;++ii){output_host+=layer1_weight_values[io*num_input+ii]*input_data_host[ii];}// sigmoidfloatprob=1/(1+exp(-output_host));printf("output_prob[%d] = %f\n",io,prob);}}先明确推理的核心目标:加载上一节课保存的engine.trtmodel,输入[1,2,3],让TRT算出输出,再手动计算验证结果是否一致。
// 辅助函数:读取二进制文件(比如engine.trtmodel)到内存#include<fstream>#include<vector>usingnamespacestd;vector<unsignedchar>load_file(conststring&file){ifstreamin(file,ios::in|ios::binary);if(!in.is_open())return{};in.seekg(0,ios::end);size_t length=in.tellg();vector<unsignedchar>data;if(length>0){in.seekg(0,ios::beg);data.resize(length);in.read((char*)&data[0],length);}in.close();returndata;}原代码的推理流程分为6个核心步骤,我们逐段讲透,补充每个步骤的“为什么”和“怎么来的”。
voidinference(){// ------------------------------ 1. 准备模型并加载 ----------------------------TRTLogger logger;// 日志对象:记录推理过程中的警告/错误(必须有)autoengine_data=load_file("engine.trtmodel");// 读取保存的模型文件到内存// 1.1 创建推理运行时(Runtime):相当于“机器的启动器”,负责加载和运行enginenvinfer1::IRuntime*runtime=nvinfer1::createInferRuntime(logger);// 1.2 反序列化engine:把二进制的模型文件还原成可运行的“机器本体”nvinfer1::ICudaEngine*engine=runtime->deserializeCudaEngine(engine_data.data(),engine_data.size());if(engine==nullptr){// 反序列化失败(比如模型文件损坏、TRT版本不匹配)printf("Deserialize cuda engine failed.\n");runtime->destroy();// 及时释放资源,避免内存泄漏return;}通俗解读:
IRuntime:机器的“启动器”——只有通过它,才能把硬盘里的engine.trtmodel(二进制文件)变成内存中可运行的ICudaEngine;deserializeCudaEngine:“组装机器”的过程——把二进制的“零件图纸”还原成能干活的“机器”;// 2.1 创建执行上下文(ExecutionContext):相当于“机器的操作面板”nvinfer1::IExecutionContext*execution_context=engine->createExecutionContext();// 2.2 创建CUDA流(Stream):相当于“独立的流水线”,异步处理推理任务cudaStream_t stream=nullptr;cudaStreamCreate(&stream);通俗解读:
IExecutionContext:一个engine(机器)可以创建多个execution_context(操作面板)——比如一台机器有多个操作面板,能同时给不同的工人用(支持多批次并行推理);CUDA流:cudaStreamCreate(&stream):创建一条空的流水线。/* 回顾上一节课的网络结构: 输入(3维) → 全连接层(2维) → Sigmoid → 输出(2维) */// 3.1 主机(CPU)上准备输入数据:原材料(比如[1,2,3])floatinput_data_host[]={1,2,3};float*input_data_device=nullptr;// GPU上的输入数据指针(空)// 3.2 主机(CPU)上准备输出缓存:装成品的空盒子floatoutput_data_host[2];float*output_data_device=nullptr;// GPU上的输出数据指针(空)// 3.3 给GPU分配内存:在GPU上造“原材料仓库”和“成品仓库”cudaMalloc(&input_data_device,sizeof(input_data_host));cudaMalloc(&output_data_device,sizeof(output_data_host));// 3.4 异步拷贝数据:把CPU的原材料搬到GPU的仓库(丢到流里,异步执行)cudaMemcpyAsync(input_data_device,input_data_host,sizeof(input_data_host),cudaMemcpyHostToDevice,stream);// 3.5 定义bindings(绑定):告诉机器“原材料仓库”和“成品仓库”的地址// bindings顺序:必须和构建模型时addInput、markOutput的顺序一致!float*bindings[]={input_data_device,output_data_device};关键细节(新手必看):
bindings是推理的核心——相当于机器的“输入输出接口”:addInput("image", ...)(第一个接口),再markOutput(...)(第二个接口);bindings[0]必须是输入的GPU指针,bindings[1]必须是输出的GPU指针;addInput和markOutput的顺序,否则推理结果错乱或报错。cudaMemcpyAsyncvscudaMemcpy:cudaMemcpy:同步拷贝——CPU等GPU拷贝完才继续;cudaMemcpyAsync:异步拷贝——把拷贝任务丢到流里,CPU立刻继续执行下一行代码,效率更高;stream:指定拷贝任务丢到哪条流水线。// 4.1 启动异步推理:按下机器的“启动键”,把任务丢到流里boolsuccess=execution_context->enqueueV2((void**)bindings,stream,nullptr);// 4.2 异步拷贝结果:把GPU成品仓库的东西搬回CPU的空盒子(丢到流里)cudaMemcpyAsync(output_data_host,output_data_device,sizeof(output_data_host),cudaMemcpyDeviceToHost,stream);// 4.3 同步流:等待流里的所有任务(拷贝输入、推理、拷贝输出)都做完cudaStreamSynchronize(stream);// 4.4 打印推理结果printf("TensorRT推理结果:output_data_host = %f, %f\n",output_data_host[0],output_data_host[1]);通俗解读:
enqueueV2:“启动机器”的核心接口(V2对应显性batch size,上一节课用的createNetworkV2(1));(void**)bindings——输入输出的GPU指针数组(必须转成void**类型);stream——把推理任务丢到这条流水线;nullptr——暂时不用(是针对多线程的同步参数);cudaStreamSynchronize(stream):“等流水线干完活”——如果不加这行,CPU可能在GPU还没算完的时候就打印结果,拿到的是随机值;// 5.1 打印提示printf("Clean memory\n");// 5.2 释放资源:按“创建顺序倒序”释放,避免内存泄漏cudaStreamDestroy(stream);// 销毁流水线execution_context->destroy();// 销毁操作面板engine->destroy();// 销毁机器本体runtime->destroy();// 销毁启动器关键规则:
destroy(),不像C++普通对象会自动析构;cudaStreamDestroy,GPU内存如果是cudaMalloc分配的,也要cudaFree(原代码里漏了,补充修正:// 补充:释放GPU内存(原代码漏了,新手一定要加)cudaFree(input_data_device);cudaFree(output_data_device);为了确认TRT的推理结果没错,我们手动复现“全连接+Sigmoid”的计算过程:
// 6.1 定义和上一节课一致的权重、偏置constintnum_input=3;constintnum_output=2;floatlayer1_weight_values[]={1.0,2.0,0.5,0.1,0.2,0.5};// 2个输出×3个输入floatlayer1_bias_values[]={0.3,0.8};// 6.2 手动计算printf("\n手动验证计算结果:\n");for(intio=0;io<num_output;++io){// 遍历2个输出神经元// 第一步:计算全连接层输出 = 偏置 + Σ(权重×输入)floatoutput_host=layer1_bias_values[io];// 先加偏置for(intii=0;ii<num_input;++ii){// 遍历3个输入// 权重索引:io×num_input + ii → 第io个输出神经元的第ii个输入权重output_host+=layer1_weight_values[io*num_input+ii]*input_data_host[ii];}// 第二步:Sigmoid激活 = 1 / (1 + e^(-x))floatprob=1/(1+exp(-output_host));printf("output_prob[%d] = %f\n",io,prob);}}手动计算过程(代入数值):
我们把输入[1,2,3]代入,一步步算,验证和TRT结果一致:
运行代码后,控制台会输出:
TensorRT推理结果:output_data_host = 0.998897, 0.942676 手动验证计算结果: output_prob[0] = 0.998897 output_prob[1] = 0.942676两者结果完全一致,证明TRT推理流程正确。
addInput的顺序是第0个binding,markOutput的顺序是第1个binding;bindings[0]必须是输入的GPU指针,bindings[1]必须是输出的GPU指针;// 打印binding的名称和维度(调试用)for(inti=0;i<engine->getNbBindings();i++){printf("binding[%d] name: %s, shape: ",i,engine->getBindingName(i));autodims=engine->getBindingDimensions(i);printf("(%d, %d, %d, %d)\n",dims.d[0],dims.d[1],dims.d[2],dims.d[3]);}cudaMemcpyAsync、enqueueV2)只是“把任务加入队列”,不会等待执行;cudaStreamSynchronize或cudaDeviceSynchronize等待任务完成,否则会拿到错误结果。engine->createExecutionContext()可以调用多次,得到多个execution_context;| 报错现象 | 常见原因 |
|---|---|
| 反序列化engine失败 | TRT版本不匹配、模型文件损坏、GPU架构不匹配 |
| enqueueV2返回false | bindings顺序错、GPU内存不足、维度不匹配 |
| 结果是随机值 | 没同步流、bindings指针是CPU指针(不是GPU) |
| 内存泄漏 | 没调用destroy()/cudaFree()/cudaStreamDestroy |
load_file读二进制文件 →createInferRuntime→deserializeCudaEngine;createExecutionContext→cudaStreamCreate;cudaMalloc分配GPU内存 →cudaMemcpyAsync拷贝到GPU;enqueueV2启动推理 →cudaMemcpyAsync拷贝结果 →cudaStreamSynchronize同步;bindings顺序必须和构建模型时的输入输出顺序一致,且必须是GPU指针;Async)要配合cudaStreamSynchronize使用,否则结果错误;destroy(),CUDA资源要手动释放;是TensorRT部署的“通用模板”——不管是简单的全连接模型,还是复杂的YOLO模型,推理的核心步骤都是这6步,只是输入输出的维度、网络结构不同而已。吃透这个案例,你就掌握了TensorRT推理的核心逻辑。