MiniMind学习笔记(三)--train_pretrain.py(预训练)

简介

train_pretrain.py是MiniMind项目中用于预训练的主脚本,负责模型的预训练流程。

train_pretrain.py文件整个分了2部分,第一部分是一个函数,

def train_epoch(epoch, loader, iters, start_step=0, wandb=None):

此函数在第二部分的执行方法中会调用,功能应该是模型训练。

第二部分是一段执行方法,

if name == "main":

即直接执行train_pretrain.py时执行,作用也是模型训练,重点看一下这部分。

直接执行代码先是做了一些参数解析,可用参数很多:

parser = argparse.ArgumentParser(description="MiniMind Pretraining")

parser.add_argument("--save_dir", type=str, default="../out", help="模型保存目录")

parser.add_argument('--save_weight', default='pretrain', type=str, help="保存权重的前缀名")

parser.add_argument("--epochs", type=int, default=1, help="训练轮数(建议1轮zero或2-6轮充分训练)")

parser.add_argument("--batch_size", type=int, default=32, help="batch size")

parser.add_argument("--learning_rate", type=float, default=5e-4, help="初始学习率")

parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")

parser.add_argument("--dtype", type=str, default="bfloat16", help="混合精度类型")

parser.add_argument("--num_workers", type=int, default=8, help="数据加载线程数")

parser.add_argument("--accumulation_steps", type=int, default=8, help="梯度累积步数")

parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值")

parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")

parser.add_argument("--save_interval", type=int, default=1000, help="模型保存间隔")

parser.add_argument('--hidden_size', default=512, type=int, help="隐藏层维度")

parser.add_argument('--num_hidden_layers', default=8, type=int, help="隐藏层数量")

parser.add_argument('--max_seq_len', default=340, type=int, help="训练的最大截断长度(中文1token≈1.5~1.7字符)")

parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1], help="是否使用MoE架构(0=否,1=是)")

parser.add_argument("--data_path", type=str, default="../dataset/pretrain_hq.jsonl", help="预训练数据路径")

parser.add_argument('--from_weight', default='none', type=str, help="基于哪个权重训练,为none则从头开始")

parser.add_argument('--from_resume', default=0, type=int, choices=[0, 1], help="是否自动检测&续训(0=否,1=是)")

parser.add_argument("--use_wandb", action="store_true", help="是否使用wandb")

parser.add_argument("--wandb_project", type=str, default="MiniMind-Pretrain", help="wandb项目名")

parser.add_argument("--use_compile", default=0, type=int, choices=[0, 1], help="是否使用torch.compile加速(0=否,1=是)")

args = parser.parse_args()

其余的部分用注释比较清晰的分成了多个步骤,下面详细看一下。

1. 初始化环境和随机种子

========== 1. 初始化环境和随机种子 ==========

local_rank = init_distributed_mode()

if dist.is_initialized(): args.device = f"cuda:{local_rank}"

setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))

1.1,环境

所谓环境,指的是训练时的硬件运行模式,即:

非分布式环境:单卡GPU训练。

分布式环境(DDP):多卡并行训练,每张卡跑一部分数据,梯度同步更新。

其中的init_distributed_mode()函数如下:

def init_distributed_mode():

if int(os.environ.get("RANK", -1)) == -1:

return 0 # 非DDP模式

dist.init_process_group(backend="nccl")

local_rank = int(os.environ["LOCAL_RANK"])

torch.cuda.set_device(local_rank)

return local_rank

其中environ的RANK变量代表这是第几个进程,启动多卡训练时,系统会自动设置此变量,如果RANK不存在(返回-1),表示当前是单卡运行。

返回的local_rank变量代表当前是第几张卡。

如果是多卡环境,需要初始化通信、绑定设备等工作。

1.2,随机种子

所谓随机种子,指的是在整个训练过程中,多个地方都会用到随机种子来产生随机结果,包括:

1,Python 内置 random 模块

2,NumPy 随机数生成器

3,PyTorch 的 CPU 随机数

4,PyTorch 的 CUDA 随机数(单卡)

5,PyTorch 的 CUDA 随机数(多卡,manual_seed_all)

6,cuDNN(深度神经网络加速库)使用确定性算法(deterministic=True),并关闭自动寻找最优算法的功能(benchmark=False)。

由于计算机的随机都是基于种子来计算结果,即伪随机,所以setup_seed函数就是将以上所有随机算法的种子固定,这样一来训练结果就是固定的,再跑一遍结果也一样,即:场景复现。

场景复现对于调试、对比实验、论文发表都非常重要。

所以代码中的:

setup_seed(42 + (dist.get_rank() if dist.is_initialized() else 0))

就是在固定种子,如果是单卡,种子就是42,如果是多卡,每个进程的种子是42+RANK(当前进程编号),不同GPU上种子不一样,彼此独立。

setup_seed()函数的内容:

def setup_seed(seed: int):

random.seed(seed)

np.random.seed(seed)

torch.manual_seed(seed)

torch.cuda.manual_seed(seed)

torch.cuda.manual_seed_all(seed)

torch.backends.cudnn.deterministic = True

torch.backends.cudnn.benchmark = False

就是在固定各个算法的种子。

2. 配置目录、模型参数、检查ckp

代码如下:

========== 2. 配置目录、模型参数、检查ckp ==========

os.makedirs(args.save_dir, exist_ok=True)

lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=bool(args.use_moe))

ckp_data = lm_checkpoint(lm_config, weight=args.save_weight, save_dir='../checkpoints') if args.from_resume==1 else None

2.1,ckp检查点

首先,代码里出现了ckp的概念,即Checkpoint,检查点,是训练过程的快照。

检查点包含的内容有:

1,模型权重。模型当前学习到的参数(神经元的连接强度)

2,优化器状态。优化器内部的一些动量、方差等中间变量,用于恢复训练时的学习率调整方向

3,训练进度。已经训练了多少轮(epoch)、多少步(step)

4,其他元信息。比如使用的GPU数量、wandb运行ID等

检查点的功能:

1,中断后恢复训练:比如训练到一半断电了,加载最新的检查点就能接着训练,不用从头开始。

2,选择最佳模型:保存验证集上表现最好的那个时刻的模型,用于后续测试或部署。

3,实验复现:配合随机种子,可以从某个检查点精确复现后续结果。

代码中的lm_checkpoint()函数,既可以保存检查点(当传入了 model 参数时),也可以加载检查点(当 model=None 时),从本次调用函数的入参可以看到,model参数没传,即为加载检查点。

关于from_resume参数:

args.from_resume 是一个命令行参数(通常为0或1)。如果设为1,表示要从之前的检查点恢复训练。

当 from_resume=1 时,则ckp_data 会被赋值为 lm_checkpoint函数返回的加载结果(即之前保存的 _resume.pth 文件中的内容)。

如果 from_resume=0,则ckp_data = None,表示从头开始训练。

lm_checkpoint函数的核心逻辑如下,可以看到是否传入model参数将逻辑分成了两部分。

def lm_checkpoint(lm_config, weight='full_sft', model=None, ..., save_dir='../checkpoints'):

根据是否使用MoE,生成带后缀的路径名

moe_path = '_moe' if lm_config.use_moe else ''

ckp_path = f'{save_dir}/{weight}_{lm_config.hidden_size}{moe_path}.pth' # 纯模型权重

resume_path = f'{save_dir}/{weight}_{lm_config.hidden_size}{moe_path}_resume.pth' # 完整恢复包

if model is not None:

【保存模式】将模型、优化器、进度等保存到两个文件

保存纯模型权重到 ckp_path

保存完整恢复信息(含优化器、epoch、step等)到 resume_path

...

else:

【加载模式】尝试读取 resume_path

if os.path.exists(resume_path):

ckp_data = torch.load(resume_path, map_location='cpu')

如果训练时GPU数量和现在不同,自动调整step(用于学习率调度)

...

return ckp_data

return None

检查点的文件:

在保存模式中用到的两个文件(开始训练的时候用不到),记录了检查点需要的信息,有两个文件:

1,纯权重文件。ckp_path,只记录了模型参数state_dict。路径示例:../checkpoints/full_sft_512.pth

2,恢复文件。resume_path,记录了完整的恢复数据,包括模型权重 + 优化器状态 + epoch + step + 其他。路径示例:../checkpoints/full_sft_512_resume.pth

2.2,配置模型结构

lm_config = MiniMindConfig(...)

此部分的功能是配置模型结构(层数、维度、是否用MoE)

3. 设置混合精度

========== 3. 设置混合精度 ==========

device_type = "cuda" if "cuda" in args.device else "cpu"

dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16

autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype)

这部分不止处理了混合精度,还查询了设备类型。

3.1,设备类型

device_type参数即查询出的设备类型。

MiniMind可支持的设备类型有两种,NVIDIA GPUCPU。用户如果想用英伟达显卡训练,入参的args.device应包含"cuda"字样,如"cuda:0",那么MiniMind将使用CUDA相关的加速。

其实PyTorch还支持"mps"(Apple Silicon),"xpu"(Intel GPU)等设备,但MiniMind不支持这些。

3.2,关于精度

深度学习常用的精度有:

1,float16 (fp16)。半精度浮点数,16位,显存占用减半,计算快,数值范围小,容易溢出。

2,bfloat16 (bf16)。脑浮点16位,指数位与fp32相同,数值范围与fp32相同,不易溢出,精度稍低,需要较新GPU支持(如A100、RTX 3090等)。

3,float32 (fp32)。单精度浮点数,32位,数值稳定,精度高,显存占用大,计算慢。

MiniMind中的这段代码:

dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16

表示用户如果用dtype参数指定精度为bfloat16(--dtype bfloat16),就用 bfloat16,否则用float16(默认)。

3.3,关于混合精度

混合精度训练是一种同时使用高精度(fp32)和低精度(fp16/bf16)的技术,目的是在不显著降低模型精度的前提下,加快训练速度并减少显存占用。

从相关介绍中可以看到,dtype参数实际上指定的是某些可以安全使用低精度的操作中(如矩阵乘法、卷积)可以用哪种精度来进行计算。

怎么用:

1,前向传播和反向传播:使用低精度(fp16/bf16)计算,显存占用减半,计算速度加快(现代GPU对fp16有专门的加速指令)。

2,梯度更新:将低精度梯度转换回 fp32,再更新主权重。这样可以避免因低精度数值过小而导致的"下溢"(梯度变成0)。

3,损失缩放(仅fp16需要):为了防止梯度下溢,在反向传播前将损失乘以一个缩放因子,梯度更新前再除回来。

3.4,关于autocast_ctx参数

autocast_ctx参数是一个上下文管理器,用于包裹需要自动混合精度的代码块。

如果如果设备是 CPU:

autocast_ctx = nullcontext(),

nullcontext类定义了一个"啥也不做"的上下文,里面所有的继承父类的方法都是什么都不做。

如果设备是 CUDA:

autocast_ctx = torch.cuda.amp.autocast(dtype=dtype),

autocast是它是一个自动混合精度的上下文,进入该上下文后,PyTorch 的有些操作会用fp16/bf16执行,比如矩阵乘法、卷积,有些操作用 fp32 保持数值稳定,比如损失计算、softmax。

4, 配置wandb(实际上是SwanLab)

========== 4. 配wandb ==========

wandb = None

if args.use_wandb and is_main_process():

import swanlab as wandb

wandb_id = ckp_data.get('wandb_id') if ckp_data else None

resume = 'must' if wandb_id else None

wandb_run_name = f"MiniMind-Pretrain-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"

wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)

4.1关于wandb

WandB(Weights & Biases)是一个机器学习实验跟踪与可视化平台,在模型训练过程中,它能帮我们实时远程查看并记录下各种关键信息,比如:

Loss曲线:损失值如何变化。

资源占用:GPU使用率、显存占用。

模型效果:准确率等。

系统参数:代码版本、训练超参数。

WandB是远程使用的,数据将传到WandB服务器,并在WandB页面查看结果,服务器在境外。

4.2关于SwanLab

SwanLab是国产开源的可视化工具,服务器在国内,可以认为是WandB 的高效国产平替,在网络稳定性、数据隐私和易用性上优势明显。

SwanLab提供了和WandB几乎相同的API。

4.3代码说明

只有在以下两个条件满足时,才启用wandb:

1,args.use_wandb。入参明确说明要用wandb

2,is_main_process()。当前进程是主进程。当多卡同时训练时,每个GPU单独创建一个进程,RANK=0的那个是主进程,主进程往往有额外的工作要做。

import swanlab as wandb

可以看到MiniMind实际上用的跟踪工具是swanlab,但还是命名成wandb,api一样。

wandb_id = ckp_data.get('wandb_id') if ckp_data else None

resume = 'must' if wandb_id else None

从第二步的检查点ckp_data获得wandb_id,即实验ID。如果能获取,则resume参数为'must',代表本次将从检查点继续训练,否则就从头训练。

wandb_run_name = f"MiniMind-Pretrain-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}"

wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume)

创建实验名字wandb_run_name,显示在WandB网页端,名称中包含了训练轮次args.epochs、批次大小args.batch_size、学习率args.learning_rate等。

然后用init方法启动实验会话,其中:

project=args.wandb_project:指定实验所属的项目名,方便对一系列相关实验进行归类管理。

name=wandb_run_name:设置本次实验运行的具体名称。

id=wandb_id:指定本次实验的唯一ID标识符。

resume=resume:按照第二步的逻辑,控制实验是新开一个还是续接旧的那一个。

另外,使用swanlab之前需要登录,可以在终端输入

swanlab login

粘贴 API Key 即可,也可以代码内登录:

swanlab.login(api_key="...")

5. 定义模型、数据、优化器

第五部分,代码如下:

========== 5. 定义模型、数据、优化器 ==========

model, tokenizer = init_model(lm_config, args.from_weight, device=args.device)

if args.use_compile == 1:

model = torch.compile(model)

Logger('torch.compile enabled')

train_ds = PretrainDataset(args.data_path, tokenizer, max_length=args.max_seq_len)

train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None

scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16'))

optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)

5.1代码分析

首先是init_model()函数,初始化了模型和分词器,重要函数。

torch.compile是PyTorch 2.0 引入的即时编译(JIT)技术,通常能让训练速度提升 10%~30%,但是第一次调用时会有一个较长的编译过程。

PretrainDataset类创建预训练数据集对象,把数据集的每一行转成TokenId列表。

DistributedSampler是分布式采样器,每个GPU(进程)拿到不同的数据。

GradScaler是梯度缩放,float16 需要梯度缩放来防止下溢,而 bfloat16 不需要。dtype设为float16将启用

AdamW是优化器,是经典 Adam 改进版,修正了权重衰减的方式。

5.2,关于init_model函数

def init_model(lm_config, from_weight='pretrain', tokenizer_path='../model', save_dir='../out', device='cuda'):

tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)

model = MiniMindForCausalLM(lm_config)

if from_weight!= 'none':

moe_suffix = '_moe' if lm_config.use_moe else ''

weight_path = f'{save_dir}/{from_weight}_{lm_config.hidden_size}{moe_suffix}.pth'

weights = torch.load(weight_path, map_location=device)

model.load_state_dict(weights, strict=False)

get_model_params(model, lm_config)

Logger(f'Trainable Params: {sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f}M')

return model.to(device), tokenizer

其中

tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)

初始化了分词器,路径默认是model目录,其中的:

tokenizer.json文件包含了分词器的词表(vocab)、合并规则(BPE merges)等完整状态。

tokenizer_config.json文件是配置文件,指定分词器的类型,特殊 token 的 ID,以及其他加载参数。

然后代码初始化了一个模型对象model,此时模型的权重没有训练好,即参数都是随机初始化的。而从HuggingFace下载的MiniMind2 文件夹是已经预训练完成的模型权重。

from_weight参数是加载权重文件,即上文检查点中的纯模型权重文件(ckp_path)。如果传入了这个参数,则会用torch.load函数加载权重到指定设备上。

get_model_params方法打印了总参数量、激活参数量等,让人清楚模型的实际计算代价。

如输出:Model Params: 500.00M-A190.00M,表示实际计算时只用190M,存储需要500M。

model.to(device)代表将模型放入指定设备,如cuda:0。

5.3,关于PretrainDataset类

PretrainDataset类定义在MiniMind的lm_dataset.py文件中:

class PretrainDataset(Dataset):

def init(self, data_path, tokenizer, max_length=512):

super().init()

self.tokenizer = tokenizer

self.max_length = max_length

self.samples = load_dataset('json', data_files=data_path, split='train')

def len(self):

return len(self.samples)

def getitem(self, index):

sample = self.samples[index]

tokens = self.tokenizer(str(sample['text']), add_special_tokens=False, max_length=self.max_length - 2, truncation=True).input_ids

tokens = [self.tokenizer.bos_token_id] + tokens + [self.tokenizer.eos_token_id]

input_ids = tokens + [self.tokenizer.pad_token_id] * (self.max_length - len(tokens))

input_ids = torch.tensor(input_ids, dtype=torch.long)

labels = input_ids.clone()

labels[input_ids == self.tokenizer.pad_token_id] = -100

return input_ids, labels

作为Dataset的子类,对象初始化时会调用__getitem__方法,可以看到:

1,方法把sample(即一行文本)切成了tokens列表。

2,把tokens列表前后加上开始结束标识。

3,把tokens列表转成input_ids,即tokenId列表。

4,把input_ids列表后面补充填充标识,使长度达到最大长度。

5,把input_ids列表转成torch.tensor格式。本质上也是个列表,此格式便于计算机进行计算。

6,input_ids列表复制出labels列表,labels列表的填充标识数值减100,后面计算会用到此设定。

7,把input_ids列表和labels列表一起返回。

6. 从ckp恢复状态

========== 6. 从ckp恢复状态 ==========

start_epoch, start_step = 0, 0

if ckp_data:

model.load_state_dict(ckp_data['model'])

optimizer.load_state_dict(ckp_data['optimizer'])

scaler.load_state_dict(ckp_data['scaler'])

start_epoch = ckp_data['epoch']

start_step = ckp_data.get('step', 0)

从之前保存的完整检查点文件(_resume.pth)中恢复训练现场,让训练能从上次中断的位置无缝继续。

而第一次训练时,ckp_data 为 None,因此这段代码会被完全跳过。

其中各组件恢复内容:

1,model.load_state_dict(...).恢复模型权重(所有神经元参数)

2,optimizer.load_state_dict(...)。恢复优化器内部状态(动量、方差等),确保学习率调度、梯度累积等行为与中断前一致

3,scaler.load_state_dict(...)。恢复混合精度训练的梯度缩放器状态(动态缩放因子),避免重新适应

4,start_epoch。从第几个 epoch 继续训练

5,start_step。从当前 epoch 内的第几步继续(用于 resume 时精确对齐 batch 进度)

7,DDP包模型

========== 7. DDP包模型 ==========

if dist.is_initialized():

model._ddp_params_and_buffers_to_ignore = {"freqs_cos", "freqs_sin"}

model = DistributedDataParallel(model, device_ids=[local_rank])

这部分用于部署分布式数据并行(Distributed Data Parallel,DDP) 的,目的是在多块GPU上高效地训练模型。

忽略了freqs_cos 与 freqs_sin,这些是用正弦、余弦函数预先算好的固定值,不需要训练。

dist来自PyTorch的torch.distributed,分布式通信包,负责处理进程间的通信和各种协作。

DistributedSampler类会将训练数据切分,为每个GPU分配不同的子集,确保每张卡"看"到的数据不重复。

8,开始训练

========== 8. 开始训练 ==========

for epoch in range(start_epoch, args.epochs):

train_sampler and train_sampler.set_epoch(epoch)

setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()

skip = start_step if (epoch == start_epoch and start_step > 0) else 0

batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)

loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)

if skip > 0:

Logger(f'Epoch [{epoch + 1}/{args.epochs}]: 跳过前{start_step}个step,从step {start_step + 1}开始')

train_epoch(epoch, loader, len(loader) + skip, start_step, wandb)

else:

train_epoch(epoch, loader, len(loader), 0, wandb)

可以看到训练的核心逻辑就是两层循环,第一层是:

for epoch in range(start_epoch, args.epochs):

args.epochs是入参里的训练轮数

start_epoch是从ckp恢复的轮数,用于断点续训,首次训练没这个参数

第二层循环是train_epoch()函数中的:

for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):

遍历每个批次(batch)的数据。loader就是之前创建的 DataLoader,每次迭代会返回一个批次的数据。

整个训练流程分几步:

8.1,初始化环境与随机种子

train_sampler and train_sampler.set_epoch(epoch)

setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist()

通过重置采样器和随机种子,确保数据在每个epoch重新打乱,防止模型产生记忆偏差。

8.2,处理断点续训时的数据偏移

skip = start_step if (epoch == start_epoch and start_step > 0) else 0

batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip)

loader = DataLoader(train_ds, batch_sampler=batch_sampler, num_workers=args.num_workers, pin_memory=True)

表示是否跳过前skip个批次。只有第一批(epoch和start_epoch都0),或者断点续训的第一批(epoch == start_epoch),而且从ckp里拿到step参数大于0,才会跳。

然后和数据加载器 DataLoader 协同工作。

8.3,开始训练,train_epoch()函数

其代码如下:

def train_epoch(epoch, loader, iters, start_step=0, wandb=None):

start_time = time.time()

for step, (input_ids, labels) in enumerate(loader, start=start_step + 1):

input_ids = input_ids.to(args.device)

labels = labels.to(args.device)

lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)

for param_group in optimizer.param_groups:

param_group['lr'] = lr

with autocast_ctx:

res = model(input_ids, labels=labels)

loss = res.loss + res.aux_loss

loss = loss / args.accumulation_steps

scaler.scale(loss).backward()

if (step + 1) % args.accumulation_steps == 0:

scaler.unscale_(optimizer)

torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)

scaler.step(optimizer)

scaler.update()

optimizer.zero_grad(set_to_none=True)

if step % args.log_interval == 0 or step == iters - 1:

spend_time = time.time() - start_time

current_loss = loss.item() * args.accumulation_steps

current_aux_loss = res.aux_loss.item() if res.aux_loss is not None else 0.0

current_logits_loss = current_loss - current_aux_loss

current_lr = optimizer.param_groups[-1]['lr']

eta_min = spend_time / (step + 1) * iters // 60 - spend_time // 60

Logger(f'Epoch:[{epoch + 1}/{args.epochs}]({step}/{iters}), loss: {current_loss:.4f}, logits_loss: {current_logits_loss:.4f}, aux_loss: {current_aux_loss:.4f}, lr: {current_lr:.8f}, epoch_time: {eta_min:.1f}min')

if wandb: wandb.log({"loss": current_loss, "logits_loss": current_logits_loss, "aux_loss": current_aux_loss, "learning_rate": current_lr, "epoch_time": eta_min})

if (step % args.save_interval == 0 or step == iters - 1) and is_main_process():

model.eval()

moe_suffix = '_moe' if lm_config.use_moe else ''

ckp = f'{args.save_dir}/{args.save_weight}_{lm_config.hidden_size}{moe_suffix}.pth'

raw_model = model.module if isinstance(model, DistributedDataParallel) else model

raw_model = getattr(raw_model, '_orig_mod', raw_model)

state_dict = raw_model.state_dict()

torch.save({k: v.half().cpu() for k, v in state_dict.items()}, ckp)

lm_checkpoint(lm_config, weight=args.save_weight, model=model, optimizer=optimizer, scaler=scaler, epoch=epoch, step=step, wandb=wandb, save_dir='../checkpoints')

model.train()

del state_dict

del input_ids, labels, res, loss

train_epoch()函数的第一步就是前面说的第二层循环。

8.4,加载数据

input_ids = input_ids.to(args.device)

labels = labels.to(args.device)

数据加载到GPU。

8.5,动态学习率计算

lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)

调用学习率调度函数,根据当前的训练进度动态计算学习率,用于平滑和提高模型的收敛效果。

8.6,前向传播与损失计算

with autocast_ctx:

进入自动混合精度的上下文管理器。

res = model(input_ids, labels=labels)

将数据喂给模型,因为模型是继承自 nn.Module 的 MiniMindForCausalLM,这里会自动调用该模块的 forward 函数。

loss / args.accumulation_steps

用 accumulation_steps 对损失进行缩放,这是梯度累积(Gradient Accumulation)的关键步骤。通过"梯度和"代替"均值化",使得等效批次(Effective Batch Size)= 物理批次 × accumulation_steps。

8.7,反向传播(梯度计算与累积)

scaler.scale(loss).backward()

在混合精度下,会先放大损失值,然后自动计算梯度。这是 loss.backward()的升级版,梯度会累加(accumulate)到模型参数的.grad 属性中,而不是被覆盖。

8.8,梯度裁剪(Gradient Clipping)

torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)

在参数更新前,将所有模型参数的梯度范数(Norm)限制在args.grad_clip阈值内。这是为了防止"梯度爆炸",使训练过程更稳定。

8.9,参数更新与缩放器调整

scaler.step(optimizer)

会先自动将累积的梯度还原(unscale),然后用这些梯度去更新模型参数。

scaler.update()

会根据是否有梯度溢出来动态调整内部缩放因子。

8.10,梯度清零

optimizer.zero_grad(set_to_none=True)

将模型参数的梯度 grad 重置为 None。这比重新计算0值更省内存,并为新一轮的梯度累积做准备。

结语:

以上是train_pretrain.py文件的大概内容,包含MiniMind预训练的流程,其细节和计算将由PyTorch模块完成。

相关推荐
OSwich2 小时前
【 Godot 4 学习笔记】数组(Array)
笔记·学习·godot
冷雨夜中漫步2 小时前
Claude Code源码分析——Claude Code Agent Loop 详细设计文档
java·开发语言·人工智能·ai
xixixi777772 小时前
英伟达Agent专用全模态模型出击,仿冒AI智能体泛滥成灾,《AI伦理安全指引》即将落地——AI治理迎来“技术-风险-规范”三重奏
人工智能·5g·安全·ai·大模型·英伟达·智能体
数据皮皮侠AI2 小时前
中国城市可再生能源数据集(2005-2021)|顶刊 Sci Data 11 种能源面板
大数据·人工智能·笔记·能源·1024程序员节
G31135422732 小时前
如何用 QClaw 龙虾做一个规律作息健康助理 Agent
大数据·人工智能·ai·云计算
其实防守也摸鱼2 小时前
面试常问问题总结--护网蓝队方向
网络·笔记·安全·面试·职场和发展·护网·初级蓝队
lwf0061642 小时前
DeepFM 学习日记
深度学习·机器学习
qcx232 小时前
Warp源码深度解析(四):AI Agent原生集成——MCP协议、代码索引与Skills系统
人工智能·ai·agent·源码解析·wrap
Fzuim3 小时前
我的大模型实践:思考模式、提示词与边界的权衡之道
ai