Python----大模型(GPT-2模型训练加速,训练策略)

一、GPT2训练加速

1.1 使用tensor_core加速

原理:NVIDIA的Tensor Core支持混合精度计算(FP16/FP32),通过Volta/Turing/Ampere架构的GPU(如V100/A100)加速矩阵运算。

Tensor Core 是 NVIDIA Volta 及更新架构(例如 Turing、Ampere、Hopper)GPU 中专门用 于加速矩阵计算的硬件单元。它们旨在高效地执行混合精度浮点运算,特别是 FP16(半精度浮 点数)和 TF32(Tensor Float 32)运算,并累加到 FP32 精度。

Tensor Core 的工作原理:

传统的 CUDA 核心主要处理标量和向量运算。而 Tensor Core 则专注于矩阵运算,特别是矩阵 乘法累加(Matrix Multiply-Accumulate,MMA)操作。一个 Tensor Core 可以在一个时钟周 期内执行多个 FP16 或 TF32 矩阵的乘法,并将结果累加到 FP32 矩阵上。

例如,在 Volta 架构中,一个 Tensor Core 可以在一个时钟周期内执行一个 4x4 的 FP16 矩阵 乘法,并将结果累加到一个 4x4 的 FP32 矩阵上。这意味着一个 Tensor Core 可以执行 4x4x4=64 次乘法和 4x4=16 次加法。通过并行使用大量的 Tensor Core,GPU 可以显著加速深 度学习模型中的矩阵运算。

注意:支持 Tensor Core 的 NVIDIA GPU: 只有 Volta 及更新架构的 NVIDIA GPU 才支持 Tensor Core。即20系列显卡以上或者V100以上。

python 复制代码
#*-=======================tensor_core加速===========================-*
# 设置张量核心算法的精度级别为TF32
torch.set_float32_matmul_precision("high")  

加速前


加速后

1.2、混合精度

优势:减少显存占用50%,加速计算1.5-3倍。

注意 :可能需调整GradScalerinit_scale以避免梯度下溢。

python 复制代码
    #*-=======================混合精度============================-*
    with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
        logits, loss = model(x, y)

加速前


加速后

1.3、torch_compile**(PyTorch 2.0+)**

介绍:torch.compile --- PyTorch 2.7 documentation

torch.compile是PyTorch2.0引入的一项技术,可以在Linux系统上使用。

在刚开始的版本中未支持Windows,后续PyTorch2.5版本开始, 在 Windows 的 CPU 环境中运行。 要在 Windows 上使用 torch.compile 已正式支持 torch.compile,需要配置 C++ 开 发环境: Accelerate PyTorch* Inference with torch.compile on Windows* CPU

对于Windows的GPU,由于依赖 Triton ,所以目前适配还不是很好。

模式选择

default:平衡编译时间和速度。

reduce-overhead:减少小batch的开销。

max-autotune:极致优化(编译时间长)。

python 复制代码
#*-=======================torch_compile===========================-*
model=torch.compile(model)

加速前


加速后

1.4、flash attention

python 复制代码
        #*-=======================flash attention============================-*
        y = F.scaled_dot_product_attention(q, k, v, is_causal=True)

        
        # att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
        # att = att.masked_fill(self.bias[:, :, :T, :T] == 0 , float("-inf"))
        # att = F.softmax(att, dim=-1)
        # y = att @ v  # (B, nh, T, T) X (B, nh, T, hs) -> (B,  nh, T, hs)

加速前


加速后

1.4、加速总结

方法 显存节省 速度提升 适用场景
Tensor Core 中等 1.5x 矩阵密集运算
混合精度 2x 所有GPU训练
torch.compile 1.3x PyTorch 2.0+
Flash Attention 极高 3x 长序列(>512 tokens)

322 --> 277 --> 114 --> 85 --> 82

二、GPT2训练技巧

2.1、梯度裁剪---训练过程操作

python 复制代码
    #*-=======================梯度裁剪===========================-*
    norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

2.2、学习调度器--余弦退火

python 复制代码
#*-=======================学习率调度器-1===========================-*
# 实现一个学习率调度器(learning rate scheduler),它根据训练的迭代次数(iteration)动态调整学习率。
# 结合了线性预热(linear warmup)和余弦衰减(cosine decay)两种策略。
max_lr = 6e-4  # 最大学习率
min_lr = max_lr * 0.1  # 最小学习率,是最大学习率的 10%
warmup_steps = 10  # 线性预热的迭代次数
max_steps = 50  # 总的训练迭代次数(或衰减到最小学习率的迭代次数)
def get_lr(it):
    """
    根据迭代次数 it 返回当前的学习率。
    Args:
        it: 当前的迭代次数。
    Returns:
        当前的学习率。
    """
    # 1) 线性预热阶段 (Linear Warmup)
    if it < warmup_steps:
        # 在 warmup_steps 步内,学习率从 0 线性增加到 max_lr
        # it 从 0 开始,所以 (it + 1) 从 1 开始,到 warmup_steps。
        # 因此,学习率从 max_lr / warmup_steps 线性增加到 max_lr。
        return max_lr * (it + 1) / warmup_steps
        

    # 2) 达到最大迭代次数后,保持最小学习率 (Minimum Learning Rate after Max Steps)
    if it > max_steps:
        # 超过 max_steps 后,学习率保持在 min_lr
        return min_lr

    # 3) 余弦衰减阶段 (Cosine Decay)
    # 计算衰减比例。
    # it 从 warmup_steps + 1 开始,到 max_steps。
    # decay_ratio 从 (warmup_steps + 1 - warmup_steps) / (max_steps - warmup_steps) = 1/(max_steps - warmup_steps) 线性增加到 (max_steps - warmup_steps) / (max_steps - warmup_steps) = 1。
    decay_ratio = (it - warmup_steps) / (max_steps - warmup_steps)
    assert 0 <= decay_ratio <= 1 # 确保衰减比例在 0 到 1 之间,这是一个良好的编程习惯,用于检查代码的正确性

    # 计算余弦衰减系数。
    # 当 decay_ratio = 0 时,coeff = 0.5 * (1 + cos(0)) = 1。
    # 当 decay_ratio = 1 时,coeff = 0.5 * (1 + cos(pi)) = 0。
    # 因此,coeff 从 1 线性减小到 0。
    coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
    
    # 计算当前学习率。
    # 当 coeff = 1 时,学习率为 min_lr + (max_lr - min_lr) = max_lr。
    # 当 coeff = 0 时,学习率为 min_lr + 0 = min_lr。
    # 因此,学习率从 max_lr 按照余弦曲线衰减到 min_lr。
    return min_lr + coeff * (max_lr - min_lr)
python 复制代码
    #*-=======================学习率调度器-2===========================-*
    # 确定并设置此迭代的学习率
    lr = get_lr(step)
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr

2.3、参数正则化----在模型操作

python 复制代码
#*-=======================参数正则化-1===========================-*
import inspect
def configure_optimizers(self, weight_decay, learning_rate, device):
    # self.named_parameters() 返回模型中所有参数的名称和参数 tensor 的迭代器。
    # 第一个 param_dict 存储了所有参数。
    # 第二个 param_dict 过滤掉了 requires_grad=False 的参数,即不需要计算梯度的参数。
    param_dict = {pn: p for pn, p in self.named_parameters()}
    param_dict = {pn: p for pn, p in param_dict.items() if p.requires_grad}
    # 根据参数的维度进行分组。
    # dim() >= 2 的参数通常是权重矩阵(例如全连接层、卷积层、embedding 层),需要进行权重衰减。
    # dim() < 2 的参数通常是偏置 (bias) 和 LayerNorm 的参数,不需要进行权重衰减。
    decay_params = [p for n, p in param_dict.items() if p.dim() >= 2]
    nodecay_params = [p for n, p in param_dict.items() if p.dim() < 2]
    # 创建优化器参数组。
    # 每个组是一个字典,包含 'params' 和 'weight_decay' 两个键。
    # 第一个组包含需要进行权重衰减的参数,其 weight_decay 设置为传入的 weight_decay 值。
    # 第二个组包含不需要进行权重衰减的参数,其 weight_decay 设置为 0.0。
    optim_groups = [
        {'params': decay_params, 'weight_decay': weight_decay},
        {'params': nodecay_params, 'weight_decay': 0.0}
    ]
    
    # 打印需要进行权重衰减和不需要进行权重衰减的参数数量,用于调试和信息展示。numel() 返回 tensor 中元素的总个数。
    num_decay_params = sum(p.numel() for p in decay_params)
    num_nodecay_params = sum(p.numel() for p in nodecay_params)
    print(f"num decayed parameter tensors: {len(decay_params)}, with {num_decay_params:,} parameters")
    print(f"num non-decayed parameter tensors: {len(nodecay_params)}, with {num_nodecay_params:,} parameters")
    
    # 检查 AdamW 是否支持 fused 版本(fused 版本通常在 CUDA 设备上速度更快)。
    # fused 是将多个操作合并成一个优化的 CUDA kernel
    # inspect.signature() 用于获取函数的签名,parameters 属性返回函数的参数。
    fused_available = 'fused' in inspect.signature(torch.optim.AdamW).parameters
    use_fused = fused_available and device == "cuda"
    print(f"using fused AdamW: {use_fused}")
    # 创建 AdamW 优化器。
    # optim_groups:参数组,用于分别设置不同参数组的 weight_decay。
    # lr:学习率。
    # betas:Adam 优化器的 beta 值。
    # eps:用于数值稳定性的 epsilon 值。
    # fused:是否使用 fused 版本。
    optimizer = torch.optim.AdamW(optim_groups, lr=learning_rate, betas=(0.9, 0.95), eps=1e-8, fused=use_fused)
    return optimizer
python 复制代码
#*-=======================优化器===========================-*
# optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
#*-=======================权重衰减-2===========================-*
optimizer = model.configure_optimizers(weight_decay=0.1, learning_rate=6e-4, device=device)

2.4、梯度累计

python 复制代码
#*-=======================梯度累计-1===========================-*
total_batch_size= 524288  # 期望的总批量大小,以 token 数量计。这里是 524288,约为 0.5M。GPT-3论文中的数据
B = 8 # 小 batch size,每个 GPU 或设备的本地批量大小。这里是 8
T = 1024 # sequence length
assert total_batch_size % (B * T)== 0, "确保 total batch_size 可以被 B * T 整除。"
grad_accum_steps = total_batch_size // (B *T)  # 梯度累积的步数,计算 total_batch_size 除以 (B * T) 的整数部分。
print(f"total batch size: {total_batch_size}")
# 表示需要累积多少个小批次的梯度才能达到期望的总批量大小。
# 524288 // (8 * 1024) = 64,即需要累积 64 个小批次的梯度才能达到 0.5M 的总批量大小。
# 现在一个step是72毫秒,如果我们累积64个step,那么一个step就是64*72=4608毫秒,约等于5秒
print(f"=> 梯度累计批次的数量: {grad_accum_steps}")
python 复制代码
    #*-=======================梯度累计-2===========================-*
    loss_accum = 0.0
    for micro_step in range(grad_accum_steps):
        x, y = train_loader.next_batch()
        x, y = x.to(device), y.to(device)
        #*-=======================混合精度============================-*
        with torch.autocast(device_type=device, dtype=torch.bfloat16):
            logits, loss = model(x, y)
        # 如果不将 loss 除以 grad_accum_steps,那么累积的梯度将会是实际梯度的 grad_accum_steps 倍。
        loss = loss / grad_accum_steps
        loss_accum += loss.detach()
        loss.backward()

2.5、DDP

分布式数据并行(DDP)是PyTorch中用于多GPU训练的核心方案,它通过将模型复制到多个GPU上并行处理数据来实现训练加速。在初始化阶段,程序首先通过检查RANK环境变量来判断当前是否处于DDP模式。当检测到DDP模式时,会使用NCCL后端初始化进程组,这是专为NVIDIA GPU设计的高性能通信后端。每个进程会根据分配的本地rank自动绑定到对应的GPU设备,这种设计确保了各进程独立工作在不同GPU上,避免了设备资源冲突。值得注意的是,进程组初始化必须是同步操作,所有进程需要同时调用init_process_group函数才能继续执行后续代码。

在设备配置方面,DDP模式下每个进程会独占一个GPU,这是通过将设备指定为cuda:{ddp_local_rank}来实现的。这种明确的设备绑定策略相比自动选择设备更加可靠,特别是在多节点训练场景下。对于非DDP模式(单GPU或CPU训练),程序提供了自动设备检测逻辑,会按照CUDA GPU、Apple MPS、CPU的优先级顺序选择合适的计算设备。为了便于后续操作,代码中还额外定义了device_type变量来区分设备类型,这在需要根据设备类型执行不同优化时特别有用。

python 复制代码
# 设置分布式数据并行 (DDP) 环境,如果不是 DDP 运行,则选择可用的设备(CPU、CUDA 或 MPS)。
# simple launch:
# python 15DDP.py
# DDP launch for e.g. 2 GPUs:
# torchrun --standalone --nproc_per_node=2 15DDP.py
import os
import torch
from torch.distributed import init_process_group, destroy_process_group
from torch.nn.parallel import DistributedDataParallel as DDP
import torch.distributed as dist

# 设置 DDP (分布式数据并行)。
# torchrun 命令会设置环境变量 RANK、LOCAL_RANK 和 WORLD_SIZE
ddp = int(os.environ.get('RANK', -1)) != -1 # 判断是否是 DDP 运行。如果设置了 RANK 环境变量,则认为是 DDP 运行。
if ddp:
    # # 如果是 DDP 运行,目前需要 CUDA,根据 rank 设置设备
    assert torch.cuda.is_available(), "DDP 的运行需要 CUDA"
    init_process_group(backend='nccl') # 初始化进程组,使用 nccl 后端(适用于 NVIDIA GPU)
    ddp_rank = int(os.environ['RANK']) # 当前进程的全局 rank(在所有进程中的排名)
    ddp_local_rank = int(os.environ['LOCAL_RANK'])  # 当前进程在当前节点上的本地 rank
    ddp_world_size = int(os.environ['WORLD_SIZE'])  # 总共有多少个进程
    device = f'cuda:{ddp_local_rank}'  # 根据本地 rank 设置设备,例如 cuda:0, cuda:1
    torch.cuda.set_device(device)  # 设置当前进程使用的 GPU 设备
    master_process = ddp_rank == 0 # # 判断当前进程是否是 master 进程(rank 为 0 的进程),master 进程负责日志记录、保存检查点等
else:
    # 非 DDP 运行(单卡或 CPU 运行)
    ddp_rank = 0  # 设置 rank 为 0
    ddp_local_rank = 0 # 设置本地rank为0
    ddp_world_size = 1 # 设置世界大小为1
    master_process = True  # 单进程,所以是 master 进程
    # 尝试自动检测设备
    device = "cpu"
    if torch.cuda.is_available():
        device = "cuda"
    elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
        device = "mps"
    if master_process:
        print(f"using device: {device}")

# 设备是CUDA:1等具体执行的设备,添加device_type 标识用来区分运行设备是CPU还是CUDA等
device_type = "cuda" if device.startswith("cuda") else "cpu"

模型封装是DDP的核心环节,通过DistributedDataParallel包装原始模型,实现了梯度同步和参数广播的自动化。值得注意的是,DDP封装后的模型会将原始模型保存在module属性中,因此通过raw_model = model.module if ddp else model这种方式可以始终访问到未经封装的原始模型。这种设计在保存检查点或访问模型自定义属性时尤为重要,因为直接操作DDP封装后的模型可能会导致意外行为。在底层实现上,DDP会自动在前向传播时广播模型参数,在反向传播时使用AllReduce操作同步梯度。

python 复制代码
#*-=======================DDP-1===========================-*
if ddp:
    model = DDP(model, device_ids=[ddp_local_rank])
# 当使用 torch.nn.parallel.DistributedDataParallel (DDP) 封装模型时,DDP 会在原始模型 (model) 的外面包装一层,创建一个新的模型对象。
# 这个新的模型对象负责在多个进程之间同步梯度、管理数据分发等。
# 原始模型会被存储在新模型的 module 属性中。因此,如果你想访问原始模型的属性或方法,你需要通过 model.module 来访问。
raw_model = model.module if ddp else model # always contains the "raw" unwrapped model
python 复制代码
#*-=======================DDP-2===========================-*
optimizer = raw_model.configure_optimizers(weight_decay=0.1, learning_rate=6e-4, device_type=device)

数据加载的分布式处理是通过自定义的DataLoaderLite实现的,它根据process_rank和num_processes参数将数据集划分到不同进程。这种数据并行策略要求每个进程处理不同的数据子集,从而在全局上实现数据的完整覆盖。为了进一步提高训练效率,代码中实现了梯度累积与同步控制的精细管理,通过model.require_backward_grad_sync属性控制在何时执行梯度同步,这使得在保持较大有效batch size的同时,可以使用较小的显存占用进行训练。

python 复制代码
#*-=======================DDP-3===========================-*
train_loader = DataLoaderLite(B=B, T=T, process_rank=ddp_rank, num_processes=ddp_world_size)
python 复制代码
        #*-=======================DDP-4===========================-*
        if ddp:
            model.require_backward_grad_sync =(micro_step == grad_accum_steps -1)

在训练过程中,损失值的同步是通过dist.all_reduce操作实现的,使用AVG操作符对各个进程计算的损失值求平均,这比简单的求和更能反映模型在全局数据上的表现。为了监控训练效率,代码还计算了tokens_per_sec指标,这个指标考虑了batch size、序列长度、梯度累积步数和GPU数量等因素,能够全面反映系统的整体吞吐量。通过这些精心设计的分布式训练组件,DDP可以在多GPU环境下实现接近线性的加速比,大幅提升模型训练效率。

python 复制代码
    #*-=======================DDP-5===========================-*    
    if ddp:
        dist.all_reduce(loss_accum, op=dist.ReduceOp.AVG)
python 复制代码
    #*-=======================DDP-6===========================-*    
    tokens_processed = train_loader.B * train_loader.T * grad_accum_steps * ddp_world_size
    tokens_per_sec = tokens_processed / dt  # 计算每秒训练的token数量

在实际部署DDP训练时,开发者需要注意几个关键点:确保数据分片的均匀性以避免负载不均衡;正确处理检查点保存,只需由rank 0进程执行即可;使用torch.profiler等工具进行性能分析,找出可能的瓶颈;注意处理可能出现的死锁问题,特别是在多节点训练环境中。此外,将DDP与其他优化技术如混合精度训练、梯度裁剪等结合使用,可以进一步发挥分布式训练的优势。通过合理配置这些组件,即使是像GPT-2这样的大型语言模型,也能在合理的时间内完成高效训练。

相关推荐
xwill*2 小时前
π∗0.6: a VLA That Learns From Experience
人工智能·pytorch·python
jiayong232 小时前
知识库概念与核心价值01
java·人工智能·spring·知识库
雨轩剑2 小时前
做 AI 功能不难,难的是把 App 发布上架
人工智能·开源软件
还不秃顶的计科生2 小时前
LeetCode 热题 100第二题:字母易位词分组python版本
linux·python·leetcode
独自破碎E2 小时前
解释一下RAG中的Rerank
gpt·语言模型
Tezign_space2 小时前
AI智能体赋能实践:从提示工程到上下文工程的架构演进
人工智能·架构·agentic ai·上下文工程·大模型智能体·长程任务·模型注意力预算
weixin_462446233 小时前
exo + tinygrad:Linux 节点设备能力自动探测(NVIDIA / AMD / CPU 安全兜底)
linux·运维·python·安全
不瘦80斤不改名3 小时前
Python 日志(logging)全解析
服务器·python·php
多米Domi0113 小时前
0x3f 第19天 javase黑马81-87 ,三更1-23 hot100子串
python·算法·leetcode·散列表