SFT:全参 / LoRA / QLoRA / 长上下文
6.1 SFT 在 LLaMA-Factory 里的四种模式
SFT (Supervised Fine-Tuning) 在框架里通过 finetuning_type 与 quantization_bit 两个字段的组合,可以走出四条路:
| 模式 | YAML 关键 | 显存(7B 模型) | 典型用途 |
|---|---|---|---|
| 全参 SFT | finetuning_type: full + DeepSpeed | ≥ 60 GB(bf16) | 有大量优质数据、想最大化效果 |
| LoRA SFT | finetuning_type: lora, lora_rank: 8 | 16 GB | 主流选项 |
| QLoRA SFT | + quantization_bit: 4, quantization_method: bnb | 8 GB(7B), 24 GB(70B) | 单卡 / 大模型 |
| Freeze SFT | finetuning_type: freeze, freeze_trainable_layers: 2 | 居中 | 只想动模型最后几层 |
6.2 全参 SFT
仓库自带 examples/train_full/qwen3_full_sft.yaml。其中关键不同于 LoRA 的是:
### method
stage: sft
do_train: true
finetuning_type: full
deepspeed: examples/deepspeed/ds_z3_config.json # 必须!
全参微调几乎都要 DeepSpeed ZeRO-3 把参数 / 梯度 / 优化器全切,单机 8 卡 Qwen3-4B 才能塞得下。它的 per_device_train_batch_size = 1, gradient_accumulation_steps = 2, learning_rate = 1e-5 —— 比 LoRA 学习率小 10 倍,因为整个模型都在动,太大就会忘掉 pretrain 习得。
save_steps 一个文件就 10+ GB;如果 save_total_limit 不限制,磁盘很快被占满。
6.3 LoRA SFT
第 3 章已经跑过 examples/train_lora/qwen3_lora_sft.yaml。这里讲关键超参的选择直觉:
lora_rank(秩 $r$)
LoRA 增量参数量:
$$\#\Delta W = 2 d r,\qquad \text{其中}\ d=\text{hidden size},\ r=\text{lora\_rank}$$
| 推荐场景 | rank | 说明 |
|---|---|---|
| 简单风格迁移 | 4 / 8 | 少量数据即可,过拟合风险小 |
| 常规行业问答 / 客服 | 8 / 16 | 默认起点 |
| 多任务、注入大量新知识 | 32 / 64 | 接近全参,注意学习率别太高 |
| QLoRA 长上下文 | 16+ | 显存允许时秩越高效果越稳 |
lora_alpha 与 lora_dropout
LoRA 实际作用到原权重的形式是:
$$y = W x + \tfrac{\alpha}{r}\, B A\, x$$
lora_alpha是 $\alpha$,默认2*lora_rank;调大会放大 LoRA 影响;lora_dropout仅在 LoRA 旁支内的 dropout,过拟合明显时升到 0.05~0.1。
lora_target
"all" = 找模型里所有 nn.Linear 都加 LoRA。当 GPU 紧时可以缩到:
lora_target: q_proj,v_proj # 只 attention 的 QV,最便宜
lora_target: q_proj,k_proj,v_proj,o_proj # 全 attention
lora_target: all # 全 attention + MLP,效果最稳,默认推荐
6.4 QLoRA SFT
QLoRA = "4-bit 加载基座" + "fp16/bf16 LoRA"。本质:基座以极低精度只读放在显存里,LoRA 仍用混合精度训练。框架里只要再加两行:
quantization_method: bnb # bitsandbytes
quantization_bit: 4
quantization_type: nf4
double_quantization: true
显存收益(来自 README 表):
| 模型 | LoRA bf16 | QLoRA 4bit |
|---|---|---|
| 7B | 16 GB | 8 GB |
| 13B | 32 GB | 12 GB |
| 70B | 2×A100 80G | 24 GB(单卡 4090 可跑) |
用已经量化好的模型做 LoRA
examples/train_qlora/llama3_lora_sft_gptq.yaml 演示了另一条路:加载预先用 GPTQ / AWQ 量化好的模型权重,再在上面挂 LoRA:
model_name_or_path: TechxGenus/Meta-Llama-3-8B-Instruct-GPTQ
# 注意:这里没有 quantization_bit,因为模型本身就已经是量化版
这种用法训完不能 merge(量化权重不可逆地损失精度,merge 回去会出错);只能在推理时同样加载 GPTQ 模型 + LoRA 适配器。
6.5 Freeze SFT(局部微调)
finetuning_type: freeze
freeze_trainable_layers: 2 # 只放开 transformer 最后 2 层
freeze_trainable_modules: all # 这 2 层里所有 module 都可训
# freeze_extra_modules: lm_head # 额外把 lm_head 也放开
常用于:
- 资源中等 + 不想用 LoRA 的研究场景;
- 想精确控制"哪一层动、哪一层不动"做 probing 实验;
- 结合
freeze_extra_modules: embed_tokens做 embedding 微调。
6.6 PT:持续预训练 vs SFT
注意 stage: pt 不属于 SFT,但流程极像。区别在数据形态:
| 维度 | stage=pt | stage=sft |
|---|---|---|
| 数据 | 长文本(无 prompt/response 划分) | 对话 / 指令 |
| Loss 计算 | 整段 token 都参与 | 默认只对 response 计 |
| 典型数据 | c4_demo、维基百科 | alpaca_*、sharegpt |
| template | 不需要 | 必须,且要和基座匹配 |
仓库自带例子:examples/train_lora/qwen3_lora_pretrain.yaml。
6.7 长上下文 SFT
当 cutoff_len 超过 8K 时,几个字段必须配合修改:
| 字段 | 建议值 |
|---|---|
cutoff_len | 8192 / 16384 / 32768 / ... |
flash_attn | fa2 |
gradient_checkpointing | true(不开就 OOM) |
per_device_train_batch_size | 1(几乎一定是 1) |
packing | true(让短样本拼起来吃满 cutoff_len) |
neat_packing | true(packing 时也带 attention mask 不串) |
rope_scaling | YaRN / linear / dynamic(基座支持的) |
典型计算:序列长度从 2048 → 32768,self-attention 显存按 $O(L^2)$ 飙升 256 倍。没有 FlashAttention-2 + gradient checkpointing 不可能跑得动。
6.9 init_adapter():全参 / freeze / LoRA / OFT 的决策入口
本章描述的"全参 / LoRA / QLoRA / 长上下文"四条路,在源码层都走同一个分发函数:src/llamafactory/model/adapter.py:293 init_adapter()。这个函数按 finetuning_type 派发到三条具体路径:
| finetuning_type | 调用 helper | 关键行为 |
|---|---|---|
full | _setup_full_tuning()(line 38–55) | 所有参数 trainable,forbidden_modules 除外(VLM 时默认排除 vision tower) |
freeze | _setup_freeze_tuning()(line 57–139) | 按 freeze_trainable_layers 取末 N / 前 N / 等间距 N 层 |
lora 或 oft | _setup_lora_tuning()(line 141–290) | PEFT 注入,最复杂的一条路 |
三条路共享的 FP32 upcasting 规则
训练侧 trainable 参数默认会被 cast 回 FP32,但下面这些情形会跳过 upcast(adapter.py:313–325):
pure_bf16: true—— 全程 bf16,不升精度;use_badam: true—— BAdam 块坐标优化器要求半精度状态;quantization_bit is None and is_deepspeed_zero3_enabled()—— ZeRO-3 自己管 fp32 master,重复 cast 反而占内存。
6.10 LoRA 路径的 11 个分支
_setup_lora_tuning() 看似复杂,可拆成几条互斥分支。读这一节相当于把 LoRA 的"所有 yaml flag 怎么落到代码"看了一遍:
| 分支 | YAML flag | 影响 |
|---|---|---|
| adapter 合并 | adapter_name_or_path: a,b,c | line 156–203:除最后一个外的 adapter 都 merge_and_unload 进基座;最后一个变 adapter_to_resume |
| 新建 vs resume | create_new_adapter | True 时即使有 adapter 也跳过 resume,新挂一份 |
| 目标模块自动找 | lora_target: all | find_all_linear_modules() 扫所有 nn.Linear(vision tower 按 freeze_vision_tower 排除) |
| LLaMA Pro 跨层扩展 | use_llama_pro: true | find_expanded_modules() 把 target 扩到 trainable layers |
| VLM 排除冲突模块 | 由 CompositeModel.lora_conflict_keys 自动定义 | line 213:例如 Gemma-3N 排除 timm_model |
| 词表扩张 | resize_vocab + add_tokens | line 222–231:自动把 embedding/lm_head 加进 additional_target |
| OFT 分支 | finetuning_type: oft | line 243–250:OFTConfig(r, oft_block_size, module_dropout, modules_to_save) |
| LoRA 分支 | finetuning_type: lora | line 233–242:LoraConfig(r, lora_alpha, lora_dropout, use_rslora, use_dora, modules_to_save) |
| PiSSA 初始化 | pissa_init: true | line 264–270:init_lora_weights = "pissa" 或 "pissa_niter_X";和量化模型互斥 |
| KTransformers 后端 | 由 KTransformers 配置触发 | line 252–257:走 KT 优化路径(LoRA-only) |
| Unsloth 后端 | use_unsloth: true | line 258–262:get_unsloth_peft_model;LoRA-only,无 OFT |
6.11 QLoRA 的三档量化路径
QLoRA = finetuning_type: lora + quantization_bit: 4 or 8。看 model/model_utils/quantization.py:82–217 能看到量化字段的三档优先级:
- PTQ 模型(line 90–130):模型本身已经 GPTQ / AWQ / AQLM / MXFP4 / FP8 量化好,加载即用。MXFP4 / FP8 与 ZeRO-3 / FSDP 互斥(line 97–101);
- 导出时量化(line 132–164):
export_quantization_bit: 4 / 8+export_quantization_dataset,训完一次性 PTQ 出 GPTQ 格式; - on-the-fly 量化(line 166–217):
quantization_bit + quantization_method:bnb:BitsAndBytes,最常用;4-bit 时bnb_4bit_quant_storage=compute_dtype对 FSDP+QLoRA 至关重要(line 178);hqq:HQQ,支持 1–8 bit;与 ZeRO-3/FSDP 互斥;eetq:EETQ,8-bit only,同上互斥。
三档之间互斥:一个模型只能走一种量化路径。"导出时量化"不要和"on-the-fly 量化"同时写。
6.12 全参 SFT 的 forbidden_modules
全参 SFT 用户常误以为"全参 = 字面意义上所有参数都训"。实际 _setup_full_tuning(adapter.py:38–55)还是有 forbidden_modules 排除:
| 来源 | 典型排除 |
|---|---|
freeze_vision_tower=True(默认 VLM 训文本侧) | vision_tower 全冻 |
freeze_multi_modal_projector=True | 多模态 projector 冻 |
CompositeModel.lora_conflict_keys | 跟 LoRA 冲突的模块(如 Gemma-3N 的 timm_model) |
所以 VLM 全参 SFT 默认只训文本侧 + projector;想让 vision tower 也一起训,必须显式 freeze_vision_tower: false。
6.13 freeze 微调的三种范围选法
_setup_freeze_tuning(adapter.py:57–139)按 freeze_trainable_layers 的正负号决定意义:
| freeze_trainable_layers | 意义 |
|---|---|
| 正数 N | 训末尾 N 层(默认;适合做"加一点知识"的微调) |
| 负数 -N | 训开头 N 层(很罕见;做底层概念注入) |
配 use_llama_pro: true | 等间距取 N 层(layer 数 ÷ N 为步长) |
额外可调整:freeze_extra_modules 加额外要训的模块(layer norm / embedding scale 等);freeze_trainable_modules 指定每层内训哪些子模块(all / mlp / self_attn)。
6.14 SFT 调优经验清单
| 现象 | 多半原因 | 动哪几个字段 |
|---|---|---|
| Loss 不降 / 卡住 | learning rate 太低 / template 不对 | learning_rate ×2 试;查 template 和基座匹配 |
| Loss 一开始就降到 ~0 | 训了 prompt 不是 response | 检查 train_on_prompt: false;template 是否能区分 user/assistant |
| Loss 振荡剧烈 | batch 太小、LR 太大 | 调大 gradient_accumulation_steps、减小 learning_rate |
| 过拟合(eval loss 上升) | 训练轮数太多 / rank 太大 | 降 num_train_epochs、lora_rank,加 lora_dropout |
| 显存忽大忽小 | 样本长度不一 | 开 packing,开 group_by_length |
| 训完模型变蠢 | 数据集 / template 与基座不匹配 | 核对 template 和数据格式;考虑加 preference 或 identity 数据稳住身份 |