CanTechLab

Can

多卡并行DDP训练模型时GPU利用率0%-100%反复跳动的问题

2025-04-26

问题

  • GPU 在训练过程中 间歇性利用率为 0%

    • 每个 iteration 开始时 GPU 会短暂运行几秒(峰值约 80~100%)

    • 然后利用率迅速回落为 0%,等待数秒后再次开始下一个 batch。

  • 总体训练速度变慢,samples/s 明显低于硬件理论能力。

排查思路

  1. 打印每个batch的加载时间,如果数据加载耗时远大于前向/反向传播时间,那就是 数据加载成为瓶颈

    start = time.time()
    
    img, label = next(data_iter)
    
    print("Data loading time:", time.time() - start)python
  2. 是否使用到了足够到的num_workers

  3. 是否启用了 pin_memory=True(加速 CPU→GPU 数据拷贝)

  4. 是否关闭了 persistent_workers=False,每次 epoch 都要重新初始化线程

  5. 减小 batch_size 看是否加快速度(排除 GPU 显存瓶颈)

原因

GPU计算速度远高于 CPU 数据准备速度,尤其是在图像任务中,数据加载和预处理(如 transforms)占用了主线程或子线程大量时间,导致 GPU空转等待数据输入。当 GPU 处理完一个 batch 后,如果下一个 batch 的数据还没准备好(还在磁盘读取或 CPU 预处理),就会出现 GPU 空转、利用率为 0% 的现象。这种状况就叫“数据瓶颈”:磁盘读取和 CPU 处理的速度跟不上 GPU 的计算速度,GPU 就不得不等待数据,导致训练速度下降、资源浪费。

解决方法

  • 引入 DataPrefetcher异步预取模块

    • 原理:在 CPU 上创建一个 prefetcher,利用 CUDAStream 异步加载、转换、并将数据搬到 GPU。在一个 iteration 运算过程中,并行预取下一个 batch

    • 效果:极大减少 batch 间的数据等待时间,GPU 利用率提升至持续接近 100%

    # utils/data_prefetcher.py
    
    import torch
    
    class DataPrefetcher:
        def __init__(self, loader, device):
            self.loader = iter(loader)
            self.device = device
            self.stream = torch.cuda.Stream(device=device)
            self.preload()
    
        def preload(self):
            try:
                self.next_inputs = next(self.loader)python
            except StopIteration:
                self.next_inputs = None
                return
            with torch.cuda.stream(self.stream):
                if isinstance(self.next_inputs, (list, tuple)):
                    self.next_inputs = [x.to(self.device, non_blocking=True) if torch.is_tensor(x) else x for x in self.next_inputs]
                else:
                    self.next_inputs = self.next_inputs.to(self.device, non_blocking=True)
    
        def next(self):
            torch.cuda.current_stream(self.device).wait_stream(self.stream)
            inputs = self.next_inputs
            self.preload()
            return inputs
  • 辅助优化

    • 使用 persistent_workers=True,避免每个 epoch 都重新启动数据加载进程。

    • 合理调参 num_workers(一般为 CPU核心数//2 或 batch_size//2)

    • 开启 pin_memory=True 加速数据转移到 GPU