From 9c23a92922d4c7d1e95f68727eaa5a6179c97604 Mon Sep 17 00:00:00 2001 From: baihaochen <1291308544@qq.com> Date: Fri, 8 May 2026 20:23:09 +0800 Subject: [PATCH] add Chinese annotations pretrain --- .gitignore | 4 ++- dataset/lm_dataset.py | 1 + model/model_minimind.py | 73 ++++++++++++++++++++++++++++++++++++-- trainer/train_pretrain.py | 24 ++++++++++--- trainer/train_tokenizer.py | 6 ++++ trainer/trainer_utils.py | 23 +++++++----- 6 files changed, 116 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 7956e1d..7c491a8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ __pycache__/ .DS_Store out website/ -docs-minimind/ \ No newline at end of file +docs-minimind/ +.idea/ +.vscode/ \ No newline at end of file diff --git a/dataset/lm_dataset.py b/dataset/lm_dataset.py index bbc4197..2a0a607 100644 --- a/dataset/lm_dataset.py +++ b/dataset/lm_dataset.py @@ -39,6 +39,7 @@ class PretrainDataset(Dataset): super().__init__() self.tokenizer = tokenizer self.max_length = max_length + #load_dataset是HuggingFace datasets库上用来加载数据集的函数,可以加载HuggingFace Hub上的数据,也可以加载本地的json/csv/txt等文件 self.samples = load_dataset('json', data_files=data_path, split='train') def __len__(self): diff --git a/model/model_minimind.py b/model/model_minimind.py index dc080e5..52d963a 100755 --- a/model/model_minimind.py +++ b/model/model_minimind.py @@ -54,43 +54,76 @@ class RMSNorm(torch.nn.Module): self.weight = nn.Parameter(torch.ones(dim)) def norm(self, x): + #rsqrt是sqrt的倒数,keepdim是保持维度不变,这里是把求平均的维度位置为1 return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) def forward(self, x): + #self.weight是一个可学习的参数 return (self.weight * self.norm(x.float())).type_as(x) def precompute_freqs_cis(dim: int, end: int = int(32 * 1024), rope_base: float = 1e6, rope_scaling: dict = None): freqs, attn_factor = 1.0 / (rope_base ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)), 1.0 + + """ + 把原本只能支持较短上下文的RoPE,平滑地扩展到更长上下文 + 普通rope的频率是f,位置m的旋转角度是mf,如果上下文长度很长,m很大,角度会转的太快,模型泛化不好 + YaRN的做法是对一部分频率进行缩放,让他们转的慢一点 + 一部分频率保持原样,保留短上下文能力 + 一部分频率平滑过渡 + 一部分频率缩小,用于支持长上下文 + """ if rope_scaling is not None: # YaRN: f'(i) = f(i)((1-γ) + γ/s), where γ∈[0,1] is linear ramp + """ + orig_max是原模型训练时支持的最大位置长度 + factor是上下文扩展倍数 + beta_fast和beat_slow用来决定哪些维度的频率保持不变 + attn_factir是乘在cos/sin上的attention的缩放因子 + """ orig_max, factor, beta_fast, beta_slow, attn_factor = ( rope_scaling.get("original_max_position_embeddings", 2048), rope_scaling.get("factor", 16), rope_scaling.get("beta_fast", 32.0), rope_scaling.get("beta_slow", 1.0), rope_scaling.get("attention_factor", 1.0) ) + #只有当你实际预计算的长度end超过原始最大长度时,才需要做rope scaling if end / orig_max > 1.0: + #inv_dim推理公式,用beta,也就是这里的b控制周期和原始上下文长度之间的关系 inv_dim = lambda b: (dim * math.log(orig_max / (b * 2 * math.pi))) / (2 * math.log(rope_base)) + #low,high是两个分界点 + #0到low的区间,ramp=0,频率不变,low到high的区间,ramp从0平滑加到1,high到dim//2的区间,ramp=1,频率除以factor low, high = max(math.floor(inv_dim(beta_fast)), 0), min(math.ceil(inv_dim(beta_slow)), dim // 2 - 1) + #torch.clamp是裁剪或限制张量中元素的数值范围,torch.clamp(input,min,max),不满足范围内的用最大最小值代替 ramp = torch.clamp((torch.arange(dim // 2, device=freqs.device).float() - low) / max(high - low, 0.001), 0, 1) freqs = freqs * (1 - ramp + ramp / factor) t = torch.arange(end, device=freqs.device) + #[end,dim//2] freqs = torch.outer(t, freqs).float() + #[end,dim] freqs_cos = torch.cat([torch.cos(freqs), torch.cos(freqs)], dim=-1) * attn_factor freqs_sin = torch.cat([torch.sin(freqs), torch.sin(freqs)], dim=-1) * attn_factor return freqs_cos, freqs_sin def apply_rotary_pos_emb(q, k, cos, sin, unsqueeze_dim=1): + + # rope作用在qk上,传统的正弦位置编码一般加在token embedding上,不用作用在v上,因为注意力分数计算只用qk + #常见的qk shape是[baych,num_heads,seq_len,head_dim]要对头和seq_len做一个维度交换以适应硬件要求 + #...表示省略前面所有的维度,前面的维度都按原样保留 def rotate_half(x): return torch.cat((-x[..., x.shape[-1] // 2:], x[..., : x.shape[-1] // 2]), dim=-1) q_embed = ((q * cos.unsqueeze(unsqueeze_dim)) + (rotate_half(q) * sin.unsqueeze(unsqueeze_dim))).to(q.dtype) k_embed = ((k * cos.unsqueeze(unsqueeze_dim)) + (rotate_half(k) * sin.unsqueeze(unsqueeze_dim))).to(k.dtype) return q_embed, k_embed def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + 用于GQA和MQA attention 把k/v heads复制成和query_heads一样多,方便后面做attention计算 + """ bs, slen, num_key_value_heads, head_dim = x.shape if n_rep == 1: return x + #None是在这里插入一个新维度,expand通常不真的复制维度,只是创建一个广播视图,所以更省内存 return (x[:, :, :, None, :].expand(bs, slen, num_key_value_heads, n_rep, head_dim).reshape(bs, slen, num_key_value_heads * n_rep, head_dim)) class Attention(nn.Module): def __init__(self, config: MiniMindConfig): super().__init__() + #qk用几个头 self.num_key_value_heads = config.num_attention_heads if config.num_key_value_heads is None else config.num_key_value_heads self.n_local_heads = config.num_attention_heads self.n_local_kv_heads = self.num_key_value_heads @@ -106,6 +139,7 @@ class Attention(nn.Module): self.attn_dropout = nn.Dropout(config.dropout) self.resid_dropout = nn.Dropout(config.dropout) self.dropout = config.dropout + #self.flash是布尔值 self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention') and config.flash_attn def forward(self, x, position_embeddings, past_key_value=None, use_cache=False, attention_mask=None): @@ -121,15 +155,25 @@ class Attention(nn.Module): xk = torch.cat([past_key_value[0], xk], dim=1) xv = torch.cat([past_key_value[1], xv], dim=1) past_kv = (xk, xv) if use_cache else None + #这段是在判断,当前情况能不能走Pytorch中内置的高效attention实现,scaled_dot_product_attention xq, xk, xv = (xq.transpose(1, 2), repeat_kv(xk, self.n_rep).transpose(1, 2), repeat_kv(xv, self.n_rep).transpose(1, 2)) if self.flash and (seq_len > 1) and (not self.is_causal or past_key_value is None) and (attention_mask is None or torch.all(attention_mask == 1)): output = F.scaled_dot_product_attention(xq, xk, xv, dropout_p=self.dropout if self.training else 0.0, is_causal=self.is_causal) else: + #@是矩阵乘法,*是逐元素相乘,两个张量形状必须相同 scores = (xq @ xk.transpose(-2, -1)) / math.sqrt(self.head_dim) + #torch.full是生成一个形状指定,且所有元素都填充为同一个固定值的张量 + #triu是pytorch中用于提取矩阵上三角的函数,diagonal参数=0时,保留主对角线及以上的元素,>0时保留主对角线上方的第N条对角线及以上的元素,<0时保留主对角线下方及以下的元素,其余用0替代 + #triu只对张量的后两个维度进行操作 if self.is_causal: scores[:, :, :, -seq_len:] += torch.full((seq_len, seq_len), float("-inf"), device=scores.device).triu(1) + #casual mask和attention_mask管的是两件事 + #casual_mask是阻止当前位置看到未来token,attention_mask是告诉模型哪些是有效的,哪些token不应该看见 + #attention_mask是在padding时忽略padding部分的注意力分数 if attention_mask is not None: scores += (1.0 - attention_mask.unsqueeze(1).unsqueeze(2)) * -1e9 + #对注意力分数做一个正则化 output = self.attn_dropout(F.softmax(scores.float(), dim=-1).type_as(xq)) @ xv output = output.transpose(1, 2).reshape(bsz, seq_len, -1) + #还要对输出结果做一个正则化 output = self.resid_dropout(self.o_proj(output)) return output, past_kv @@ -155,20 +199,34 @@ class MOEFeedForward(nn.Module): def forward(self, x): batch_size, seq_len, hidden_dim = x.shape + #[bs*seq_len,hidden_dim] x_flat = x.view(-1, hidden_dim) + #[bs*seq_len,num_experts] scores = F.softmax(self.gate(x_flat), dim=-1) + #[bs*seq_len,num_experts_per_tok] topk_weight, topk_idx = torch.topk(scores, k=self.config.num_experts_per_tok, dim=-1, sorted=False) if self.config.norm_topk_prob: topk_weight = topk_weight / (topk_weight.sum(dim=-1, keepdim=True) + 1e-20) + #[bs*seq_len,hidden_dim] y = torch.zeros_like(x_flat) for i, expert in enumerate(self.experts): - mask = (topk_idx == i) + #[bs*seq_len,num_experts_per_tok] + mask = (topk_idx == i) + #any()用来判断是否至少有一个元素为真 if mask.any(): + #nonzero用来返回非零元素的索引,flatten将结果展成一维向量 + #[num_selected_tokens] token_idx = mask.any(dim=-1).nonzero().flatten() + #[num_selected_tokens,1],这里mask是一个和top_weight同维度的布尔矩阵,会把top_weight中和mask矩阵位置相同且为True的部分提取出来 weight = topk_weight[mask].view(-1, 1) + #x_flat[token_idx]->[num_selected_tokens,hidden_dim] + #y[token_idx] += expert_output,index_add_(dim,index,source) y.index_add_(0, token_idx, (expert(x_flat[token_idx]) * weight).to(y.dtype)) elif self.training: + #某个专家没参与计算,也把它挂到计算图上 y[0, 0] += 0 * sum(p.sum() for p in expert.parameters()) if self.training and self.config.router_aux_loss_coef > 0: + #F.one_hot(topk_idx, self.config.num_experts)->[bs*seq_len,num_experts_per_tok,num_experts],会把原来的每一个数字变成一组one-hot向量 + #做mean(0)后,会在第一个维度做平均,[num_experts_per_tok,num_experts] load = F.one_hot(topk_idx, self.config.num_experts).float().mean(0) self.aux_loss = (load * scores.mean(0)).sum() * self.config.num_experts * self.config.router_aux_loss_coef else: @@ -202,6 +260,7 @@ class MiniMindModel(nn.Module): self.dropout = nn.Dropout(config.dropout) self.layers = nn.ModuleList([MiniMindBlock(l, config) for l in range(self.num_hidden_layers)]) self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + #[max_position_embeddings,head_dim] freqs_cos, freqs_sin = precompute_freqs_cis(dim=config.head_dim, end=config.max_position_embeddings, rope_base=config.rope_theta, rope_scaling=config.rope_scaling) self.register_buffer("freqs_cos", freqs_cos, persistent=False) self.register_buffer("freqs_sin", freqs_sin, persistent=False) @@ -228,6 +287,7 @@ class MiniMindModel(nn.Module): ) presents.append(present) hidden_states = self.norm(hidden_states) + #sum方法中第二个参数为初始值,new_zeros创建与当前张量同类型,同设备的新零张量。里面的参数是形状,设置一个初值0 aux_loss = sum([l.mlp.aux_loss for l in self.layers if isinstance(l.mlp, MOEFeedForward)], hidden_states.new_zeros(1).squeeze()) return hidden_states, presents, aux_loss @@ -239,7 +299,9 @@ class MiniMindForCausalLM(PreTrainedModel, GenerationMixin): super().__init__(self.config) self.model = MiniMindModel(self.config) self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False) + #nn.Embedding的权重矩阵存储是(input_dim,output_dim),nn.Linear的权重矩阵存储是转秩过后的 if self.config.tie_word_embeddings: self.model.embed_tokens.weight = self.lm_head.weight + #在模型所有子模块创建完成后,执行Huggingface规定的后处理初始化逻辑 self.post_init() def forward(self, input_ids, attention_mask=None, past_key_values=None, use_cache=False, logits_to_keep=0, labels=None, **kwargs): @@ -250,11 +312,14 @@ class MiniMindForCausalLM(PreTrainedModel, GenerationMixin): if labels is not None: x, y = logits[..., :-1, :].contiguous(), labels[..., 1:].contiguous() loss = F.cross_entropy(x.view(-1, x.size(-1)), y.view(-1), ignore_index=-100) + #把loss,aux_loss,logits等封装成一个标准输出对象,方便训练,推理,以后可以用字典的方式访问 return MoeCausalLMOutputWithPast(loss=loss, aux_loss=aux_loss, logits=logits, past_key_values=past_key_values, hidden_states=hidden_states) # https://github.com/jingyaogong/minimind/discussions/611 + #表示这个函数在推理模式下运行,不计算梯度 @torch.inference_mode() def generate(self, inputs=None, attention_mask=None, max_new_tokens=8192, temperature=0.85, top_p=0.85, top_k=50, eos_token_id=2, streamer=None, use_cache=True, num_return_sequences=1, do_sample=True, repetition_penalty=1.0, **kwargs): + #这里的repeat是向量复制,这里是在第0维复制num_return_sequences次,第1维复制1次,也就是不变 input_ids = kwargs.pop("input_ids", inputs).repeat(num_return_sequences, 1) attention_mask = attention_mask.repeat(num_return_sequences, 1) if attention_mask is not None else None past_key_values = kwargs.pop("past_key_values", None) @@ -265,17 +330,21 @@ class MiniMindForCausalLM(PreTrainedModel, GenerationMixin): outputs = self.forward(input_ids[:, past_len:], attention_mask, past_key_values, use_cache=use_cache, **kwargs) attention_mask = torch.cat([attention_mask, attention_mask.new_ones(attention_mask.shape[0], 1)], -1) if attention_mask is not None else None logits = outputs.logits[:, -1, :] / temperature + #重复惩罚,如果某个token已经在当前生成序列里出现过,就降低它下一次再次被选中的概率 if repetition_penalty != 1.0: + #torch.unique是去重,给张量 for i in range(input_ids.shape[0]): seen = torch.unique(input_ids[i]); score = logits[i, seen]; logits[i, seen] = torch.where(score > 0, score / repetition_penalty, score * repetition_penalty) if top_k > 0: logits[logits < torch.topk(logits, top_k)[0][..., -1, None]] = -float('inf') + #按概率从高到低,只保留累计概率达到top_P之前的token if top_p < 1.0: sorted_logits, sorted_indices = torch.sort(logits, descending=True) mask = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1) > top_p mask[..., 1:], mask[..., 0] = mask[..., :-1].clone(), 0 logits[mask.scatter(1, sorted_indices, mask)] = -float('inf') - next_token = torch.multinomial(torch.softmax(logits, dim=-1), num_samples=1) if do_sample else torch.argmax(logits, dim=-1, keepdim=True) + next_token = torch.multinomial(torch.softmax(logits, dim=-1),num_samples=1) if do_sample else torch.argmax(logits, dim=-1, keepdim=True) + #torch.where是pytorch的条件选择函数,类似三元运算符torch.where(condition,x,y) if eos_token_id is not None: next_token = torch.where(finished.unsqueeze(-1), next_token.new_full((next_token.shape[0], 1), eos_token_id), next_token) input_ids = torch.cat([input_ids, next_token], dim=-1) past_key_values = outputs.past_key_values if use_cache else None diff --git a/trainer/train_pretrain.py b/trainer/train_pretrain.py index 5d33de4..1698294 100644 --- a/trainer/train_pretrain.py +++ b/trainer/train_pretrain.py @@ -35,16 +35,17 @@ def train_epoch(epoch, loader, iters, start_step=0, wandb=None): res = model(input_ids, labels=labels) loss = res.loss + res.aux_loss loss = loss / args.accumulation_steps - + #将loss放大后再反向传播,防止fp16下梯度下溢 scaler.scale(loss).backward() if step % args.accumulation_steps == 0: scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) - + #step是让优化器更新参数,同时更新优化器内部状态 scaler.step(optimizer) + #动态调整scale值,如果scale太大,loss会出现上溢,scale太小会发生下溢 scaler.update() - + #把梯度全部设置为None,不用保留全0梯度的tensor,可以节省内存和加速下一次反向传播 optimizer.zero_grad(set_to_none=True) if step % args.log_interval == 0 or step == iters: @@ -64,13 +65,14 @@ def train_epoch(epoch, loader, iters, start_step=0, wandb=None): 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() + #转成fp16,减小文件大小,转到cpu,避免保存gpu tensor 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 - + #处理还没更新的梯度 if last_step > start_step and last_step % args.accumulation_steps != 0: scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) @@ -118,6 +120,7 @@ if __name__ == "__main__": # ========== 3. 设置混合精度 ========== device_type = "cuda" if "cuda" in args.device else "cpu" dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16 + #根据设备自动选择是否转半精度。cuda自动混精度加速 autocast_ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast(dtype=dtype) # ========== 4. 配wandb ========== @@ -127,12 +130,17 @@ if __name__ == "__main__": 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}" + #project是实验的项目名称,id是指定或恢复一个实验运行的唯一标识符,resume是当提供的id存在时是否继续运行('must'表示必须继续,None表示不继续)。如果提供了wandb_id并且resume设置为'must',则会尝试恢复之前的运行,否则会创建一个新的运行。 + #id等于none时会认为正在启动一个全新的实验运行 wandb.init(project=args.wandb_project, name=wandb_run_name, id=wandb_id, resume=resume) # ========== 5. 定义模型、数据、优化器 ========== model, tokenizer = init_model(lm_config, args.from_weight, device=args.device) train_ds = PretrainDataset(args.data_path, tokenizer, max_length=args.max_seq_len) + #当处于分布式环境时,实例化DistributedSampler会将数据集切分成若干份,确保每个gpu只读取它自己那一部分数据 train_sampler = DistributedSampler(train_ds) if dist.is_initialized() else None + #梯度缩放器,在用fp16计算时,可能会出现梯度下溢现象,GradScaler会将loss乘一个很大的数,从而放大梯度,在优化器更新权重之前,再把放大的梯度恢复到原本的大小 + #当dtype=float16时启动 scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype == 'float16')) optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate) @@ -147,6 +155,7 @@ if __name__ == "__main__": # ========== 7. 编译和分布式包装 ========== if args.use_compile == 1: + #这行代码会对模型进行编译,生成一个优化过的版本,优化过程包括算子融合、生成更高效的底层代码,让模型跑的更快 model = torch.compile(model) Logger('torch.compile enabled') if dist.is_initialized(): @@ -154,10 +163,17 @@ if __name__ == "__main__": # ========== 8. 开始训练 ========== for epoch in range(start_epoch, args.epochs): + #set_epoch是在每个epoch重新设置数据集的采样顺序,会根据epoch的索引,重新设置随机数种子 + #使每个epoch产生不同的随机顺序,从而确保每个epoch用不同的样本顺序进行训练 train_sampler and train_sampler.set_epoch(epoch) + #代码中的分号;是python中的语法特性,可以用来在同一行中执行多个语句 + #torch.randperm是在生成一个随机打乱后的数据索引列表,会生成从0到len(train_ds)的随机排列,然后再转为list setup_seed(42 + epoch); indices = torch.randperm(len(train_ds)).tolist() skip = start_step if (epoch == start_epoch and start_step > 0) else 0 + #dataset负责给它一个idx,返回一条数据,Sampler负责决定按上面顺序给dataset提供idx,所以sampler本质上就是一个索引生成器 batch_sampler = SkipBatchSampler(train_sampler or indices, args.batch_size, skip) + #这里传的是batch_sampler,dataloader不再自己决定batch怎么组成,而是听sampler的 + #根据sampler提供的索引来取数据 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}开始') diff --git a/trainer/train_tokenizer.py b/trainer/train_tokenizer.py index 336a688..fca8b61 100644 --- a/trainer/train_tokenizer.py +++ b/trainer/train_tokenizer.py @@ -44,6 +44,7 @@ def train_tokenizer(data_path, tokenizer_dir, vocab_size, special_tokens_num=SPE vocab_size=vocab_size, show_progress=True, initial_alphabet=pre_tokenizers.ByteLevel.alphabet(), + #BpeTrainer会优先给special_token分配id,而且一般是按special_tokens列表顺序分配 special_tokens=all_special_tokens ) texts = get_texts(data_path) @@ -68,10 +69,15 @@ def train_tokenizer(data_path, tokenizer_dir, vocab_size, special_tokens_num=SPE idx = tokenizer.token_to_id(token) added_tokens_decoder[str(idx)] = { "content": token, + #lstrip表示匹配这个token时,是否把它左边的空白字符一起吃掉 "lstrip": False, + #表示这个token按原始字符串精确匹配,不被normalizer改动 "normalized": False, + #是否把右边的字符吃掉 "rstrip": False, + #这个token是否只能作为一个完整单词匹配 "single_word": False, + #被tokenizer的special token逻辑特殊处理 "special": True if token in special_tokens_list else False } diff --git a/trainer/trainer_utils.py b/trainer/trainer_utils.py index b6211d5..4f8c758 100644 --- a/trainer/trainer_utils.py +++ b/trainer/trainer_utils.py @@ -17,6 +17,7 @@ from model.model_minimind import MiniMindForCausalLM def get_model_params(model, config): total = sum(p.numel() for p in model.parameters()) / 1e6 + #getattr用于动态获取对象的属性或方法 n_routed = getattr(config, 'n_routed_experts', getattr(config, 'num_experts', 0)) n_active = getattr(config, 'num_experts_per_tok', 0) n_shared = getattr(config, 'n_shared_experts', 0) @@ -52,13 +53,13 @@ def init_distributed_mode(): 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 + random.seed(seed)#设置python的随机种子 + np.random.seed(seed)#设置numpy的随机种子 + torch.manual_seed(seed)#设置cpu随机种子 + torch.cuda.manual_seed(seed)#设置pytorch在当前GPU上的随机种子 + torch.cuda.manual_seed_all(seed)#设置pytorch在所有GPU上的随机种子 + torch.backends.cudnn.deterministic = True#使用确定性算法 + torch.backends.cudnn.benchmark = False#禁用cudnn的自动优化 def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoch=0, step=0, wandb=None, save_dir='../checkpoints', **kwargs): os.makedirs(save_dir, exist_ok=True) @@ -66,13 +67,16 @@ def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoc 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: + if model is not None:#存储模型模式 + #如果模型被DDP包了,真正的模型在model.module里 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() + #half表示转成float16,然后移到cpu上 state_dict = {k: v.half().cpu() for k, v in state_dict.items()} ckp_tmp = ckp_path + '.tmp' torch.save(state_dict, ckp_tmp) + #os.replace用于将一个文件替换成另一个文件 os.replace(src, dst) 将 src 文件替换为 dst 文件,如果 dst 已经存在,则会被替换掉。这是一个原子操作,确保在替换过程中不会出现文件损坏或丢失的情况。 os.replace(ckp_tmp, ckp_path) wandb_id = None if wandb: @@ -87,6 +91,7 @@ def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoc 'optimizer': optimizer.state_dict(), 'epoch': epoch, 'step': step, + #训练时用了几张GPU 'world_size': dist.get_world_size() if dist.is_initialized() else 1, 'wandb_id': wandb_id } @@ -102,10 +107,12 @@ def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoc resume_tmp = resume_path + '.tmp' torch.save(resume_data, resume_tmp) os.replace(resume_tmp, resume_path) + #删除临时变量,然后清理cuda缓存 del state_dict, resume_data torch.cuda.empty_cache() else: # 加载模式 if os.path.exists(resume_path): + #加载到gpu ckp_data = torch.load(resume_path, map_location='cpu') saved_ws = ckp_data.get('world_size', 1) current_ws = dist.get_world_size() if dist.is_initialized() else 1