实测对比|GPU推理性能优化:重复执行 data.to(“cuda“) 为什么会拖慢吞吐(基于 MobileNetV2)
一、前言做 CV 模型 GPU 推理时很多初学者容易写出一个隐形性能问题for data in dataset: out model(data.to(cuda))从代码逻辑上看完全正确但如果实际场景是对同一批数据进行重复推理测试或者基准测试中反复将同一个 Tensor 从 CPU 拷贝到 GPU那么这种写法会引入大量额外的数据传输开销。对于 MobileNetV2、ShuffleNet 等轻量级模型来说模型本身计算速度很快此时 CPU→GPU 的数据传输甚至可能成为主要耗时来源从而影响整体吞吐。本文基于 MobileNetV2 的简单实验对比两种数据迁移方式并分析其中的性能差异。二、原始问题代码循环内反复拷贝CPU张量到GPU1.原始问题代码import torch import torchvision from tqdm import tqdm DEVICE cuda # 加载轻量MobileNetV2预训练模型并移至GPU model torchvision.models.mobilenet_v2(pretrainedTrue) model model.to(DEVICE) model.eval() with torch.no_grad(): #Batch1张量创建在CPU循环内每次to(DEVICE) data torch.rand(size[1, 3, 224, 224]) for i in tqdm(range(1024)): out model.forward(data.to(DEVICE)) #Batch128同样循环内重复拷贝 data torch.rand(size[128, 3, 224, 224]) for i in tqdm(range(128)): out model.forward(data.to(DEVICE))2.实测吞吐结果Batch尺寸吞吐Batch/Sec等效单图每秒处理量1272272×1272张/秒12822.422.4×1282867张/秒3.性能差核心原因①循环次数和拷贝次数相等由于.toDEVICE写在循环里面也就是说每一轮推理都会完整的进行一遍cpu内存-PCIe总线-GPU显存的数据拷贝流程。当batch1时循环1024次即相当于完整拷贝1024次即进行1024次小张量PCIe传输当batch128时循环128次即相当于完整拷贝128次即进行128次超大张量PCIe传输测试代码特意设计了两组循环次数batch1 迭代 1024 轮、batch128 迭代 128 轮目的是用充足的迭代次数抹平单次计时波动测出稳定的平均吞吐速度。②PCIe带宽远弱于GPU内部显存带宽MobileNetV2计算量极小GPU跑完一个batch的耗时极短所以当batch很大的时候CPU-GPU之间PCIe传输延迟、带宽会成为很明显的短板。当batch1小batch时单张图数据量很小拷贝耗时占比不大速度下滑不夸张当batch128大batch时单次传输数据量暴增PCIe 总线带宽上限被快速占满、传输延迟显著抬高PCIe 本身带宽性能远弱于 GPU 片内显存带宽此时GPU 计算单元会大量空闲全程等待CPU 侧的数据拷贝完成才能启动运算硬件算力被传输开销严重拖累无法跑出理论峰值性能4.总结理想状态就是CPU持续预处理、持续送数据而GPU这边计算不会断档几乎不会说停下来去等待数据的到来但是循环内反复.toDEVICE的写法就会强行打断流水线因为每一轮都会强制停下来完整重传一遍一模一样的张量。三、优化方案推理前一次性把张量预加载到 GPU1.优化后代码import torch import torchvision from tqdm import tqdm DEVICE cuda model torchvision.models.mobilenet_v2(pretrainedTrue) model model.to(DEVICE) model.eval() with torch.no_grad(): #Batch1创建张量后立刻迁移GPU data torch.rand(size[1, 3, 224, 224]).to(DEVICE) for i in tqdm(range(1024)): out model.forward(data) #Batch128一次性迁移到大批量张量 data torch.rand(size[128, 3, 224, 224]).to(DEVICE) for i in tqdm(range(128)): out model.forward(data)2.优化后实测吞吐对比Batch尺寸优化前Batch/Sec优化后Batch/Sec单图每秒处理量优化后1272272272张/秒无提升12822.428.13596.8张/秒大幅提升3.优化后收益①传输次数压缩至1次只在循环启动前执行 1 次 CPU→GPU 传输后续推理全程无 PCIe 数据搬运GPU 直接读取本地显存张量运算②小batch几乎无提升的原因单张 3×224×224 张量体积很小单次 PCIe 传输耗时微乎其微重复拷贝的损耗本身就低优化空间极小大批量张量传输开销巨大消除重复拷贝后效果显著四、原理解析1.GPU采用SIMT并行计算架构Batch尺寸会影响GPU资源利用效率。通常情况下Batch较小时单次计算任务规模有限Kernel执行效率和硬件利用率往往低于大Batch场景随着Batch增大更多数据能够同时参与计算GPU资源利用率通常会进一步提高从而获得更高的整体吞吐。不过Batch1并不意味着GPU完全闲置实际利用率还与模型结构、Kernel实现以及GPU架构有关2.对于轻量模型MobileNetV2、ShuffleNet等由于GPU计算速度极快PCIe数据传输耗时占总耗时大头反复拷贝会让计算性能降低对于计算量更大的模型ResNet等因为模型自己前向计算耗时很长传输开销被巨大的计算时间稀释to(DEVICE)放置的位置两种数据拷贝写法带来的性能差距就会变小五、本节整体总结本文实验表明如果对同一批数据进行重复推理测试那么应避免在循环内部反复执行data.to(cuda)因为这样会产生大量额外的 CPU→GPU 数据传输开销。对于 MobileNetV2、ShuffleNet 等轻量级模型由于前向计算速度较快数据传输耗时更容易成为性能瓶颈从而影响整体吞吐。更合理的做法是先完成一次数据迁移再进行多次推理这样能够避免无意义的重复拷贝让 GPU 始终直接访问显存中的数据。需要注意的是本文结论主要针对“重复推理同一批数据”的测试场景。在真实业务中每个 Batch 通常都是新的数据因此 CPU→GPU 数据传输不可避免此时更重要的优化方向是 pin_memory、non_blocking、数据预取以及 CUDA Stream 等异步流水线技术。喜欢的朋友麻烦点赞收藏关注后续会继续分享技术干货、学习总结与实战踩坑记录