Bootstrap

深度学习批次数据处理的理解

基础介绍

在计算机视觉深度学习网络中,在训练阶段数据输入通常是一个批次,即不是一次输入单张图片,而是一次性输入多张图片,而神经网络的结构内部一次只能处理一张图片,这时候很自然就会考虑为什么要这样的输入?神经网络是如何处理多个数据的,下面从硬件架构的角度去分析处理。

GPU

硬件架构

GPU的硬件架构设计是批处理能够高效运行的关键原因之一。GPU现阶段一般采用SIMT架构,它的特点如下:

SIMT(Single Instruction, Multiple Threads)架构是一种并行计算模型,主要用于图形处理单元(GPU)和其他高性能计算平台。它与传统的 SIMD(Single Instruction, Multiple Data)架构有所不同,尽管两者都允许同时执行多个操作。

在SIMT架构中,多个线程同时执行相同的指令,这类似于SIMD。不同之处在于,SIMT允许每个线程拥有自己的寄存器和状态,因此它们可以在执行过程中有不同的执行路径。这种灵活性使得SIMT能够处理更多种类的计算任务,适合于并行计算和图形处理中的高度数据并行任务

 SIMT可以将计算任务分为多个线程,每个线程独立运行,这使得程序可以有效的利用并行硬件资源,提升计算性能。注意有的cpu也支持了SIMD架构,如intel的SSE和avx。所以很多使用使用cpu进行训练时就是调用的cpu的SSE或avx

GPU的硬件架构特征:

CPU: 少量强大的核心,适合串行处理
[Core 1] [Core 2] [Core 3] [Core 4]

GPU: 大量简单的核心,适合并行处理
[Core][Core][Core][Core][Core][Core]...(数千个核心)
[Core][Core][Core][Core][Core][Core]...
[Core][Core][Core][Core][Core][Core]...

有的GPU可以多大上千个核心,而CPU核心的数量则远远达不到这个数量。 

GPU的特点

  • 高吞吐量

单精度浮点运算性能(FLOPS)示例:
高端CPU:    ~1-2 TFLOPS
高端GPU:    ~30-40 TFLOPS

  • 高内存带宽 

内存带宽示例:
CPU:        ~50-100 GB/s
GPU:        ~700-1000 GB/s

 流处理器

# GPU处理批次数据的示意
batch_images = torch.randn(32, 3, 224, 224)  # 32张图片

# GPU会自动将计算分配到多个流处理器
# 假设一个简单的操作
def gpu_parallel_operation(batch):
    # 在GPU中,这个操作会自动分配到多个处理器并行执行
    return batch * 2  # 每个元素的操作都可以并行

GPU与批处理的关系

并行计算能力

class ConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 64, 3)
        
    def forward(self, x):
        # batch_size=32时
        # GPU可以同时处理32张图片的卷积运算
        # 每个CUDA核心可以负责部分计算
        return self.conv(x)

# 使用GPU
model = ConvNet().cuda()
batch = torch.randn(32, 3, 224, 224).cuda()
output = model(batch)  # 并行处理32张图片

内存层级

GPU内存层级:
Global Memory (显存) -> L2 Cache -> L1 Cache/Shared Memory -> Registers

数据流动示例:
batch_data (Global Memory)
    → 加载到Shared Memory
        → 分配给各个CUDA核心的Registers
            → 并行计算
                → 结果写回Global Memory

如何选择合适批处理大小

可以从一个角度出发,即显存的利用率。通过设置一个合适的显存利用率来判断当前GPU的利用是否充分。一个简单的方法如下:

# 批大小需要平衡以下因素:
# 1. GPU显存大小
# 2. 计算效率
# 3. 训练效果

# 示例:显存管理
def get_optimal_batch_size(model, input_size, max_memory=0.8):
    """估算最优批大小"""
    try:
        batch_size = 1
        while True:
            # 尝试运行一个批次
            x = torch.randn(batch_size, *input_size).cuda()
            _ = model(x)
            
            # 检查显存使用
            memory_used = torch.cuda.memory_allocated() / torch.cuda.max_memory_allocated()
            if memory_used > max_memory:
                return batch_size - 1
            
            batch_size *= 2
    except RuntimeError:  # 显存溢出
        return batch_size // 2

利用gpu的流水线

# 使用DataLoader的pin_memory和num_workers优化数据加载
train_loader = DataLoader(
    dataset,
    batch_size=32,
    pin_memory=True,  # 将数据固定在内存中,加速GPU传输
    num_workers=4     # 多进程数据加载
)

计算和数据传输重叠

# 使用CUDA流实现计算和数据传输重叠
stream1 = torch.cuda.Stream()
stream2 = torch.cuda.Stream()

# 在两个流上并行处理
with torch.cuda.stream(stream1):
    output1 = model(batch1)

with torch.cuda.stream(stream2):
    output2 = model(batch2)

模型参数体积大于GPU显存应该如何处理?

 首先检查显存的状态:显存总量,显存当前使用量。方法如下:

import torch
import psutil
import GPUtil

def memory_check():
    # 检查系统内存
    system_memory = psutil.virtual_memory()
    print(f"系统内存总量: {system_memory.total / 1e9:.2f}GB")
    print(f"系统内存使用: {system_memory.used / 1e9:.2f}GB")
    
    # 检查GPU显存
    if torch.cuda.is_available():
        gpu = GPUtil.getGPUs()[0]
        print(f"GPU显存总量: {gpu.memoryTotal/1024:.2f}GB")
        print(f"GPU显存使用: {gpu.memoryUsed/1024:.2f}GB")

查明了GPU的当前状态后,如果模型参数的大小超过了可使用的显存,基本有如下几种思路去解决:

  1. 梯度检查点:这个属于不懂的地方
  2. 混合精度训练:这个也属于不懂的
  3. 模型并行策略,即将模型数据按块分别加载到不同的GPU上,每个GPU完成模型推理的某一部分
  4.  流水线并行:这个也属于不懂的
  5. 模型压缩技术:量化和知识蒸馏
//模型压缩技术
# 1. 量化
def quantization_example():
    import torch.quantization
    
    # 动态量化
    model_int8 = torch.quantization.quantize_dynamic(
        model,  # 原FP32模型
        {torch.nn.Linear},  # 量化层类型
        dtype=torch.qint8  # 量化为8位整数
    )
    
    # 显存节省约75%

# 2. 知识蒸馏
class SmallModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        # 更小的架构
        self.features = torch.nn.Sequential(
            torch.nn.Conv2d(3, 64, 3),
            torch.nn.ReLU()
        )

def distillation_training(teacher, student, data):
    criterion = torch.nn.KLDivLoss()
    temp = 3.0  # 温度参数
    
    # 教师模型输出
    with torch.no_grad():
        teacher_outputs = teacher(data)
    
    # 学生模型输出
    student_outputs = student(data)
    
    # 蒸馏损失
    loss = criterion(
        torch.log_softmax(student_outputs/temp, dim=1),
        torch.softmax(teacher_outputs/temp, dim=1)
    )


//模型分割和流水线并行
# 1. 模型分割
class SplitModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        # 前半部分放在第一个GPU
        self.features1 = torch.nn.Sequential(
            torch.nn.Conv2d(3, 64, 3),
            torch.nn.ReLU()
        ).to('cuda:0')
        
        # 后半部分放在第二个GPU
        self.features2 = torch.nn.Sequential(
            torch.nn.Conv2d(64, 128, 3),
            torch.nn.ReLU()
        ).to('cuda:1')
    
    def forward(self, x):
        x = self.features1(x.to('cuda:0'))
        x = self.features2(x.to('cuda:1'))
        return x

# 2. 梯度累积
def gradient_accumulation(model, dataloader, num_accumulation_steps=4):
    optimizer.zero_grad()
    
    for i, (data, target) in enumerate(dataloader):
        output = model(data)
        # 损失除以累积步数
        loss = criterion(output, target) / num_accumulation_steps
        loss.backward()
        
        if (i + 1) % num_accumulation_steps == 0:
            optimizer.step()
            optimizer.zero_grad()

//显存优化技术
# 1. 显存优化器
def memory_efficient_training():
    # 使用Adam优化器的显存效率版本
    from torch.optim import AdamW
    optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
    
    # 使用梯度检查点
    from torch.utils.checkpoint import checkpoint
    
    def forward_with_checkpoint(self, x):
        return checkpoint(self.block, x)

# 2. 混合精度训练
def mixed_precision_example():
    scaler = torch.cuda.amp.GradScaler()
    
    for data, target in dataloader:
        with torch.cuda.amp.autocast():
            output = model(data)
            loss = criterion(output, target)
        
        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

GPU缓存的作用

  • 加速数据传输(Pin_memory)
  • 减少内存碎片
  • 优化内存分配效率
  • 提供快速的内存重用

如何理解GPU快速处理矩阵计算

假设我们要计算两个矩阵相乘:C = A × B

# 假设矩阵大小
A: (M × K)
B: (K × N)
C: (M × N)

GPU内部如何对这个计算任务进行分配呢?请看下面的介绍

# 每个线程负责计算C矩阵中的一个元素
# 例如计算C[2,3]的线程:
Thread(2,3) 负责计算:
C[2,3] = sum(A[2,:] * B[:,3]) 

每个线程负责计算矩阵中的一个元素,等所有线程执行完成后,矩阵的计算结果也出来了。每个线程将计算的结果放到共享内存的指定位置。从这个角度看可以矩阵运算实现了并行。 

总结

GPU非常适合于矩阵运算、卷积运算、元素级操作(比如每个元素乘2)。现在的GPU有的显存能够达到80GB甚至更高。

CPU

CPU也可以处理批次数据,这里不重点介绍。主要有以下手段:

  1. 向量化操作(SIMD):Numpy和Pytorch在cpu上的运算会自动使用SIMD指令(如SSE,AVX)
  2. 多线程处理
  3. 多进程处理
;