随着开源数据的日益丰富以及算力价格的持续下降,对于个人或小型机构而言,预训练一个小型的 LLM 已逐渐成为可能。开源中文预训练语言模型 Steel - LLM 就是一个典型案例,其模型参数量与数据量并非十分庞大,基本处于参数量为 B 级别、数据量为 T 级别的规模。
本文将着重阐述项目实施过程中所遭遇的问题、复盘后的思考以及相关技术细节,期望能为在资源有限条件下开展 LLM 训练的开发者们提供一定的启发与助力。该模型已上线始智AI-wisemodel开源社区,欢迎大家前去体验。
模型和代码地址:
https://wisemodel.cn/models/zhanshijinwat/Steel-LLM
https://wisemodel.cn/codes/zhanshijinwat/Steel-LLM
Steel LLM使用的全部数据都是开源的,预训练阶段里Skywork/Skypile-150B数据集(600GB)、wanjuan1.0(nlp部分)(1TB)、starcoder的python、java、c++部分(200GB)占了绝大部分,英文数据比较少,只有wanjuan1.0有400GB。如果读者想现在也预训练训练一个,可以考虑使用MAP-NEO、BAAI的CCI3.0-HQ数据集等比较新的数据集。
除了这些大数据量的预训练数据外,项目还在预训练阶段加入了本应在SFT阶段加入的对话数据,比如百度百科问答数据、BELLE对话数据、moss项目对话数据等,这些对话数据仍然遵循如下这种对话形式的,数据的prompt部分在训练时也计算loss。
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
{问题}<|im_end|>
<|im_start|>assistant
{回答}<|im_end|>
在预训练阶段加入SFT数据的方法在minicpm的训练过程中也使用了,但是只在预训练末期的退火阶段加入了SFT数据,这种方法预训练出来的模型指令跟随能力应该会比较强。如果预训练全程都杂糅SFT数据,这些少量的SFT数据会淹没在海量的原始文本中,起的作用会比较小。
Steel-LLM的预训练没有退火阶段,退火阶段的的核心是“高质量”数据,但是高质量的定义我觉得还是比较模糊的。不同类型的数据配比合理可以理解为是一种“高质量”,但是开源数据往往并没有给出数据的类型。经过各种规则筛选的数据也可以被称为高质量数据,但是开源数据基本都是经过数据清洗pipeline处理的。
Steel LLM对开源数据集的处理流程如下图所示。“处理为统一格式”和“将文本转化为token id”的代码在github仓库中给出,小一些的数据集使用了阿里的data-juicer工具进行了过滤,该工具将每个数据处理步骤抽象为一个算子,用户可以方便的配置yaml文件实现自定义的数据处理流程。
但是有个问题是,data juicer处理数据的过程中产生的中间缓存有点多,大概占了几百GB的空间。Steel-LLM的数据处理pipeline并不是端到端的,比如下图中“格式1”和“格式2”的数据都要回落到硬盘上的,因此需要存同一份数据不同格式的多份拷贝,因此建议机器配有3~4T的硬盘空间。如果想在几个小时内处理完数据,需要开多进程,建议有100GB+的CPU内存。
在训练代码方面,主要参考了 TinyLlama。Steel LLM 是一个规模为 1B 的较小模型,这是未选用 Megatron 的根本原因。具体而言,Megatron 虽支持 pipeline 并行与 tensor 并行,对训练大尺寸模型颇具优势,但对于 1B 模型,在 A100 上使用原生 PyTorch 的 FSDP 甚至 DDP 便可实现稳定训练。此外,Megatron 所提供的算子融合、dataloader 等功能,在其他项目中同样具备,且后者更为简洁,在集成与修改时更为便利。
项目基于TinyLlama训练代码进行了如下几点改进:
对于预训练来说,进行模型的训练效率的优化是必不可少的。卡少的情况下,即使预训练个小模型也得跑好几十天,那么训练效率优化个10%,就能省下好几天的电钱呢。个人或者小机构训练模型很难去优化底层的分布式训练机制,那么性价比最高的优化方式就是对模型的部分算子用算子融合。
模型在训练时候,除了gpu的计算时间外,从显存把数据搬运到缓存也占用了很多时间。举个例子,算一个最简单的标准化(x-mean)/var,需要涉及到如下步骤:1.读x+读mean 2.写x-mean 3.读x-mean,读var 4.写(x-mean)/var,看起来十分的啰嗦。
如果想让gpu端到端的计算结果(只读一次写一次),就需要做算子融合了,用cuda编程或者triton编程都行。在LLM中,RMSNorm、RoPE、self-attention、交叉熵损失函数都是频繁读取数据的大户,因此能把这些操作融合掉能提高不少的训练时间。
对self-attention进行算子融合的实现就是大名鼎鼎的flash attention。在我的往期文章中,笔者对各个算子的融合进行了消融,使用算子融合整体上能提升50%的训练效率,以及节省12%的显存,MFU(指模型一次前反向计算消耗的算力与机器能够提供的算力的比值)也能从43%提高到63%,大大提高了GPU的利用率。
分布式训练方式上,笔者沿用了FSDP(Fully-sharded data-parallel,完全分片数据并行)方法。有的读者对FSDP可能不是很熟悉,它的原理和deepspeed的ZeRO stage3一致,FSDP是pytorch的官方实现,将优化器参数、模型参数和梯度分布存储到不同的GPU上,节省分布式训练时候的显存,模型越大,显存节省的越多。
公司训练模型大部分都是沿用传统的LLM结构(self attention、RMSnorm、RoPE位置编码等),一来是LLM的效果主要是靠数据、二来是team leader也没必要去冒风险搞一些不一定有确定收益的创新。但Steel LLM的tokenizer直接使用的是qwen的,并没有重新训练。
起初,项目也尝试过训练recurrent gemma结构的模型,只不过它的pytorch实现训练效率太差了,和训transformer结构相比慢了几十倍,遂放弃,gemma是google训的模型,他们在TPU上训练时候做了不少的工程优化,训练效率才是可接受的。
对于主流的LLM结构来说,self attention和FFN是两大核心模块。目前self attention这块的实现基本都用flash attention,修改self attention的实现比较困难。之前测试,如果不使用flash attention会掉25%左右的训练效率。因此,魔改结构的重点放到FFN上边。Steel LLM在FFN上的修改有两部分,soft MOE和SENet。
很多大型号的LLM使用hard MOE(每个token选择top k个FFN),目的是减少训练和推理时的计算量,但是hard MOE并不省显存,在训练和推理时仍然需要将完整的模型加载到显存中。
考虑到只有1台机器训练Steel LLM,显存并不富裕,模型也比较小,想充分训练每一份参数,并且hard MOE并不好训练,需要考虑高效的token路由实现、训练稳定性以及负载均衡等问题。hard MOE存在种种问题,但笔者仍然想尝试一下MOE结构,因此使用了soft形式的MOE,这在搜广推多任务学习上被广泛使用。
soft MOE的原理如下图所示,input就是一个向量,通过不同的专家网络计算出结果,然后再通过input计算出来的权重进行加权求和。在LLM中的FFN层使用soft MOE原理类似,只不过有多个input(即多个token),分别通过各个专家并进行加权求和。
SENet(Squeeze-and-Excitation Networks)来自于计算机视觉领域ImageNet 2017竞赛的冠军方案,目的是通过学习的方式来获取到每个图像特征通道的重要程度,通过重要程度加强或抑制特征通道,经过简单变换之后可变为如下图所示的形式:
我们再来看看Qwen模型的FFN层实现,大致可分为两层,它的第一层就是SENet的思想,用了gate_proj和up_proj,FFN的第二层也换成了SENet。
class Qwen2MoeMLP(nn.Module):
def __init__(self, config, intermediate_size=None):
super().__init__()
self.config = config
self.hidden_size = config.hidden_size
self.intermediate_size = intermediate_size
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
self.act_fn = ACT2FN[config.hidden_act]
def forward(self, x):
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
预训练输入的最大序列长度是2048,将收集的数据训练了两个epoch,大概1.1T个token,需要训练1M个step。如果使用SXM版本的A100 80G*8,大概需要60天,H800*8需要30天左右。训练时的batchsize 为8,梯度累计步数为8。优化器使用的AdamW,最大学习率时3e-4,学习率衰减策略是CosineAnnealingWarmUp。训练的wandb链接如下:
https://api.wandb.ai/links/steel-llm-lab/vqf297nr
因为是单机8卡,训练还是比较稳定的,没有出现过啥error。唯一一次故障重启是机器网断了,wandb把训练进度给卡住了。
微调阶段数据量不大,为了方便直接用llama factory训练了,选择如下4份数据:
Steel LLM的预训练数据里80%以上都是中文的,所以只测了ceval和cmmlu这两个中文benchmark。第一版实验用了700w条全量的Infinity-Instruct数据,ceval能有33%的准确率。
后来发现Infinity-Instruct里90%数据都是英文的,和预训练数据分布严重不符(预训练数据里只有20%英文)。之后从Infinity-Instruct里抽出了70w的中文数据,并糅合其他3个数据集,最终在ceval取得了38%准确率,cmmlu取得了33%准确率。
同时,还做了刷榜测试,直接将cmmlu数据也放到sft数据里。在cmmlu上的正确率从33%提高了36%,在ceval上的准确率几乎没变化。这说明让模型去死记硬背答案也没那么容易。
cmmlu在sft训练时候的标签只有一个选项字母,如果标签中有答案的解释,应该会效果更好一些。除此之外,还尝试了一下让模型以COT的方式进行答案生成,即先输出解释再输出答案,发现在benchmark上并没有拿到更好的结果。
Steel LLM模型和其他部分模型在ceval和cmmlu上的对比如下所示,其他模型的结果出自ceval、MiniCPM、MAP-Neo、CT-LLM的论文或技术报告。
本次项目属于个人项目,在精力和资源有限的情况下,还存在一些没做到位的地方,比如训练tokenizer、数据配比探究、全局数据清洗、模型英文能力较弱等。后续依算力情况,还将基于Steel LLM做一些微调方面的探索(样本筛选等)、强化学习或者是VLM,欢迎持续关注。
文章来自于“始智AI wisemodel”,作者“始智AI wisemodel”。
【开源免费】graphrag是微软推出的RAG项目,与传统的通过 RAG 方法使用向量相似性作为搜索技术不同,GraphRAG是使用知识图谱在推理复杂信息时大幅提高问答性能。
项目地址:https://github.com/microsoft/graphrag
【开源免费】Dify是最早一批实现RAG,Agent,模型管理等一站式AI开发的工具平台,并且项目方一直持续维护。其中在任务编排方面相对领先对手,可以帮助研发实现像字节扣子那样的功能。
项目地址:https://github.com/langgenius/dify
【开源免费】RAGFlow是和Dify类似的开源项目,该项目在大文件解析方面做的更出色,拓展编排方面相对弱一些。
项目地址:https://github.com/infiniflow/ragflow/tree/main
【开源免费】phidata是一个可以实现将数据转化成向量存储,并通过AI实现RAG功能的项目
项目地址:https://github.com/phidatahq/phidata
【开源免费】TaskingAI 是一个提供RAG,Agent,大模型管理等AI项目开发的工具平台,比LangChain更强大的中间件AI平台工具。
项目地址:https://github.com/TaskingAI/TaskingAI
【开源免费】XTuner 是一个高效、灵活、全能的轻量化大模型微调工具库。它帮助开发者提供一个简单易用的平台,可以对大语言模型(LLM)和多模态图文模型(VLM)进行预训练和轻量级微调。XTuner 支持多种微调算法,如 QLoRA、LoRA 和全量参数微调。
项目地址:https://github.com/InternLM/xtuner
【开源免费】LangGPT 是一个通过结构化和模板化的方法,编写高质量的AI提示词的开源项目。它可以让任何非专业的用户轻松创建高水平的提示词,进而高质量的帮助用户通过AI解决问题。
项目地址:https://github.com/langgptai/LangGPT/blob/main/README_zh.md
在线使用:https://kimi.moonshot.cn/kimiplus/conpg00t7lagbbsfqkq0