From 8d36134e2490942aa1316e7048fe37f145d6a25d Mon Sep 17 00:00:00 2001 From: 0x0059 Date: Mon, 5 May 2025 22:25:02 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=B8=BA=E5=A4=9A=E4=B8=AA=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=B7=BB=E5=8A=A0=E8=AF=A6=E7=BB=86=E7=9A=84=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为scripts和trainer目录下的多个Python脚本添加了详细的中文注释,包括函数说明、参数解释和代码逻辑描述,以提高代码的可读性和维护性 --- model/model_minimind.py | 111 +++++++++++++++++++--- scripts/chat_openai_api.py | 13 ++- scripts/train_tokenizer.py | 96 ++++++++++++------- trainer/train_distill_reason.py | 65 ++++++++++++- trainer/train_distillation.py | 30 +++++- trainer/train_dpo.py | 128 ++++++++++++++++++-------- trainer/train_full_sft.py | 158 ++++++++++++++++++++++---------- trainer/train_lora.py | 82 +++++++++++------ trainer/train_pretrain.py | 24 ++++- 9 files changed, 543 insertions(+), 164 deletions(-) diff --git a/model/model_minimind.py b/model/model_minimind.py index dc2cceb..18d0c41 100644 --- a/model/model_minimind.py +++ b/model/model_minimind.py @@ -398,10 +398,22 @@ class MOEFeedForward(nn.Module): @torch.no_grad() def moe_infer(self, x, flat_expert_indices, flat_expert_weights): - expert_cache = torch.zeros_like(x) - idxs = flat_expert_indices.argsort() - tokens_per_expert = flat_expert_indices.bincount().cpu().numpy().cumsum(0) - token_idxs = idxs // self.config.num_experts_per_tok + """推理阶段的混合专家前向计算 + + 为了提高推理效率,将相同专家处理的token批量处理,避免重复计算 + + Args: + x: 输入张量 + flat_expert_indices: 每个token对应的专家索引 + flat_expert_weights: 每个token对应的专家权重 + + Returns: + expert_cache: 专家处理后的输出 + """ + expert_cache = torch.zeros_like(x) # 初始化输出缓存 + idxs = flat_expert_indices.argsort() # 对专家索引排序,便于批量处理 + tokens_per_expert = flat_expert_indices.bincount().cpu().numpy().cumsum(0) # 计算每个专家处理的token数量 + token_idxs = idxs // self.config.num_experts_per_tok # 计算token在原序列中的位置 # 当tokens_per_expert = [6, 15, 20, 26],tokens_per_expert.shape[0]即为专家数量(此时为4) # 且token_idxs = [3, 7, 19, 21, 24, 25, 4, 5, 6, 10, 11, 12...] 时 # 意味token_idxs[:6] -> [3, 7, 19, 21, 24, 25]这6个位置属于专家0处理的token(每个token有可能被多个专家处理,这取决于num_experts_per_tok) @@ -421,26 +433,63 @@ class MOEFeedForward(nn.Module): class MiniMindBlock(nn.Module): + """MiniMind模型的基本构建块 + + 每个Block包含以下组件: + 1. 多头注意力层(支持GQA优化) + 2. 前馈网络层(可选MoE或标准FFN) + 3. 两个RMSNorm归一化层 + 4. 残差连接 + """ def __init__(self, layer_id: int, config: MiniMindConfig): super().__init__() + # 注意力相关配置 self.num_attention_heads = config.num_attention_heads self.hidden_size = config.hidden_size self.head_dim = config.hidden_size // config.num_attention_heads - self.self_attn = Attention(config) + self.self_attn = Attention(config) # 多头注意力层 - self.layer_id = layer_id + self.layer_id = layer_id # 层的索引号 + # 两个RMSNorm层,用于注意力层的输入和输出 self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + # 根据配置选择使用标准前馈网络还是混合专家网络 self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config) def forward(self, hidden_states, position_embeddings, past_key_value=None, use_cache=False, attention_mask=None): + """Block的前向传播函数 + + 实现了Pre-LN Transformer的标准流程: + 1. 对输入进行归一化后送入注意力层 + 2. 添加第一个残差连接 + 3. 对注意力输出进行归一化后送入前馈网络 + 4. 添加第二个残差连接 + + Args: + hidden_states: 输入的隐藏状态 + position_embeddings: 位置编码(cos和sin值) + past_key_value: KV缓存,用于加速自回归生成 + use_cache: 是否使用KV缓存 + attention_mask: 注意力掩码 + + Returns: + hidden_states: 处理后的隐藏状态 + present_key_value: 当前层的KV缓存 + """ + # 第一个残差分支:注意力层 residual = hidden_states hidden_states, present_key_value = self.self_attn( - self.input_layernorm(hidden_states), position_embeddings, - past_key_value, use_cache, attention_mask + self.input_layernorm(hidden_states), # Pre-LN:先归一化再送入注意力层 + position_embeddings, + past_key_value, + use_cache, + attention_mask ) - hidden_states += residual - hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states)) + hidden_states += residual # 第一个残差连接 + + # 第二个残差分支:前馈网络(标准FFN或MoE) + hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states)) # Pre-LN + 残差连接 + return hidden_states, present_key_value @@ -513,14 +562,27 @@ class MiniMindModel(nn.Module): class MiniMindForCausalLM(PreTrainedModel, GenerationMixin): - config_class = MiniMindConfig + """基于MiniMind架构的因果语言模型 + + 继承自HuggingFace的PreTrainedModel和GenerationMixin,实现了完整的自回归语言模型功能 + 支持KV缓存、混合专家机制等特性 + """ + config_class = MiniMindConfig # 指定配置类,用于模型加载和保存 def __init__(self, config: MiniMindConfig = None): - self.config = config or MiniMindConfig() - super().__init__(self.config) - self.model = MiniMindModel(self.config) + """初始化因果语言模型 + + Args: + config: 模型配置,如果为None则使用默认配置 + """ + self.config = config or MiniMindConfig() # 使用传入配置或创建默认配置 + super().__init__(self.config) # 调用父类初始化 + self.model = MiniMindModel(self.config) # 创建模型主体 + # 语言模型头,将隐藏状态映射到词表空间 self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False) + # 共享输入输出嵌入权重,提升训练稳定性 self.model.embed_tokens.weight = self.lm_head.weight + # 预初始化输出容器,用于存储forward方法的计算结果 self.OUT = CausalLMOutputWithPast() def forward(self, @@ -530,6 +592,24 @@ class MiniMindForCausalLM(PreTrainedModel, GenerationMixin): use_cache: bool = False, logits_to_keep: Union[int, torch.Tensor] = 0, **args): + """前向传播方法 + + Args: + input_ids: 输入token IDs,形状为[batch_size, seq_len] + attention_mask: 注意力掩码,形状与input_ids相同 + past_key_values: 缓存的KV对,用于加速自回归生成 + use_cache: 是否使用KV缓存 + logits_to_keep: 控制输出哪些位置的logits,可以是整数或张量 + **args: 其他可选参数 + + Returns: + CausalLMOutputWithPast: 包含以下字段的输出对象: + - last_hidden_state: 最后一层的隐藏状态 + - logits: 语言模型头的输出 + - aux_loss: 混合专家机制的辅助损失 + - past_key_values: 更新的KV缓存 + """ + # 通过模型主体计算隐藏状态、KV缓存和辅助损失 h, past_kvs, aux_loss = self.model( input_ids=input_ids, attention_mask=attention_mask, @@ -537,8 +617,11 @@ class MiniMindForCausalLM(PreTrainedModel, GenerationMixin): use_cache=use_cache, **args ) + # 根据logits_to_keep参数确定需要保留的logits位置 slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep + # 计算语言模型头的输出 logits = self.lm_head(h[:, slice_indices, :]) + # 将结果存入输出容器 self.OUT.__setitem__('last_hidden_state', h) self.OUT.__setitem__('logits', logits) self.OUT.__setitem__('aux_loss', aux_loss) diff --git a/scripts/chat_openai_api.py b/scripts/chat_openai_api.py index 57f61c9..61122c2 100644 --- a/scripts/chat_openai_api.py +++ b/scripts/chat_openai_api.py @@ -1,13 +1,20 @@ +# 导入OpenAI API客户端 from openai import OpenAI +# 初始化OpenAI客户端 +# 使用本地部署的模型服务,通过OpenAI兼容的API接口访问 client = OpenAI( - api_key="ollama", - base_url="http://127.0.0.1:8998/v1" + api_key="ollama", # API密钥,这里使用ollama作为占位符 + base_url="http://127.0.0.1:8998/v1" # 本地模型服务的API地址 ) +# 是否使用流式输出,实现打字机效果 stream = True +# 初始化对话历史 conversation_history_origin = [] conversation_history = conversation_history_origin.copy() -history_messages_num = 2 # 设置为偶数(Q+A),为0则每次不携带历史对话进行独立QA +# 设置历史对话轮数,必须为偶数(一轮包含一问一答) +# 为0则不携带历史对话,每次都是独立的问答 +history_messages_num = 2 while True: query = input('[Q]: ') conversation_history.append({"role": "user", "content": query}) diff --git a/scripts/train_tokenizer.py b/scripts/train_tokenizer.py index 9cf5934..e442ca3 100644 --- a/scripts/train_tokenizer.py +++ b/scripts/train_tokenizer.py @@ -1,3 +1,10 @@ +# 分词器训练和评估脚本 +# 本脚本实现了基于BPE算法的分词器训练、配置和评估功能 +# 主要包含以下功能: +# 1. 使用HuggingFace tokenizers库训练自定义分词器 +# 2. 配置特殊token和chat template用于对话格式化 +# 3. 评估分词器的编码解码一致性 + import random import json from tokenizers import ( @@ -9,60 +16,73 @@ from tokenizers import ( ) import os +# 设置随机种子以确保结果可复现 random.seed(42) def train_tokenizer(): - # 读取JSONL文件并提取文本数据 + # 内部函数:读取JSONL格式的训练数据 + # 使用生成器模式逐行读取,避免一次性加载全部数据到内存 def read_texts_from_jsonl(file_path): with open(file_path, 'r', encoding='utf-8') as f: for line in f: data = json.loads(line) yield data['text'] + # 训练数据路径 data_path = '../dataset/pretrain_hq.jsonl' - # 初始化tokenizer + # 初始化BPE分词器 + # BPE(Byte-Pair Encoding)是一种常用的分词算法 + # 它通过迭代合并最频繁出现的字符对来学习子词单元 tokenizer = Tokenizer(models.BPE()) + # 设置字节级别的预分词器,不在词首添加空格 tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) # 定义特殊token + # endoftext: 文本结束标记 + # im_start: 对话角色开始标记 + # im_end: 对话角色结束标记 special_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>"] - # 设置训练器并添加特殊token + # 配置BPE训练器 trainer = trainers.BpeTrainer( - vocab_size=6400, - special_tokens=special_tokens, # 确保这三个token被包含 - show_progress=True, - initial_alphabet=pre_tokenizers.ByteLevel.alphabet() + vocab_size=6400, # 词表大小 + special_tokens=special_tokens, # 确保特殊token被包含在词表中 + show_progress=True, # 显示训练进度 + initial_alphabet=pre_tokenizers.ByteLevel.alphabet() # 使用字节级别的初始字母表 ) - # 读取文本数据 + # 读取训练文本数据 texts = read_texts_from_jsonl(data_path) - # 训练tokenizer + # 训练分词器 + # 使用迭代器训练模式,支持大规模数据集 tokenizer.train_from_iterator(texts, trainer=trainer) - # 设置解码器 + # 设置字节级别解码器 + # 确保解码结果与原始文本格式一致 tokenizer.decoder = decoders.ByteLevel() - # 检查特殊token的索引 + # 验证特殊token的索引 + # 确保特殊token被正确分配到指定的索引位置 assert tokenizer.token_to_id("<|endoftext|>") == 0 assert tokenizer.token_to_id("<|im_start|>") == 1 assert tokenizer.token_to_id("<|im_end|>") == 2 - # 保存tokenizer + # 创建模型目录并保存分词器 tokenizer_dir = "../model/" os.makedirs(tokenizer_dir, exist_ok=True) tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json")) tokenizer.model.save("../model/") - # 手动创建配置文件 + # 创建分词器配置 + # 包含token处理规则和chat template配置 config = { - "add_bos_token": False, - "add_eos_token": False, - "add_prefix_space": False, - "added_tokens_decoder": { + "add_bos_token": False, # 是否在序列开始添加BOS token + "add_eos_token": False, # 是否在序列结束添加EOS token + "add_prefix_space": False, # 是否在词首添加空格 + "added_tokens_decoder": { # 特殊token的详细配置 "0": { "content": "<|endoftext|>", "lstrip": False, @@ -88,18 +108,20 @@ def train_tokenizer(): "special": True } }, - "additional_special_tokens": [], - "bos_token": "<|im_start|>", - "clean_up_tokenization_spaces": False, - "eos_token": "<|im_end|>", - "legacy": True, - "model_max_length": 32768, - "pad_token": "<|endoftext|>", - "sp_model_kwargs": {}, - "spaces_between_special_tokens": False, - "tokenizer_class": "PreTrainedTokenizerFast", - "unk_token": "<|endoftext|>", - "chat_template": "{% if messages[0]['role'] == 'system' %}{% set system_message = messages[0]['content'] %}{{ '<|im_start|>system\\n' + system_message + '<|im_end|>\\n' }}{% else %}{{ '<|im_start|>system\\nYou are a helpful assistant<|im_end|>\\n' }}{% endif %}{% for message in messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|im_start|>user\\n' + content + '<|im_end|>\\n<|im_start|>assistant\\n' }}{% elif message['role'] == 'assistant' %}{{ content + '<|im_end|>' + '\\n' }}{% endif %}{% endfor %}" + "additional_special_tokens": [], # 额外的特殊token列表 + "bos_token": "<|im_start|>", # 序列开始token + "clean_up_tokenization_spaces": False, # 是否清理分词时产生的空格 + "eos_token": "<|im_end|>", # 序列结束token + "legacy": True, # 是否使用遗留模式 + "model_max_length": 32768, # 模型支持的最大序列长度 + "pad_token": "<|endoftext|>", # 填充token + "sp_model_kwargs": {}, # SentencePiece模型参数 + "spaces_between_special_tokens": False, # 特殊token之间是否添加空格 + "tokenizer_class": "PreTrainedTokenizerFast", # 分词器类名 + "unk_token": "<|endoftext|>", # 未知token + # chat template定义了对话格式的模板 + # 支持system、user和assistant三种角色的消息格式化 + "chat_template": "{% if messages[0]['role'] == 'system' %}{% set system_message = messages[0]['content'] %}{{ '<|im_start|>system\n' + system_message + '<|im_end|>\n' }}{% else %}{{ '<|im_start|>system\nYou are a helpful assistant<|im_end|>\n' }}{% endif %}{% for message in messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|im_start|>user\n' + content + '<|im_end|>\n<|im_start|>assistant\n' }}{% elif message['role'] == 'assistant' %}{{ content + '<|im_end|>' + '\n' }}{% endif %}{% endfor %}" } # 保存配置文件 @@ -110,35 +132,47 @@ def train_tokenizer(): def eval_tokenizer(): + """评估分词器的功能 + 包括: + 1. chat template的格式化效果 + 2. 词表大小统计 + 3. 编码长度检查 + 4. 编码解码一致性验证 + """ from transformers import AutoTokenizer - # 加载预训练的tokenizer + # 加载训练好的分词器 tokenizer = AutoTokenizer.from_pretrained("../model/") + # 构造测试用的对话消息 messages = [ {"role": "system", "content": "你是一个优秀的聊天机器人,总是给我正确的回应!"}, {"role": "user", "content": '你来自哪里?'}, {"role": "assistant", "content": '我来自地球'} ] + # 应用chat template进行格式化 new_prompt = tokenizer.apply_chat_template( messages, tokenize=False ) print(new_prompt) - # 获取实际词汇表长度(包括特殊符号) + # 统计词表大小 actual_vocab_size = len(tokenizer) print('tokenizer实际词表长度:', actual_vocab_size) + # 测试编码功能 model_inputs = tokenizer(new_prompt) print('encoder长度:', len(model_inputs['input_ids'])) + # 验证编码解码的一致性 input_ids = model_inputs['input_ids'] response = tokenizer.decode(input_ids, skip_special_tokens=False) print('decoder和原始文本是否一致:', response == new_prompt) def main(): + # 执行分词器训练和评估 train_tokenizer() eval_tokenizer() diff --git a/trainer/train_distill_reason.py b/trainer/train_distill_reason.py index 579fedb..4a62cd3 100644 --- a/trainer/train_distill_reason.py +++ b/trainer/train_distill_reason.py @@ -1,6 +1,8 @@ +# 导入必要的系统库 import os import sys +# 设置包名和添加项目根目录到系统路径 __package__ = "trainer" sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) @@ -22,16 +24,34 @@ warnings.filterwarnings('ignore') def Logger(content): + """日志打印函数,在分布式训练时只在主进程上打印 + Args: + content: 需要打印的内容 + """ if not ddp or dist.get_rank() == 0: print(content) def get_lr(current_step, total_steps, lr): + """使用余弦退火策略计算学习率 + Args: + current_step: 当前训练步数 + total_steps: 总训练步数 + lr: 基础学习率 + Returns: + 当前步数对应的学习率 + """ return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) def train_epoch(epoch, wandb): - # 思考标签占位符 + """训练一个epoch + Args: + epoch: 当前训练轮数 + wandb: wandb日志记录器 + """ + # 获取特殊标记的token ID + # 用于标记模型思考和回答的开始结束位置 start_of_think_ids = tokenizer('').input_ids end_of_think_ids = tokenizer('').input_ids start_of_answer_ids = tokenizer('').input_ids @@ -109,51 +129,94 @@ def train_epoch(epoch, wandb): def init_model(lm_config): + """初始化模型和分词器 + Args: + lm_config: 模型配置参数 + Returns: + model: 初始化好的模型 + tokenizer: 分词器 + """ + # 加载分词器 tokenizer = AutoTokenizer.from_pretrained('../model') + # 初始化模型 model = MiniMindForCausalLM(lm_config) + # 根据是否使用MoE设置检查点路径 moe_path = '_moe' if lm_config.use_moe else '' ckp = f'{args.save_dir}/rlhf_{lm_config.hidden_size}{moe_path}.pth' + # 加载预训练权重 state_dict = torch.load(ckp, map_location=args.device) model.load_state_dict(state_dict, strict=False) + # 打印模型参数量 Logger(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') + # 将模型移动到指定设备 model = model.to(args.device) return model, tokenizer def init_distributed_mode(): + """初始化分布式训练环境 + 设置分布式训练的各项参数,包括进程组、设备等 + """ if not ddp: return global ddp_local_rank, DEVICE + # 初始化分布式进程组,使用NCCL后端 dist.init_process_group(backend="nccl") + # 获取当前进程的全局排名 ddp_rank = int(os.environ["RANK"]) + # 获取当前进程在本机的局部排名 ddp_local_rank = int(os.environ["LOCAL_RANK"]) + # 获取总进程数 ddp_world_size = int(os.environ["WORLD_SIZE"]) + # 设置当前进程使用的GPU设备 DEVICE = f"cuda:{ddp_local_rank}" torch.cuda.set_device(DEVICE) if __name__ == "__main__": + # 解析命令行参数 parser = argparse.ArgumentParser(description="MiniMind Distill Reasoning") + # 输出目录,用于保存模型检查点和日志 parser.add_argument("--out_dir", type=str, default="../out") + # 训练轮数 parser.add_argument("--epochs", type=int, default=1) + # 训练批次大小 parser.add_argument("--batch_size", type=int, default=8) + # 基础学习率 parser.add_argument("--learning_rate", type=float, default=1e-6) + # 训练设备,默认使用GPU parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") + # 训练精度,支持float16和bfloat16 parser.add_argument("--dtype", type=str, default="bfloat16") + # 是否使用wandb记录训练日志 parser.add_argument("--use_wandb", action="store_true") + # wandb项目名称 parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT") + # 数据加载进程数 parser.add_argument("--num_workers", type=int, default=1) + # 是否使用分布式训练 parser.add_argument("--ddp", action="store_true") + # 梯度累积步数 parser.add_argument("--accumulation_steps", type=int, default=1) + # 梯度裁剪阈值 parser.add_argument("--grad_clip", type=float, default=1.0) + # 预热迭代次数 parser.add_argument("--warmup_iters", type=int, default=0) + # 日志打印间隔 parser.add_argument("--log_interval", type=int, default=1) + # 模型保存间隔 parser.add_argument("--save_interval", type=int, default=50) + # 分布式训练的局部进程号 parser.add_argument('--local_rank', type=int, default=-1) + # 模型隐藏层维度 parser.add_argument('--hidden_size', default=512, type=int) + # Transformer层数 parser.add_argument('--num_hidden_layers', default=8, type=int) + # 最大序列长度 parser.add_argument('--max_seq_len', default=1024, type=int) + # 是否使用MoE(混合专家)架构 parser.add_argument('--use_moe', default=False, type=bool) + # 训练数据路径 parser.add_argument("--data_path", type=str, default="../dataset/r1_mix_1024.jsonl") args = parser.parse_args() diff --git a/trainer/train_distillation.py b/trainer/train_distillation.py index be790a4..c0607de 100644 --- a/trainer/train_distillation.py +++ b/trainer/train_distillation.py @@ -1,7 +1,9 @@ +# 导入必要的库 import os import sys -__package__ = "trainer" +__package__ = "trainer" # 设置包名 +# 将项目根目录添加到系统路径 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import argparse import time @@ -22,16 +24,23 @@ from dataset.lm_dataset import SFTDataset warnings.filterwarnings('ignore') +# 日志打印函数 +# 在分布式训练时只在主进程(rank=0)上打印日志 def Logger(content): if not ddp or dist.get_rank() == 0: print(content) +# 余弦学习率调度器 +# 在训练过程中逐渐降低学习率,最终降到初始值的1/10 def get_lr(current_step, total_steps, lr): return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) +# 知识蒸馏损失函数 +# 通过KL散度计算学生模型和教师模型输出分布之间的差异 def distillation_loss_fn(student_logits, teacher_logits, temperature=1.0, reduction='batchmean'): + # 计算教师模型的概率分布(停止梯度传播) with torch.no_grad(): teacher_probs = F.softmax(teacher_logits / temperature, hidden_size=-1).detach() @@ -45,9 +54,13 @@ def distillation_loss_fn(student_logits, teacher_logits, temperature=1.0, reduct return (temperature ** 2) * kl +# 训练一个epoch +# alpha: 用于平衡蒸馏损失和交叉熵损失的权重 +# temperature: 软目标的温度系数,用于控制知识蒸馏中概率分布的平滑程度 def train_epoch(epoch, wandb, alpha=0.0, temperature=1.0): start_time = time.time() + # 确保教师模型处于评估模式且参数冻结 if teacher_model is not None: teacher_model.eval() teacher_model.requires_grad_(False) @@ -146,26 +159,41 @@ def train_epoch(epoch, wandb, alpha=0.0, temperature=1.0): model.train() +# 初始化学生模型 +# 学生模型通常比教师模型更小,但需要保持相同的任务能力 def init_student_model(lm_config): + # 加载预训练的分词器 tokenizer = AutoTokenizer.from_pretrained('../model/') + # 初始化学生模型 model = MiniMindForCausalLM(lm_config) + # 根据是否使用MoE设置权重路径 moe_path = '_moe' if lm_config.use_moe else '' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' + # 加载预训练权重 state_dict = torch.load(ckp, map_location=args.device) model.load_state_dict(state_dict, strict=False) + # 打印模型可训练参数总量 Logger(f'学生模型(LLM)总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') + # 将模型移动到指定设备 model = model.to(args.device) return model, tokenizer +# 初始化教师模型 +# 教师模型通常是一个更大、性能更好的预训练模型 def init_teacher_model(lm_config): + # 初始化教师模型 model = MiniMindForCausalLM(lm_config) + # 根据是否使用MoE设置权重路径 moe_path = '_moe' if lm_config.use_moe else '' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' + # 加载预训练权重 state_dict = torch.load(ckp, map_location=args.device) model.load_state_dict(state_dict, strict=False) + # 打印模型可训练参数总量 Logger(f'教师模型(LLM)总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') + # 将模型移动到指定设备 model = model.to(args.device) return model diff --git a/trainer/train_dpo.py b/trainer/train_dpo.py index c7701d0..7987d93 100644 --- a/trainer/train_dpo.py +++ b/trainer/train_dpo.py @@ -31,36 +31,65 @@ def get_lr(current_step, total_steps, lr): def logits_to_probs(logits, labels): - # logits shape: (batch_size, seq_len, vocab_size) - # labels shape: (batch_size, seq_len) - # probs shape: (batch_size, seq_len) + """将模型输出的logits转换为token级别的概率 + + Args: + logits: 模型输出的logits,形状为(batch_size, seq_len, vocab_size) + labels: 真实标签,形状为(batch_size, seq_len) + + Returns: + probs: token级别的概率,形状为(batch_size, seq_len) + """ + # 对logits进行softmax得到概率分布 log_probs = F.log_softmax(logits, dim=2) + # 收集每个位置上目标token的概率 probs = torch.gather(log_probs, dim=2, index=labels.unsqueeze(2)).squeeze(-1) return probs def dpo_loss(ref_probs, probs, mask, beta): - # ref_probs 和 probs 都是 shape: (batch_size, seq_len) - # https://github.com/jingyaogong/minimind/issues/298 + """计算Direct Preference Optimization (DPO)损失 + + DPO通过最大化偏好数据中chosen和rejected样本的对数概率比来优化模型。 + 具体来说,它使用一个参考模型(通常是训练前的模型)来防止策略过度偏离。 + + Args: + ref_probs: 参考模型的token概率,形状为(batch_size, seq_len) + probs: 当前模型的token概率,形状为(batch_size, seq_len) + mask: 有效token的掩码,形状为(batch_size, seq_len) + beta: 温度系数,用于控制策略的保守程度 + + Returns: + loss: 标量损失值 + """ + # 计算序列级别的对数概率,通过mask只考虑有效token seq_lengths = mask.sum(dim=1, keepdim=True) # (batch_size, 1) ref_probs = (ref_probs * mask).sum(dim=1) / seq_lengths.squeeze() probs = (probs * mask).sum(dim=1) / seq_lengths.squeeze() - # 将 chosen 和 rejected 数据分开 + # 将batch分成chosen和rejected两部分 batch_size = ref_probs.shape[0] - chosen_ref_probs = ref_probs[:batch_size // 2] - reject_ref_probs = ref_probs[batch_size // 2:] - chosen_probs = probs[:batch_size // 2] - reject_probs = probs[batch_size // 2:] + chosen_ref_probs = ref_probs[:batch_size // 2] # 参考模型对chosen样本的概率 + reject_ref_probs = ref_probs[batch_size // 2:] # 参考模型对rejected样本的概率 + chosen_probs = probs[:batch_size // 2] # 当前模型对chosen样本的概率 + reject_probs = probs[batch_size // 2:] # 当前模型对rejected样本的概率 - pi_logratios = chosen_probs - reject_probs - ref_logratios = chosen_ref_probs - reject_ref_probs + # 计算当前模型和参考模型的对数概率比 + pi_logratios = chosen_probs - reject_probs # 当前模型的对数比 + ref_logratios = chosen_ref_probs - reject_ref_probs # 参考模型的对数比 + # 计算最终的logits并应用温度系数 logits = pi_logratios - ref_logratios + # 计算损失:最大化chosen相对于rejected的概率 loss = -F.logsigmoid(beta * logits) return loss.mean() def train_epoch(epoch, wandb): + """训练一个epoch的DPO优化 + + 使用人类偏好数据进行训练,每个batch包含chosen和rejected两部分样本。 + 通过最大化chosen样本相对于rejected样本的概率来优化模型。 + """ start_time = time.time() for step, batch in enumerate(train_loader): x_chosen = batch['x_chosen'].to(args.device) @@ -131,19 +160,31 @@ def train_epoch(epoch, wandb): def init_model(lm_config): + """初始化DPO训练所需的模型 + + 包括: + 1. 主模型:用于优化的模型 + 2. 参考模型:用于计算KL散度,防止策略过度偏离 + 3. 分词器:用于处理输入文本 + """ + # 加载分词器 tokenizer = AutoTokenizer.from_pretrained('../model/') + + # 初始化主模型 model = MiniMindForCausalLM(lm_config) moe_path = '_moe' if lm_config.use_moe else '' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' state_dict = torch.load(ckp, map_location=args.device) model.load_state_dict(state_dict, strict=False) - # 初始化参考模型 + + # 初始化参考模型(固定参数的SFT模型) ref_model = MiniMindForCausalLM(lm_config) ref_model.load_state_dict(state_dict, strict=False) - ref_model.eval() - ref_model.requires_grad_(False) + ref_model.eval() # 设置为评估模式 + ref_model.requires_grad_(False) # 冻结所有参数 Logger(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') + # 将模型移动到指定设备 model = model.to(args.device) ref_model = ref_model.to(args.device) @@ -163,29 +204,42 @@ def init_distributed_mode(): if __name__ == "__main__": + # 配置命令行参数 parser = argparse.ArgumentParser(description="MiniMind RLHF") - parser.add_argument("--out_dir", type=str, default="../out") - parser.add_argument("--epochs", type=int, default=2) - parser.add_argument("--batch_size", type=int, default=4) - # sft阶段学习率为 「5e-6」->「5e-7」长度512,建议离线正负样本「概率」偏好对齐阶段lr <=「1e-8」长度3000,否则很容易遗忘训坏 - parser.add_argument("--learning_rate", type=float, default=1e-8) - parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") - parser.add_argument("--dtype", type=str, default="bfloat16") - parser.add_argument("--use_wandb", action="store_true") - parser.add_argument("--wandb_project", type=str, default="MiniMind-RLHF-SFT") - parser.add_argument("--num_workers", type=int, default=1) - parser.add_argument("--ddp", action="store_true") - parser.add_argument("--accumulation_steps", type=int, default=1) - parser.add_argument("--grad_clip", type=float, default=1.0) - parser.add_argument("--warmup_iters", type=int, default=0) - parser.add_argument("--log_interval", type=int, default=100) - parser.add_argument("--save_interval", type=int, default=100) - parser.add_argument('--local_rank', type=int, default=-1) - parser.add_argument('--hidden_size', default=512, type=int) - parser.add_argument('--num_hidden_layers', default=8, type=int) - parser.add_argument('--max_seq_len', default=1024, type=int) - parser.add_argument('--use_moe', default=False, type=bool) - parser.add_argument("--data_path", type=str, default="../dataset/dpo.jsonl") + + # 基础训练参数 + parser.add_argument("--out_dir", type=str, default="../out", help="输出目录") + parser.add_argument("--epochs", type=int, default=2, help="训练轮数") + parser.add_argument("--batch_size", type=int, default=4, help="批次大小") + # DPO训练使用较小的学习率,防止策略过度偏离 + parser.add_argument("--learning_rate", type=float, default=1e-8, help="学习率,建议<=1e-8,否则容易遗忘训坏") + 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("--use_wandb", action="store_true", help="是否使用wandb记录训练过程") + parser.add_argument("--wandb_project", type=str, default="MiniMind-RLHF-SFT", help="wandb项目名称") + parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔") + parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔") + + # 分布式训练参数 + parser.add_argument("--num_workers", type=int, default=1, help="数据加载进程数") + parser.add_argument("--ddp", action="store_true", help="是否使用分布式训练") + parser.add_argument('--local_rank', type=int, default=-1, help="分布式训练的本地进程编号") + + # 优化器参数 + parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数") + parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值") + parser.add_argument("--warmup_iters", type=int, default=0, help="学习率预热迭代次数") + + # 模型参数 + parser.add_argument('--hidden_size', default=512, type=int, help="隐藏层维度") + parser.add_argument('--num_hidden_layers', default=8, type=int, help="Transformer层数") + parser.add_argument('--max_seq_len', default=1024, type=int, help="最大序列长度") + parser.add_argument('--use_moe', default=False, type=bool, help="是否使用MoE") + + # 数据参数 + parser.add_argument("--data_path", type=str, default="../dataset/dpo.jsonl", help="训练数据路径,包含人类偏好对") args = parser.parse_args() diff --git a/trainer/train_full_sft.py b/trainer/train_full_sft.py index e2a9a2f..981d926 100644 --- a/trainer/train_full_sft.py +++ b/trainer/train_full_sft.py @@ -1,9 +1,12 @@ +# 导入必要的系统库 import os import sys +# 设置包名和添加项目根目录到系统路径 __package__ = "trainer" -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # 将项目根目录添加到系统路径 +# 导入训练所需的库 import argparse import time import math @@ -18,51 +21,69 @@ from transformers import AutoTokenizer, AutoModelForCausalLM from model.model_minimind import MiniMindConfig, MiniMindForCausalLM from dataset.lm_dataset import SFTDataset -warnings.filterwarnings('ignore') +warnings.filterwarnings('ignore') # 忽略警告信息 +# 日志打印函数 +# 在分布式训练时只在主进程(rank=0)上打印日志 def Logger(content): if not ddp or dist.get_rank() == 0: print(content) +# 余弦学习率调度器 +# 在训练过程中逐渐降低学习率,最终降到初始值的1/10 def get_lr(current_step, total_steps, lr): return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) +# 训练一个epoch的函数 def train_epoch(epoch, wandb): + # 使用交叉熵损失函数,reduction='none'以便后续通过mask处理填充token loss_fct = nn.CrossEntropyLoss(reduction='none') start_time = time.time() + + # 遍历数据加载器获取批次数据 for step, (X, Y, loss_mask) in enumerate(train_loader): - X = X.to(args.device) - Y = Y.to(args.device) - loss_mask = loss_mask.to(args.device) + # 将数据移动到指定设备 + X = X.to(args.device) # 输入序列 + Y = Y.to(args.device) # 目标序列 + loss_mask = loss_mask.to(args.device) # 损失掩码 + + # 更新学习率 lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate) for param_group in optimizer.param_groups: param_group['lr'] = lr - with ctx: - res = model(X) + # 前向传播和损失计算 + with ctx: # 使用自动混合精度训练 + res = model(X) # 模型前向传播 + # 计算交叉熵损失 loss = loss_fct( - res.logits.view(-1, res.logits.size(-1)), - Y.view(-1) + res.logits.view(-1, res.logits.size(-1)), # 展平logits + Y.view(-1) # 展平目标序列 ).view(Y.size()) + # 应用损失掩码并计算平均损失 loss = (loss * loss_mask).sum() / loss_mask.sum() - loss += res.aux_loss - loss = loss / args.accumulation_steps + loss += res.aux_loss # 添加辅助损失(如果有的话,例如MoE的负载均衡损失) + loss = loss / args.accumulation_steps # 根据梯度累积步数缩放损失 - scaler.scale(loss).backward() + # 反向传播 + scaler.scale(loss).backward() # 使用梯度缩放器缩放损失并反向传播 + # 梯度更新 if (step + 1) % args.accumulation_steps == 0: - scaler.unscale_(optimizer) + scaler.unscale_(optimizer) # 反缩放梯度 + # 梯度裁剪,防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) - scaler.step(optimizer) - scaler.update() + scaler.step(optimizer) # 优化器更新参数 + scaler.update() # 更新梯度缩放器 - optimizer.zero_grad(set_to_none=True) + optimizer.zero_grad(set_to_none=True) # 清空梯度 + # 打印训练日志 if step % args.log_interval == 0: spend_time = time.time() - start_time Logger( @@ -75,109 +96,149 @@ def train_epoch(epoch, wandb): optimizer.param_groups[-1]['lr'], spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60)) + # 记录wandb日志(如果启用) if (wandb is not None) and (not ddp or dist.get_rank() == 0): wandb.log({"loss": loss * args.accumulation_steps, "lr": optimizer.param_groups[-1]['lr'], "epoch_Time": spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60}) + # 保存模型检查点 if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0): - model.eval() + model.eval() # 切换到评估模式 + # 根据是否使用MoE设置检查点路径 moe_path = '_moe' if lm_config.use_moe else '' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' + + # 获取模型状态字典 if isinstance(model, torch.nn.parallel.DistributedDataParallel): state_dict = model.module.state_dict() else: state_dict = model.state_dict() - state_dict = {k: v.half() for k, v in state_dict.items()} # 半精度保存 + + # 将模型参数转换为半精度格式并保存 + state_dict = {k: v.half() for k, v in state_dict.items()} torch.save(state_dict, ckp) - model.train() + model.train() # 切换回训练模式 +# 初始化模型和分词器 def init_model(lm_config): + # 加载预训练的分词器 tokenizer = AutoTokenizer.from_pretrained('../model') + # 初始化因果语言模型 model = MiniMindForCausalLM(lm_config) + # 根据是否使用MoE设置权重路径 moe_path = '_moe' if lm_config.use_moe else '' ckp = f'{args.save_dir}/pretrain_{lm_config.hidden_size}{moe_path}.pth' + # 加载预训练权重 state_dict = torch.load(ckp, map_location=args.device) model.load_state_dict(state_dict, strict=False) + # 打印模型可训练参数总量 Logger(f'LLM可训练总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') + # 将模型移动到指定设备 model = model.to(args.device) return model, tokenizer +# 初始化分布式训练环境 def init_distributed_mode(): if not ddp: return global ddp_local_rank, DEVICE + # 初始化分布式进程组 dist.init_process_group(backend="nccl") - ddp_rank = int(os.environ["RANK"]) - ddp_local_rank = int(os.environ["LOCAL_RANK"]) - ddp_world_size = int(os.environ["WORLD_SIZE"]) + # 获取分布式训练的相关参数 + ddp_rank = int(os.environ["RANK"]) # 全局进程编号 + ddp_local_rank = int(os.environ["LOCAL_RANK"]) # 本地进程编号 + ddp_world_size = int(os.environ["WORLD_SIZE"]) # 总进程数 + # 设置当前进程使用的GPU DEVICE = f"cuda:{ddp_local_rank}" torch.cuda.set_device(DEVICE) +# 主函数 if __name__ == "__main__": + # 配置命令行参数 parser = argparse.ArgumentParser(description="MiniMind Full SFT") - parser.add_argument("--out_dir", type=str, default="../out") - parser.add_argument("--epochs", type=int, default=2) - parser.add_argument("--batch_size", type=int, default=16) - parser.add_argument("--learning_rate", type=float, default=5e-7) - parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") - parser.add_argument("--dtype", type=str, default="bfloat16") - parser.add_argument("--use_wandb", action="store_true") - parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT") - parser.add_argument("--num_workers", type=int, default=1) - parser.add_argument("--ddp", action="store_true") - parser.add_argument("--accumulation_steps", type=int, default=1) - parser.add_argument("--grad_clip", type=float, default=1.0) - parser.add_argument("--warmup_iters", type=int, default=0) - parser.add_argument("--log_interval", type=int, default=100) - parser.add_argument("--save_interval", type=int, default=100) - parser.add_argument('--local_rank', type=int, default=-1) - parser.add_argument('--hidden_size', default=512, type=int) - parser.add_argument('--num_hidden_layers', default=8, type=int) - parser.add_argument('--max_seq_len', default=512, type=int) - parser.add_argument('--use_moe', default=False, type=bool) - parser.add_argument("--data_path", type=str, default="../dataset/sft_mini_512.jsonl") + # 基础训练参数 + parser.add_argument("--out_dir", type=str, default="../out", help="输出目录") + parser.add_argument("--epochs", type=int, default=2, help="训练轮数") + parser.add_argument("--batch_size", type=int, default=16, help="批次大小") + parser.add_argument("--learning_rate", type=float, default=5e-7, 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("--use_wandb", action="store_true", help="是否使用wandb记录训练过程") + parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT", help="wandb项目名称") + parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔") + parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔") + + # 分布式训练参数 + parser.add_argument("--num_workers", type=int, default=1, help="数据加载进程数") + parser.add_argument("--ddp", action="store_true", help="是否使用分布式训练") + parser.add_argument('--local_rank', type=int, default=-1, help="分布式训练的本地进程编号") + + # 优化器参数 + parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数") + parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值") + parser.add_argument("--warmup_iters", type=int, default=0, help="学习率预热迭代次数") + + # 模型参数 + parser.add_argument('--hidden_size', default=512, type=int, help="隐藏层维度") + parser.add_argument('--num_hidden_layers', default=8, type=int, help="Transformer层数") + parser.add_argument('--max_seq_len', default=512, type=int, help="最大序列长度") + parser.add_argument('--use_moe', default=False, type=bool, help="是否使用MoE") + parser.add_argument("--data_path", type=str, default="../dataset/sft_mini_512.jsonl", help="训练数据路径") args = parser.parse_args() + # 初始化模型配置 lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, use_moe=args.use_moe) + # 创建输出目录 args.save_dir = os.path.join(args.out_dir) os.makedirs(args.save_dir, exist_ok=True) os.makedirs(args.out_dir, exist_ok=True) + + # 计算每次迭代处理的token数量 tokens_per_iter = args.batch_size * args.max_seq_len device_type = "cuda" if "cuda" in args.device else "cpu" + # 设置wandb运行名称 args.wandb_run_name = f"MiniMind-Full-SFT-Epoch-{args.epochs}-BatchSize-{args.batch_size}-LearningRate-{args.learning_rate}" + # 设置自动混合精度训练上下文 ctx = nullcontext() if device_type == "cpu" else torch.cuda.amp.autocast() - ddp = int(os.environ.get("RANK", -1)) != -1 # is this a ddp run? + # 检查是否为分布式训练 + ddp = int(os.environ.get("RANK", -1)) != -1 ddp_local_rank, DEVICE = 0, "cuda:0" + + # 设置随机种子 base_seed = 1337 torch.manual_seed(base_seed) torch.cuda.manual_seed(base_seed) + # 初始化分布式训练环境 if ddp: init_distributed_mode() args.device = torch.device(DEVICE) rank = dist.get_rank() torch.manual_seed(base_seed + rank) - # 同时设置 CUDA 的随机种子 torch.cuda.manual_seed(base_seed + rank) + # 初始化wandb(如果启用) if args.use_wandb and (not ddp or ddp_local_rank == 0): import wandb - wandb.init(project=args.wandb_project, name=args.wandb_run_name) else: wandb = None + # 初始化模型和分词器 model, tokenizer = init_model(lm_config) + # 准备训练数据 train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len) train_sampler = DistributedSampler(train_ds) if ddp else None train_loader = DataLoader( @@ -190,13 +251,18 @@ if __name__ == "__main__": sampler=train_sampler ) + # 初始化优化器和梯度缩放器 scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16'])) optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate) + # 配置分布式训练 if ddp: model._ddp_params_and_buffers_to_ignore = {"pos_cis"} model = DistributedDataParallel(model, device_ids=[ddp_local_rank]) + # 计算每个epoch的迭代次数 iter_per_epoch = len(train_loader) + + # 开始训练循环 for epoch in range(args.epochs): train_epoch(epoch, wandb) diff --git a/trainer/train_lora.py b/trainer/train_lora.py index 4711062..04fefcf 100644 --- a/trainer/train_lora.py +++ b/trainer/train_lora.py @@ -21,17 +21,19 @@ from model.model_lora import load_lora, save_lora, apply_lora warnings.filterwarnings('ignore') -# Logger function +# 日志打印函数,在分布式训练时只在主进程上打印 def Logger(content): if not ddp or dist.get_rank() == 0: print(content) +# 余弦学习率调度器 def get_lr(current_step, total_steps, lr): + # 在训练过程中逐渐降低学习率,最终降到初始值的1/10 return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) -# 代码和full_sft「几乎」一致 +# 训练一个epoch,使用LoRA方法进行参数高效微调 def train_epoch(epoch, wandb): loss_fct = nn.CrossEntropyLoss(reduction='none') start_time = time.time() @@ -90,51 +92,72 @@ def train_epoch(epoch, wandb): model.train() +# 初始化模型和分词器 def init_model(lm_config): + # 加载预训练的分词器 tokenizer = AutoTokenizer.from_pretrained('../model/') + # 初始化因果语言模型 model = MiniMindForCausalLM(lm_config) + # 根据是否使用MoE设置权重路径 moe_path = '_moe' if lm_config.use_moe else '' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' + # 加载预训练权重 state_dict = torch.load(ckp, map_location=args.device) model.load_state_dict(state_dict, strict=False) return model.to(args.device), tokenizer +# 初始化分布式训练环境 def init_distributed_mode(): if not ddp: return global ddp_local_rank, DEVICE + # 初始化分布式进程组 dist.init_process_group(backend="nccl") - ddp_rank = int(os.environ["RANK"]) - ddp_local_rank = int(os.environ["LOCAL_RANK"]) - ddp_world_size = int(os.environ["WORLD_SIZE"]) + # 获取分布式训练的相关参数 + ddp_rank = int(os.environ["RANK"]) # 全局进程编号 + ddp_local_rank = int(os.environ["LOCAL_RANK"]) # 本地进程编号 + ddp_world_size = int(os.environ["WORLD_SIZE"]) # 总进程数 + # 设置当前进程使用的GPU DEVICE = f"cuda:{ddp_local_rank}" torch.cuda.set_device(DEVICE) if __name__ == "__main__": + # 配置命令行参数 parser = argparse.ArgumentParser(description="MiniMind SFT with LoRA") - parser.add_argument("--out_dir", type=str, default="../out") - parser.add_argument("--epochs", type=int, default=10) - parser.add_argument("--batch_size", type=int, default=32) - parser.add_argument("--learning_rate", type=float, default=1e-4) - parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") - parser.add_argument("--dtype", type=str, default="bfloat16") - parser.add_argument("--use_wandb", action="store_true") - parser.add_argument("--wandb_project", type=str, default="MiniMind-LoRA-SFT") - parser.add_argument("--num_workers", type=int, default=1) - parser.add_argument("--ddp", action="store_true") - parser.add_argument("--accumulation_steps", type=int, default=1) - parser.add_argument("--grad_clip", type=float, default=1.0) - parser.add_argument("--warmup_iters", type=int, default=0) - parser.add_argument("--log_interval", type=int, default=100) - parser.add_argument("--save_interval", type=int, default=100) - parser.add_argument('--local_rank', type=int, default=-1) - parser.add_argument('--hidden_size', default=512, type=int) - parser.add_argument('--num_hidden_layers', default=8, type=int) - parser.add_argument('--max_seq_len', default=512, type=int) - parser.add_argument('--use_moe', default=False, type=bool) - parser.add_argument("--data_path", type=str, default="../dataset/lora_medical.jsonl") + # 基础训练参数 + parser.add_argument("--out_dir", type=str, default="../out", help="输出目录") + parser.add_argument("--epochs", type=int, default=10, help="训练轮数") + parser.add_argument("--batch_size", type=int, default=32, help="批次大小") + parser.add_argument("--learning_rate", type=float, default=1e-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("--use_wandb", action="store_true", help="是否使用wandb记录训练过程") + parser.add_argument("--wandb_project", type=str, default="MiniMind-LoRA-SFT", help="wandb项目名称") + parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔") + parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔") + + # 分布式训练参数 + parser.add_argument("--num_workers", type=int, default=1, help="数据加载进程数") + parser.add_argument("--ddp", action="store_true", help="是否使用分布式训练") + parser.add_argument('--local_rank', type=int, default=-1, help="分布式训练的本地进程编号") + + # 优化器参数 + parser.add_argument("--accumulation_steps", type=int, default=1, help="梯度累积步数") + parser.add_argument("--grad_clip", type=float, default=1.0, help="梯度裁剪阈值") + parser.add_argument("--warmup_iters", type=int, default=0, help="学习率预热迭代次数") + + # 模型参数 + parser.add_argument('--hidden_size', default=512, type=int, help="隐藏层维度") + parser.add_argument('--num_hidden_layers', default=8, type=int, help="Transformer层数") + parser.add_argument('--max_seq_len', default=512, type=int, help="最大序列长度") + parser.add_argument('--use_moe', default=False, type=bool, help="是否使用MoE") + + # 数据和LoRA参数 + parser.add_argument("--data_path", type=str, default="../dataset/lora_medical.jsonl", help="训练数据路径") parser.add_argument("--lora_name", type=str, default="lora_medical", help="根据任务保存成lora_(英文/医学/心理...)") args = parser.parse_args() @@ -169,9 +192,12 @@ if __name__ == "__main__": else: wandb = None + # 初始化模型和分词器 model, tokenizer = init_model(lm_config) + # 应用LoRA参数高效微调方法 apply_lora(model) + # 统计模型参数量 total_params = sum(p.numel() for p in model.parameters()) # 总参数数量 lora_params_count = sum(p.numel() for name, p in model.named_parameters() if 'lora' in name) # LoRA 参数数量 if not ddp or dist.get_rank() == 0: @@ -179,15 +205,17 @@ if __name__ == "__main__": print(f"LoRA 参数量: {lora_params_count}") print(f"LoRA 参数占比: {lora_params_count / total_params * 100:.2f}%") + # 冻结非LoRA参数 for name, param in model.named_parameters(): if 'lora' not in name: param.requires_grad = False + # 收集需要优化的LoRA参数 lora_params = [] for name, param in model.named_parameters(): if 'lora' in name: lora_params.append(param) - # 只对 LoRA 参数进行优化 + # 初始化优化器,只更新LoRA参数 optimizer = optim.AdamW(lora_params, lr=args.learning_rate) train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len) train_sampler = DistributedSampler(train_ds) if ddp else None diff --git a/trainer/train_pretrain.py b/trainer/train_pretrain.py index f0cfc58..b276eb7 100644 --- a/trainer/train_pretrain.py +++ b/trainer/train_pretrain.py @@ -1,6 +1,8 @@ +# 导入必要的库 import os import sys -__package__ = "trainer" +__package__ = "trainer" # 设置包名 +# 将项目根目录添加到系统路径 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import argparse @@ -20,16 +22,22 @@ from dataset.lm_dataset import PretrainDataset warnings.filterwarnings('ignore') +# 日志打印函数 +# 在分布式训练时只在主进程(rank=0)上打印日志 def Logger(content): if not ddp or dist.get_rank() == 0: print(content) +# 余弦学习率调度器 +# 在训练过程中逐渐降低学习率,最终降到初始值的1/10 def get_lr(current_step, total_steps, lr): return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) +# 训练一个epoch def train_epoch(epoch, wandb): + # 使用交叉熵损失函数,reduction='none'以便后续通过mask处理填充token loss_fct = nn.CrossEntropyLoss(reduction='none') start_time = time.time() for step, (X, Y, loss_mask) in enumerate(train_loader): @@ -94,21 +102,29 @@ def train_epoch(epoch, wandb): model.train() +# 初始化模型和分词器 def init_model(lm_config): + # 加载预训练的分词器 tokenizer = AutoTokenizer.from_pretrained('../model/') + # 初始化因果语言模型并移动到指定设备 model = MiniMindForCausalLM(lm_config).to(args.device) + # 打印模型可训练参数总量 Logger(f'LLM可训练总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') return model, tokenizer +# 初始化分布式训练环境 def init_distributed_mode(): if not ddp: return global ddp_local_rank, DEVICE + # 初始化分布式进程组,使用NCCL后端 dist.init_process_group(backend="nccl") - ddp_rank = int(os.environ["RANK"]) - ddp_local_rank = int(os.environ["LOCAL_RANK"]) - ddp_world_size = int(os.environ["WORLD_SIZE"]) + # 获取分布式训练的相关参数 + ddp_rank = int(os.environ["RANK"]) # 全局进程编号 + ddp_local_rank = int(os.environ["LOCAL_RANK"]) # 本地进程编号 + ddp_world_size = int(os.environ["WORLD_SIZE"]) # 总进程数 + # 设置当前进程使用的GPU DEVICE = f"cuda:{ddp_local_rank}" torch.cuda.set_device(DEVICE)