docs: 为多个脚本添加详细的中文注释

为scripts和trainer目录下的多个Python脚本添加了详细的中文注释,包括函数说明、参数解释和代码逻辑描述,以提高代码的可读性和维护性
pull/394/head
0x0059 2025-05-05 22:25:02 +08:00
parent f74117e5d9
commit 8d36134e24
9 changed files with 543 additions and 164 deletions

View File

@ -398,10 +398,22 @@ class MOEFeedForward(nn.Module):
@torch.no_grad() @torch.no_grad()
def moe_infer(self, x, flat_expert_indices, flat_expert_weights): 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批量处理,避免重复计算
token_idxs = idxs // self.config.num_experts_per_tok
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) # 当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 = [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) # 意味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): class MiniMindBlock(nn.Module):
"""MiniMind模型的基本构建块
每个Block包含以下组件:
1. 多头注意力层(支持GQA优化)
2. 前馈网络层(可选MoE或标准FFN)
3. 两个RMSNorm归一化层
4. 残差连接
"""
def __init__(self, layer_id: int, config: MiniMindConfig): def __init__(self, layer_id: int, config: MiniMindConfig):
super().__init__() super().__init__()
# 注意力相关配置
self.num_attention_heads = config.num_attention_heads self.num_attention_heads = config.num_attention_heads
self.hidden_size = config.hidden_size self.hidden_size = config.hidden_size
self.head_dim = config.hidden_size // config.num_attention_heads 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.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.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
# 根据配置选择使用标准前馈网络还是混合专家网络
self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config) 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): 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 residual = hidden_states
hidden_states, present_key_value = self.self_attn( hidden_states, present_key_value = self.self_attn(
self.input_layernorm(hidden_states), position_embeddings, self.input_layernorm(hidden_states), # Pre-LN:先归一化再送入注意力层
past_key_value, use_cache, attention_mask position_embeddings,
past_key_value,
use_cache,
attention_mask
) )
hidden_states += residual hidden_states += residual # 第一个残差连接
hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states))
# 第二个残差分支:前馈网络(标准FFN或MoE)
hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states)) # Pre-LN + 残差连接
return hidden_states, present_key_value return hidden_states, present_key_value
@ -513,14 +562,27 @@ class MiniMindModel(nn.Module):
class MiniMindForCausalLM(PreTrainedModel, GenerationMixin): class MiniMindForCausalLM(PreTrainedModel, GenerationMixin):
config_class = MiniMindConfig """基于MiniMind架构的因果语言模型
继承自HuggingFace的PreTrainedModel和GenerationMixin,实现了完整的自回归语言模型功能
支持KV缓存、混合专家机制等特性
"""
config_class = MiniMindConfig # 指定配置类,用于模型加载和保存
def __init__(self, config: MiniMindConfig = None): 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.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False)
# 共享输入输出嵌入权重,提升训练稳定性
self.model.embed_tokens.weight = self.lm_head.weight self.model.embed_tokens.weight = self.lm_head.weight
# 预初始化输出容器,用于存储forward方法的计算结果
self.OUT = CausalLMOutputWithPast() self.OUT = CausalLMOutputWithPast()
def forward(self, def forward(self,
@ -530,6 +592,24 @@ class MiniMindForCausalLM(PreTrainedModel, GenerationMixin):
use_cache: bool = False, use_cache: bool = False,
logits_to_keep: Union[int, torch.Tensor] = 0, logits_to_keep: Union[int, torch.Tensor] = 0,
**args): **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( h, past_kvs, aux_loss = self.model(
input_ids=input_ids, input_ids=input_ids,
attention_mask=attention_mask, attention_mask=attention_mask,
@ -537,8 +617,11 @@ class MiniMindForCausalLM(PreTrainedModel, GenerationMixin):
use_cache=use_cache, use_cache=use_cache,
**args **args
) )
# 根据logits_to_keep参数确定需要保留的logits位置
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
# 计算语言模型头的输出
logits = self.lm_head(h[:, slice_indices, :]) logits = self.lm_head(h[:, slice_indices, :])
# 将结果存入输出容器
self.OUT.__setitem__('last_hidden_state', h) self.OUT.__setitem__('last_hidden_state', h)
self.OUT.__setitem__('logits', logits) self.OUT.__setitem__('logits', logits)
self.OUT.__setitem__('aux_loss', aux_loss) self.OUT.__setitem__('aux_loss', aux_loss)

View File

@ -1,13 +1,20 @@
# 导入OpenAI API客户端
from openai import OpenAI from openai import OpenAI
# 初始化OpenAI客户端
# 使用本地部署的模型服务,通过OpenAI兼容的API接口访问
client = OpenAI( client = OpenAI(
api_key="ollama", api_key="ollama", # API密钥,这里使用ollama作为占位符
base_url="http://127.0.0.1:8998/v1" base_url="http://127.0.0.1:8998/v1" # 本地模型服务的API地址
) )
# 是否使用流式输出,实现打字机效果
stream = True stream = True
# 初始化对话历史
conversation_history_origin = [] conversation_history_origin = []
conversation_history = conversation_history_origin.copy() conversation_history = conversation_history_origin.copy()
history_messages_num = 2 # 设置为偶数(Q+A),为0则每次不携带历史对话进行独立QA # 设置历史对话轮数,必须为偶数(一轮包含一问一答)
# 为0则不携带历史对话,每次都是独立的问答
history_messages_num = 2
while True: while True:
query = input('[Q]: ') query = input('[Q]: ')
conversation_history.append({"role": "user", "content": query}) conversation_history.append({"role": "user", "content": query})

View File

@ -1,3 +1,10 @@
# 分词器训练和评估脚本
# 本脚本实现了基于BPE算法的分词器训练、配置和评估功能
# 主要包含以下功能:
# 1. 使用HuggingFace tokenizers库训练自定义分词器
# 2. 配置特殊token和chat template用于对话格式化
# 3. 评估分词器的编码解码一致性
import random import random
import json import json
from tokenizers import ( from tokenizers import (
@ -9,60 +16,73 @@ from tokenizers import (
) )
import os import os
# 设置随机种子以确保结果可复现
random.seed(42) random.seed(42)
def train_tokenizer(): def train_tokenizer():
# 读取JSONL文件并提取文本数据 # 内部函数:读取JSONL格式的训练数据
# 使用生成器模式逐行读取,避免一次性加载全部数据到内存
def read_texts_from_jsonl(file_path): def read_texts_from_jsonl(file_path):
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
for line in f: for line in f:
data = json.loads(line) data = json.loads(line)
yield data['text'] yield data['text']
# 训练数据路径
data_path = '../dataset/pretrain_hq.jsonl' data_path = '../dataset/pretrain_hq.jsonl'
# 初始化tokenizer # 初始化BPE分词器
# BPE(Byte-Pair Encoding)是一种常用的分词算法
# 它通过迭代合并最频繁出现的字符对来学习子词单元
tokenizer = Tokenizer(models.BPE()) tokenizer = Tokenizer(models.BPE())
# 设置字节级别的预分词器,不在词首添加空格
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
# 定义特殊token # 定义特殊token
# endoftext: 文本结束标记
# im_start: 对话角色开始标记
# im_end: 对话角色结束标记
special_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>"] special_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>"]
# 设置训练器并添加特殊token # 配置BPE训练器
trainer = trainers.BpeTrainer( trainer = trainers.BpeTrainer(
vocab_size=6400, vocab_size=6400, # 词表大小
special_tokens=special_tokens, # 确保这三个token被包含 special_tokens=special_tokens, # 确保特殊token被包含在词表中
show_progress=True, show_progress=True, # 显示训练进度
initial_alphabet=pre_tokenizers.ByteLevel.alphabet() initial_alphabet=pre_tokenizers.ByteLevel.alphabet() # 使用字节级别的初始字母表
) )
# 读取文本数据 # 读取训练文本数据
texts = read_texts_from_jsonl(data_path) texts = read_texts_from_jsonl(data_path)
# 训练tokenizer # 训练分词器
# 使用迭代器训练模式,支持大规模数据集
tokenizer.train_from_iterator(texts, trainer=trainer) tokenizer.train_from_iterator(texts, trainer=trainer)
# 设置解码器 # 设置字节级别解码器
# 确保解码结果与原始文本格式一致
tokenizer.decoder = decoders.ByteLevel() tokenizer.decoder = decoders.ByteLevel()
# 检查特殊token的索引 # 验证特殊token的索引
# 确保特殊token被正确分配到指定的索引位置
assert tokenizer.token_to_id("<|endoftext|>") == 0 assert tokenizer.token_to_id("<|endoftext|>") == 0
assert tokenizer.token_to_id("<|im_start|>") == 1 assert tokenizer.token_to_id("<|im_start|>") == 1
assert tokenizer.token_to_id("<|im_end|>") == 2 assert tokenizer.token_to_id("<|im_end|>") == 2
# 保存tokenizer # 创建模型目录并保存分词器
tokenizer_dir = "../model/" tokenizer_dir = "../model/"
os.makedirs(tokenizer_dir, exist_ok=True) os.makedirs(tokenizer_dir, exist_ok=True)
tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json")) tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
tokenizer.model.save("../model/") tokenizer.model.save("../model/")
# 手动创建配置文件 # 创建分词器配置
# 包含token处理规则和chat template配置
config = { config = {
"add_bos_token": False, "add_bos_token": False, # 是否在序列开始添加BOS token
"add_eos_token": False, "add_eos_token": False, # 是否在序列结束添加EOS token
"add_prefix_space": False, "add_prefix_space": False, # 是否在词首添加空格
"added_tokens_decoder": { "added_tokens_decoder": { # 特殊token的详细配置
"0": { "0": {
"content": "<|endoftext|>", "content": "<|endoftext|>",
"lstrip": False, "lstrip": False,
@ -88,18 +108,20 @@ def train_tokenizer():
"special": True "special": True
} }
}, },
"additional_special_tokens": [], "additional_special_tokens": [], # 额外的特殊token列表
"bos_token": "<|im_start|>", "bos_token": "<|im_start|>", # 序列开始token
"clean_up_tokenization_spaces": False, "clean_up_tokenization_spaces": False, # 是否清理分词时产生的空格
"eos_token": "<|im_end|>", "eos_token": "<|im_end|>", # 序列结束token
"legacy": True, "legacy": True, # 是否使用遗留模式
"model_max_length": 32768, "model_max_length": 32768, # 模型支持的最大序列长度
"pad_token": "<|endoftext|>", "pad_token": "<|endoftext|>", # 填充token
"sp_model_kwargs": {}, "sp_model_kwargs": {}, # SentencePiece模型参数
"spaces_between_special_tokens": False, "spaces_between_special_tokens": False, # 特殊token之间是否添加空格
"tokenizer_class": "PreTrainedTokenizerFast", "tokenizer_class": "PreTrainedTokenizerFast", # 分词器类名
"unk_token": "<|endoftext|>", "unk_token": "<|endoftext|>", # 未知token
"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 %}" # 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(): def eval_tokenizer():
"""评估分词器的功能
包括:
1. chat template的格式化效果
2. 词表大小统计
3. 编码长度检查
4. 编码解码一致性验证
"""
from transformers import AutoTokenizer from transformers import AutoTokenizer
# 加载预训练的tokenizer # 加载训练好的分词器
tokenizer = AutoTokenizer.from_pretrained("../model/") tokenizer = AutoTokenizer.from_pretrained("../model/")
# 构造测试用的对话消息
messages = [ messages = [
{"role": "system", "content": "你是一个优秀的聊天机器人,总是给我正确的回应!"}, {"role": "system", "content": "你是一个优秀的聊天机器人,总是给我正确的回应!"},
{"role": "user", "content": '你来自哪里?'}, {"role": "user", "content": '你来自哪里?'},
{"role": "assistant", "content": '我来自地球'} {"role": "assistant", "content": '我来自地球'}
] ]
# 应用chat template进行格式化
new_prompt = tokenizer.apply_chat_template( new_prompt = tokenizer.apply_chat_template(
messages, messages,
tokenize=False tokenize=False
) )
print(new_prompt) print(new_prompt)
# 获取实际词汇表长度(包括特殊符号) # 统计词表大小
actual_vocab_size = len(tokenizer) actual_vocab_size = len(tokenizer)
print('tokenizer实际词表长度:', actual_vocab_size) print('tokenizer实际词表长度:', actual_vocab_size)
# 测试编码功能
model_inputs = tokenizer(new_prompt) model_inputs = tokenizer(new_prompt)
print('encoder长度:', len(model_inputs['input_ids'])) print('encoder长度:', len(model_inputs['input_ids']))
# 验证编码解码的一致性
input_ids = model_inputs['input_ids'] input_ids = model_inputs['input_ids']
response = tokenizer.decode(input_ids, skip_special_tokens=False) response = tokenizer.decode(input_ids, skip_special_tokens=False)
print('decoder和原始文本是否一致:', response == new_prompt) print('decoder和原始文本是否一致:', response == new_prompt)
def main(): def main():
# 执行分词器训练和评估
train_tokenizer() train_tokenizer()
eval_tokenizer() eval_tokenizer()

View File

@ -1,6 +1,8 @@
# 导入必要的系统库
import os import os
import sys import sys
# 设置包名和添加项目根目录到系统路径
__package__ = "trainer" __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__), '..')))
@ -22,16 +24,34 @@ warnings.filterwarnings('ignore')
def Logger(content): def Logger(content):
"""日志打印函数,在分布式训练时只在主进程上打印
Args:
content: 需要打印的内容
"""
if not ddp or dist.get_rank() == 0: if not ddp or dist.get_rank() == 0:
print(content) print(content)
def get_lr(current_step, total_steps, lr): 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)) return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))
def train_epoch(epoch, wandb): def train_epoch(epoch, wandb):
# 思考标签占位符 """训练一个epoch
Args:
epoch: 当前训练轮数
wandb: wandb日志记录器
"""
# 获取特殊标记的token ID
# 用于标记模型思考和回答的开始结束位置
start_of_think_ids = tokenizer('<think>').input_ids start_of_think_ids = tokenizer('<think>').input_ids
end_of_think_ids = tokenizer('</think>').input_ids end_of_think_ids = tokenizer('</think>').input_ids
start_of_answer_ids = tokenizer('<answer>').input_ids start_of_answer_ids = tokenizer('<answer>').input_ids
@ -109,51 +129,94 @@ def train_epoch(epoch, wandb):
def init_model(lm_config): def init_model(lm_config):
"""初始化模型和分词器
Args:
lm_config: 模型配置参数
Returns:
model: 初始化好的模型
tokenizer: 分词器
"""
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained('../model') tokenizer = AutoTokenizer.from_pretrained('../model')
# 初始化模型
model = MiniMindForCausalLM(lm_config) model = MiniMindForCausalLM(lm_config)
# 根据是否使用MoE设置检查点路径
moe_path = '_moe' if lm_config.use_moe else '' moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/rlhf_{lm_config.hidden_size}{moe_path}.pth' ckp = f'{args.save_dir}/rlhf_{lm_config.hidden_size}{moe_path}.pth'
# 加载预训练权重
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
model.load_state_dict(state_dict, strict=False) 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} 百万') Logger(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
# 将模型移动到指定设备
model = model.to(args.device) model = model.to(args.device)
return model, tokenizer return model, tokenizer
def init_distributed_mode(): def init_distributed_mode():
"""初始化分布式训练环境
设置分布式训练的各项参数,包括进程组、设备等
"""
if not ddp: return if not ddp: return
global ddp_local_rank, DEVICE global ddp_local_rank, DEVICE
# 初始化分布式进程组,使用NCCL后端
dist.init_process_group(backend="nccl") dist.init_process_group(backend="nccl")
# 获取当前进程的全局排名
ddp_rank = int(os.environ["RANK"]) ddp_rank = int(os.environ["RANK"])
# 获取当前进程在本机的局部排名
ddp_local_rank = int(os.environ["LOCAL_RANK"]) ddp_local_rank = int(os.environ["LOCAL_RANK"])
# 获取总进程数
ddp_world_size = int(os.environ["WORLD_SIZE"]) ddp_world_size = int(os.environ["WORLD_SIZE"])
# 设置当前进程使用的GPU设备
DEVICE = f"cuda:{ddp_local_rank}" DEVICE = f"cuda:{ddp_local_rank}"
torch.cuda.set_device(DEVICE) torch.cuda.set_device(DEVICE)
if __name__ == "__main__": if __name__ == "__main__":
# 解析命令行参数
parser = argparse.ArgumentParser(description="MiniMind Distill Reasoning") parser = argparse.ArgumentParser(description="MiniMind Distill Reasoning")
# 输出目录,用于保存模型检查点和日志
parser.add_argument("--out_dir", type=str, default="../out") parser.add_argument("--out_dir", type=str, default="../out")
# 训练轮数
parser.add_argument("--epochs", type=int, default=1) parser.add_argument("--epochs", type=int, default=1)
# 训练批次大小
parser.add_argument("--batch_size", type=int, default=8) parser.add_argument("--batch_size", type=int, default=8)
# 基础学习率
parser.add_argument("--learning_rate", type=float, default=1e-6) 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") 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") parser.add_argument("--dtype", type=str, default="bfloat16")
# 是否使用wandb记录训练日志
parser.add_argument("--use_wandb", action="store_true") parser.add_argument("--use_wandb", action="store_true")
# wandb项目名称
parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT") parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT")
# 数据加载进程数
parser.add_argument("--num_workers", type=int, default=1) parser.add_argument("--num_workers", type=int, default=1)
# 是否使用分布式训练
parser.add_argument("--ddp", action="store_true") parser.add_argument("--ddp", action="store_true")
# 梯度累积步数
parser.add_argument("--accumulation_steps", type=int, default=1) parser.add_argument("--accumulation_steps", type=int, default=1)
# 梯度裁剪阈值
parser.add_argument("--grad_clip", type=float, default=1.0) parser.add_argument("--grad_clip", type=float, default=1.0)
# 预热迭代次数
parser.add_argument("--warmup_iters", type=int, default=0) parser.add_argument("--warmup_iters", type=int, default=0)
# 日志打印间隔
parser.add_argument("--log_interval", type=int, default=1) parser.add_argument("--log_interval", type=int, default=1)
# 模型保存间隔
parser.add_argument("--save_interval", type=int, default=50) parser.add_argument("--save_interval", type=int, default=50)
# 分布式训练的局部进程号
parser.add_argument('--local_rank', type=int, default=-1) parser.add_argument('--local_rank', type=int, default=-1)
# 模型隐藏层维度
parser.add_argument('--hidden_size', default=512, type=int) parser.add_argument('--hidden_size', default=512, type=int)
# Transformer层数
parser.add_argument('--num_hidden_layers', default=8, 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('--max_seq_len', default=1024, type=int)
# 是否使用MoE(混合专家)架构
parser.add_argument('--use_moe', default=False, type=bool) parser.add_argument('--use_moe', default=False, type=bool)
# 训练数据路径
parser.add_argument("--data_path", type=str, default="../dataset/r1_mix_1024.jsonl") parser.add_argument("--data_path", type=str, default="../dataset/r1_mix_1024.jsonl")
args = parser.parse_args() args = parser.parse_args()

View File

@ -1,7 +1,9 @@
# 导入必要的库
import os import os
import sys import sys
__package__ = "trainer" __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 argparse
import time import time
@ -22,16 +24,23 @@ from dataset.lm_dataset import SFTDataset
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
# 日志打印函数
# 在分布式训练时只在主进程(rank=0)上打印日志
def Logger(content): def Logger(content):
if not ddp or dist.get_rank() == 0: if not ddp or dist.get_rank() == 0:
print(content) print(content)
# 余弦学习率调度器
# 在训练过程中逐渐降低学习率,最终降到初始值的1/10
def get_lr(current_step, total_steps, lr): def get_lr(current_step, total_steps, lr):
return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) 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'): def distillation_loss_fn(student_logits, teacher_logits, temperature=1.0, reduction='batchmean'):
# 计算教师模型的概率分布(停止梯度传播)
with torch.no_grad(): with torch.no_grad():
teacher_probs = F.softmax(teacher_logits / temperature, hidden_size=-1).detach() 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 return (temperature ** 2) * kl
# 训练一个epoch
# alpha: 用于平衡蒸馏损失和交叉熵损失的权重
# temperature: 软目标的温度系数,用于控制知识蒸馏中概率分布的平滑程度
def train_epoch(epoch, wandb, alpha=0.0, temperature=1.0): def train_epoch(epoch, wandb, alpha=0.0, temperature=1.0):
start_time = time.time() start_time = time.time()
# 确保教师模型处于评估模式且参数冻结
if teacher_model is not None: if teacher_model is not None:
teacher_model.eval() teacher_model.eval()
teacher_model.requires_grad_(False) teacher_model.requires_grad_(False)
@ -146,26 +159,41 @@ def train_epoch(epoch, wandb, alpha=0.0, temperature=1.0):
model.train() model.train()
# 初始化学生模型
# 学生模型通常比教师模型更小,但需要保持相同的任务能力
def init_student_model(lm_config): def init_student_model(lm_config):
# 加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained('../model/') tokenizer = AutoTokenizer.from_pretrained('../model/')
# 初始化学生模型
model = MiniMindForCausalLM(lm_config) model = MiniMindForCausalLM(lm_config)
# 根据是否使用MoE设置权重路径
moe_path = '_moe' if lm_config.use_moe else '' moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth'
# 加载预训练权重
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
model.load_state_dict(state_dict, strict=False) 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} 百万') Logger(f'学生模型(LLM)总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
# 将模型移动到指定设备
model = model.to(args.device) model = model.to(args.device)
return model, tokenizer return model, tokenizer
# 初始化教师模型
# 教师模型通常是一个更大、性能更好的预训练模型
def init_teacher_model(lm_config): def init_teacher_model(lm_config):
# 初始化教师模型
model = MiniMindForCausalLM(lm_config) model = MiniMindForCausalLM(lm_config)
# 根据是否使用MoE设置权重路径
moe_path = '_moe' if lm_config.use_moe else '' moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth'
# 加载预训练权重
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
model.load_state_dict(state_dict, strict=False) 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} 百万') Logger(f'教师模型(LLM)总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
# 将模型移动到指定设备
model = model.to(args.device) model = model.to(args.device)
return model return model

View File

@ -31,36 +31,65 @@ def get_lr(current_step, total_steps, lr):
def logits_to_probs(logits, labels): def logits_to_probs(logits, labels):
# logits shape: (batch_size, seq_len, vocab_size) """将模型输出的logits转换为token级别的概率
# labels shape: (batch_size, seq_len)
# probs shape: (batch_size, seq_len) 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) log_probs = F.log_softmax(logits, dim=2)
# 收集每个位置上目标token的概率
probs = torch.gather(log_probs, dim=2, index=labels.unsqueeze(2)).squeeze(-1) probs = torch.gather(log_probs, dim=2, index=labels.unsqueeze(2)).squeeze(-1)
return probs return probs
def dpo_loss(ref_probs, probs, mask, beta): def dpo_loss(ref_probs, probs, mask, beta):
# ref_probs 和 probs 都是 shape: (batch_size, seq_len) """计算Direct Preference Optimization (DPO)损失
# https://github.com/jingyaogong/minimind/issues/298
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) seq_lengths = mask.sum(dim=1, keepdim=True) # (batch_size, 1)
ref_probs = (ref_probs * mask).sum(dim=1) / seq_lengths.squeeze() ref_probs = (ref_probs * mask).sum(dim=1) / seq_lengths.squeeze()
probs = (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] batch_size = ref_probs.shape[0]
chosen_ref_probs = ref_probs[:batch_size // 2] chosen_ref_probs = ref_probs[:batch_size // 2] # 参考模型对chosen样本的概率
reject_ref_probs = ref_probs[batch_size // 2:] reject_ref_probs = ref_probs[batch_size // 2:] # 参考模型对rejected样本的概率
chosen_probs = probs[:batch_size // 2] chosen_probs = probs[:batch_size // 2] # 当前模型对chosen样本的概率
reject_probs = probs[batch_size // 2:] 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 logits = pi_logratios - ref_logratios
# 计算损失:最大化chosen相对于rejected的概率
loss = -F.logsigmoid(beta * logits) loss = -F.logsigmoid(beta * logits)
return loss.mean() return loss.mean()
def train_epoch(epoch, wandb): def train_epoch(epoch, wandb):
"""训练一个epoch的DPO优化
使用人类偏好数据进行训练,每个batch包含chosen和rejected两部分样本。
通过最大化chosen样本相对于rejected样本的概率来优化模型。
"""
start_time = time.time() start_time = time.time()
for step, batch in enumerate(train_loader): for step, batch in enumerate(train_loader):
x_chosen = batch['x_chosen'].to(args.device) x_chosen = batch['x_chosen'].to(args.device)
@ -131,19 +160,31 @@ def train_epoch(epoch, wandb):
def init_model(lm_config): def init_model(lm_config):
"""初始化DPO训练所需的模型
包括:
1. 主模型:用于优化的模型
2. 参考模型:用于计算KL散度,防止策略过度偏离
3. 分词器:用于处理输入文本
"""
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained('../model/') tokenizer = AutoTokenizer.from_pretrained('../model/')
# 初始化主模型
model = MiniMindForCausalLM(lm_config) model = MiniMindForCausalLM(lm_config)
moe_path = '_moe' if lm_config.use_moe else '' moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth'
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
model.load_state_dict(state_dict, strict=False) model.load_state_dict(state_dict, strict=False)
# 初始化参考模型
# 初始化参考模型(固定参数的SFT模型)
ref_model = MiniMindForCausalLM(lm_config) ref_model = MiniMindForCausalLM(lm_config)
ref_model.load_state_dict(state_dict, strict=False) ref_model.load_state_dict(state_dict, strict=False)
ref_model.eval() ref_model.eval() # 设置为评估模式
ref_model.requires_grad_(False) ref_model.requires_grad_(False) # 冻结所有参数
Logger(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') Logger(f'LLM总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
# 将模型移动到指定设备
model = model.to(args.device) model = model.to(args.device)
ref_model = ref_model.to(args.device) ref_model = ref_model.to(args.device)
@ -163,29 +204,42 @@ def init_distributed_mode():
if __name__ == "__main__": if __name__ == "__main__":
# 配置命令行参数
parser = argparse.ArgumentParser(description="MiniMind RLHF") 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) parser.add_argument("--out_dir", type=str, default="../out", help="输出目录")
# sft阶段学习率为 「5e-6」->「5e-7」长度512,建议离线正负样本「概率」偏好对齐阶段lr <=「1e-8」长度3000,否则很容易遗忘训坏 parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
parser.add_argument("--learning_rate", type=float, default=1e-8) parser.add_argument("--batch_size", type=int, default=4, help="批次大小")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") # DPO训练使用较小的学习率,防止策略过度偏离
parser.add_argument("--dtype", type=str, default="bfloat16") parser.add_argument("--learning_rate", type=float, default=1e-8, help="学习率,建议<=1e-8,否则容易遗忘训坏")
parser.add_argument("--use_wandb", action="store_true") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
parser.add_argument("--wandb_project", type=str, default="MiniMind-RLHF-SFT") parser.add_argument("--dtype", type=str, default="bfloat16", help="训练精度")
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("--use_wandb", action="store_true", help="是否使用wandb记录训练过程")
parser.add_argument("--grad_clip", type=float, default=1.0) parser.add_argument("--wandb_project", type=str, default="MiniMind-RLHF-SFT", help="wandb项目名称")
parser.add_argument("--warmup_iters", type=int, default=0) parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
parser.add_argument("--log_interval", type=int, default=100) parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔")
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_workers", type=int, default=1, help="数据加载进程数")
parser.add_argument('--num_hidden_layers', default=8, type=int) parser.add_argument("--ddp", action="store_true", help="是否使用分布式训练")
parser.add_argument('--max_seq_len', default=1024, type=int) parser.add_argument('--local_rank', type=int, default=-1, help="分布式训练的本地进程编号")
parser.add_argument('--use_moe', default=False, type=bool)
parser.add_argument("--data_path", type=str, default="../dataset/dpo.jsonl") # 优化器参数
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() args = parser.parse_args()

View File

@ -1,9 +1,12 @@
# 导入必要的系统库
import os import os
import sys import sys
# 设置包名和添加项目根目录到系统路径
__package__ = "trainer" __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 argparse
import time import time
import math import math
@ -18,51 +21,69 @@ from transformers import AutoTokenizer, AutoModelForCausalLM
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from dataset.lm_dataset import SFTDataset from dataset.lm_dataset import SFTDataset
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore') # 忽略警告信息
# 日志打印函数
# 在分布式训练时只在主进程(rank=0)上打印日志
def Logger(content): def Logger(content):
if not ddp or dist.get_rank() == 0: if not ddp or dist.get_rank() == 0:
print(content) print(content)
# 余弦学习率调度器
# 在训练过程中逐渐降低学习率,最终降到初始值的1/10
def get_lr(current_step, total_steps, lr): def get_lr(current_step, total_steps, lr):
return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))
# 训练一个epoch的函数
def train_epoch(epoch, wandb): def train_epoch(epoch, wandb):
# 使用交叉熵损失函数,reduction='none'以便后续通过mask处理填充token
loss_fct = nn.CrossEntropyLoss(reduction='none') loss_fct = nn.CrossEntropyLoss(reduction='none')
start_time = time.time() start_time = time.time()
# 遍历数据加载器获取批次数据
for step, (X, Y, loss_mask) in enumerate(train_loader): for step, (X, Y, loss_mask) in enumerate(train_loader):
X = X.to(args.device) # 将数据移动到指定设备
Y = Y.to(args.device) X = X.to(args.device) # 输入序列
loss_mask = loss_mask.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) lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)
for param_group in optimizer.param_groups: for param_group in optimizer.param_groups:
param_group['lr'] = lr param_group['lr'] = lr
with ctx: # 前向传播和损失计算
res = model(X) with ctx: # 使用自动混合精度训练
res = model(X) # 模型前向传播
# 计算交叉熵损失
loss = loss_fct( loss = loss_fct(
res.logits.view(-1, res.logits.size(-1)), res.logits.view(-1, res.logits.size(-1)), # 展平logits
Y.view(-1) Y.view(-1) # 展平目标序列
).view(Y.size()) ).view(Y.size())
# 应用损失掩码并计算平均损失
loss = (loss * loss_mask).sum() / loss_mask.sum() loss = (loss * loss_mask).sum() / loss_mask.sum()
loss += res.aux_loss loss += res.aux_loss # 添加辅助损失(如果有的话,例如MoE的负载均衡损失)
loss = loss / args.accumulation_steps loss = loss / args.accumulation_steps # 根据梯度累积步数缩放损失
scaler.scale(loss).backward() # 反向传播
scaler.scale(loss).backward() # 使用梯度缩放器缩放损失并反向传播
# 梯度更新
if (step + 1) % args.accumulation_steps == 0: if (step + 1) % args.accumulation_steps == 0:
scaler.unscale_(optimizer) scaler.unscale_(optimizer) # 反缩放梯度
# 梯度裁剪,防止梯度爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip) torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
scaler.step(optimizer) scaler.step(optimizer) # 优化器更新参数
scaler.update() scaler.update() # 更新梯度缩放器
optimizer.zero_grad(set_to_none=True) optimizer.zero_grad(set_to_none=True) # 清空梯度
# 打印训练日志
if step % args.log_interval == 0: if step % args.log_interval == 0:
spend_time = time.time() - start_time spend_time = time.time() - start_time
Logger( Logger(
@ -75,109 +96,149 @@ def train_epoch(epoch, wandb):
optimizer.param_groups[-1]['lr'], optimizer.param_groups[-1]['lr'],
spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60)) 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): if (wandb is not None) and (not ddp or dist.get_rank() == 0):
wandb.log({"loss": loss * args.accumulation_steps, wandb.log({"loss": loss * args.accumulation_steps,
"lr": optimizer.param_groups[-1]['lr'], "lr": optimizer.param_groups[-1]['lr'],
"epoch_Time": spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60}) "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): 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 '' moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth'
# 获取模型状态字典
if isinstance(model, torch.nn.parallel.DistributedDataParallel): if isinstance(model, torch.nn.parallel.DistributedDataParallel):
state_dict = model.module.state_dict() state_dict = model.module.state_dict()
else: else:
state_dict = model.state_dict() 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) torch.save(state_dict, ckp)
model.train() model.train() # 切换回训练模式
# 初始化模型和分词器
def init_model(lm_config): def init_model(lm_config):
# 加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained('../model') tokenizer = AutoTokenizer.from_pretrained('../model')
# 初始化因果语言模型
model = MiniMindForCausalLM(lm_config) model = MiniMindForCausalLM(lm_config)
# 根据是否使用MoE设置权重路径
moe_path = '_moe' if lm_config.use_moe else '' moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/pretrain_{lm_config.hidden_size}{moe_path}.pth' ckp = f'{args.save_dir}/pretrain_{lm_config.hidden_size}{moe_path}.pth'
# 加载预训练权重
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
model.load_state_dict(state_dict, strict=False) 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} 百万') Logger(f'LLM可训练总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
# 将模型移动到指定设备
model = model.to(args.device) model = model.to(args.device)
return model, tokenizer return model, tokenizer
# 初始化分布式训练环境
def init_distributed_mode(): def init_distributed_mode():
if not ddp: return if not ddp: return
global ddp_local_rank, DEVICE global ddp_local_rank, DEVICE
# 初始化分布式进程组
dist.init_process_group(backend="nccl") dist.init_process_group(backend="nccl")
ddp_rank = int(os.environ["RANK"]) # 获取分布式训练的相关参数
ddp_local_rank = int(os.environ["LOCAL_RANK"]) ddp_rank = int(os.environ["RANK"]) # 全局进程编号
ddp_world_size = int(os.environ["WORLD_SIZE"]) ddp_local_rank = int(os.environ["LOCAL_RANK"]) # 本地进程编号
ddp_world_size = int(os.environ["WORLD_SIZE"]) # 总进程数
# 设置当前进程使用的GPU
DEVICE = f"cuda:{ddp_local_rank}" DEVICE = f"cuda:{ddp_local_rank}"
torch.cuda.set_device(DEVICE) torch.cuda.set_device(DEVICE)
# 主函数
if __name__ == "__main__": if __name__ == "__main__":
# 配置命令行参数
parser = argparse.ArgumentParser(description="MiniMind Full SFT") 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("--out_dir", type=str, default="../out", help="输出目录")
parser.add_argument("--batch_size", type=int, default=16) parser.add_argument("--epochs", type=int, default=2, help="训练轮数")
parser.add_argument("--learning_rate", type=float, default=5e-7) parser.add_argument("--batch_size", type=int, default=16, help="批次大小")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") parser.add_argument("--learning_rate", type=float, default=5e-7, help="学习率")
parser.add_argument("--dtype", type=str, default="bfloat16") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
parser.add_argument("--use_wandb", action="store_true") parser.add_argument("--dtype", type=str, default="bfloat16", help="训练精度")
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("--use_wandb", action="store_true", help="是否使用wandb记录训练过程")
parser.add_argument("--accumulation_steps", type=int, default=1) parser.add_argument("--wandb_project", type=str, default="MiniMind-Full-SFT", help="wandb项目名称")
parser.add_argument("--grad_clip", type=float, default=1.0) parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
parser.add_argument("--warmup_iters", type=int, default=0) parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔")
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("--num_workers", type=int, default=1, help="数据加载进程数")
parser.add_argument('--hidden_size', default=512, type=int) parser.add_argument("--ddp", action="store_true", help="是否使用分布式训练")
parser.add_argument('--num_hidden_layers', default=8, type=int) parser.add_argument('--local_rank', type=int, default=-1, help="分布式训练的本地进程编号")
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("--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() args = parser.parse_args()
# 初始化模型配置
lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers, lm_config = MiniMindConfig(hidden_size=args.hidden_size, num_hidden_layers=args.num_hidden_layers,
use_moe=args.use_moe) use_moe=args.use_moe)
# 创建输出目录
args.save_dir = os.path.join(args.out_dir) args.save_dir = os.path.join(args.out_dir)
os.makedirs(args.save_dir, exist_ok=True) os.makedirs(args.save_dir, exist_ok=True)
os.makedirs(args.out_dir, exist_ok=True) os.makedirs(args.out_dir, exist_ok=True)
# 计算每次迭代处理的token数量
tokens_per_iter = args.batch_size * args.max_seq_len tokens_per_iter = args.batch_size * args.max_seq_len
device_type = "cuda" if "cuda" in args.device else "cpu" 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}" 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() 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" ddp_local_rank, DEVICE = 0, "cuda:0"
# 设置随机种子
base_seed = 1337 base_seed = 1337
torch.manual_seed(base_seed) torch.manual_seed(base_seed)
torch.cuda.manual_seed(base_seed) torch.cuda.manual_seed(base_seed)
# 初始化分布式训练环境
if ddp: if ddp:
init_distributed_mode() init_distributed_mode()
args.device = torch.device(DEVICE) args.device = torch.device(DEVICE)
rank = dist.get_rank() rank = dist.get_rank()
torch.manual_seed(base_seed + rank) torch.manual_seed(base_seed + rank)
# 同时设置 CUDA 的随机种子
torch.cuda.manual_seed(base_seed + rank) torch.cuda.manual_seed(base_seed + rank)
# 初始化wandb(如果启用)
if args.use_wandb and (not ddp or ddp_local_rank == 0): if args.use_wandb and (not ddp or ddp_local_rank == 0):
import wandb import wandb
wandb.init(project=args.wandb_project, name=args.wandb_run_name) wandb.init(project=args.wandb_project, name=args.wandb_run_name)
else: else:
wandb = None wandb = None
# 初始化模型和分词器
model, tokenizer = init_model(lm_config) model, tokenizer = init_model(lm_config)
# 准备训练数据
train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len) train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
train_sampler = DistributedSampler(train_ds) if ddp else None train_sampler = DistributedSampler(train_ds) if ddp else None
train_loader = DataLoader( train_loader = DataLoader(
@ -190,13 +251,18 @@ if __name__ == "__main__":
sampler=train_sampler sampler=train_sampler
) )
# 初始化优化器和梯度缩放器
scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16'])) scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate) optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate)
# 配置分布式训练
if ddp: if ddp:
model._ddp_params_and_buffers_to_ignore = {"pos_cis"} model._ddp_params_and_buffers_to_ignore = {"pos_cis"}
model = DistributedDataParallel(model, device_ids=[ddp_local_rank]) model = DistributedDataParallel(model, device_ids=[ddp_local_rank])
# 计算每个epoch的迭代次数
iter_per_epoch = len(train_loader) iter_per_epoch = len(train_loader)
# 开始训练循环
for epoch in range(args.epochs): for epoch in range(args.epochs):
train_epoch(epoch, wandb) train_epoch(epoch, wandb)

View File

@ -21,17 +21,19 @@ from model.model_lora import load_lora, save_lora, apply_lora
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
# Logger function # 日志打印函数,在分布式训练时只在主进程上打印
def Logger(content): def Logger(content):
if not ddp or dist.get_rank() == 0: if not ddp or dist.get_rank() == 0:
print(content) print(content)
# 余弦学习率调度器
def get_lr(current_step, total_steps, lr): 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)) return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))
# 代码和full_sft「几乎」一致 # 训练一个epoch,使用LoRA方法进行参数高效微调
def train_epoch(epoch, wandb): def train_epoch(epoch, wandb):
loss_fct = nn.CrossEntropyLoss(reduction='none') loss_fct = nn.CrossEntropyLoss(reduction='none')
start_time = time.time() start_time = time.time()
@ -90,51 +92,72 @@ def train_epoch(epoch, wandb):
model.train() model.train()
# 初始化模型和分词器
def init_model(lm_config): def init_model(lm_config):
# 加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained('../model/') tokenizer = AutoTokenizer.from_pretrained('../model/')
# 初始化因果语言模型
model = MiniMindForCausalLM(lm_config) model = MiniMindForCausalLM(lm_config)
# 根据是否使用MoE设置权重路径
moe_path = '_moe' if lm_config.use_moe else '' moe_path = '_moe' if lm_config.use_moe else ''
ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth' ckp = f'{args.save_dir}/full_sft_{lm_config.hidden_size}{moe_path}.pth'
# 加载预训练权重
state_dict = torch.load(ckp, map_location=args.device) state_dict = torch.load(ckp, map_location=args.device)
model.load_state_dict(state_dict, strict=False) model.load_state_dict(state_dict, strict=False)
return model.to(args.device), tokenizer return model.to(args.device), tokenizer
# 初始化分布式训练环境
def init_distributed_mode(): def init_distributed_mode():
if not ddp: return if not ddp: return
global ddp_local_rank, DEVICE global ddp_local_rank, DEVICE
# 初始化分布式进程组
dist.init_process_group(backend="nccl") dist.init_process_group(backend="nccl")
ddp_rank = int(os.environ["RANK"]) # 获取分布式训练的相关参数
ddp_local_rank = int(os.environ["LOCAL_RANK"]) ddp_rank = int(os.environ["RANK"]) # 全局进程编号
ddp_world_size = int(os.environ["WORLD_SIZE"]) ddp_local_rank = int(os.environ["LOCAL_RANK"]) # 本地进程编号
ddp_world_size = int(os.environ["WORLD_SIZE"]) # 总进程数
# 设置当前进程使用的GPU
DEVICE = f"cuda:{ddp_local_rank}" DEVICE = f"cuda:{ddp_local_rank}"
torch.cuda.set_device(DEVICE) torch.cuda.set_device(DEVICE)
if __name__ == "__main__": if __name__ == "__main__":
# 配置命令行参数
parser = argparse.ArgumentParser(description="MiniMind SFT with LoRA") 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("--out_dir", type=str, default="../out", help="输出目录")
parser.add_argument("--batch_size", type=int, default=32) parser.add_argument("--epochs", type=int, default=10, help="训练轮数")
parser.add_argument("--learning_rate", type=float, default=1e-4) parser.add_argument("--batch_size", type=int, default=32, help="批次大小")
parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu") parser.add_argument("--learning_rate", type=float, default=1e-4, help="学习率")
parser.add_argument("--dtype", type=str, default="bfloat16") parser.add_argument("--device", type=str, default="cuda:0" if torch.cuda.is_available() else "cpu", help="训练设备")
parser.add_argument("--use_wandb", action="store_true") parser.add_argument("--dtype", type=str, default="bfloat16", help="训练精度")
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("--use_wandb", action="store_true", help="是否使用wandb记录训练过程")
parser.add_argument("--accumulation_steps", type=int, default=1) parser.add_argument("--wandb_project", type=str, default="MiniMind-LoRA-SFT", help="wandb项目名称")
parser.add_argument("--grad_clip", type=float, default=1.0) parser.add_argument("--log_interval", type=int, default=100, help="日志打印间隔")
parser.add_argument("--warmup_iters", type=int, default=0) parser.add_argument("--save_interval", type=int, default=100, help="模型保存间隔")
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("--num_workers", type=int, default=1, help="数据加载进程数")
parser.add_argument('--hidden_size', default=512, type=int) parser.add_argument("--ddp", action="store_true", help="是否使用分布式训练")
parser.add_argument('--num_hidden_layers', default=8, type=int) parser.add_argument('--local_rank', type=int, default=-1, help="分布式训练的本地进程编号")
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("--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_(英文/医学/心理...)") parser.add_argument("--lora_name", type=str, default="lora_medical", help="根据任务保存成lora_(英文/医学/心理...)")
args = parser.parse_args() args = parser.parse_args()
@ -169,9 +192,12 @@ if __name__ == "__main__":
else: else:
wandb = None wandb = None
# 初始化模型和分词器
model, tokenizer = init_model(lm_config) model, tokenizer = init_model(lm_config)
# 应用LoRA参数高效微调方法
apply_lora(model) apply_lora(model)
# 统计模型参数量
total_params = sum(p.numel() for p in model.parameters()) # 总参数数量 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 参数数量 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: 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}")
print(f"LoRA 参数占比: {lora_params_count / total_params * 100:.2f}%") print(f"LoRA 参数占比: {lora_params_count / total_params * 100:.2f}%")
# 冻结非LoRA参数
for name, param in model.named_parameters(): for name, param in model.named_parameters():
if 'lora' not in name: if 'lora' not in name:
param.requires_grad = False param.requires_grad = False
# 收集需要优化的LoRA参数
lora_params = [] lora_params = []
for name, param in model.named_parameters(): for name, param in model.named_parameters():
if 'lora' in name: if 'lora' in name:
lora_params.append(param) lora_params.append(param)
# 只对 LoRA 参数进行优化 # 初始化优化器,只更新LoRA参数
optimizer = optim.AdamW(lora_params, lr=args.learning_rate) optimizer = optim.AdamW(lora_params, lr=args.learning_rate)
train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len) train_ds = SFTDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
train_sampler = DistributedSampler(train_ds) if ddp else None train_sampler = DistributedSampler(train_ds) if ddp else None

View File

@ -1,6 +1,8 @@
# 导入必要的库
import os import os
import sys import sys
__package__ = "trainer" __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 argparse
@ -20,16 +22,22 @@ from dataset.lm_dataset import PretrainDataset
warnings.filterwarnings('ignore') warnings.filterwarnings('ignore')
# 日志打印函数
# 在分布式训练时只在主进程(rank=0)上打印日志
def Logger(content): def Logger(content):
if not ddp or dist.get_rank() == 0: if not ddp or dist.get_rank() == 0:
print(content) print(content)
# 余弦学习率调度器
# 在训练过程中逐渐降低学习率,最终降到初始值的1/10
def get_lr(current_step, total_steps, lr): def get_lr(current_step, total_steps, lr):
return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps)) return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))
# 训练一个epoch
def train_epoch(epoch, wandb): def train_epoch(epoch, wandb):
# 使用交叉熵损失函数,reduction='none'以便后续通过mask处理填充token
loss_fct = nn.CrossEntropyLoss(reduction='none') loss_fct = nn.CrossEntropyLoss(reduction='none')
start_time = time.time() start_time = time.time()
for step, (X, Y, loss_mask) in enumerate(train_loader): for step, (X, Y, loss_mask) in enumerate(train_loader):
@ -94,21 +102,29 @@ def train_epoch(epoch, wandb):
model.train() model.train()
# 初始化模型和分词器
def init_model(lm_config): def init_model(lm_config):
# 加载预训练的分词器
tokenizer = AutoTokenizer.from_pretrained('../model/') tokenizer = AutoTokenizer.from_pretrained('../model/')
# 初始化因果语言模型并移动到指定设备
model = MiniMindForCausalLM(lm_config).to(args.device) model = MiniMindForCausalLM(lm_config).to(args.device)
# 打印模型可训练参数总量
Logger(f'LLM可训练总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万') Logger(f'LLM可训练总参数量:{sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6:.3f} 百万')
return model, tokenizer return model, tokenizer
# 初始化分布式训练环境
def init_distributed_mode(): def init_distributed_mode():
if not ddp: return if not ddp: return
global ddp_local_rank, DEVICE global ddp_local_rank, DEVICE
# 初始化分布式进程组,使用NCCL后端
dist.init_process_group(backend="nccl") dist.init_process_group(backend="nccl")
ddp_rank = int(os.environ["RANK"]) # 获取分布式训练的相关参数
ddp_local_rank = int(os.environ["LOCAL_RANK"]) ddp_rank = int(os.environ["RANK"]) # 全局进程编号
ddp_world_size = int(os.environ["WORLD_SIZE"]) ddp_local_rank = int(os.environ["LOCAL_RANK"]) # 本地进程编号
ddp_world_size = int(os.environ["WORLD_SIZE"]) # 总进程数
# 设置当前进程使用的GPU
DEVICE = f"cuda:{ddp_local_rank}" DEVICE = f"cuda:{ddp_local_rank}"
torch.cuda.set_device(DEVICE) torch.cuda.set_device(DEVICE)