当前位置: 首页 > news >正文

Batch Size与预热导致深度学习模型推理时间忽快忽慢

最近在做深度学习推理优化时,遇到一个“灵异现象”:
同样的模型和输入尺寸,第一次推理要几百毫秒,之后就只要几毫秒
而且batch size 从 1 改成 8,平均推理时间有时还更,有时又突然得离谱。

最近我在测试超分辨率模型 FMEN 的时候,就完整踩了一遍坑。这里把过程、原因和正确的测法梳理一下,分享给大家。


1. 首帧为什么慢?预热的意义

我经常会遇到首帧推理时间较长的情况。在4090上,当处理 224x224 输入图像时,首张图片的推理时间可能达到 400ms,而后续每张图片的推理时间通常会降到仅 6ms

为什么首帧慢?

首帧慢的原因主要有以下几点:

  1. CUDA 上下文初始化
    在第一次使用 GPU 时,CUDA 会为 GPU 创建一个上下文并分配内存池。这是一个比较耗时的过程,特别是当运行的模型比较大或者需要初始化大量资源时。

  2. cuDNN 算法搜索
    在使用深度学习框架(如 PyTorch)时,如果开启了 torch.backends.cudnn.benchmark=True,cuDNN 会在第一次运行时试一遍不同的卷积算法,找出最快的实现。这个过程需要一定的时间,但一旦确定了最佳算法,后续推理就会非常快。

  3. GPU 动态频率调整
    当 GPU 刚开始工作时,尤其是在省电模式下,GPU 的频率通常较低。为了提升性能,GPU 需要花费时间将频率提升至 P0/Boost 模式,这样能提供更高的计算能力。

  4. 缓存冷启动
    在推理开始时,GPU 的缓存尚未加载模型的权重和输入的特征图,因此第一次的数据访问速度较慢。之后的数据访问会更高效,因为权重和特征图已经被加载到缓存中。

预热的意义

由于首帧推理时间受到初始化过程的影响,如果只计算第一次的推理时间,它会严重影响整个推理过程的平均时间。因此,在进行性能评估时,通常会先执行几次预热,以确保模型加载、CUDA 上下文创建、cuDNN 算法选择等过程已经完成,并且GPU的状态已经稳定。

 正确的做法

在正式计时之前,先进行预热。预热会帮助初始化 GPU 所需的资源,并确保 GPU 达到最佳的工作状态。下面是一个常见的预热代码示例:

inp = torch.randn(1, 3, 224, 224, device=device)
with torch.inference_mode():for _ in range(10):  # 预热 10 次_ = model(inp)
torch.cuda.synchronize()

预热的形状与推理的不一致时会影响推理速度!

在使用 PyTorch 进行推理时,特别是在处理 GPU 上的模型时,输入形状与模型实际推理时的输入形状不一致会影响推理的效率。原因如下:

  1. 动态调整的内存分配
    预热时使用的输入形状和正式推理时的形状不一致,可能导致 GPU 内存的分配和调整过程不符合实际推理需求。GPU 在接收到不同形状的输入时,可能需要调整内存池大小、重新编排计算图,或者调整数据传输方式,这会浪费额外的计算资源,导致预热和正式推理之间的性能差异。

  2. 缓存和内存布局
    GPU 在执行推理时,会利用缓存来提高数据访问速度。如果在预热时使用的输入形状和正式推理时的形状不同,缓存可能无法有效复用,这会影响推理速度。例如,较大的输入可能需要更多的内存带宽和缓存大小,而小的输入可能无法充分利用这些资源。

  3. 优化计算图
    PyTorch 和 cuDNN 会根据输入的形状来优化计算图。例如,某些卷积操作可能会根据输入的特征图尺寸选择不同的优化算法。如果预热时使用的形状与正式推理时的形状差异较大,可能会导致错误的优化决策,从而影响性能。

我写了个代码,使得预热和推理的形状一样

# 3) === 一次性预热(与 DataLoader 形状一致)===with torch.inference_mode():# 用数据集的第一张确定 C,H,W;用 DataLoader 的 batch_size 确定 Nsample = infer_loader.dataset[0]  # 假设返回 Tensor: [C,H,W]if isinstance(sample, (list, tuple)):sample = sample[0]  # 兼容 (lr, hr) 之类返回C, H, W = sample.shape[-3], sample.shape[-2], sample.shape[-1]N = infer_loader.batch_size or 1dummy = torch.randn(N, C, H, W, device=device, dtype=sample.dtype)for _ in range(10):  # 多跑几次更稳_ = net(dummy)if device.type == "cuda":torch.cuda.synchronize()print(f"=> warmup done for shape: ({N},{C},{H},{W})")

2. 如何正确计时?

很多初学者用 time.time() 包裹 model(x),得到的时间很短,这是因为 PyTorch 的 GPU 调用是异步的:CPU 把 kernel 提交给 GPU 就返回了,实际上 GPU 还在算。

正确方式是加同步,并使用高精度的 perf_counter()

from time import perf_countertorch.cuda.synchronize()
t1 = perf_counter()
_ = model(inp)
torch.cuda.synchronize()
t2 = perf_counter()latency_ms = (t2 - t1) * 1000
print(f"真实单次推理延迟: {latency_ms:.3f} ms")

这样测到的才是 GPU 真正完成一次前向传播的耗时。


3. Batch Size 对推理时间的影响

这里才是最有趣的部分。我做了两组实验:

实验一:数据集 14 张图

  • Batch=1 → 平均 6ms/张

  • Batch=8 → 平均 16ms/张 (反而更慢!

实验二:数据集 16 张图

  • Batch=1 → 平均 6ms/张

  • Batch=8 → 平均 0.8ms/张 (快得离谱!)

为什么会这样?

当 batch size 不能整除数据集时,最后一批就是“残缺批”。比如 14 张图 + bs=8 → 运行一次 8 张、一次 6 张:

  1. 固定开销摊不均:每个 batch 都要付一次 kernel 启动、调度的固定成本。满 8 张能把成本均匀分摊,但只跑 6 张时,成本摊得更少,单张更贵

  2. 算法缓存失效cuDNN 是按形状缓存最优算法的。预热时缓存了 (8,3,224,224),结果尾批变成 (6,3,224,224),相当于新形状 → 可能重新选算法,速度慢

所以在 14 张图的情况下,尾批拖慢整体,平均值被拉高到了 16ms/张。换成 16 张图,正好两个满批 (8+8),固定开销被均匀摊薄,平均值就掉到了 0.8ms/张。


总结

推理时间的“灵异现象”背后是 GPU 工作机制和统计口径的叠加结果:

  1. 首帧慢:CUDA 上下文初始化、cuDNN 算法搜索、GPU 频率提升等固定开销,导致第一张图片远慢于后续。解决方法:预热几次。

  2. 计时要同步:PyTorch 的 GPU 调用是异步的,用 time.time() 只算提交时间。→ 解决方法:perf_counter() + torch.cuda.synchronize()

  3. Batch Size 的陷阱

    • 如果数据集大小不是 batch 的整数倍,尾批是“残缺批”,固定开销摊不下去,还可能触发新形状的算法搜索,平均时间就会被拉高。

    • 测试时要么保证总图片数能被 batch 整除,要么剔除尾批再统计。

http://www.dtcms.com/a/390208.html

相关文章:

  • 过滤器(Filter)与拦截器(Interceptor)知识点总结
  • 深度学习与机器学习
  • Linux服务器从零开始-mysql安装
  • Emacs 折腾日记(三十)——打造C++ IDE 续
  • 解密DNS:互联网的隐形导航系统
  • Mysql修改用户密码,修改MySQL密码如何安全的步骤是什么
  • PS练习2:将图片贴入实际环境中
  • cocos shader 流光环绕
  • kali nethunter 开启ssh
  • vue3滚动到顶部钩子函数+组件简单示例
  • Linux 开发工具(3)
  • Hive 运行
  • PPT中为图片添加透明渐变的蒙版
  • 数字签名过程中的消息摘要和加密的作用
  • Unity物理系统笔记
  • 区分同步(Synchronous)和异步(Asynchronous)
  • 隐语开源隐私计算SecretFlow,实测性能提升10倍,纵向联邦SecureBoost算法(已开源)
  • 云南食品安全管理员考试都考哪些知识点
  • AAAI2025 | 视觉语言模型 | 西电等提出少样本语言驱动多模态分类模型DiffCLIP
  • Coze(扣子)零基础开发02-建一个简单机器人
  • 混合架构(SpringCloud+Dubbo)的整合方案与适用场景(三)
  • SPI 通信协议
  • vue3学习日记(十六):路由配置详解
  • 河南省 ERA5 日值气象数据处理教程(2020–2025)
  • 继承和多态常见面试问题解析
  • 博士生如何进行文献阅读和文献整理?
  • 矩阵分析线性表示例题
  • OpenEuler---jumpserver堡垒机部署
  • STM32 驱动 MAX31865 读取 PT100 温度方案
  • 第四次编程记录