docs: 为多个脚本添加详细的中文注释
为scripts和trainer目录下的多个Python脚本添加了详细的中文注释,包括函数说明、参数解释和代码逻辑描述,以提高代码的可读性和维护性pull/394/head
parent
f74117e5d9
commit
8d36134e24
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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('<think>').input_ids
|
||||
end_of_think_ids = tokenizer('</think>').input_ids
|
||||
start_of_answer_ids = tokenizer('<answer>').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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue