【导读】
本文是一篇理论与实践结合的综述文章,综合性全面介绍大模型微调技术。本文先介绍大模型训练的两类场景:预训练和后训练,了解业界常见的模型训练方法。在后训练介绍内容中,引出模型微调(模型微调是属于后训练的一种)。然后,通过介绍业界常见的模型微调方法,以及通过模型微调实操案例的参数优化、微调过程介绍、微调日志解读,让读者对模型微调有更加直观的了解。最后,我们详细探讨数据并行训练DDP与模型并行训练MP两类模型并行训练技术,讨论在实际项目中如何选择两类并行训练技术。
一、大模型训练:预训练和后训练
预训练:
利用海量训练数据,借助成熟模型训练方法+强大算力集群建立模型的通用基础能力。
后训练:
基于成熟通用模型,利用大量具有特定场景的指令式训练数据,借助成熟模型微调方法+少量算力,建立特定应用场景任务能力的增强模型。
1.1 预训练(Pre-Training)
大模型的预训练技术是大型语言模型(LLMs)开发过程中的关键阶段,通过不断优化预训练方法、提高数据质量和利用算力资源,研究人员可以提升大模型的性能和应用范围。在预训练阶段,模型会在海量数据进行训练,以学习语言模式、结构和语境细微差别。大规模预训练本质上是在对世界知识进行压缩,学习到的模型能够通过解压缩知识来解决实际任务。
预训练有以下核心内容:
- 训练目的:训练出能力出众的“通用大模型”。本质上是在对世界知识进行压缩,学习到的模型能够通过解压缩知识来解决实际任务。
- 资源量:对算力需求极高,训练千亿参数规模的通用大模型至少需要数千GPU卡规模的算力集群,联合训练数月,资源消耗非常惊人。
- 训练数据集:目前主流大模型(qwen、deepseek等),通常采用数万亿到数十万亿规模的tokens词元进行预训练,并且有趋势进一步扩大词元规模。
- 数据质量:预训练对数据质量要求比较高。需要准备大规模经过严格清晰的数据集,数据的质量和多样性对模型性能有着重要影响,因此收集高质量、多样化、跨领域的数据是构建大语言通用模型的关键步骤。
- 训练成功率:预训练通用大模型的成功率比较低,为了训练出性能优越的大模型,往往需要反复实验数十次/上百次。
- 训练经验至关重要:根据综述论文《Survey of different Large Language Model Architectures: Trends, Benchmarks, and Challenges》介绍、以及业界对于模型预训练的普遍认知,大模型预训练涉及大量需要深入探索的经验,比如不同类型数据比例、训练参数调优、模型异常行为处理方法等,这些经验对于模型预训练的成功至关重要。但是,模型预训练的技术细节并不会公开发表,每家公司都依靠研究人员的经验,具备丰富的训练经验和异常处理能力,保障在最少算力浪费的基础上训练出高质量的通用模型。在这方面,deepseek系列模型取得的成功,是一个行业内的经典优质案例。
1.2 后训练(Post-Training)
后训练是在预训练模型的基础上,利用大量具有特定场景的指令式训练数据,借助成熟模型微调方法+少量算力,建立特定应用场景任务能力的增强模型,提高模型在具体应用中的性能。
后训练通常可分为三类常见技术:
1.2.1 增量预训练(Continued Pre-Training, CPT)
增量预训练也叫“继续预训练”。
原理:增量预训练是指在已有预训练模型的基础上,通过引入新的数据或任务,继续在大规模无监督文本数据(如行业领域数据)上进行自回归语言建模(如GPT)或自监督训练(如BERT),进一步对模型进行训练优化。
具体内容:目的是让模型适应新的数据分布或任务需求,扩展模型的知识范围或适应新的领域。一般使用某个领域的大规模数据集,包含特定领域的数据或新的任务相关数据。也就是说,增量预训练更侧重于某个专业领域扩展,需要的训练数据量相比于微调而言更大。
优缺点:
(1)可以显著提升特定领域的知识掌握能力。
(2)训练成本较高,数据要求较大,可能会丢失部分通用知识。
1.2.2 有监督微调训练(Supervised Fine-Tuning, SFT)
有监督微调训练也叫“指令微调”。
原理:采用有监督学习方式,选用成熟的通用大模型,利用少量的人工标注的输入-输出对(如问题-答案)进行训练。
具体内容:按照人类的指令,训练数据是有标注的数据,使用特定任务的小规模标注数据集。采用有监督学习方式,使用人工标注的输入-输出对(如问题-答案)进行训练,让模型更符合特定任务需求。
优缺点:
(1)训练数据清晰,收敛速度快,适合某些特定应用场景的任务适配。
(2)依赖高质量人工标注数据,模型微调容易出现过拟合,不适应真实用户偏好。
1.2.3 强化学习 (Reinforcement Learning, RL) 微调
原理:选用成熟的通用大模型,在指令微调后使用强化学习加强模型的对齐能力,将模型生成结果与人类的期望、需求以及价值观对齐(Alignment)。
具体内容:比较知名的强化学习方法就是openAI提出的基于人类反馈的强化学习对齐方法(Reinforcement Learning from Human Feedback,RLHF)。RLHF 方法需要训练一个符合人类价值观的奖励模型(Reward Model)。
优缺点:
(1)能够利用人类反馈让模型逐步校准到符合人类需求的方向。
(2)模型微调极度依靠高质量数据集,偏好学习结果受数据影响很大。模型应用场景单一,而且容易受到“人类坏主意”的影响。
强化学习的进化方法是DPO(Direct Preference Optimization,直接偏好优化),可以直接使用用户偏好数据进行模型优化,提高用户满意度。
1.3 模型微调分类
模型微调主要体现在后训练环节。SFT是一种微调形式,通过有监督的学习方法对模型进行调整。RL也是一种微调形式,通过强化学习的方法优化模型的行为。
模型微调是一项发展久远且相对成熟的技术领域,在大模型面世之前、甚至神经网络算法框架面世之前,就已经有很多模型微调的技术手段。在大模型时代,由于大模型具备了拟人化的通用能力,通过模型微调让大模型具备某些专业领域的技能,成为一种热门的技术手段。将成熟通用大模型类比于人类高中生,具备了很多通识性知识和技巧,那么模型微调就类比于让高中生上大学本科学习某个专业,让人类具备某个领域的专业能力。
通常而言,模型微调可以分为“全参微调”、“高效微调”。
1.4 全参微调(Full Parameter Fine-Tuning)
1.4.1 什么是全参微调
全参微调(FPFT)是指调整预训练大模型的所有参数权重以适应特定下游任务的训练方法。与高效微调(如P-Tunning、LoRA、Adapter)仅更新少量参数不同,FPFT会更新模型中的每一个权重矩阵。
全参微调核心特点:
- 更新模型100%的参数
- 需要完整加载原始预训练模型
- 计算资源消耗最大
- 通常在领域适配任务中效果最优
常见全参微调方法简介:
1.4.2 基础微调法(Vanilla Fine-Tuning)
基础微调法(Vanilla Fine-Tuning)是最早的全参微调方法,需要一次性加载模型的所有参数。
实现原理:
for param in model.parameters(): param.requires_grad = True # 解锁所有权重 optimizer = AdamW(model.parameters(), lr=5e-5) # 全参数优化
适用场景:
- 领域数据与预训练数据分布差异大(如医疗、法律等专业领域)
- 训练资源充足(典型需求:qwen2.5-7B模型需4到6张 A100 80GB的GPU卡)
1.4.3 分阶段微调(Progressive Fine-Tuning)
分阶段微调(Progressive Fine-Tuning),顾名思义就是将大模型的神经网络层进行分段,每次训练某几层的参数(其他层参数冻结)。首先对模型的高层(靠近输出层的层)进行微调,然后逐步解冻并微调底层。
实现原理:
for name, param in model.named_parameters(): if name.startswith("layers.xxxx,layers.xxxx,layers.xxxx"): # 仅解冻某几层 param.requires_grad = True
使用场景:
- 与基础微调法(Vanilla Fine-Tuning)类似,领域数据与预训练数据分布差异大(如医疗、法律等专业领域)。
- 训练资源是逐步增加的,一开始需要的资源少,越到后面需要的资源越多,最后需要的资源量和基础微调法(Vanilla Fine-Tuning)差不多。
1.5 常见的高效模型微调技术
1.5.1 Prompt Tuning
Prompt Tuning 是一种在预训练语言模型上进行微调的方法,它通过调整输入的提示(prompt)而不是直接修改模型的参数来实现特定任务的适应。
Prompt Tuning原理如下图所示:冻结主模型全部参数,在训练数据前加入一小段Prompt,只训练Prompt的表示层,即一个Embedding模块。《The Power of Scale for Parameter-Efficient Prompt Tuning》(https://arxiv.org/abs/2104.08691)论文实验表明,只要模型规模够大,简单加入 Prompt tokens 进行微调,就能取得很好的效果。
在输入序列的开头添加一组固定长度的虚拟 token(通常 20-100 个),每个 token 对应一个可训练的嵌入向量,维度与模型的词嵌入相同。软提示的嵌入与输入文本的词嵌入拼接,形成完整的输入序列:
模型按标准方式处理整个输入序列(包括软提示和文本 token),通过注意力机制和前馈网络生成输出。软提示可以看作一种任务特定的“上下文”,通过调整模型的输入分布引导输出。
通过上面介绍可知,Prompt Tunning 软提示的参数量极少,通常参数量占比模型总参数的不到 0.01%。例如,对于一个 32B 参数的模型,100 个软提示(每个 768 维)仅需约 0.076M 参数,占据32B参数总量的0.0002%。
1.5.2 P-Tuning
P-Tuning 是一种参数高效的微调方法,它通过学习一组连续的提示嵌入(continuous prompts)来引导预训练模型完成特定任务。这些提示嵌入是可训练的向量,它们被插入到输入序列中,以引导模型生成符合任务需求的输出。
P-Tuning原理如下图所示:在Prompt-Tuning的基础上,对Prompt部分进行进一步的编码计算,加速收敛。与Prompt-Tuning不同的是,Prompt的形式只有Soft Prompt。P-Tuning是Prompt-Tuning的改进版本微调算法。
不论是Prompt Tuning、还是P-Tuning ,模型微调高度依赖软提示的初始化、长度和任务特性,在复杂任务或与预训练数据差异较大的场景中效果不如全参数微调或 LoRA微调。
1.5.3 LoRA(Low-Rank Adaptation)
LoRA是一种参数高效的微调方法。神经网络包含很多全连接层,其借助于矩阵乘法得以实现,然而,很多全连接层的权重矩阵都是满秩的。当针对特定任务进行微调后,模型中权重矩阵其实具有很低的本征秩(intrinsic rank),因此通过引入低秩矩阵来调整预训练模型的权重。
LoRA(论文:LoRA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS),该方法的核心思想就是通过低秩分解来模拟参数的改变量,从而以极小的参数量来实现大模型的间接训练。
LoRA 假设权重更新的过程中也有一个较低的本征秩,对于预训练的权重参数矩阵 (d 为上一层输出维度,k 为下一层输入维度),使用低秩分解来表示其更新:
可训练层维度和预训练模型层维度一致为d,先将维度d通过全连接层降维至r,再从r通过全连接层映射回d维度,其中,r<<d,r是矩阵的秩,这样矩阵计算就从d x d变为d x r + r x d,参数量减少很多。
由于训练方法简单、需要的参数量低、训练后模型效果较好,LoRA成为最常用的模型微调方法。多篇论文的实验发现,在众多数据集上LoRA在只训练极少量参数(Rank=8或者16)的前提下,最终在性能上能和全参微调匹配,甚至在某些任务上优于全参微调。
1.5.4 Adapter Tuning(适配器微调)
在预训练模型的每一层(如Transformer的FFN层后)插入小型神经网络模块-Adapter。每个Adapter通常包含两个线性层(下投影+上投影)和激活函数,如:
Adapter(x) = x + W_up(σ(W_down(x)))
训练时,只优化适配器的参数,而预训练模型的权重保持不变。这种方法特别适合资源受限的场景或需要快速适配多个任务的情况。
与 LoRA 相比:适配器微调引入额外计算层(推理延迟略高20%左右),但模块化设计更灵活,适合多领域任务场景。LoRA 更适合单个业务领域的任务的高效适配。
1.5.5 LoRA+
LoRA+是LoRA的增强版模型微调技术。在论文《LoRA+: Efficient Low Rank Adaptation of Large Models》(https://papers.cool/arxiv/2402.12354)提出在微调时让B矩阵的学习率比A矩阵大几倍,微调效果会好2%左右。LoRA+的作者从理论角度断定最优解必然是右矩阵的学习率大于左矩阵的学习率。简而言之,LoRA+称得上是理论指导训练并且在实践中确实有效的经典例子,值得仔细学习一番。
这里我直接截图苏神对LoRA+的解释(https://spaces.ac.cn/archives/10001),非常精彩:
1.5.6 技术对比矩阵
微调方法 |
参数量占比 |
修改位置 |
训练内存 |
性能(相对全参数训练微调) |
典型应用场景 |
Prompt Tuning |
0.01%-0.1% |
输入嵌入层 |
最低 |
较差 |
文本分类/简单生成 |
P-Tuning |
0.01%-0.1% |
输入嵌入层 |
较低 |
较差 |
P-Tuning是Prompt-Tuning的改进版本微调算法 |
LoRA |
0.1%-1% |
Adapter 层权重矩阵旁路 |
中等 |
略差 |
适配某个专业领域的大模型微调 |
Adapter Tunining |
0.1%-5% |
在预训练模型的每一层插入小型神经网络模块 |
中等 |
略差 |
适配某些专业领域的大模型微调 |
LoRA+ |
0.1%-1% |
Adapter 层权重矩阵旁路 |
中等 |
略差 |
适配某个专业领域的大模型微调,效果略好于LoRA。 |
在实际工程项目中,LoRA微调是最常用的微调技术,因此本文接下来的内容将会重点介绍LoRA,同时在实操环节对比LoRA+ 与 LoRA,看看是否像论文描述的那样有2%左右的效果提升。
二、LoRA技术详解
建议大家阅读LoRA论文原文:LoRA: Low-rank Adaptation of Large Language Models 。本文对论文总结核心内容,并且增加作者的解读,方便读者快速了解LoRA微调技术。
2.1 LoRA是什么?
神经网络包含很多全连接层,借助于矩阵乘法得以实现,然而,很多全连接层的权重矩阵都是满秩的。当针对特定任务进行微调后,模型中权重矩阵其实具有很低的本征秩(intrinsic rank),因此,论文的作者认为权重更新的那部分参数矩阵尽管随机投影到较小的子空间,仍然可以有效的学习,可以理解为针对特定的下游任务这些权重矩阵就不要求满秩。
LoRA(论文:LoRA: LOW-RANK ADAPTATION OF LARGE LANGUAGE MODELS),该方法的核心思想就是通过低秩分解来模拟参数的改变量,从而以极小的参数量来实现大模型的间接训练。
2.2 LoRA微调的层次
在涉及到矩阵相乘的模块,在原始的PLM旁边增加一个新的通路,通过前后两个矩阵A,B相乘,第一个矩阵A负责降维,第二个矩阵B负责升维,中间层维度为r,从而来模拟所谓的本征秩(intrinsic rank)。
在 Transformer 结构中,LoRA 技术主要应用在注意力模块的四个权重矩阵:Wq、Wk 、Wv、Wo 而冻结 MLP 的权重矩阵。通过消融实验发现同时调整 Wq 和 Wv 会产生最佳结果。
备注:LoRA默认微调Wq、Wk 、Wv层。比如下面的LoRA实操的日志显示:
[INFO:swift] target_modules: ['q_proj', 'k_proj', 'v_proj']
2.3 LoRA的低秩矩阵原理
直接引用论文原图:
可训练层维度和预训练模型层维度一致为d,先将维度d通过全连接层降维至r,再从r通过全连接层映射回d维度,其中,r<<d,r是矩阵的秩,这样矩阵计算就从d x d变为d x r + r x d,参数量减少很多:
如果你还不太理解低秩分解的过程,那么我们回到神经网络的表达式,并把各个向量、矩阵的维度标好,假设输入𝑋是1024×8维的向量,输出𝑌是1024×8维的向量,那么𝑊是一个1024×1024的矩阵,写作𝑊∈𝑅1024×1024,共包含2^20次方个参数。单层神经网络可比表示为:
𝑌1024×8=σ(𝑊1024×1024 ⋅ 𝑋1024×8)
而矩阵的秩通俗一点可以理解为:矩阵的秩𝑅代表的是它的有效信息量,也就是我们大学线性代数学习的矩阵的秩,该矩阵可以通过最少r行向量代表整个矩阵的有效信息。LoRA通过低秩矩阵将神经网络某一层的有效信息提取,进而减少微调训练的参数量。
以下是低秩矩阵训练与融合的架构图:
上述架构图可以归纳为一个简单的矩阵计算公式:
𝑊微调后𝑑×𝑑=微调(B𝑑×𝑟∗A𝑟×𝑑)+不变(𝑊微调前𝑑×𝑑)
2.4 LoRA的秩r应该取值多少最合适?
LoRA微调时,主要的可调参数是我们假定的低秩𝑟,它预设得越大,越能捕捉更复杂的特征变化,但模型更难微调,需要更多的显存和训练轮次。
根据论文原文的实验结论、以及我们工程实践的经验,r取值4、8、16、32几个值最合适。
(1)当 r<4 的时候,微调几乎没有效果;
(2)当 r>32 的时候,微调需要的GPU算力资源猛增,但是效果不仅没有提高反而下降。
因此,我们建议:
(1)对于小的训练集(1百-10k样本):建议 𝑟≤16(优先r=8),避免模型训练轮次太多,而模型去背训练集而不是学习里面的特征。
(2)对于大的训练集(10k+样本):可以尝试 𝑟=32(优先r=16),可以充分挖掘训练数据集的业务规律。
当然,LoRA秩的选择还与模型的参数量有关。建议参数量不超过16B的模型,r不要超过16;对于32B以上参数量的模型,r可以设置为16或者32。
2.5 模型微调需要的显存大小评估
模型微调消耗的显存主要由三部分组成:
显存消耗=模型参数消耗显存+数据集消耗的显存+其他开销消耗的显存
我们以qwen2.5-3B模型微调,设置的max_length=1024、batch_size=8、数据精度采用bf16为例,大概需要的GPU卡显存量为:
GPU显存 =
3*GB * 2(bf16单个占用2字节)
+ 约12GB(1024*8 tokens参数经过64层显化)
+ 其他消耗2GB
= 20GB
所以采用24GB显存的GPU卡就可以完成微调。
三、模型微调实操详解
本章节将会用模型微调案例,详细解读模型微调的参数优化、微调过程介绍、微调日志解读,从而对LoRA模型微调的全链路过程进行掌握。
3.1 模型微调前环境准备工作
3.1.1 环境安装和数据集准备
我们使用swift(https://github.com/modelscope/ms-swift)作为模型微调的技术框架。本文使用的操作系统是 Ubuntu 22.04.5,python是3.10。以下是需要使用pip3安装的依赖环境:
gradio==4.32.0 faiss-cpu==1.8.0.post1 dashscope==1.20.14 openai==1.55.3 httpx==0.27.0 llama-index-vector-stores-faiss==0.1.2 llama-index-embeddings-dashscope==0.1.4 llama-index-readers-file==0.1.33 docx2txt==0.8 openpyxl==3.1.5 llama-index-core==0.10.67 llama-index-llms-dashscope==0.1.2 llama-index-readers-dashscope==0.1.2 llama-index-llms-openai-like==0.1.3 uvicorn==0.30.6 fastapi==0.114.1 llama-index-postprocessor-dashscope-rerank-custom==0.1.0 simplejson==3.19.3 matplotlib==3.9.2 ragas==0.1.9 langchain_community==0.2.17 alibabacloud_bailian20231229==1.8.2 pandas==2.0.3 alibabacloud_green20220302==2.2.11 oss2==2.19.0 lagent==0.1.2 mmengine==0.10.3 accelerate==1.0.1 rouge-score==0.1.2 nltk==3.9.1 ms-swift[llm]==2.4.2.post2 evalscope==0.5.5rc1
使用 modelscope 命令可以从魔搭社区(https://modelscope.cn/)下载微调的基模。比如下载Qwen2.5-3B-Instruct模型:
mkdir ./model_3b modelscope download --model qwen/Qwen2.5-3B-Instruct --local_dir './model_3b'
3.1.2 数据集准备
建议将训练数据集、验证数据集分开,而不是让模型自己切分数据集。原因是模型自己切分数据集会出现比例失调的情况。比如以下是微调的日志:
Map: 100%| 1167/1167 [00:00<00:00, 14920.80 examples/s] [INFO:swift] train_dataset: Dataset({ features: ['query', 'response'], num_rows: 1156 }) [INFO:swift] val_dataset: Dataset({ features: ['query', 'response'], num_rows: 11 })
整个数据集有1167条数据,但是划分的训练数据集有1156条,验证数据集只有11条数据,相当于模型是按照1%的比例划分验证数据集,这是非常不合理的比例。通常而言,验证数据集条数需要是训练数据集的10%--20%。
本文的训练数据集有1167条,验证数据集有120条,按照大约10%比例构建。
以下是我们构建的数据集5条示例数据(小学数据计算题):
{"messages": [{"role": "system", "content": "你是一个数学题目计算助手"}, {"role": "user", "content": "#输出#\n输出要符合经过推理后将最终答案放在 {{ans:xxx}} 内。\n#数学题#\n王小姐积攒了48000元钱,存入银行,定期三年.三年定期存款的年利率是4.8%,到期时,王小姐的利息有多少元?"}, {"role": "assistant", "content": "王小姐存入48000元,定期三年,年利率4.8%。利息计算为:\n\n**利息**= 本金 × (1+年利率×时间) - 本金\n\n= 48000 × (1+4.8%×3) - 48000\n\n= 48000 × 1.144 - 48000\n\n≈ **6912元** \n\n{{ans:6912}}元"}]} {"messages": [{"role": "system", "content": "你是一个数学题目计算助手"}, {"role": "user", "content": "#输出#\n输出要符合经过推理后将最终答案放在 {{ans:xxx}} 内。\n#数学题#\n水果店新进一批苹果,上午卖出总量的30%,下午卖出余下的40%,此时还剩84千克。这批苹果原有多少千克?"}, {"role": "assistant", "content": "设总重量为x千克。上午卖出0.3x,剩余0.7x。下午卖出0.7x×40%=0.28x,最终剩余0.7x-0.28x=0.42x。根据题意0.42x=84,解得x=200。\n\n{{ans:200}}"}]} {"messages": [{"role": "system", "content": "你是一个数学题目计算助手"}, {"role": "user", "content": "#输出#\n输出要符合经过推理后将最终答案放在 {{ans:xxx}} 内。\n#数学题#\n三角形底边长度扩大到原来的3倍,对应的高缩短到原来的1/3,现在面积是原来的多少倍?"}, {"role": "assistant", "content": "原面积=1/2×底×高。新面积=1/2×(3底)×(高/3)=1/2×底×高。\n\n{{ans:1}}"}]} {"messages": [{"role": "system", "content": "你是一个数学题目计算助手"}, {"role": "user", "content": "#输出#\n输出要符合经过推理后将最终答案放在 {{ans:xxx}} 内。\n#数学题#\n夏令营从7月15日开始到8月3日结束,期间有多少天?"}, {"role": "assistant", "content": "7月15日-31日:17天(含首日)\n8月1日-3日:3天\n总计17+3=20天\n\n{{ans:20}}"}]} {"messages": [{"role": "system", "content": "你是一个数学题目计算助手"}, {"role": "user", "content": "#输出#\n输出要符合经过推理后将最终答案放在 {{ans:xxx}} 内。\n#数学题#\n打印店用A机打印需6分钟,B机打印需4分钟,C机打印需3分钟。若三台机器同时工作,完成10份文件最少需要多少分钟?"}, {"role": "assistant", "content": "三台机器每分钟分别完成1/6、1/4、1/3份。总效率=1/6+1/4+1/3=3/4份/分钟。时间=10÷(3/4)=13.33分钟,向上取整为14分钟。\n\n{{ans:14}}"}]}
3.1.3 微调参数详解
模型微调大部分参数都可以保持默认,有几个参数需要根据微调数据集情况、微调场景和目标进行合理设定。根据模型微调经验,我们总结以下几个常用参数需要格外细致的设置:
1、CUDA_VISIBLE_DEVICES 设备参数
一般设置为环境变量参数,表示本次微调需要用大的GPU卡有哪几张。比如:
%env CUDA_VISIBLE_DEVICES=0,1
表示使用第1和第2张GPU卡进行微调。本文实操用了1张GPU卡,因此设置为 CUDA_VISIBLE_DEVICES=0。
2、learning_rate 学习率
学习率是训练过程中控制模型权重更新步长的超参数。较小的学习率会使训练更稳定但可能收敛较慢,较大的学习率可能加快收敛但可能导致不稳定。在微调阶段,通常使用较小的学习率(万分之一左右)
比如设置:--learning_rate '0.00005'
3、lora_rank 秩
rank(秩)是低秩矩阵的维度,秩越大,可训练参数越多,模型能力越强,但计算开销也越大;秩越小,可训练参数越少,效率越高,但可能影响性能。
根据经验,r取值4、8、16、32几个值最合适。
(1)当 r<4 的时候,微调几乎没有效果;
(2)当 r>32 的时候,微调需要的GPU算力资源猛增,但是效果不仅没有提高反而下降。
因此,我们建议:
(1)对于小的训练集(1百-10k样本):建议 𝑟≤16(优先r=8),避免模型训练轮次太多,而模型去背训练集而不是学习里面的特征。
(2)对于大的训练集(10k+样本):可以尝试 𝑟=32(优先r=16),可以充分挖掘训练数据集的业务规律。
当然,LoRA秩的选择还与模型的参数量有关。建议参数量不超过16B的模型,r不要超过16;对于32B以上参数量的模型,r可以设置为16或者32。
比如设置:--lora_rank 8
4、num_train_epochs 训练轮数
一个epoch表示整个训练数据集被完整遍历一次。训练多个epoch可以让模型多次学习整个数据集,通常随着epoch增加,模型性能会提升,但也要注意过拟合。根据经验,同一个数据集的训练轮数不要超过15轮。
比如设置:--num_train_epochs 10
5、batch_size 样本批量大小
批量大小(batch size)决定了每次模型权重更新前处理的样本数量。较大的批量大小可以提高计算效率(尤其是GPU并行计算),但需要更多的显存;较小的批量大小则更灵活,但可能增加训练时间。
batch_size决定了显存的消耗程度,必须根据微调数据集的单条数据长度决定。越长的数据,就应该使用越小的批次大小,防止显存溢出错误。
比如设置:--batch_size '16'
6、max_length 输入序列最大长度
在自然语言处理任务中,这个参数用于指定模型输入(如文本)的最大长度(以token为单位)。超过这个长度的文本会被截断,不足的会填充。
这个值需要非常慎重设置。我们需要提前将训练数据集进行遍历,找到训练数据集的平均长度和最大长度,建议max_length不低于最大长度。如果最大长度过大,而平均长度不大(比如最大长度9000,平均长度1000,那么就表示有少数训练数据存在异常情况,需要提出这部分不合理的超长训练数据),建议提出超长训练数据,将最长训练数据控制在平均训练长度的2倍以内。
比如设置:--max_length 1024
7、eval_step
训练多少步就进行评估。这里的“步”是指模型权重更新的次数(即每处理eval_step个batch_size样本,就进行一次模型评估,然后更新一次权重checkpoint)。评估会在验证集上进行,以监控模型在训练过程中的性能变化,便于早停或调整超参数。
比如设置:--eval_step 20。含义:设置每训练20步(steps)进行一次评估。
3.2 微调实操的过程详解
以下是我们利用1167条微调数据集、120条验证数据集的微调操作:
%env CUDA_VISIBLE_DEVICES=0 %env LOG_LEVEL=INFO !swift sft \ --learning_rate '0.00005' \ --lora_rank 8 \ --num_train_epochs 10 \ --dataset './resources/train_1k.jsonl' \ --val_dataset './resources/eval_100.jsonl' \ --batch_size '8' \ --max_length 512 \ --eval_step 20 \ --model_type 'qwen2_5-3b-instruct' \ --model_id_or_path './model_3b'
首先,swift sft默认的微调方法是LoRA,也就是--sft_type lora ,因此不需要指定微调方法。上述参数的具体含义,可以阅读3.1.3章节微调参数详解。
我们接下来通过微调日志详细介绍微调的过程。
3.2.1 微调涉及的层 target_modules
以下是我们做模型微调的实际日志:
[INFO:swift] target_modules: ['q_proj', 'k_proj', 'v_proj'] ...... [INFO:swift] PeftModelForCausalLM( (base_model): LoraModel( (model): Qwen2ForCausalLM( (model): Qwen2Model( (embed_tokens): Embedding(151936, 2048) (layers): ModuleList( (0-35): 36 x Qwen2DecoderLayer( (self_attn): Qwen2SdpaAttention( (q_proj): lora.Linear( (base_layer): Linear(in_features=2048, out_features=2048, bias=True) (lora_dropout): ModuleDict( (default): Dropout(p=0.05, inplace=False) ) (lora_A): ModuleDict( (default): Linear(in_features=2048, out_features=8, bias=False) ) (lora_B): ModuleDict( (default): Linear(in_features=8, out_features=2048, bias=False) ) (lora_embedding_A): ParameterDict() (lora_embedding_B): ParameterDict() (lora_magnitude_vector): ModuleDict() ) (k_proj): lora.Linear( (base_layer): Linear(in_features=2048, out_features=256, bias=True) (lora_dropout): ModuleDict( (default): Dropout(p=0.05, inplace=False) ) (lora_A): ModuleDict( (default): Linear(in_features=2048, out_features=8, bias=False) ) (lora_B): ModuleDict( (default): Linear(in_features=8, out_features=256, bias=False) ) (lora_embedding_A): ParameterDict() (lora_embedding_B): ParameterDict() (lora_magnitude_vector): ModuleDict() ) (v_proj): lora.Linear( (base_layer): Linear(in_features=2048, out_features=256, bias=True) (lora_dropout): ModuleDict( (default): Dropout(p=0.05, inplace=False) ) (lora_A): ModuleDict( (default): Linear(in_features=2048, out_features=8, bias=False) ) (lora_B): ModuleDict( (default): Linear(in_features=8, out_features=256, bias=False) ) (lora_embedding_A): ParameterDict() (lora_embedding_B): ParameterDict() (lora_magnitude_vector): ModuleDict() ) (o_proj): Linear(in_features=2048, out_features=2048, bias=False) (rotary_emb): Qwen2RotaryEmbedding() ) (mlp): Qwen2MLP( (gate_proj): Linear(in_features=2048, out_features=11008, bias=False) (up_proj): Linear(in_features=2048, out_features=11008, bias=False) (down_proj): Linear(in_features=11008, out_features=2048, bias=False) (act_fn): SiLU() ) (input_layernorm): Qwen2RMSNorm((2048,), eps=1e-06) (post_attention_layernorm): Qwen2RMSNorm((2048,), eps=1e-06) ) ) (norm): Qwen2RMSNorm((2048,), eps=1e-06) (rotary_emb): Qwen2RotaryEmbedding() ) (lm_head): Linear(in_features=2048, out_features=151936, bias=False) ) ) )
可以看上,上述LoRA微调涉及到三个层:'q_proj', 'k_proj', 'v_proj'。这里的 q_proj 即为注意力机制中的Wq,'k_proj'即为注意力机制中的 Wk,v_proj 即为注意力机制中的 Wv。表示会同时训练的注意力机制中的Wq、Wk 、Wv层。
3.2.2 微调的参数量到底是多少?
以下是我们做模型微调的实际日志:
[INFO:swift] PeftModelForCausalLM: 3088.4454M Params (2.5068M Trainable [0.0812%]), 0.0024M Buffers.
可以看出,qwen2_5-3b-instruct模型的总参数量是 3088.4454M(也就是30.884454亿个)。本次微调涉及的参数量是:2.5068M,也就是约 250万个参数。训练参数占据模型总参数量的 0.0812%。
可以很直观地看到:使用LoRA微调方法,仅训练基模的0.0812%参数量,非常适合特定场景的高效微调。
3.2.3 训练集和验证集如何区分
以下是我们做模型微调的实际日志:
[INFO:swift] train_dataset: Dataset({ features: ['query', 'response'], num_rows: 1167 }) [INFO:swift] val_dataset: Dataset({ features: ['query', 'response'], num_rows: 120 })
可以看出,通过设置参数:
--dataset './resources/train_1k.jsonl' \
--val_dataset './resources/eval_100.jsonl' \
可以让模型区分微调的训练集和验证集,保证验证集的规模不低于训练集的10%。
如果不使用单独的验证集,系统会自动划分训练集和验证集,比如以下是微调的日志:
Map: 100%| 1167/1167 [00:00<00:00, 14920.80 examples/s] [INFO:swift] train_dataset: Dataset({ features: ['query', 'response'], num_rows: 1156 }) [INFO:swift] val_dataset: Dataset({ features: ['query', 'response'], num_rows: 11 })
整个数据集有1167条数据,但是划分的训练数据集有1156条,验证数据集只有11条数据,相当于模型是按照1%的比例划分验证数据集,这是非常不合理的比例。
3.2.4 训练数据集出现数据丢失
当训练数据集和测试数据集的某条数据长度大于 max_length 参数,这条数据将会被丢弃。
比如 --max_length=512时,出现以下的日志:
Map: 61%| 701/1156 [00:00<00:00, 1736.67it/s][WARNING:swift] Current length of row(593) is larger than the max_length(512), deleted.
但是,max_length参数不能设置太大,否则会造成显存溢出错误。
3.2.5 训练需要的显存大小评估
按照2.5章节介绍的“模型微调需要的显存大小评估”方法,本次微调需要的显存大约20GB,因此采用24GB显存的GPU卡即可。
3.2.6 怎么查看最佳微调结果best_model_checkpoint
下面的日志很清晰展示了模型微调的 best_model_checkpoint 和 last_model_checkpoint:
[INFO:swift] last_model_checkpoint: /mnt/workspace/trainning/output/qwen2_5-3b-instruct/v8-20250905-102700/checkpoint-730 [INFO:swift] best_model_checkpoint: /mnt/workspace/trainning/output/qwen2_5-3b-instruct/v8-20250905-102700/checkpoint-700 [INFO:swift] images_dir: /mnt/workspace/trainning/output/qwen2_5-3b-instruct/v8-20250905-102700/images
可以看出本次微调的best_model_checkpoint最好效果在 checkpoint-700,也就是第700 steps。模型微调总共730 steps,因此 last_model_checkpoint 是 checkpoint-730。
3.2.7 关于训练 max_steps = 730 步是怎么来的?
在训练过程中,max_steps(总训练步数)的计算与训练轮数(num_train_epochs)、据集大小(数据行数)、批次大小(batch_size)和梯度累积步数(gradient_accumulation_steps) 直接相关。以下是详细推导过程公式:
max_steps = num_train_epochs * [total_samples/(batch_size*gradient_accumulation_steps)]
total_samples: 训练集总样本数(从 train_1k.jsonl 中读取,1167条数据)
batch_size: 每批次处理的样本数(本次微调设置为 8)
num_train_epochs: 训练轮数(本次微调设置为 10)
gradient_accumulation_steps:本次微调默认值是 2。
[]:向上取整函数(若样本数无法被 batch_size 整除,最后一个批次保留)。
按照上述公式,得到:
max_steps = 10 * [1167/(8*2)] = 10 * 73 = 730
每步训练 8 条数据,每2次梯度累计步数,相当于8*2=16条数据为1步。1167条数据训练:[1167/16]=73步。然后训练10轮,总共训练 10*73=730步。
四、LoRA+ VS LoRA 模型微调对比
4.1 LoRA+ 介绍
LoRA+是LoRA的增强版模型微调技术。在论文《LoRA+: Efficient Low Rank Adaptation of Large Models》(https://papers.cool/arxiv/2402.12354)提出在微调时让B矩阵的学习率比A矩阵大几倍,微调效果会好2%左右。LoRA+的作者从理论角度断定最优解必然是右矩阵的学习率大于左矩阵的学习率。简而言之,LoRA+称得上是理论指导训练并且在实践中确实有效的经典例子,值得仔细学习一番。
这里我直接截图苏神对LoRA+的解释(https://spaces.ac.cn/archives/10001),非常精彩:
在实际工程项目中,LoRA微调是最常用的微调技术。我们在本章节使用LoRA+ 对上述模型微调再进行实操,并与LoRA 模型微调效果做对比,看看是否像论文描述的那样有2%左右的效果提升。
使用LoRA+ 微调技术,需要增加一个参数:--lora_lr_ratio ,该参数设置了权重B与权重A的比例。这个参数值可以建议设置为4 -- 15。
本文使用的LoRA+ 微调命令如下:
%env CUDA_VISIBLE_DEVICES=0 %env LOG_LEVEL=INFO !swift sft \ --lora_lr_ratio 5 \ --learning_rate '0.00005' \ --lora_rank 8 \ --num_train_epochs 10 \ --dataset './resources/train_1k.jsonl' \ --val_dataset './resources/eval_100.jsonl' \ --batch_size '8' \ --max_length 512 \ --eval_step 20 \ --model_type 'qwen2_5-3b-instruct' \ --model_id_or_path './model_3b'
4.2 LoRA+ VS LoRA 效果
我们主要对比四个指标:train_acc训练准确率、train_loss训练损失、eval_acc验证准确率、eval_loss验证损失,这四个指标的含义:
- train_acc:训练准确率。模型在训练集上预测正确的样本比例,衡量模型在训练数据上的预测能力,通常随着训练进行逐渐提升。计算方法:
train_acc = (正确预测的样本数) / 训练集当前批次样本总数
- train_loss:训练损失。模型在训练集上的预测误差量化值,反映模型在训练数据上的拟合程度,理想情况下应随训练逐步下降。 train_loss是一个方差值,不是百分比。
- eval_acc:验证准确率。模型在独立验证集上的预测正确率,评估模型在未见过的数据上的泛化能力,若出现异常高值需检查数据集是否泄露。计算方法:
eval_acc = (验证集正确数) / 验证集当前批次样本总数
- eval_loss:验证损失。模型在未见数据上的预测误差,反映模型在验证数据上的拟合效果,通常与验证准确率呈负相关。eval_loss是一个方差值,不是百分比。
我们将LoRA 和 LoRA+ 两个训练方法训练之后,得到的上述四个指标展示如下:
4.2.1 train_acc 训练准确率对比
LoRA:
LoRA+:
可以看出,LoRA+ 的训练准确率最高接近90%,LoRA最高在86%左右,提升4%的效果。
4.2.2 eval_acc 验证准确率对比
LoRA:
LoRA+:
可以看出,LoRA+ 的验证准确率最高接近89%,LoRA最高在85%左右,提升4%的效果。
4.2.3 train_loss 训练损失对比
LoRA:
LoRA+:
可以看出,LoRA+ 的训练损失降低 0.8以内,LoRA的训练损失在 1.0以内,提升10%的效果。
4.2.4 eval_loss 验证损失对比
LoRA:
LoRA+:
可以看出,LoRA+ 的验证损失降低 0.5以内,LoRA的训练损失在 0.4以内,提升10%的效果。
4.2.5 效果对比总结
从上述四个指标对比,LoRA+ 相较于 LoRA 确实有至少2%的效果提升。
4.3 进一步LoRA+ 调优
接下来,我们还可以调整--lora_lr_ratio ,该参数设置了权重B与权重A的比例,从4提升到20。
4.3.1 lora_lr_ratio参数最佳取值
我们发现当 lora_lr_ratio 设置为10的时候,train_acc训练准确率、train_loss训练损失、eval_acc验证准确率、eval_loss验证损失这四个指标最佳。
4.3.2 lora_lr_ratio参数过大可能造成过拟合
当 lora_lr_ratio从11提升到20,train_acc和eval_acc出现比较大的波动。当lora_lr_ratio=20的时候,甚至出现了过拟合的情况:
4.3.3 使用LoRA+ 微调的优化建议
开发人员在用LoRA+ 微调方法的时候,务必通过多次调整lora_lr_ratio参数,找到效果最佳的参数值。一般建议将lora_lr_ratio参数设置在4--10之间比较合适。
五、数据并行DPP与模型并行MP微调
5.1 什么是并行微调
简单理解就是:利用多张GPU进行模型微调,利用多卡并发加快微调速度。同时,对于超过7B参数的大模型,由于加载参数需要的显存多,单GPU卡往往不能承载模型参数,此时也需要利用多卡并行微调。
5.2 并行微调分类
并行微调主要分为两类:
5.2.1 数据并行DDP(Data Parallelism)
每张GPU加载完整的模型副本,但是将训练数据集分成多个子集,每张GPU卡处理不同的数据集子集。 划重点:数据并行方式下,每张GPU卡还是需要加载基模完整的参数,只是将训练数据集并发处理。
以两张GPU卡数据并行微调举例:
5.2.2 模型并行MP(Model Parallelism)
将模型横向切分到不同GPU,每张GPU卡存储基模参数的一部分。同时,将训练数据集分成多个子集,每张GPU卡处理不同的数据集子集。
划重点:模型并行方式下,基模参数被划分到不同的GPU卡,同时将训练数据集并发处理。
以两张GPU卡模型并行微调举例:
5.3 数据并行微调DPP实践
适用场景:
1、基模参数量不大:对于7B以下的模型,由于基模消耗GPU卡显存在14GB--15GB左右,因此单GPU卡可以承载。
2、训练数据集庞大:如果微调训练的数据集很大,比如几十万条/百万条/千万条数据,使用单GPU卡微调耗时太久。此时,可以使用多张GPU卡进行数据并行DDP微调,加快训练速度。
我们示例使用两张GPU卡实操的数据并行DDP微调示例:
%env CUDA_VISIBLE_DEVICES=0,1 %env LOG_LEVEL=INFO %env NCCL_DEBUG=INFO %env NCCL_PROTO=simple %env NPROC_PER_NODE=2 !swift sft \ --lora_lr_ratio 5 \ --learning_rate '0.00005' \ --lora_rank 8 \ --num_train_epochs 10 \ --dataset './resources/train_1k.jsonl' \ --val_dataset './resources/eval_100.jsonl' \ --batch_size '8' \ --gradient_accumulation_steps 2 \ --max_length 512 \ --eval_step 20 \ --model_type 'qwen2_5-3b-instruct' \ --model_id_or_path './model_3b'
5.4 模型并行微调MP实践
适用场景
1、基模参数量大:对于14B以上的模型(特别是32B以上模型),由于基模消耗GPU卡显存很高,单GPU卡很难承载完整的基模参数+数据集显存耗用量,因此必须使用模型并行MP微调。
2、训练数据集庞大:如果微调训练的数据集很大,比如几十万条/百万条/千万条数据,可以使用多张GPU卡进行模型并行MP微调,加快训练速度。
3、全参微调:对于全参微调,需要加载模型的全部参数,比如qwen2.5-7B模型需4到6张 A100 80GB的GPU卡。此时必须使用模型并行MP训练。
实践中,模型并行MP微调一般使用deepspeed ZeRO机制。ZeRO 提供了三阶段的优化方法(1,2,3),分别为优化器状态分割、梯度分割、参数分割:
(1)ZeRO-1:分割Optimizer States,加入优化器分割;
(2)ZeRO-2:分割Optimizer States与Gradients,加入梯度分割;
(3)ZeRO-3:分割Optimizer States、Gradients与Parameters,加入参数分割;
我们示例使用ZeRO-3机制,使用两张GPU卡实操的模型并行MP微调示例:
%env CUDA_VISIBLE_DEVICES=0,1 %env LOG_LEVEL=INFO %env NCCL_DEBUG=INFO %env NCCL_PROTO=simple %env NPROC_PER_NODE=2 !swift sft \ --deepspeed default-zero3 \ --lora_lr_ratio 5 \ --learning_rate '0.00005' \ --lora_rank 8 \ --num_train_epochs 10 \ --dataset './resources/train_1k.jsonl' \ --val_dataset './resources/eval_100.jsonl' \ --batch_size '8' \ --gradient_accumulation_steps 1 \ --max_length 512 \ --eval_step 20 \ --model_type 'qwen2_5-3b-instruct' \ --model_id_or_path './model_3b'
日志显示:Using deepspeed,表示本次微调正确使用了 deepspeed机制:
[INFO:swift] Using deepspeed: {'fp16': {'enabled': 'auto', 'loss_scale': 0, 'loss_scale_window': 1000, 'initial_scale_power': 16, 'hysteresis': 2, 'min_loss_scale': 1}, 'bf16': {'enabled': 'auto'}, 'optimizer': {'type': 'AdamW', 'params': {'lr': 'auto', 'betas': 'auto', 'eps': 'auto', 'weight_decay': 'auto'}}, 'scheduler': {'type': 'WarmupCosineLR', 'params': {'total_num_steps': 'auto', 'warmup_num_steps': 'auto'}}, 'zero_optimization': {'stage': 3, 'offload_optimizer': {'device': 'none', 'pin_memory': True}, 'offload_param': {'device': 'cpu', 'pin_memory': True}, 'overlap_comm': True, 'contiguous_gradients': True, 'sub_group_size': 1000000000.0, 'reduce_bucket_size': 'auto', 'stage3_prefetch_bucket_size': 'auto', 'stage3_param_persistence_threshold': 'auto', 'stage3_max_live_parameters': 1000000000.0, 'stage3_max_reuse_distance': 1000000000.0, 'stage3_gather_16bit_weights_on_model_save': True}, 'gradient_accumulation_steps': 'auto', 'gradient_clipping': 'auto', 'steps_per_print': 2000, 'train_batch_size': 'auto', 'train_micro_batch_size_per_gpu': 'auto', 'wall_clock_breakdown': False}