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)