11.27课堂作业:显存优化与混合精度训练

2024年11月27日 深度学习优化

作业题目

问题一:batch_size对显存占用的影响:batch_size=1和batch_size=2的显存占用区别

问题二:降低临时变量精度的问题及解决方案

问题一:batch_size对显存占用的影响

核心结论

batch_size=2的显存占用不会简单地是batch_size=1的2倍,通常会略少于2倍

1.1 原因分析

1. 激活值的显存占用(线性相关)

  • batch_size=1: 每层的激活值只存储1个样本
  • batch_size=2: 每层的激活值存储2个样本
  • 显存占用基本成线性关系:2 × batch_size=1的激活显存

2. 模型参数和梯度(固定开销)

  • 模型参数: 与batch_size无关,只与模型结构有关
  • 优化器状态: Adam等优化器需要额外的动量和方差参数
  • 这部分显存占用在batch_size=1和2时完全相同

3. 固定开销(与batch_size无关)

  • CUDA上下文: GPU驱动和CUDA运行时的固定开销
  • 框架开销: PyTorch/TensorFlow等框架的内部缓存
  • 内存管理: GPU内存分配器的基本开销

4. 内存管理效率

  • 内存对齐: GPU内存有对齐要求,可能造成轻微的内存浪费
  • 向量化优化: 更大的batch_size可能获得更好的计算效率
  • 内存碎片: 不同batch_size下,内存碎片化程度不同

1.2 理论显存占用公式

$$\text{总显存} \approx \text{模型参数} + \text{优化器状态} + \text{固定开销} + (\text{batch\_size} \times \text{激活值内存})$$

其中:

  • 模型参数 = \(4 \times \text{参数数量}\) 字节(FP32)
  • 优化器状态 ≈ \(8 \times \text{参数数量}\) 字节(Adam优化器)
  • 激活值内存 ≈ \(4 \times \text{激活值数量} \times \text{batch\_size}\) 字节

1.3 总结

非线性关系

batch_size与显存占用不是简单的线性关系

固定开销占主导

存在大量与batch_size无关的固定显存开销

边际效应递减

增加batch_size的边际显存成本递减

优化机会

更大的batch_size可能获得更好的内存管理效率

问题二:降低临时变量精度的问题及解决方案

2.1 降低精度的作用

降低临时变量精度(如从FP32降到FP16或BF16)可以:

显存占用减少

约50%

训练速度提升

支持Tensor Core的GPU

更大batch_size

允许使用更大的batch_size或模型

2.2 降低精度存在的问题

1. 数值不稳定问题

  • 下溢问题: 小数值可能会变成零,导致梯度消失
  • 上溢问题: 大数值可能溢出,导致梯度爆炸
  • 数值范围缩小: FP16的动态范围(6×10⁻⁸到6×10⁴)比FP32(10⁻³⁸到10³⁸)小得多

会影响softmax、sigmoid激活函数的输出,梯度更新过程,BatchNorm层的数值计算等

2. 训练精度下降

  • 权重更新的精度降低
  • 激活值计算精度损失
  • 可能导致模型收敛困难或收敛到次优解
  • 某些敏感的网络结构受影响较大

3. 某些操作的不兼容

  • 某些数学运算在低精度下不稳定
  • 某些网络层(如BatchNorm)对精度敏感
  • 特殊的激活函数可能出现数值问题
  • 累积计算中的精度损失

2.3 对应解决方案

解决方案1:混合精度训练(Mixed Precision Training)

核心思想
  • 保持FP32精度的主权重副本用于精度保证
  • 使用FP16进行前向和反向传播节省显存
  • 累加梯度时使用FP32精度防止精度损失
PyTorch实现
import torch
from torch.cuda.amp import autocast, GradScaler

# 初始化梯度缩放器
scaler = GradScaler()

for data, target in train_loader:
    optimizer.zero_grad()
    
    # 自动混合精度上下文
    with autocast():
        output = model(data)
        loss = criterion(output, target)
    
    # 缩放损失以防止梯度下溢
    scaler.scale(loss).backward()
    
    # 优化器步进(自动缩放回原始精度)
    scaler.step(optimizer)
    scaler.update()

解决方案2:梯度缩放(Gradient Scaling)

原理
  • 在反向传播前放大损失值,防止小梯度下溢
  • 梯度更新时缩小回原始尺度,保持数值稳定
实现示例
def manual_gradient_scaling(model, loss, scale_factor=1024.0):
    """手动梯度缩放实现"""
    scaled_loss = loss * scale_factor
    scaled_loss.backward()
    
    # 梯度缩放回原始值
    for param in model.parameters():
        if param.grad is not None:
            param.grad = param.grad / scale_factor

解决方案3:动态损失缩放(Dynamic Loss Scaling)

自适应策略
  • 自动调整损失缩放因子
  • 检测梯度溢出并动态调整
  • 平衡数值稳定性和精度
算法伪代码
class DynamicLossScaler:
    def __init__(self, init_scale=65536.0, scale_factor=2.0, scale_window=2000):
        self.scale = init_scale
        self.scale_factor = scale_factor
        self.scale_window = scale_window
        self.counter = 0
    
    def step(self, model, optimizer):
        # 检查梯度溢出
        has_inf = torch.isinf(torch.cat([p.grad.flatten()
                                        for p in model.parameters()
                                        if p.grad is not None])).any()
        
        if has_inf:
            # 梯度溢出,减小缩放因子
            self.scale /= self.scale_factor
            optimizer.zero_grad()
            return False  # 需要重新前向传播
        else:
            # 梯度正常,更新缩放因子
            self.counter += 1
            if self.counter >= self.scale_window:
                self.scale *= self.scale_factor
                self.counter = 0
            return True  # 可以继续优化

解决方案4:特殊数据格式支持

BF16(BFloat16)格式
  • 保持FP32的动态范围
  • 精度略低于FP16但比FP16更适合训练
  • 数值范围:10⁻³⁸到10³⁸(与FP32相同)
实现示例
# 使用BF16格式(需要硬件支持)
model = model.to(dtype=torch.bfloat16)

# 在支持BF16的硬件上训练
for data, target in train_loader:
    data = data.to(dtype=torch.bfloat16)
    output = model(data)
    loss = criterion(output, target)
    loss.backward()
    optimizer.step()

解决方案5:网络架构优化

数值稳定的网络设计
class NumericalStableNet(nn.Module):
    def __init__(self):
        super().__init__()
        # 使用数值稳定的激活函数
        self.activation = nn.ReLU()  # 而非nn.Tanh()
        self.norm = nn.LayerNorm(eps=1e-6)  # 增加数值稳定性
    
    def forward(self, x):
        x = self.activation(x)
        x = self.norm(x)
        return x

# 数值稳定的softmax实现
def stable_softmax(x, dim=1):
    """数值稳定的softmax,防止上溢下溢"""
    x_max = torch.max(x, dim=dim, keepdim=True)[0]
    x_exp = torch.exp(x - x_max)  # 减去最大值防止上溢
    return x_exp / torch.sum(x_exp, dim=dim, keepdim=True)

解决方案6:硬件感知优化

Tensor Core优化
  • 利用现代GPU的Tensor Core硬件加速
  • 选择合适的计算精度配置
  • 优化内存访问模式
针对不同硬件的精度选择
def get_optimal_dtype():
    if torch.cuda.is_available():
        gpu_name = torch.cuda.get_device_name()
        if 'A100' in gpu_name or 'V100' in gpu_name:
            return torch.bfloat16  # 新一代GPU支持BF16
        else:
            return torch.float16   # 老一代GPU使用FP16
    return torch.float32

总结

关键要点

batch_size与显存占用不是简单的线性关系,存在大量固定开销

降低精度可以显著减少显存占用,但需要解决数值稳定性问题

混合精度训练是最佳实践,结合了效率和稳定性

梯度缩放技术是解决低精度训练问题的核心方法