2. 配置PyTorch自动混合精度(AMP)

任务目标

在单卡GPU上,分别用纯FP32和自动混合精度(AMP)训练同一个小型CNN模型(CIFAR-10分类),对比两者的训练耗时与最终测试准确率。AMP使用torch.cuda.amp提供的GradScaler与autocast,在保持数值稳定的前提下利用FP16加速计算。

环境依赖

  • Python 3.8+,PyTorch 1.10+(需支持CUDA),torchvision,CUDA可用GPU
  • 安装:pip install torch torchvision

完整实现

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import time

# 固定随机种子保证可复现
torch.manual_seed(0)

# 数据加载(CIFAR-10,仅用前2000张训练/500张测试加速演示)
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
trainset = torch.utils.data.Subset(trainset, range(2000))
testset = torch.utils.data.Subset(testset, range(500))
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)

# 小型CNN模型
class SmallCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(32 * 8 * 8, 64)
        self.fc2 = nn.Linear(64, 10)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.pool(self.relu(self.conv1(x)))
        x = self.pool(self.relu(self.conv2(x)))
        x = x.view(x.size(0), -1)
        x = self.relu(self.fc1(x))
        return self.fc2(x)

def train_eval(use_amp):
    device = torch.device('cuda')
    model = SmallCNN().to(device)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
    scaler = torch.cuda.amp.GradScaler(enabled=use_amp)

    model.train()
    start = time.time()
    for epoch in range(3):
        for inputs, labels in trainloader:
            inputs, labels = inputs.to(device), labels.to(device)
            optimizer.zero_grad()
            with torch.cuda.amp.autocast(enabled=use_amp):
                outputs = model(inputs)
                loss = criterion(outputs, labels)
            scaler.scale(loss).backward()
            scaler.step(optimizer)
            scaler.update()
    train_time = time.time() - start

    # 测试
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for inputs, labels in testloader:
            inputs, labels = inputs.to(device), labels.to(device)
            with torch.cuda.amp.autocast(enabled=use_amp):
                outputs = model(inputs)
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    acc = 100.0 * correct / total
    return train_time, acc

if __name__ == '__main__':
    fp32_time, fp32_acc = train_eval(use_amp=False)
    amp_time, amp_acc = train_eval(use_amp=True)
    print(f'FP32: 耗时 {fp32_time:.2f}s, 准确率 {fp32_acc:.2f}%')
    print(f'AMP:  耗时 {amp_time:.2f}s, 准确率 {amp_acc:.2f}%')
    print(f'加速比: {fp32_time / amp_time:.2f}x')

运行方式

python amp_demo.py

验证方法与判定标准

  • 程序正常结束并输出两行对比结果,无CUDA错误或NaN损失。
  • AMP模式训练耗时明显低于FP32(通常加速1.2~2倍,视GPU型号而定),且两者测试准确率差距在2个百分点以内,说明AMP在提速的同时保持了精度。
  • 若AMP准确率显著下降(>5%),检查是否遗漏scaler.update()或未在反向传播前调用scaler.scale(loss)。

关键说明

  • autocast上下文自动为CUDA算子选择FP16/FP32,无需手动转换输入张量。
  • GradScaler通过放大loss防止FP16梯度下溢,step()内部先反缩放梯度再更新权重。
  • 本案例使用数据子集和3个epoch仅为快速演示,实际训练需使用完整数据集并增加epoch数。
AMP训练流程 vs FP32训练流程 FP32 前向传播 (FP32) 计算损失 反向传播 (FP32梯度) 优化器更新 AMP autocast: 前向 (FP16/FP32) 计算损失 scaler.scale(loss).backward() scaler.step(optimizer) + update() 关键差异 梯度缩放