ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LLaMA-Factory 指令增量微调实战:显存预算、数据配比与效果评估

LLaMA-Factory 指令增量微调实战:显存预算、数据配比与效果评估

指令微调这件事,真正上手做过的人都知道,难的不是"跑起来",而是"跑对"。LLaMA-Factory 这套工具把分布式训练、LoRA、量化、模板适配这些脏活都包了,一条命令就能把 8B 模型训起来,但它并不会替你想清楚三件事:这次增量微调到底要解决什么、数据该配多少、训完怎么证明它真的变好了。这篇内容就围绕"从零开始的 LLaMA-Factory 指令增量微调"这条线,把环境搭建、显存预算、数据构造、参数取值、训练观察、合并导出、效果评估这一整套流程拆开讲。所谓"增量",指的是在一个已经具备通用指令跟随能力的基座上,用少量领域数据或格式数据做二次对齐,而不是从预训练权重开始教模型说话。适合两类人看:一类是刚接触微调、手里有一张 24G 或 48G 卡想跑通全流程的开发者;另一类是已经跑通过 demo、但发现模型训完变得"只会说那几句话"、通用能力掉得厉害,想搞清楚原因的人。下面所有配置和数字都来自实际踩坑后的经验值,可以直接抄,但参数一定要按你自己的模型和显存重算一遍。

1. 先把需求想透:增量微调到底在解决什么问题

1.1 全量微调、LoRA 与增量微调的关系

很多人一开始会把这三个概念混在一起。全量微调和 LoRA 说的是"训练多少参数",增量微调说的是"在什么基础上、用什么数据继续训"。它们是两个维度,不冲突。全量微调是把模型所有权重都放开更新,7B 模型光优化器状态就能吃掉一百多 G 显存,个人开发者基本不用考虑;LoRA 是冻结主干、只在注意力层和 MLP 层旁挂一小对低秩矩阵,可训练参数通常只占千分之五到百分之一,显存门槛直接降一个数量级。

增量微调通常配 LoRA 做,因为它的目标本身就不大:让模型学会一种新的输出格式、一套领域术语、一种固定话术,或者补上基座在某个细分任务上的短板。这种目标用几千条高质量样本、训两三个 epoch 就够,不需要动主干。我见过有人拿几万条数据全量微调一个 7B 模型去学 JSON 输出格式,最后格式是学会了,但原来的多轮对话能力明显退化,这就是典型的用大炮打蚊子,顺带把墙打穿了。

选 LoRA 还有一个隐性好处:适配器文件只有几十兆,一个基座可以挂十几个不同任务的适配器,推理时按需切换。做多业务线的时候这个特性非常实用,不用为每个任务存一份完整模型。

1.2 什么场景该微调,什么场景不该

这是最该先回答的问题,因为相当一部分需求根本不需要微调。我的判断顺序是这样的:先试提示词,再试少样本示例,再试检索增强,最后才轮到微调。如果提示词能把任务做到九成准确率,微调带来的收益很难覆盖它引入的维护成本。

真正该上微调的场景,我归纳成四类。第一类是输出格式的强约束,比如必须稳定产出可解析的 JSON、必须带固定的字段顺序、必须遵循特定的分隔符,这类任务靠提示词约束的稳定性通常只有八成多,微调能推到九成八以上。第二类是领域术语和表达习惯,基座模型面对大量行业缩写、内部黑话时会胡编,微调相当于给它补一本词典。第三类是延迟和成本敏感,微调可以把几百字的长提示词压缩成几十字,每次调用省下的 token 在量大时很可观。第四类是私有化部署,模型必须离线跑在自己的机器上,没有条件调用外部服务。

反过来,不该微调的场景也很明确:知识类问答(微调记不住事实,还容易编造)、频繁变动的信息(今天训完明天就过期)、需要精确计算的逻辑推理(微调改善有限)。这些交给检索或工具调用更靠谱。

1.3 灾难性遗忘与数据配比的基本盘

增量微调最容易翻车的地方不是训不起来,而是训完之后模型"变窄"了。你喂给它两千条客服话术,它学会了话术,但你问它一个通用的常识问题,它开始用客服腔回答,或者干脆答非所问。这就是灾难性遗忘,本质是模型在新数据分布上过拟合,把原来权重里承载的通用能力覆盖掉了。

对抗它的手段有三层,从便宜到贵依次是:控制学习率和 epoch、混入通用回放数据、用 KL 正则约束输出分布。第二层最实用。我的经验配比是增量任务数据占八成到九成,通用指令数据占一成到两成,通用数据不用多,一两百条覆盖各类任务类型(问答、摘要、改写、推理、闲聊)就够,它的作用是给模型一个"别忘了其他活也要干"的锚点。

还有一个细节:回放数据要和你基座原本的训练分布接近,不要随便找一批风格完全不同的数据塞进去,否则会引入新的分布偏移。如果实在找不到合适的通用数据,宁可把学习率降一半、epoch 减到 2,也别硬凑。

2. 环境与数据准备:从零搭起一套能跑的微调环境

2.1 显存预算怎么估,别上来就 OOM

在动手之前先把显存账算清楚,能省掉大量试错。粗略公式是这样的:

  • 全量微调:每参数约 16 字节(bf16 权重 2 + bf16 梯度 2 + fp32 主权重 4 + Adam 一阶 4 + 二阶 4)。7B 模型约 112 GB,单卡无解。
  • LoRA 微调:冻结权重每参数 2 字节,7B 约 14 GB;可训练参数占比极小,梯度和优化器状态可以忽略;剩下的大头是激活值。
  • QLoRA:权重按 4bit 存,每参数约 0.5 字节,7B 约 3.5 GB,代价是训练速度慢两到三成。

激活值这一块跟per_device_train_batch_size × cutoff_len × hidden_size × 层数成正比,开梯度检查点后能压到原来的三分之一左右。按实际跑下来的经验:8B 模型、cutoff_len=2048、batch_size=1、开梯度检查点和 FlashAttention,24 GB 卡能跑但余量很紧,32 GB 或 48 GB 会从容很多;13B 模型同样的配置要 28 GB 起步,24 GB 卡必须上 4bit 量化。

我一般会留 15% 的显存余量,因为训练过程中激活值峰值不是恒定的,长样本偶尔会顶上去。刚好卡满的配置,跑到第几百步突然 OOM,排查起来非常浪费时间。

2.2 安装与依赖版本避坑

LLaMA-Factory 的安装本身不复杂,麻烦的是依赖版本冲突。稳妥的做法是建独立虚拟环境,先装匹配 CUDA 版本的 PyTorch,再装框架本体:

conda create -n lf python=3.10 -y conda activate lf # 先装 torch,版本必须和驱动对应的 CUDA 匹配 pip install torch==2.4.0 torchvision==0.19.0 --index-url https://download.pytorch.org/whl/cu121 # 再装框架 git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e ".[torch,metrics]"

依赖下载慢的话,把 pip 源换成国内镜像,这一步能省掉一半等待时间。装完先跑一句llamafactory-cli version确认命令行可用,再去检查transformers、peft、accelerate的版本是否被自动降级——这三个包的版本冲突是最常见的启动失败原因。

注意:不要在有其他项目的环境里直接装,peft和transformers的版本要求很敏感,装串了会导致别的项目也起不来。虚拟环境隔离是硬要求。

装完还需要确认 FlashAttention 是否可用。有的话训练速度和显存都能改善,没有也不影响跑通,把配置里的flash_attn去掉就行,会自动回退到普通注意力实现。

2.3 指令数据的格式、清洗与注册

LLaMA-Factory 支持两种主流数据格式。一种是三字段的指令格式,包含instruction、input、output,适合单轮任务;另一种是对话格式,用conversations数组存多轮,每条带from和value。选哪种取决于你的真实使用场景:线上是多轮对话就训多轮,别拿单轮数据训完去跑多轮,模板对不上会出现奇怪的格式泄漏。

数据质量的重要性远大于数量。我清洗数据一般走五步:

  1. 去重:完全重复的直接删,近似重复用文本相似度筛,阈值我一般设在 0.85 左右,太高漏筛、太低误删。
  2. 去空与去噪:output为空、全空白、只有标点的一律丢弃;输入里带乱码、敏感信息、内部链接的也要清掉。
  3. 长度过滤:超过cutoff_len的样本要么截断输入、要么直接丢,别指望模型处理超长内容,截断位置不当会把关键信息切掉。
  4. 格式校验:检查字段名拼写、JSON 是否合法、conversations的from角色是否规范(只能是固定的几个角色值)。
  5. 抽样人工看:随机抽三十条逐条读,这一步最不能省,很多问题只有人眼能看出来。

清洗完之后在数据目录下建一个dataset_info.json注册数据集:

{ "my_instruct_v1": { "file_name": "my_instruct_v1.json", "columns": { "prompt": "instruction", "query": "input", "response": "output" } } }

file_name要和你实际的文件名一致,columns里的映射关系决定了框架怎么读字段。这一步配错是最常见的报错来源,症状通常是训练启动后提示找不到某个 key,或者 loss 一直是零。

3. 配置与实操:一次完整的增量微调流程

3.1 基座模型选择和模板匹配

选基座看三件事:语言能力、上下文长度、有没有现成的对话模板适配。做中文任务优先选中文语料占比高的模型,英文基座硬训中文效果会打折扣,而且容易在中文标点上出错。

更关键的是template参数。这个参数决定了框架怎么把对话拼成模型认识的格式,包括角色标记、分隔符、结束符。基座自带哪种模板,就去框架的模板列表里找对应的名字填进去。填错的后果很隐蔽:训练能跑、loss 也降,但推理时模型的输出会带着奇怪的标记符,或者干脆不停止生成。我见过有人把模板填成通用默认值,训出来的模型每句话结尾都多一个角色标记,查了两天才发现是这里的问题。

3.2 关键参数怎么取值,这里给出计算过程

先看一份可以直接用的配置:

### model model_name_or_path: /data/models/base-8b trust_remote_code: true quantization_bit: 4 # 显存够就删掉这行 ### method stage: sft do_train: true finetuning_type: lora lora_rank: 16 lora_alpha: 32 lora_dropout: 0.05 lora_target: all ### dataset dataset: my_instruct_v1 template: default cutoff_len: 2048 max_samples: 100000 overwrite_cache: true preprocessing_num_workers: 8 ### output output_dir: saves/base-8b/lora/instruct-v1 logging_steps: 10 save_steps: 200 plot_loss: true overwrite_output_dir: true ### train per_device_train_batch_size: 1 gradient_accumulation_steps: 16 learning_rate: 1.0e-4 num_train_epochs: 3.0 lr_scheduler_type: cosine warmup_ratio: 0.1 bf16: true gradient_checkpointing: true flash_attn: fa2

逐个说取值理由。lora_rank和lora_alpha是一对,alpha一般取rank的两倍,这个比例能让适配器的更新幅度保持在合理区间。rank 取 8 适合只学简单格式,取 16 是通用档,取 32 以上适合数据量大、任务复杂的场景,但过大的 rank 会让可训练参数暴涨,24 GB 卡上容易 OOM,收益也不明显。我默认用 16,只在数据超过五千条时才试 32。

lora_target: all表示给所有线性层都挂适配器,效果比只挂注意力层好,代价是参数多三成左右。显存紧张时可以退化成只挂注意力的四个投影层。

learning_rate是 LoRA 微调里最敏感的参数。全量微调常用 1e-5 到 2e-5,LoRA 因为只更新小矩阵,学习率要高一个量级,1e-4 到 2e-4 是常用区间。数据量少、任务和基座能力接近,就往 5e-5 靠;数据量大、任务跨度大,用 2e-4。我一般从 1e-4 起步。

gradient_accumulation_steps要反推。有效批大小等于per_device_batch × 累积步数 × 卡数,指令微调的有效批大小在 32 到 64 之间比较稳。单卡、单样本一批的情况下,累积 16 得到有效批 16,累积 32 得到 32。批太小梯度噪声大、loss 抖;批太大显存吃不消且收敛慢。

num_train_epochs我给 3.0,但这是上限。实际会看 loss 曲线提前停。数据量在两千条以内、训三轮左右通常刚好;如果数据里有大量重复模式,两轮就够,多训一定过拟合。

cutoff_len按数据实际长度分布定。先统计一下样本长度的 95 分位数,取略大于它的值。盲目设 4096 会让显存翻倍但大部分样本用不到。warmup_ratio取 0.1 是常规做法,前 10% 步数线性升温,避免一上来就用大学习率把适配器打坏。

3.3 启动训练与日志观察

配置写好之后一条命令启动:

llamafactory-cli train configs/instruct_v1_lora.yaml

训练开始后第一件事是核对打印出来的信息:可训练参数占比是否在千分之几的量级、样本数是否和你预期一致、模板名对不对。可训练参数占比如果显示成百分之几十,说明finetuning_type没生效,变成全量训练了,赶紧停。

loss 曲线的观察有几点经验。前五十步波动大是正常的,因为 warmup 阶段学习率在爬升;进入稳定期后应该呈缓慢下降趋势,从 2.0 附近降到 0.8 到 1.2 之间比较健康。如果 loss 卡在某个值不动,先查数据格式,再查学习率是不是太小。如果 loss 降得飞快、两三百步就到 0.2 以下,基本可以判定过拟合,要么数据重复度高,要么任务太简单,这时候要回去看grad_norm。

grad_norm这个指标很多人不看,但它很有用。正常训练时它在一个区间内小幅波动,如果突然飙到几十甚至上百,说明遇到了脏样本或者学习率过大,模型参数被推飞了。这时先别急着调参,去把那个 step 对应的数据捞出来看看,我遇到过几次都是样本里混进了超长乱码。

3.4 合并导出与推理验证

LoRA 适配器是独立文件,推理时可以直接加载,也可以合并进基座得到一个完整模型。生产环境我倾向合并,少一层依赖,部署简单:

llamafactory-cli export \ --model_name_or_path /data/models/base-8b \ --adapter_name_or_path saves/base-8b/lora/instruct-v1/checkpoint-600 \ --template default \ --finetuning_type lora \ --export_dir /data/models/base-8b-instruct-v1 \ --export_size 4

export_size控制分片大小,单位 GB,设 4 能兼容大多数加载场景。合并完用命令行直接对话验证:

llamafactory-cli chat \ --model_name_or_path /data/models/base-8b-instruct-v1 \ --template default

验证要准备三组问题:任务内的、任务边缘的、完全无关的。第三组最能暴露问题,如果模型对完全无关的问题也开始用你的任务话术回答,说明遗忘已经发生,得回去调数据配比。这个检查我每次都会做,五分钟的事,能挡掉后面大量的返工。

4. 常见问题与排查技巧实录

4.1 显存与启动类问题

OOM 是最常见的。排查顺序是:先降cutoff_len,再降per_device_train_batch_size到 1,再开quantization_bit: 4,最后才考虑换卡。梯度检查点一定要开,它用约两成的速度损失换三成的显存节省,非常划算。

有一类 OOM 很迷惑:开始训练时正常,跑到几百步突然炸。这通常是遇到了超长样本,激活值峰值顶上去。解决办法是提前统计长度分布,把极端长的样本剔掉,或者把cutoff_len调小。

另一类启动失败是模板报错,提示某个特殊 token 不存在。这时要确认基座的分词器里到底有哪些特殊 token,模板名要和基座严格对应,别用近似匹配。

4.2 训练效果类问题

模型复读、输出重复同一句话,多半是学习率过大或 epoch 过多,适配器把某几个高频模式学死了。降到 5e-5、epoch 减到 2 再试。

模型答非所问、格式混乱,先检查template和数据格式是否一致。如果训练数据是单轮三字段,推理时却按多轮对话模板拼,模型的输入分布和训练时完全不同,效果必然崩。

模型学会任务但语气变僵、变得机械,这是数据风格太单一导致的。往数据里掺一些表达多样的样本,或者降低增量数据占比,都能缓解。

4.3 常见问题速查表

现象最可能原因优先动作
启动即 OOMcutoff_len 过大或未开梯度检查点降到 1024,确认 gradient_checkpointing 为 true
训练中途 OOM长样本激活峰值剔除长度 99 分位以上样本
loss 不下降学习率过小或数据字段映射错先验数据,再把 lr 提到 2e-4
loss 骤降到 0.2 以下数据重复或任务过简去重,减 epoch 到 2
grad_norm 飙高脏样本或 lr 过大捞出对应样本检查,lr 减半
输出带奇怪标记template 与基座不匹配换成基座自带的模板名
通用能力明显退化无回放数据或 epoch 过多掺 10% 到 20% 通用数据,减 epoch
推理速度变慢明显用了 4bit 量化且未合并合并导出后用 bf16 推理

5. 效果评估与迭代:怎么判断这次微调真的成了

5.1 三套评测集,缺一不可

训练 loss 低不等于模型好用,这是最需要建立的认知。我一般准备三套评测:任务集、格式集、回归集。任务集是从真实业务里留出的 100 到 200 条,训练时绝对不能碰,用来算准确率;格式集专门测结构化输出的可解析率,用脚本跑一遍 JSON 解析,通过率一目了然;回归集是五十条左右的通用问题,覆盖问答、算术、改写、闲聊,用来监控遗忘。

评测方式能自动化的尽量自动化。格式类的直接用规则匹配,任务类里有明确答案的用关键词或正则,开放式的人工抽检三十条。全部靠人看太慢,全部靠机器又容易漏掉语义问题,两者结合最实际。

5.2 迭代节奏与版本绑定

一次成功的微调往往要跑三到五轮。第一轮用全量数据跑通流程,看 loss 和基础效果;第二轮调数据配比,重点看回归集;第三轮调学习率和 epoch,找效果拐点;后面几轮基本是数据增补。

这里有个必须养成的习惯:把数据版本、配置文件、checkpoint 三者绑定记录。我见过太多人训出一个效果不错的模型,过两周想复现,发现数据改过了、配置也没存,只能重头再来。最简单的做法是每次实验建一个目录,把 yaml、数据统计、评测结果、checkpoint 路径写进一个文本文件,成本极低,回报极高。

提示:评测集一定要在训练前就切分好并存到训练数据目录之外。我吃过一次亏,切分脚本写错导致评测数据混进训练集,指标漂亮得不像话,上线才发现全是背答案。

迭代过程中还有一个容易被忽略的点:不同 checkpoint 之间的差异可能比你想的大。同一个配置下,第 400 步和第 600 步的模型在格式遵从率上能差五个百分点。所以不要只看最后一个 checkpoint,把中间几个都导出来测一遍,选综合表现最好的那个,这一步花不了多少时间。

我个人在多次实操后的体会是,增量微调这个事的成败,七成在数据,两成在参数,一成在工具。LLaMA-Factory 已经把工具那一成做到了几乎零门槛,真正需要花心思的是把数据洗干净、把评测集留好、把回放比例调对。还有一个小技巧分享:训练前先拿基座模型把评测集跑一遍,把它的输出存下来当基线,训完之后对比着看,哪些是变好的、哪些是变差的,比单看绝对指标清楚得多,也能帮你判断下一轮该往哪个方向调数据。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进