想给一个 7B 模型做业务微调,第一件事不是打开控制台选卡,是确认我到底要微调什么。这件事我拖了两周才想清楚,导致中间租了三次不该租的机器。
先说结论,因为大部分人卡在这一步:
一张 24GB 显存的卡,量化到 4bit,跑 7B 的 LoRA,是可行的最小配置。 下面是我实际走通的顺序,以及每一步的判断依据。
先算显存,而不是先挑卡
LoRA 的显存账跟全参微调不是一个数量级,值得先讲明白。
全参微调要存权重、梯度、优化器状态,7B 大约 112GB 起。LoRA 只训练注入的低秩矩阵,原始权重冻结:
- 权重:4bit 量化后约 3.5GB 到 4GB
- LoRA 参数:rank=16 时百万级,梯度与优化器状态可以忽略
- 激活值:这是真正的大头,随序列长度和 batch 涨
激活值为什么是大头?因为每层的中间结果都得留着等反向传播。序列长度从 512 提到 2048,激活值大致是四倍关系。梯度检查点(gradient checkpointing)能砍掉大半,代价是慢 20% 到 30%。
我的实际数字:24GB 卡,序列长度 1024,per-device batch 1,梯度累积 8,跑 QLoRA 占 21GB 左右。换成 48GB 卡只是宽裕一点,训练速度差别很小——瓶颈早就不在显存而在算力了。
这里有个反直觉的结论:卡更大不等于训练更快。 我第一次租了 80GB 的卡,以为能开更大 batch 提速,结果数据只有两千条,batch 开大反而梯度噪声大、效果变差,每个 step 还被拉长。多出来的显存全成了闲置。
选卡:只看三件事
- 显存够不够 24GB。低于这个数,要么换更小的基座(1.5B / 3B),要么把序列长度压到 512。
- 卡上有没有一张空闲的、驱动版本对的。同一台物理机上如果已经有别的任务占着,选"整卡独占"也没用。
- 磁盘:权重 + 数据集 + checkpoint,一张 7B 的 4bit 量化权重不到 4GB,但加上原始权重要下载(十几 GB),磁盘留够 100GB 以上。
我这次用的是一张 24GB 的 A 系列卡,跑下来没有遇到带宽瓶颈,所以没有动力上更贵的卡。
数据:这一步最容易白干
我的第一个版本,数据是直接从业务库里导出的问答对,转成 JSON 之后就开始训。训完一测,格式全乱。
问题出在对话模板上。自回归模型的微调不是"给一对问答"就行的,输入得按基座模型自己的 chat 模板拼好,带上正确的角色标记和结束符。模板错了,模型学到的就是"另一种格式的问答",而评测脚本用的是另一种格式,看起来像模型没学会,其实是格式没对齐。
改对模板之后,同样的数据、同样步数,效果差别巨大。
数据量上,两千条结构化样本就够了。LoRA 的样本效率很高,数据质量比数量重要得多——清掉重复样本、把超长的截断到 1024 token 之后,效果比原始数据还略好。
我的数据文件长这样:
{"messages": [
{"role": "system", "content": "你是一个技术支持助手"},
{"role": "user", "content": "客户说连不上数据库"},
{"role": "assistant", "content": "请确认数据库服务是否已启动..."}
]}
用 messages 而不是 instruction/input/output 这种自定义字段,是因为主流微调框架(PEFT / TRL / LLaMA-Factory)都原生认这个结构,少一层转换就少一层出错机会。
跑通顺序:先冒烟,再正式
我第一次直接上全量数据、全量步数跑,中途才发现显存不够,白等了两个小时。
现在的顺序固定:
第一步,冒烟测试。 用 20 条样本、跑 5 步、max_length 设 256。目的只有一个:确认环境、模板、显存不炸,几分钟就结束。
第二步,看 loss 曲线。 冒烟通过后跑 200 步,看 loss 是不是在下降。如果 loss 一开始就是平的甚至上升,不要往下走,八成是模板或数据格式问题,不是超参问题。
第三步,全量训练。
我的 LoRA 配置长这样:
# lora.yaml
model_name_or_path: <基座模型路径>
dataset_path: ./data/train.jsonl
output_dir: ./out
# 量化:4bit 是 24GB 卡能跑 7B 的前提
load_in_4bit: true
bnb_4bit_quant_type: nf4
bnb_4bit_compute_dtype: bfloat16
bnb_4bit_use_double_quant: true
# LoRA 本身
lora_r: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target_modules: q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj
# 训练
per_device_train_batch_size: 1
gradient_accumulation_steps: 8
gradient_checkpointing: true
num_train_epochs: 3
learning_rate: 0.0002
lr_scheduler_type: cosine
warmup_ratio: 0.03
bf16: true
logging_steps: 10
save_steps: 100
save_total_limit: 3
几个参数的取值逻辑:
lora_target_modules选全了七种投影,比只选 q/v 效果好一些,显存多一点但可接受。lora_alpha通常取r的两倍,实际效果上等价于缩放学习率。learning_rate用 2e-4 配 cosine 衰减,在两千条样本这个量级上比较稳。save_total_limit: 3很重要。checkpoint 每个都存一整套 adapter,多了硬盘会满,而且选的时候容易拿错。
三个我真踩过的坑
bf16 不被支持。 老显卡不支持 bf16 时,训练会直接报"数值类型不支持"。我当时的解法是改成 fp16,但 fp16 更容易 loss 变成 NaN。后来加了一句判断:
import torch
if not torch.cuda.is_bf16_supported():
# 老卡退回 fp16,并把 loss scale 调小
pass
显存不够的报错出现在第一步而不是第几步。 我以为是跑久了碎片化,其实不是——是 save_steps 触发保存时,模型和优化器状态要额外占一份。把 save_total_limit 调小,或用 save_strategy: "epoch" 就能避开。
训练时别关掉日志。 有一次我为了省终端输出把 logging_steps 设成 100,结果 loss 曲线是空的,训完才发现第 30 步就 NaN 了,白跑一整晚。日志落盘到文件,不占屏幕。
训完之后
adapter 本身很小,几十到几百 MB,存对象存储或拉回本地都行。但基座模型必须在评测环境里也存在,只拷 adapter 跑不起来——这是每次给同事部署都要解释一遍的事。
我现在的做法是把基座模型、adapter、chat 模板三者打成一个目录,模板一起带上,避免版本错位:
# 合并后导出,推理端只认一个目录
merge_lora.py \
--base <基座模型路径> \
--adapter ./out \
--output ./merged
# 别忘了把模板文件一起拷过去
cp chat_template.json ./merged/
然后在 24GB 卡上起推理服务验证:加载 merged 目录,跑几条业务里的真实问法,看是不是按格式回答。
收尾
跑通一次完整流程,机器时长大概三小时,真正训练不到四十分钟,其余都在下载、装环境、排查格式。工程时间比算力时间贵得多,这句话在微调上体现得特别明显。
最后照例提醒:卡型、显存和上面那些估算的关系是结构性的,可以放心照着估;但具体套餐给什么规格、单小时什么价位,各家调整得比模型版本还快,以官方页面为准。