Day10 | SFT 训练实操——用 QLoRA 微调 Qwen3-8B
苦猿的大模型日记 · Day10 · SFT 训练实操——用 QLoRA 微调 Qwen3-8B-帮普通人把AI学进简历系列前言那个 lr2e-5 的凌晨我给你讲个事。上个月某个周三凌晨两点半我对着屏幕上跳动的 loss 数字发呆。那时候我在调一个 QLoRA 微调任务base 模型是 Qwen3-8B。我按照教程默认值配了r64、alpha128、lr2e-5自以为很稳——毕竟参数都是网上抄来的。结果跑了 200 步train loss 从 1.2 缓慢爬到 1.3越训越烂。我当时第一反应是——数据有问题。换了一份数据集还是烂。第二反应——r 不够大。我把r调到 128显存当场爆掉。折腾到凌晨四点我才反应过来一件事——不是 r 的问题是 lr 太小。QLoRA 只训那 0.5% 的参数你给它配一个为全量微调设计的2e-5它当然纹丝不动。我把它提到1e-4loss 立马开始下降。那一刻我突然想明白——Day09 给的是地图Day10 要给的是铲子。地图告诉你 SFT 是什么、LoRA 怎么省显存。但真到铲子下去那一刻决定成败的不是你懂不懂原理而是你会不会看 loss、会不会调那个该死的 lr。今天这篇就带你真刀真枪跑通一次 Qwen3-8B 的 QLoRA 微调——从数据准备到合并权重从超参默认值到 loss 曲线诊断手册Day09 埋的 5 个坑我一个个带你填上。PART 01目标 显存账本先把话说前面——今天的目标让 Qwen3-8B 学会按 Alpaca-zh 的指令风格回答问题。不追求 SOTA不追求商用只追求一件事跑通且能看出来微调生效了。为什么选 Qwen3-8B我对比过几个常见尺寸7BQwen2.5/Llama经典但有点过气中文表现被 Qwen3 压一头8BQwen3当前中文甜点尺寸——比 7B 新一代比 14B 省一半显存14B效果好但 QLoRA 都要 16G 起步普通人玩不起8B 是普通人在 12G 显存上能舒服跑起来的最大尺寸。这就是它的价值。显存账本必须重算Day09 我说过6G 显存微调 7B那是 Qwen2.5 时代的乐观估算。到 Qwen3-8B 这里账要重算项目占用8B 权重4bit 量化~5-6 GB激活值batch1, seq512~1-2 GBLoRA 参数 优化器状态~0.5-1 GB梯度 临时缓冲~0.5 GB合计8-10 GB所以硬件建议是——12G3060 12G / 4070 12G舒服batch 能开到 28G4060 / 4060Ti极限可跑batch1 grad_accum8 凑等效 batch86G 以下建议换 7B 或者直接用云算力别信那些4G 显存微调大模型的标题党——能跑起来和能训出东西是两回事。工具链选择今天主讲底层transformers peft trl。为什么不用 LLaMA-Factory 一把梭因为出 bug 的时候你看不懂。面试官问你QLoRA 的 target_modules 挂了哪些你不能回答我点了一下 yaml 就跑起来了。PART 07 我会附一份 LLaMA-Factory 的等价配置给懒人但主篇幅必须走底层。PART 02数据准备坑①②高发区数据准备是 SFT 里最容易翻车的环节——80% 的训练不收敛都是数据问题不是模型问题。Alpaca-zh 字段说明我们用的是 Alpaca-zh经典中文指令数据集约 4.8 万条。每条三个字段{ instruction: 请把这句话翻译成英文, input: 今天天气真好, output: The weather is nice today. }instruction指令本身input指令的输入可为空output期望的回答看起来很简单对吧坑就在看起来简单上。Qwen3 的 ChatML template你不能直接把这三段拼成一坨丢给模型。Qwen3 用的是ChatML 格式|im_start|user {instruction}{input}|im_end| |im_start|assistant {output}|im_end|那两个|im_start|和|im_end|是特殊 token模型在预训练阶段就认识它们。你少了任何一个模型就听不懂你在说啥。loss mask只算答案那段这是 SFT 最关键的一个细节——前文user 那段不算 loss只算 assistant 那段。为什么因为 SFT 的目标是教模型怎么回答不是教它怎么提问。如果你把 user 段也算进 loss模型会学着模仿你的提问方式反而稀释了回答能力。代码上这通过labels实现user 段的位置填-100PyTorch 的 ignore_indexassistant 段填真实 token id。坑①chat template 拼错最常见的错误是手拼 template——# ❌ 错误做法手拼 text f|im_start|user\n{instruction}{input}|im_end|\n|im_start|assistant\n{output}|im_end|为什么错因为你不知道|im_end|后面到底有没有换行、特殊 token 有没有被正确 tokenize。手拼出来的字符串到 tokenizer 里很可能把|im_start|拆成 5 个普通字符而不是 1 个特殊 token。正确做法——# ✅ 正确做法用 apply_chat_template messages [ {role: user, content: instruction input}, {role: assistant, content: output}, ] text tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptFalse)让 tokenizer 自己拼它知道每个特殊 token 怎么处理。坑②target_modules 漏挂LoRA 不是随便挂哪都行——挂错位置等于没挂。很多人直接抄网上配置target_modules[q_proj, v_proj]这是早期 LoRA 论文的配置只挂 attention 的 Q 和 V。但 Qwen3-8B 是个 36 层的大家伙光挂 Q/V 太保守。正确做法是先 print 模型架构看清楚有哪些线性层——model AutoModelForCausalLM.from_pretrained(Qwen/Qwen3-8B, torch_dtypeauto) print(model.model.layers[0].self_attn) # 看 attention print(model.model.layers[0].mlp) # 看 MLP输出里会告诉你有q_proj/k_proj/v_proj/o_projattention 四件套和gate_proj/up_proj/down_projMLP 三件套。我建议的配置是——attention 全挂 MLP 全挂target_modules [q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj]PART 03 会详细讲只挂 attention vs 加 MLP的效果差异。PART 03QLoRA 配置 超参到底怎么填这是本文最干货的部分。每个超参我都给你三段式默认值 / 为什么 / 调错后果。LoRAr能力上限旋钮r是 LoRA 的秩决定你能学多少新东西。r8默认值。够应付改语气、改格式这种轻量任务r16我推荐的新手起步值。能学新知识显存涨得不多r32适合数据集大、任务重比如代码生成、长文改写r64除非你知道为什么需要否则别碰调错后果r 太小 →欠拟合loss 降不下去模型学不会新行为r 太大 → 显存暴涨 过拟合模型只会背训练集换个问法就废我的经验先从 r16 起步看 loss 曲线再决定调不调。r 不是越大越好是够用就好。alpha缩放系数alpha控制 LoRA 那部分更新的音量。默认公式alpha 2 × r。r8 → alpha16r16 → alpha32什么时候偏离这个公式训练不收敛但 r 已经够大→ 试着把 alpha 调小比如 alphar让更新温和一点loss 降得太慢→ 试着把 alpha 调大比如 alpha4r让更新激进一点但新手别动 alpha老老实实2r就行。它是高级玩家的微调旋钮不是入门工具。dropout防过拟合开关LoRA 默认dropout0.05。什么时候调到 0.1训练集 loss 远低于验证集 loss比如 train0.3val1.5→ 典型过拟合加 dropout数据集很小1000 条→ 容易背书加 dropout训练步数很多3 epoch→ 同上调错后果dropout 太大0.2→ 模型学不进去欠拟合dropout0 → 大多数时候没事但小数据集上必过拟合target_modules挂哪些层前面说过attention 全挂 MLP 全挂是我的推荐。但你要知道差异——配置可训参数效果显存只挂 q_proj, v_proj~0.3%轻量任务够用最省attention 四件套~0.5%大多数 SFT 任务够用省一点attention MLP 全挂~0.8%重任务、学新知识稍涨我的建议8B 模型上直接全挂。多出来的显存占用对 12G 卡不算啥但效果差异是肉眼可见的。4bit 量化NF4 还是 8bitQLoRA 的核心是 4bit 量化把权重压到 4bit 省显存。NF4NormalFloat 4默认推荐正态分布优化的 4bit 格式double quant再量化一次量化常数再省 0.5GB8bit质量更好但显存翻倍什么时候退回 8bit微调后效果明显变差生成内容质量下降、出现乱码→ 4bit 损失太大换 8bit显存充裕16G→ 直接 8bit质量优先99% 的情况 NF4 double quant 就够了。lr / batch / grad_accum调参三件套这是新手最容易抄错的三个值——参数全量微调值QLoRA 推荐值为什么差这么多learning_rate2e-51e-4 ~ 2e-4QLoRA 只训 0.5% 参数步子要迈大per_device_batch8-321-2显存受限gradient_accumulation18-16凑等效大 batch这就是我开头那个故事的根源——我把全量微调的 lr2e-5 抄到 QLoRA 上结果模型纹丝不动。其他几个——warmup_ratio0.03前 3% 步数线性升温防止初期梯度爆炸weight_decay0.01正则化防过拟合num_train_epochs3Alpaca-zh 这种数据量3 epoch 起步PART 04跑起来 训练监控怎么读配好超参该跑了。SFTTrainer 启动代码直接上代码——from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training from trl import SFTTrainer, SFTConfig from datasets import load_dataset # 1. 加载 tokenizer 模型4bit tokenizer AutoTokenizer.from_pretrained(Qwen/Qwen3-8B) model AutoModelForCausalLM.from_pretrained( Qwen/Qwen3-8B, load_in_4bitTrue, device_mapauto, ) # 2. 准备 4bit 训练 model prepare_model_for_kbit_training(model) # 3. LoRA 配置 lora_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj], task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) # 4. 加载数据假设已转成 ChatML 格式的 jsonl dataset load_dataset(json, data_filesalpaca_zh_chatml.jsonl, splittrain) # 5. 训练配置 sft_config SFTConfig( output_dir./qwen3-8b-qlora, num_train_epochs3, per_device_train_batch_size1, gradient_accumulation_steps8, # 等效 batch8 learning_rate1e-4, # ⚠️ 不是 2e-5 warmup_ratio0.03, weight_decay0.01, logging_steps10, save_strategyepoch, bf16True, # 40系卡用 bf1630系换 fp16 ) # 6. 开训 trainer SFTTrainer( modelmodel, argssft_config, train_datasetdataset, processing_classtokenizer, ) trainer.train()跑起来你会看到每 10 步打印一次 loss——{loss: 1.85, learning_rate: 9.5e-05, epoch: 0.01} {loss: 1.62, ...} {loss: 1.43, ...} ...这串数字怎么读这是本文最值钱的部分。loss 曲线诊断手册我总结过四种典型 loss 形态每种对应一种病——形态 1正常下降1.85 → 1.62 → 1.43 → 1.28 → 1.15 → ...前 50 步快速下降之后缓慢收敛。这是健康的样子。形态 2train 降val 不降过拟合train loss 一路走低但验证集 loss 开始回升——train: 1.85 → 1.20 → 0.80 → 0.50 → 0.30 val: 1.90 → 1.40 → 1.20 → 1.35 → 1.60 ← 这里开始回升信号模型在背训练集没学到泛化能力。对策加 dropout / 减 epoch / 加数据。形态 3前 20% 步不降lr 问题1.85 → 1.86 → 1.84 → 1.85 → 1.83 → ...20 步后才开始动信号lr 太小模型在原地踏步。对策lr × 5比如 1e-4 → 5e-4。这就是我开头那个坑。形态 4loss 突然飙 NaN梯度爆炸1.20 → 1.15 → 1.10 → NaN → NaN → NaN信号梯度炸了权重被污染。对策立刻停别等lr 减半重启检查数据里有没有超长样本2048 tokenbf16 不稳的话换 fp16反之亦然什么时候该早停别迷信训满 3 epoch。判断信号——train loss 连续 100 步不创新低 → 早停生成质量开始下降模型开始重复、胡说→ 早停val loss 触底回升 → 立刻停SFT 不是训得越久越好。很多模型在 1.5-2 epoch 就到顶了第 3 epoch 反而训废。如何判断微调真的生效别只看 loss 数字。loss 从 1.8 降到 0.5听起来很美好但不代表模型变聪明了——可能只是它学会了短回答短回答天然 loss 低。真正判断微调生效的方法是肉眼看生成质量——# 训练完后立刻测一条 inputs tokenizer.apply_chat_template( [{role: user, content: 用三句话介绍一下中国春节}], return_tensorspt, add_generation_promptTrue ).to(model.device) output model.generate(inputs, max_new_tokens200) print(tokenizer.decode(output[0], skip_special_tokensTrue))对比微调前后的输出——微调前base 模型可能续写成用三句话介绍一下中国春节\n用三句话介绍一下美国圣诞节\n...接龙机器微调后应该规规矩矩给你三句话这才是微调生效的硬证据。PART 05调参经验法则小结我把前面散落的经验整理成一张表这是本文最该截图保存的部分——症状大概率原因该动哪个旋钮loss 降不下去lr 太小 / r 太小先 lr ×5不行再 r ×2loss 突然 NaN梯度爆炸lr 减半检查数据长度train 降 val 升过拟合dropout↑ / epoch↓ / 加数据生成内容重复lr 太大或训太久lr 减半 / 早停模型不响应指令template 拼错用 apply_chat_template输出乱码merge / tokenizer 问题见 PART 06还有几条心法——心法 1先调 lr再调 r。lr 是步长r 是脑子大小。步长不对脑子再大也走不动。心法 2数据不够先加数据不是调参。100 条数据调出花来也是过拟合。1 万条平庸数据 100 条精调数据。心法 3r 和 lr 要联动。r 调大参数变多lr 要适当调小否则容易炸。这是一个联动旋钮不是独立旋钮。心法 4永远先看 loss再调参。不要凭感觉动超参。loss 是模型在跟你说话你得先听懂它在说什么。PART 06合并权重 推理验证坑⑤训完之后你拿到的是一个 LoRA adapter几十 MB 的小文件不是一个完整的模型。要部署的话得合并。merge LoRA 回 basefrom peft import PeftModel from transformers import AutoModelForCausalLM # 1. 加载 base 模型这次不量化用 fp16 base AutoModelForCausalLM.from_pretrained( Qwen/Qwen3-8B, torch_dtypefloat16, device_mapauto ) # 2. 挂上 LoRA adapter model PeftModel.from_pretrained(base, ./qwen3-8b-qlora/checkpoint-xxx) # 3. 合并 model model.merge_and_unload() # 4. 保存 model.save_pretrained(./qwen3-8b-merged) tokenizer.save_pretrained(./qwen3-8b-merged)坑⑤合并后输出乱码三段式排查症状合并完一推理模型输出胡言乱语 / 重复 / 不响应指令。排查三步template 对齐了吗合并后的模型推理时必须用和训练时完全一致的 chat template。训练用 ChatML推理也得用 ChatML。这是最常见的翻车点。tokenizer 配置丢了吗save_pretrained默认只保存模型权重tokenizer 的特殊 token 配置可能没保存全。检查合并目录下有没有tokenizer_config.json、special_tokens_map.json。没有的话手动复制一份过去。merge 真的成功了吗有时候 LoRA adapter 加载路径错了合并出来其实是个裸 base。验证方法——# 合并前后各生成一条对比输出 # 如果完全一样说明 merge 失败LoRA 没生效修复后跑一遍对话验证——# 简单对话验证 def chat(question): inputs tokenizer.apply_chat_template( [{role: user, content: question}], return_tensorspt, add_generation_promptTrue ).to(model.device) output model.generate(inputs, max_new_tokens200, do_sampleTrue, temperature0.7) return tokenizer.decode(output[0][inputs.shape[1]:], skip_special_tokensTrue) print(chat(用三句话介绍中国春节))输出规规矩矩给你三句话微调闭环完成。PART 07懒人附赠——LLaMA-Factory 等价 yaml前面讲了一堆底层我知道有人会想——我不想知道原理我只想跑起来。行给你一份等价的 LLaMA-Factory 配置——# qwen3-8b-qlora.yaml model_name_or_path: Qwen/Qwen3-8B stage: sft do_train: true finetuning_type: lora lora_target: q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj lora_rank: 16 lora_alpha: 32 lora_dropout: 0.05 quantization_bit: 4 quantization_method: nf4 dataset: alpaca_zh template: qwen cutoff_len: 1024 output_dir: ./qwen3-8b-qlora per_device_train_batch_size: 1 gradient_accumulation_steps: 8 learning_rate: 1e-4 num_train_epochs: 3 warmup_ratio: 0.03 bf16: true一行命令跑起来——llamafactory-cli train qwen3-8b-qlora.yaml什么时候用懒人方案✅适合用 LLaMA-Factory快速验证想法这个数据集值不值得训不想懂底层、只要结果跑标准流程、不折腾❌必须回到底层训练出 bug 需要排查yaml 报错你看不懂要定制非标准流程比如自定义 loss面试被问QLoRA 的 target_modules 挂了哪些我的建议——先用 LLaMA-Factory 跑通一遍找信心再用底层重跑一遍找理解。两条腿走路最稳。PART 08效果对比与避坑总结不同 r / 不同 lr 下的效果差异我用同一份 Alpaca-zh 子集5000 条做过对比——配置训练 loss生成质量评价r8, lr2e-51.5不降没变化抄默认值的悲剧r8, lr1e-40.9能响应指令回答偏短轻量够用r16, lr1e-40.7响应好回答流畅推荐配置r32, lr1e-40.6接近 r16略过拟合边际递减r64, lr2e-51.3不降没变化r 大 lr 小走不动r64, lr2e-40.5过拟合开始胡说太激进看出规律没r 和 lr 是联动旋钮。光调 r 不调 lr等于换了个更大的脑子但没给它吃饭。Day09 留的 5 个坑一次盘点回到 Day09 结尾我埋的 5 个坑——chat template 拼错→ 用apply_chat_template别手拼PART 02target_modules 漏挂→ print 架构attention MLP 全挂PART 02lr 抄全量微调的值→ QLoRA 用 1e-4不是 2e-5PART 03本文最大的坑4bit 量化质量损失→ 默认 NF4 double quant效果差再退 8bitPART 03合并后输出乱码→ template 对齐 tokenizer 配置 merge 验证PART 065 个坑今天全填完了。什么时候该上 DPO/RLHFSFT 解决的是会不会回答。但有些问题 SFT 解决不了——模型回答太啰嗦作为 AI 语言模型我认为...模型回答有害教人做坏事模型回答不一致同一个问题不同时候答得不一样这些是对齐问题需要RLHF 或 DPO——基于人类偏好再训一轮。这就是 Day10 之后的下一站。先把 SFT 跑通再谈对齐。结尾跑通是入场券调对才是本事最后说句掏心窝子的话——跑通一次 QLoRA 微调真的不难。网上教程一抓一大把复制粘贴半小时就能跑起来。但跑通和调对中间隔着一万个 loss 曲线。我见过太多人照着教程跑通了就以为我会 SFT 了。一问为什么 lr 用 1e-4、train 降 val 不降怎么办瞬间哑火。模型不会因为你跑通了代码就更聪明只会因为你读懂了 loss 而更听话。这才是 SFT 实操真正的门槛——不是代码是听懂模型在说什么。跑通是入场券调对才是本事。下一篇我们往哪走RLHF 还是 DPO你说了算。互动时间你第一次跑 SFT 时栽在哪个坑里评论区聊聊我把高赞的坑整理成读者踩坑合集。下一篇预告Day11——把会回答的模型调成答得好的助手RLHF/DPO 二选一— END —