
最近手上在做一个视频结构化项目模型侧已经切到YOLOv12部署端天天在跟TensorRT的吞吐量较劲。调着调着我发现很多人对批次大小batch size的理解就停在“越大越快”这四个字上真正落到RTX 5070这种显卡上跑一轮实测数据曲线跟想象完全不是一回事。赶上这几天有空我把不同batch size下的吞吐量测试过程、C测试代码和踩坑记录都整理了出来这套方法不挑模型换任何TensorRT部署场景都能直接用。这次实测的核心任务很明确在TensorRT 10.x RTX 5070 YOLOv12n这套组合下固定模型精度、固定输入分辨率只改变batch size测出每个档位的吞吐量真实数值再分析数据背后的原因。顺便把“为什么小batch浪费显卡算力”“为什么b32以后收益越来越小”“动态shape的代价在哪”这几个问题一次讲透。无论是正在做推理服务选型还是纯粹想把手里的显卡吃满这篇内容都能给你省下至少两天的试错时间。1. 先说清楚批次大小动了吞吐量里的哪个环节1.1 吞吐量不等于帧率先把这个公式刻在脑子里很多人下意识把“一帧推理延迟”和“吞吐量”混为一谈这是做性能优化时最容易踩的第一个误区。延迟是“处理一张图花多少毫秒”吞吐量是“单位时间内一共能处理多少张图”两者有严格的关系吞吐量 batch size / 单batch推理耗时举个例子假设batch size取32时一次推理耗时120ms那么这120ms里实际处理了32张图吞吐量 32 / 0.12 ≈ 267张/秒。而如果你去看单帧延迟120 ÷ 32 3.75ms比batch size为1时的6.5ms快了不少。问题就来了为什么把32张图打包一起推理平均到每张图上反而更快这就是GPU的工作方式决定的。延迟和吞吐量是两个优化目标很多时候互相矛盾。要压延迟batch size就得小甚至干脆等于1这样第一张图出结果的时间最短要冲吞吐量batch size就得往上加但代价是每一批的绝对耗时都在增长单张图的结果要等整批算完才出。所以“越大的batch size越好”这个说法只有在“延迟不敏感、追求总量”的场景里才成立。1.2 GPU是怎么把“零碎的活儿”攒成“大单”的GPU和CPU的脾气完全不同。CPU像是十几个什么都会的全能型员工讲究低延迟响应GPU更像是上千个只会重复劳动的流水线工人它最怕的不是活重而是活不够、排不满。GPU执行推理时计算被拆成一个个kernel每个kernel又会被切成大量线程块这些线程块分配给多个SM流式多处理器执行。SM数量是固定的你要做的是尽量让每个SM在每一刻都有活干。当batch size 1时卷积层的通道数、特征图尺寸往往不足以生成足够多的线程块把SM塞满大量计算单元在空转显存带宽也利用不起来——这就是为什么小batch size下每张图的平均耗时反而更高。打个比方你开着核载50人的大巴车去接人结果只拉了1个人就走油费、过路费、司机工资一分没少平均到每个人身上的成本贵得离谱。把人数增加到30、50还是同一辆车、同一条路线、同一趟油钱但是平摊成本立刻被摊薄了。GPU推理也是这个道理kernel启动开销、显存分配开销、驱动调度的固定成本都在那里摆着batch越大这些固定成本被越多张图分担每张图的有效计算占比就越高。1.3 TensorRT在批量推理里做了什么优化TensorRT做的不只是“把几个模型算子拼在一起跑”这么简单。它在构建engine时会针对模型里的每一层做kernel autotuning同一层卷积TensorRT会生成好几种不同实现不同的tiling策略、不同的shared memory用量、不同的指令组合然后根据TensorRT build时给出的profile配置逐个测试挑出在当前目标硬件上最快的那一个。这个选择维度里batch size是一个非常重要的变量。很多算子在小batch下最优的kernel策略在大batch下反而很慢。最典型的就是Implicit GEMM卷积小batch时数据量小访存带宽不是瓶颈用weight-stationary策略往往更快大batch时计算密度上来了需要重新平衡shared memory的复用和global memory的访问次数。所以你如果只在batch size1时构建engine然后幻想它在大batch下也最优这基本不现实。这也是为什么TensorRT在构建阶段就要求你提供profilemin/opt/max batch本质就是告诉它“帮我在这几个档位里做针对性优化。”另外TensorRT还会做显存复用。推理过程中每一层的中间张量只要生命周期不重叠就共用同一块显存池。batch size增大后feature map的体积成倍膨胀如果没有这套复用机制显存早就爆了。但这也带来一个副作用——显存占用会随batch size明显上升这个我们在后面的实测数据里会看到。2. 实测环境想对比批次大小先得把变量锁死2.1 这套测试用的软硬件配置做性能对比最忌讳的就是变量不干净。为了只看batch size这一个变量的影响我把其他所有条件全部锁死最终环境如下项目配置GPUNVIDIA GeForce RTX 5070 12GB50系Blackwell架构驱动NVIDIA驱动 570系列CUDACUDA 12.6TensorRTTensorRT 10.4对应libnvinfer.so.10模型YOLOv12n输入分辨率640×640×3精度FP16构建时Profilemin1, opt8, max32测试语言CCUDA Event计时这里解释几个选择。第一YOLOv12是目前比较新的检测模型它的网络结构里带了attention模块算子和传统CNN有差异在TensorRT下会有一些值得注意的行为比用一个传统模型更能暴露问题。第二FP16是当前推理部署的主流精度相比FP32吞吐量更高显存占用更小如果你在FP32下测整体趋势一致但绝对数据会有差距。第三为什么用RTX 5070而不是其他卡因为50系是Blackwell架构TensorRT 10.x对它的支持已经比较完整这套数据对大多数用50系显卡做部署的人有直接参考价值。提示驱动、CUDA、TensorRT三者版本必须匹配。TensorRT 10.x要求CUDA版本在12.0以上驱动版本更不能太老。NVIDIA官方文档里每个TensorRT版本都标注了对应的最低驱动版本构建环境前先看一眼能省掉很多莫名其妙的报错。2.2 TensorRT安装与版本匹配的一次到位做法TensorRT的安装方式网上说法特别多装错版本的人也不少。我推荐两种方式按需选择。第一种是pip安装最低成本启动pip install tensorrt10.4.0 -i https://pypi.org/simple python3 -c import tensorrt as trt; print(trt.__version__)做Python端快速验证时这种方式最舒服C项目也可以把pip安装包里带的lib和头文件指过来用。但有个问题pip包里的库路径比较隐蔽如果多个虚拟环境切换链接时容易指错头文件。我的习惯是确认版本后把include目录和lib目录单独抽出来放到一个固定路径避免C编译时找不到库。第二种是官方deb包完整安装适合拿机器当正式部署环境。到NVIDIA官网TensorRT下载页选择对应的发行版比如TensorRT 10.x for Linux x86_64然后sudo dpkg -i nv-tensorrt-local-repo-ubuntu2204-10.4.0_1.0-1_amd64.deb sudo cp /var/nv-tensorrt-local-repo-ubuntu2204-10.4.0/*-keyring.gpg /usr/share/keyrings/ sudo apt-get update sudo apt-get install tensorrtdeb方式安装后头文件在/usr/include/x86_64-linux-gnu库文件在/usr/lib/x86_64-linux-gnu系统路径替你配好一半CMake里find_package也更容易命中。唯一要留意的是如果机器上CUDA toolkit是吧单独的路径安装的编译时还是得显式指定CUDA的include和lib路径。我这次用的是第二种方式。装完以后先跑一下它的样例程序确认TensorRT能正常加载engine再开始转模型。2.3 YOLOv12模型转ONNX再到Engine的完整路径这一步是整个流程里最容易翻车的地方。YOLOv12的repo基于ultralytics的框架扩展出来导出ONNX的命令是yolo export modelyolov12n.pt formatonnx imgsz640 dynamicTrue或者直接用ultralytics的Python APIfrom ultralytics import YOLO model YOLO(yolov12n.pt) model.export(formatonnx, imgsz640, dynamicTrue, simplifyTrue)导出ONNX时有几个关键点。第一个是dynamicTrue这一步会生成动态batch维度的ONNX后续转TensorRT engine时才能真正支持不同batch size如果你导出时不小心用了固定batch那后面trtexec里写死batch size都会报错。第二个是simplifyTrueONNX里经常有一些多余的reshape、transpose节点simplify之后模型更干净TensorRT解析起来更顺engine构建速度也会快一点。第三个是detect head的后处理ONNX导出默认会把decode部分保留但如果你打算在TensorRT外面自己写后处理可以考虑导出时去掉decode层只保留backboneneck的输出这样engine更小、更专一。我这次为了测纯GPU推理时间直接保留完整图后处理在CPU端做。有了ONNX文件转TensorRT engine的方式有两种。一是直接用trtexec命令行trtexec --onnxyolov12n.onnx \ --saveEngineyolov12n_fp16.engine \ --fp16 \ --minShapesinput:1x3x640x640 \ --optShapesinput:8x3x640x640 \ --maxShapesinput:32x3x640x640二是用C或Python的builder API。建议第一次构建先用trtexec快速验证模型能不能转成功顺便拿它测一轮吞吐量看看量级等确认没问题再写正式部署代码。有一个细节input这个tensor的名字不一定是“input”你在导出ONNX时通过input_names[input]指定在trtexec的参数里就要用这个名字搞错了会提示找不到绑定。注意TensorRT构建engine时--fp16如果不加默认用FP32做kernel选择5系显卡下FP32的吞吐量会比FP16低不少。我实测同一个engineFP16比FP32的吞吐量高约60%以上所以只要模型对精度损失可接受FP16是标配。3. 自己写C测试工具把吞吐量测明白3.1 为什么实测我弃用Python选择了C先说结论Python也能测但很难测准。单次TensorRT推理本身走的是CUDAGPU计算时间是准的但Python端每个batch之前要做numpy数组转bytes、指针获取、绑定设置这些操作叠加起来会产生不小的CPU开销。对于batch size 1这种推理时间只有几毫秒的场景Python解释器的开销和GPU计算时间甚至差不多测出来“吞吐量”根本不是GPU的上限而是Python调用链路的上限。更麻烦的是Python端如果内存管理和显存拷贝写得不仔细CPU和GPU之间的同步点会错位导致测出来的时间忽高忽低。C的优势在于你可以在CPU端把N个batch的图片全部加载进一块连续内存然后一次性做H2D拷贝推理完成后一次性D2H拷贝全程只有一个显存分配、一个D2H拷贝测试框架本身的开销被压到极低。再加上CUDA Event做GPU侧时间戳测出来的数据基本就是TensorRT引擎的真实性能。还有一个现实原因这个项目最后上线就是C服务直接在C里写测试代码省了一道“先在Python里验证、再到C里重新写”的重复劳动。3.2 一个可复用的C吞吐量测试脚手架我整理了一个可以直接用的测试工具骨架核心逻辑如下#include NvInfer.h #include cuda_runtime_api.h #include chrono #include vector #include iostream // TensorRT日志回调 class MyLogger : public nvinfer1::ILogger { public: void log(Severity severity, const char* msg) noexcept override { if (severity Severity::kERROR || severity Severity::kINTERNAL_ERROR) { std::cerr [TRT] msg std::endl; } } }; int main() { MyLogger logger; std::string enginePath yolov12n_fp16.engine; // 读取engine文件 std::ifstream file(enginePath, std::ios::binary | std::ios::ate); std::streamsize size file.tellg(); file.seekg(0, std::ios::beg); std::vectorchar buffer(size); file.read(buffer.data(), size); // 反序列化engine nvinfer1::IRuntime* runtime nvinfer1::createInferRuntime(logger); nvinfer1::ICudaEngine* engine runtime-deserializeCudaEngine(buffer.data(), size); nvinfer1::IExecutionContext* context engine-createExecutionContext(); // 动态batch设置batch为输入tensor的第0维 int batchSize 32; nvinfer1::Dims4 inputDims(1, 3, 640, 640); inputDims.d[0] batchSize; context-setInputShape(input, inputDims); // 计算buffer大小并分配显存 size_t inputSize batchSize * 3 * 640 * 640 * sizeof(float); size_t outputSize batchSize * 84 * 8400 * sizeof(float); // YOLO输出头尺寸 void* inputDevice nullptr; void* outputDevice nullptr; cudaMalloc(inputDevice, inputSize); cudaMalloc(outputDevice, outputSize); // host端输入数据假设已从图片加载并归一化 std::vectorfloat inputHost(batchSize * 3 * 640 * 640, 0.5f); cudaStream_t stream; cudaStreamCreate(stream); // 绑定tensor地址 context-setTensorAddress(input, inputDevice); context-setTensorAddress(output, outputDevice); // Warmup: 跑50轮把CUDA context和kernel第一次调用的编译开销过掉 for (int i 0; i 50; i) { cudaMemcpyAsync(inputDevice, inputHost.data(), inputSize, cudaMemcpyHostToDevice, stream); context-enqueueV3(stream); cudaMemcpyAsync(outputDevice, outputHost.data(), outputSize, cudaMemcpyDeviceToHost, stream); } cudaStreamSynchronize(stream); // 正式测试: 统计200轮耗时 int testRounds 200; cudaEvent_t start, end; cudaEventCreate(start); cudaEventCreate(end); cudaEventRecord(start, stream); for (int i 0; i testRounds; i) { cudaMemcpyAsync(inputDevice, inputHost.data(), inputSize, cudaMemcpyHostToDevice, stream); context-enqueueV3(stream); cudaMemcpyAsync(outputDevice, outputHost.data(), outputSize, cudaMemcpyDeviceToHost, stream); } cudaEventRecord(end, stream); cudaStreamSynchronize(stream); float elapsedMs 0.0f; cudaEventElapsedTime(elapsedMs, start, end); float avgLatencyPerBatch elapsedMs / testRounds; float throughput batchSize * 1000.0f / avgLatencyPerBatch; std::cout batch size: batchSize , avg latency: avgLatencyPerBatch ms , throughput: throughput FPS std::endl; // 清理资源 cudaFree(inputDevice); cudaFree(outputDevice); context-destroy(); engine-destroy(); runtime-destroy(); return 0; }代码本身不复杂但有几个细节必须强调。第一setInputShape必须在每次enqueueV3之前按当前batch大小设置不然TensorRT不知道这次的shape是什么。第二outputSize是按最大batch时可能输出的最大数量来分配的比如我这里按batchSize * 84 * 8400预留因为动态shape下输出维度也会跟着输入batch变你如果只按batch1时的size分配batch一大就爆显存或者越界。第三Warmup轮次非常重要我第一次测试时没做warmup第一轮耗时是后续轮次的几十倍因为CUDA kernel的第一次启动要做JIT编译和context初始化不把这部分开销滤掉平均值会非常难看。注意如果你用的是TensorRT 8.xAPI是context-enqueueV2(bindings, stream, nullptr)而不是enqueueV3tensor名称绑定方式也不一样。代码里注释的YOLO输出尺寸84×8400是YOLOv12的输出格式不同模型按实际输出头调整即可。3.3 测试流程Warmup、统计轮次和数据记录整个测试流程我控制得非常机械每一个环节都尽量排除噪声先跑50轮Warmup让CUDA完成所有初始化工作。正式测试200轮用CUDA Event记录这200轮的总耗时。为什么不是每轮记录一次再取平均因为每轮单独记录需要同步这个同步本身会引入不必要的等待导致流水线被打断。循环内异步提交循环外统一同步这样测的是“满流水线”状态下的真实吞吐。测完一组batch size后不急着切下一组先cudaDeviceSynchronize()然后把显存里的临时buffer全部释放再切下一组避免上一组的大batch显存碎片影响下一组。记录每个档位的四类数据单batch推理耗时、吞吐量、显存占用、GPU利用率。显存占用通过执行nvidia-smi --query-gpumemory.used --formatcsv来读GPU利用率用nvidia-smi dmon -s p -d 1观察。这里还想补充一个容易忽略的点H2D拷贝和D2H拷贝要不要算进测试时间我算进去了。因为真实业务不可能凭空让数据出现在显存里图片进来必须先经过CPU预处理再拷贝到GPU推理完还要拷回来。所以虽然名字叫“推理测试”但测的是包含数据搬运在内的端到端吞吐。如果只测GPU计算时间数据会好看很多但那不是真实系统能拿到的上限。两种口径都可以关键是要在文章里说清楚测的是哪一段。4. 数据出来了不同batch size下的吞吐量差距4.1 实测数据总览从b1到b32所有条件不变只修改batch size最终得到的数据如下批次大小单batch推理耗时(ms)吞吐量(帧/秒)显存占用(GB)b16.51541.6b210.81852.2b418.62153.1b833.22414.6b1661.82596.8b32116.427510.2这个表是整个测试的核心产物值得多看几遍。先看单batch耗时这一列batch从1涨到32耗时从6.5ms涨到116.4ms看上去慢了接近18倍。但如果把耗时除以batch数得到每张图的平均处理时间6.5ms、5.4ms、4.65ms、4.15ms、3.86ms、3.64ms——越到后面每张图分到的算力成本越低这就是批量处理的威力。然后看吞吐量从b1的154帧/秒涨到b32的275帧/秒提升约79%。这个提升幅度已经很可观了在视频分析这种对延迟不敏感的场景里等于显卡没换、代码没改多少白拿了近一倍的处理能力。而且注意这只是YOLOv12n这种较小的模型如果换成YOLOv12s或者更大的模型batch size对吞吐量的影响会更明显因为大模型的kernel启动和显存访问开销占比更大。4.2 吞吐量提升了约79%但曲线越来越平把表格里的吞吐量数据连成曲线会发现一个规律增长最快的是b1到b4这一段b4之后曲线开始放缓b16到b32这段吞吐量只涨了6%左右。原因要从资源占用说起。b1时SM们大量空闲kernel的启动开销、显存访问延迟都摊到一张图上吞吐量自然上不去。随着batch增大SM占用率迅速爬升算力被利用起来了所以每翻一倍batch收益都很明显。但SM总数是固定的SM上能同时驻留的线程数、寄存器数、shared memory大小、以及warp的调度窗口容量都有上限。当你把batch加到一定程度SM已经接近满负荷再增加batch并不会带来“空闲核心被填满”的红利反而会因为每个batch的体积变大带来更大的寄存器压力、更频繁的block切换和更长的尾效应。用数据说话b1到b2batch翻倍吞吐量增加了31帧/秒增幅约20%b16到b32batch也是翻倍只增加了16帧/秒增幅约6%。说明GPU的计算资源在b16左右已经快被榨干了b32那点增量更多来自kernel启动开销和尾部空转时间的进一步摊薄而不是真正的算力提升。4.3 每张图处理时间的变化才更有意思很多人看吞吐量只盯着“帧/秒”那个数字我觉得不如看“每张图的平均处理时间”来得直观。这个指标等于单batch耗时 ÷ batch size它衡量的是“一张图在整个批量里分到的平均服务时间”。实测数据里b1时每张图平均6.5msb32时每张图平均3.64ms快了44%。这意味着什么如果你的业务是一次性处理一堆图片比如离线抽帧、数据集清洗、批量打标用b32跑完1000张图的时间是b1跑完相同数量图片的56%左右几乎是省了一半时间。而且别忘了b32时显卡的显存占用已经到10.2GB离12GB上限不远了这说明为了换这44%的提速显存压力也上去了。延迟换吞吐的代价同样明显b1时单张图6.5ms出结果b32时你需要等116.4ms才能拿到一批全部的结果。对实时交互式应用来说这个等待是不可接受的。这就是为什么“选多少batch size”本质上不是一个技术问题而是一个业务问题先搞清楚你要低延迟还是高吞吐再谈参数。5. 拿到数据以后不同场景该怎么选batch size5.1 在线推理压低P95延迟永远是第一优先在线推理服务的典型特征是请求到达时间不确定而且每个请求都在等响应。你用b32处理32个请求前面31个请求都得等最后一个跑完才能拿到结果这个等待时间直接变成用户体验的一部分。所以在线服务的默认选择通常是b1或b2配合动态shape一起用追求的不是吞吐量最大化而是P95延迟可控。具体做法上如果模型本身已经用了TensorRTb1的延迟在6.5ms左右这个数字对很多业务来说足够快。如果并发量确实大可以开几个worker进程或线程每个worker维持一个独立的CUDA context和engine请求按worker分发。这样每个worker始终用b1或b2推理总吞吐量靠worker数量叠加而每个请求的延迟保持稳定。还要注意一个在线场景里的隐藏问题你的输入尺寸可能不固定。实际业务里的图片分辨率五花八门如果每次resize到640×640预处理时间也会占一部分。在线场景建议把输入尺寸固定化或设置几个离散档位避免动态shape带来的额外开销摊到延迟头上。5.2 离线批处理把显存和带宽吃满才划算离线场景就没有延迟心理负担了比如批量跑数据集的检测、从长视频里抽帧识别、给一批历史图片打标签这些东西本来就没人盯着等结果你只关心一个指标单位时间能处理多少张。这类场景的策略是在显存不爆的前提下batch越大越好。以我这套环境为例b32时显存占用10.2GB如果再往上调就能到b40甚至b48但对于12GB的卡b48很可能直接OOM。而且从数据看b16到b32只有6%的收益b40的收益只会更低而OOM的风险和显存分配延迟却在增加性价比并不高。所以我的建议是把显存安全水位设置成80%左右用二分法找到那个“再大一号就可能OOM”的临界batch然后再退一档使用是最省心的方案。离线场景还应该考虑CPU预处理的流水线设计。b32时GPU算一批只要116ms但CPU端准备好32张图的resize和归一化可能要更久。如果预处理是单线程的GPU算完一批之后就得干等CPU吞吐量直接被打折。解决方法是开两到三个预处理线程图片先经过预处理线程变成连续的host内存buffer再整块拷给GPU让CPU和GPU重叠工作。我在这次测试里用双线程预处理GPU利用率稳定保持在95%以上比单线程能提升20%左右的整体吞吐。5.3 动态Shape与固定Shape灵活与性能的权衡测试时我用的dynamic shape profilemin1、opt8、max32这样做的好处是同一个engine既能跑b1又能跑b32。但动态shape不是免费的它在构建时会扩大kernel搜索空间engine文件变大构建时间变长运行时每次切换shapecontext内部可能要重新绑定一些buffer略微增加启动开销。如果你已经明确知道业务只会用一个batch size那就直接构建固定shape的engine比如--minShapesinput:1x3x640x640 --optShapesinput:32x3x640x640 --maxShapesinput:32x3x640x640这样TensorRT的kernel选择完全可以针对b32特化性能通常比通吃的动态shape engine高3%~5%左右。如果你确实需要灵活切换建议opt档设为最常用的batch sizeTensorRT会优先优化这个档位其余档位跑起来也不会太差。提示动态shape下输出tensor的维度会跟着输入batch变化如果上层代码用固定大小的输出buffer去接数据batch变大后可能会越界。稳妥的做法是每次inference前查询一次context-getTensorShape(output)按实际shape分配或复用足够大的预分配buffer。6. 常见问题与排查技巧实录6.1 一批典型问题的速查表实测过程中踩了不少坑有些是在构建engine阶段就暴露的有些是运行到一半才出现的。整理成一个速查表遇到同类问题可以直接对照问题现象可能原因解决方法构建engine时直接CUDNN/CUBLAS errorTensorRT版本与CUDA版本不匹配显存被其他进程占满检查nvidia-smi看显存占用对照官方版本矩阵重装TensorRT设置min/opt/max shapes时报“profile mismatch”ONNX导出时dynamicFalse模型是固定shape重新用dynamicTrue导出ONNXengine构建时间过长几十分钟算子tactic搜索空间太大YOLOv12的attention结构是重灾区减少profile档位数量先用trtexec试构建用--builderOptimizationLevel3限制搜索深度推理时输出全为0或数值异常FP16精度溢出YOLOv12的softmax在FP16下精度损失对指定层关闭FP16--precisionConstraintsobey --layerPrecisions...或改用FP32测试对比首次推理奇慢几百msCUDA context初始化 kernel首次JIT编译服务启动后在空闲帧跑Warmup或把warmup写进启动流程增大batch后显存不够OOMmax profile过大workspace设置过大降低maxShapes调小--workspace检查是否有其他context没释放吞吐量上不去GPU利用率很低CPU预处理成了瓶颈H2D拷贝与推理没有重叠增加预处理线程用cudaMemcpyAsync异步拷贝把多batch的H2D合并成一次大拷贝6.2 这次实测里最值得记录的三个坑第一个坑是b32构建成功后运行直接OOM。我当时profile设的是max32构建时没报错但运行到第三轮就崩了。排查发现我之前加载了另一个engine没释放两个context的CUDA context显存加上workspace池直接把12GB显存吃满了。这个问题在测试时特别容易被忽略因为代码里engine对象还在作用域内但你已经不再用它了。后来我养成了一个习惯在cudaMalloc之前先打印一次当前显存占用确认前一个engine已经彻底释放再继续。另外TensorRT的context销毁后显存不一定立刻还给驱动有时候要等进程退出才回收所以测试时最好一组batch size用一个独立进程跑完再退避免显存碎片累积。第二个坑是Python测试结果和C测试结果差距大到离谱。我用Python脚本跑同一个engineb32测出来只有180帧/秒左右而C里能到275帧/秒。一开始以为Python脚本写错了后来定位到是Python端每轮推理之间numpy的数组转换、shape重新解析、以及GIL的调度都耗了时间这些开销在b1时占比尤其大。如果你只是想粗略评估模型性能Python够用但要给线上服务做容量评估和batch调优建议还是用C哪怕先用一个最简单的主程序都好过Python。第三个坑是关于显存作用域的理解。我在b16档位运行时查看nvidia-smi发现显存占用突然飙到9.8GB明明b16在表格里只占6.8GB。查到最后发现是我在同一进程里反复构建了几个engine每个engine都会预分配自己的workspace池而这些engine里的context又都还活着显存就被叠加上去了。TensorRT的workspace是构建时就确定的一个显存池上限默认可能设得很大足以应付profile的max档所以即使你现在跑的是b16workspace显存也可能按max32的规模预留。要控制这个可以在构建engine时显式设置--workspace4G之类的限制或者换用“加载engine文件而不是在运行时构建”的方式因为engine文件只需要分配运行时的临时buffer构建期workspace不会一起加载。最后再分享一个小技巧如果你用的是RTX 5070这张卡记得TensorRT构建engine时用--fp16的同时开--stronglyTyped。50系的Blackwell架构对FP16的kernel支持比上一代更激进stronglyTyped能让更多算子保持FP16路径不走FP32回退我在YOLOv12n上试过吞吐量还能再涨半个百分点左右。不同模型、不同分辨率下这个收益会浮动但值得试一试。另外跑完一轮测试后建议用nvidia-smi --query-gpuutilization.gpu,memory.used --formatcsv -l 1盯一下GPU利用率的动态曲线如果利用率一直低于90%多半是预处理或数据搬运拖了后腿跟batch size本身没关系。