Skip to main content

03 - 训练配置

这一篇产出一个能直接跑的配置文件,以及一次开跑前的核对。

配置文件本身在 01-agent-sft/qwen3_4b_lora_sft.yaml,这里只讲里面几个值凭什么这么填,以及填错了会怎样。

一、cutoff_len:先量,别拍脑袋

超长的轨迹会被从右边截断。截掉的正好是最后那个 assistant 回答 —— 模型永远学不到「拿到工具结果之后怎么组织成回答」,而这是 L4 任务成功率的主要来源。

所以先量:

python -c "
import json
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained('Qwen/Qwen3-4B-Instruct-2507', trust_remote_code=True)
L = [len(tok(json.dumps(d, ensure_ascii=False))['input_ids']) for d in json.load(open('train.json'))]
L.sort(); print('中位', L[len(L)//2], 'p95', L[int(len(L)*0.95)], '最长', L[-1])"

取 p95 往上取整到 2 的幂。不取最长值 —— 极少数超长样本会把整批的显存需求拉高,为了几条数据让 batch 减半不划算。

你量到的 p95cutoff_len
1200 左右2048
2500 左右4096
5000 以上检查数据,多半是有轨迹陷进了工具调用循环

二、掩码核对:整个项目里性价比最高的 5 分钟

02 篇讲过掩码是什么。这里要做的是确认它真的生效了。

掩码写错不报错。你会正常训完三小时,得到一个 loss 曲线漂亮但行为诡异的模型 —— 典型症状是不调工具,直接把编好的「工具返回」和「回答」一起吐出来。

先让 LlamaFactory 把数据预处理落盘,再看:

llamafactory-cli train qwen3_4b_lora_sft.yaml --tokenized_path saves/tokenized --do_train false
python verify_mask.py saves/tokenized

输出长这样,逐段核对:

样本 #0  总 683 token,参与 loss 214(31%)

[掩] 52 tok | <|im_start|>system⏎你是订单助手。需要数据时调用工具…
[掩] 18 tok | <|im_start|>user⏎查一下华东区 8 月的订单<|im_end|>
[学] 64 tok | <|im_start|>assistant⏎<tool_call>{"name":"query_orders"…
[掩] 41 tok | <|im_start|>user⏎<tool_response>{"count":2,"total":1618.0…
[学] 39 tok | 华东区 8 月共 2 笔订单,合计 1618 元。<|im_end|>

三条判据:

要确认不对说明
system / user / 工具返回段全是「掩」dataset_info.jsontags 写错了
function_call / 回答段全是「学」同上,或者 template 选错
参与 loss 的比例在 20% 到 40% 之间超过 80% 基本等于掩码没生效
别跳过这一步

它不报错、不影响启动、不影响 loss 下降,唯一的表现是训出来的模型不好用 —— 而那时候你已经花掉三小时和一轮机时了。

verify_mask.py 最后有一条自动判据:参与 loss 超过 80% 会直接警告。但光看那条不够,掩码错位一格(比如把 assistant 的第一个特殊 token 掩掉)比例上看不出来,只能逐段读。

三、显存怎么排

01 篇算过 4B 的账:权重 8 G、LoRA 与优化器状态 0.4 G、激活约 3.5 G、杂项 1 G,合计约 12.9 G,24G 卡上余量约 11 G。

激活那一项是唯一你能调的,它由三个值决定:

参数配置里的值调大会怎样
cutoff_len按第一节量出来的定激活线性涨;这个值是数据决定的,不该为省显存动它
per_device_train_batch_size2激活线性涨;OOM 时第一个降它
gradient_accumulation_steps8不占显存。降 batch 之后把它乘回去,等效 batch 保持 16

等效 batch = 2 × 8 = 16。OOM 了就改成 batch 1 / 累积 16,训练行为几乎不变。

四、LoRA 超参

lora_rank: 16
lora_target: all
learning_rate: 1.0e-4
num_train_epochs: 3.0

四个值的依据:

  • rank 16:官方示例给 8。工具调用要学的是行为模式不是新知识,8 也能用;给 16 是因为显存余量足,而 rank 太小的症状(欠拟合,训完分数没动)比 rank 太大的症状(过拟合)更难判断
  • target all:所有线性层都挂。只挂 q/v 更省,但在多轮 + 工具调用这种结构化输出上不稳
  • lr 1e-4:比全参微调高一到两个数量级。原因在 01 篇 2.2:旁路的 B 初始化为 0,训练起点和原模型完全一致,不存在「一步迈大了把预训练知识冲掉」的风险
  • 3 个 epoch:几千条数据的常规值。跑完看验证 loss 什么时候开始抬头,抬头之前那个 epoch 才是该要的

五、开跑前的检查清单

  • cutoff_len 是量出来的,不是抄的
  • verify_mask.py 逐段读过,三条判据都对
  • template: qwen3_nothink,且推理时也会关掉思考模式
  • save_steps 不大于 100(抢占式实例随时会被回收)
  • output_dir 指向持久化盘,不是容器内的临时目录
  • 基线分数已经量完并存好(01 篇七节

最后两条最贵。 前者的代价是实例回收后 checkpoint 一起没,后者的代价是训完不知道好了多少。