本周末跑完了一次模型微调:2082 条语料、2 小时 1 分、最终 loss 0.1518。目标是让 Qwen2.5-Coder-7B 熟练编写活字格(Forguncy)插件代码——一个垂直得不能再垂直的小众场景。
这篇不做「AI 大法好」的科普,只把全过程、真实数据、踩到的坑摊开来讲。文末附损失曲线原图,所有数字都来自训练日志,不是估算。
一、先说结论:值得做,但别指望它变聪明
这次训练没有任何致命错误,一次跑通,loss 曲线干净得像教科书。真正需要复盘的不是模型效果,而是三个工程细节:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
需要提前说清楚的一点:这次训练不会让模型「更聪明」。它的作用是把「活字格插件怎么写」这类知识,从通用代码模型的一个模糊印象,变成一个可以按需调用的具体技能。垂直领域微调的价值从来不在通用能力,而在特定场景的稳定性——让模型少犯那种「看起来对、实际跑不通」的错。
二、方案选型:为什么是 QLoRA
7B 模型在单卡上做全参数微调,显存直接劝退。QLoRA 的思路是:先把基座量化成 4bit 冻住,只训一层极小的 LoRA 适配器。实测下来,可训练参数只有 80,740,352,占 76.96 亿总参数的1.05%。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
三、数据集:2082 条,和它背后的 system prompt
数据集是train_alpaca.json,alpaca 格式(instruction / input / output)。切分后 2040 条训练、42 条验证,实际进入训练循环的样本 934 条。
比数据量更值得说的是system prompt 怎么写的。它直接决定了模型输出的「性格」:
你是活字格(Forguncy)插件开发专家,熟悉 .NET 插件 API 与 JavaScript 插件 API。 · PluginConfig.json 必须含 assembly / javascript / name / pluginType / guid / version(4 段) / dependenceVersion; · pluginType 只能是 "cellType"、"command" 或 "cellType,command"。 · 要求"表格内渲染"就必须有 paintInListView。 宁可少写,也不要"嘴上答应、代码里没做"。 |
注意最后那句。这不是文学修辞,是给模型设的验收标准——插件开发最常见的失败不是不会写,而是「代码看着齐全、实际注册不上」。把这条写进 system prompt,比在数据集里多塞 500 条样例更有效。
样本形态是<|im_start|>user … <|im_start|>assistant成对出现,答案统一带 ```csharp / ```json 代码块,每条样本结尾还附了「参考文档:corpus/function-reference/…」的索引路径。语料里的典型类名如HzgAgingCommand、AttendanceApi、SimpleJsonHelper。
四、损失曲线:好看,但要读出信息
完整的 loss / grad_norm / learning_rate 记录:
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
三个必须读出来的信号:
①第 1 个 epoch 结束就进平台期。step 60(epoch 1.02)loss 已降到 0.2172,此后 110 个 step 只从 0.217 磨到 0.152。算力紧张的话 2 epochs 就能拿到九成收益;追求极限,3 epoch + cosine 退火是合理配置。
②grad_norm 全程小于 0.2(收敛后),没有任何尖刺。梯度平稳意味着没有梯度爆炸,也没有梯度消失——这是「训练健康」最直接的证据,比 loss 曲线好看更重要。
③eval_loss = 0.1833,高于 train_loss 均值 0.3506。这不矛盾:train_loss 是整个训练过程所有步的滑动平均,包含了早期那些 2.0 的高 loss;而 eval_loss 只反映收敛后的模型在未见数据上的表现。用终点对比起点,才是正确读法。
学习率从 1e-4 按余弦退火降到 5.4e-7,最后几步几乎归零——这是标准的「收尾」阶段,让模型在最优解附近稳定下来。
五、时间都去哪了
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可操作的结论:把基座模型放进本地缓存。下载占了三成时间,而它是一次性成本。同样这 3.5 小时,第一次跑和第十次跑之间差着整整 1 小时。
另一个数字:41.5 秒 / step。单卡跑 7B 的 QLoRA,这个速度属于正常偏慢。如果想提速,优先考虑开flash_attn(本次用的是 vanilla attention)和packing,单这两项通常能砍掉三到四成时间。
六、三个警告,和一个真问题
日志里有三条警告,都不致命,但有一条必须处理:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最后这条不是 warning,是我在复盘时发现的潜在事故:7B 全量合并权重约 15GB,AWQ 量化产物另需 4–5GB,中间还有临时文件——在只剩 19G 的分区上直接跑合并+量化,有把输出目录写爆的风险。
两个解法,都很简单:
① 删掉checkpoint-177——训练已完成,它只是个恢复点,adapter 权重已经在最终目录里了;
② 把合并输出路径指到本地盘(那边还有 450G 空余),而不是 NAS。
七、产物与下一步
|
|
|
|---|---|
|
|
|
|
|
|
下一步是合并 LoRA + AWQ 量化。这一步的收益很实在:合并后是完整的 7B 权重,不再依赖 Python 推理框架;AWQ 4bit 量化后显存占用能降到 4G 级别,单卡甚至 CPU 都能跑,对「接进现有业务系统」这个目标来说是刚需。
但量化之前,先把磁盘这件事解决掉。
八、如果只记住三句话
①垂直微调的价值在稳定性,不在聪明。让它在特定场景少犯错,比让它多懂一点通用知识有用得多。
②system prompt 是最高杠杆的投入。一条「宁可少写,也不要嘴上答应代码里没做」,胜过几百条重复样例。
③把工程时间算进去。下载 64 分钟、训练 122 分钟、磁盘只剩 19G——真正拖慢产出的,从来不是模型本身。
📌 本文数据说明
全部数值取自 LlamaFactory 训练日志(2082 条样本 / 177 优化步 / 3 epochs),loss、grad_norm、learning_rate 为日志原始记录,未做任何修饰。环境:单卡 cuda:0,bfloat16,world_size=1。
如果这篇对你有帮助,欢迎点个「赞」和「在看」——你的支持是我继续输出这类实战复盘的最大动力。有想深入的部分,评论区留言,下一篇挑数据最多的展开。

