小白本地部署微调耍起
为什么写这篇
首先我们都知道思维链能反映出大模型如何思考和解决问题,所以我在想能不能通过微调把「讲解 + 逐步推导」固化进权重,让本地的笨模型不用提示词就自带这个能力?(笔记本配置有限,本地部署的模型相对差)这是我这次想验证的东西,算是动手学大模型课程之外的一次延伸探索。
方向在 7/28 定下来:
本地微调一个 7B 模型(Qwen2.5-7B),既会解题、又会讲概念。
数据策略当时是怎么定的?其实我当时脑子里想的是:一个模型就 15G,光是一个大方向的一个还没处理的原始数据集就 52G------这个训练成本太大了吧?跑完先不说最终效果,光是训练周期都遭不住。(又要处理数据还要来复盘输出结果,太难等待了)
所以就选择把方向更具体和清晰:聚焦自己需要的几个方向(最终是整理两千多条训练数据)。
事后证明这个决定是对的。这两千多条训练,AI告诉我预计跑五个小时,我笔记本实际跑了十四个小时(从早上起来跑到晚上睡觉)------还好当时制止了从零处理52G。
就这样走了一周。收官那天(8/5)我是这么说的:
「V4终于跑完了,实际跑了十四个小时,今天开始最后的测试吧(呜呜呜,太不容易了,走到这一步)」
最后 V4 部署到了 Ollama:能解题、会讲概念(实际效果其实还是差,单题讲解效果不错,输出速度平均 18 token/s,但是多处理时准确率没多大提升)。
不过说句公道话,这本来就是个入门模型 。那些公司、配了正经台式的人,都能搞能力更强的模型、做更大规模的训练;我这是一台 8G 显存的笔记本,模型上限就摆在这------拉高权重、再怎么微调,突破不了多少。微调的意义,是在我需要的方面提供定向帮助,不是让它超越上限。
但在展开之前,我得先交代一件事------这不是一个人干的。我和我的 AI 助手基本五五开。 后面每一步,我都会标出来:这一步是谁做的。
一、数据:不是「量」说了算
开始之前得先说一句:微调的第一步其实不是数据,是环境------我一开始就栽在环境上(装错、GPU 空转,这坑跟第二篇一个德行)。但那个后面单独讲,这里先聊数据。
方向定了之后,最花心思的是数据。但后来我才意识到,数据的重点不在「量」,在于「你对数据的标准和方向掌握」。
1.1 处理到什么程度,这个尺度是你定的
数据不会自己干净。力扣代码随想录那批原始数据,要删 968 行营销、1048 处链接、832 处图片引用------这些清洗是 agent 帮我做的。但「要处理到什么程度、规范是什么」,这个标准只有我知道也只能我来规定。
为什么?因为 agent 不知道你想要什么。它可以帮你把处理过程整个跑完,但「干净到什么程度才算达标」------这取决于你要让模型学会什么。这个抉择,只能自己做。
1.2 数据量多少,跟着方向走
数据要多少条?不是拍脑袋定的,是跟着你的方向走。
我最终定的是三个方向:数学、算法、桥接概念。三个方向基本五五开,所以数据量也相差不大------最后合并 2213 条:
scss
地基 527 = 力扣代码随想录(234) + 蒸馏代码(93) + 考研数学真题(200)
P4 965 = 数学扩充(160+155+120=435) + 代码扩充(220+310=530)
桥接 721 = 概率→CS(248) + CS基础(230) + 线代→CS(102) + 离散→CS(71) + 微积分→CS(70)
数学和代码(做题)加起来约 1400 条,桥接(讲概念)721 条------不是完全均匀,但大致上三个方向谁也不缺。方向定得聚焦,数据量自然就有数。
1.3 质量 vs 数量:几百条可能就够,也可能不够
这是我这次体会最深的一点。
V1 只用了 232 条,跑通了,loss 从 1.66 降到 0.80。V2 加到 525 条,效果反而差了------不是量的问题,是数据的六大问题暴露了(代码量不足、来源权威性差、数学量不足、缺本科教材、无交叉验证......)。V3 1492 条、V4 2213 条,才慢慢对路。
所以「数据量」不是决定因素。质量高的数据,几百条也可能出效果;质量不行,再多也白搭。 V2 那 525 条就是活证据------加量不加质,越加越偏。
1.4 这部分的协作分工
| 做什么 | 谁做 |
|---|---|
| 定数据标准(处理到什么程度/规范/方向比例) | 我 |
| 定「做/讲」两条线(题→推导→答案 / 概念→为什么→怎么用) | 我 |
| 力扣地基深度清洗(删营销/链接/图片引用) | agent |
| 数据蒸馏(思维链蒸馏出来) | agent |
| 逐条校验 + 抽样检查 | agent 逐条 + 我抽查 |
| 格式转换、合并脚本 | agent |
二、环境:显卡在「空转」
2.1 一上来就栽在环境上
数据还没碰,环境先给我来了个下马威------而且这坑跟第二篇一模一样。
第二篇讲的是「你的 PyTorch 为什么用不了 GPU」,我当时踩的是显卡驱动、CUDA 版本对不上。这次更直接:装 LLaMA-Factory 的时候,一条 pip install -e . 把 PyTorch 从 2.11.0+cu128 悄悄覆盖成了 2.13.0+cpu。
我当时问了句:
「意思就是我之前自己学微调时配置好的 GPU 版本被覆盖成了 CPU 版本?」
对,就是这个意思。同样的坑,换了个方式又踩了一遍。
(装 torchaudio 的时候还踩了个版本号不存在的坑------torchvision 0.28.0 在 CUDA 12.8 源里不存在。后来不纠结版本号、直接装最新,反正是纯文本训练用不到它,import 不报错就行。)
2.2 GPU 在空转:Blackwell 的坑
修好之后我以为能跑了。结果跑起来,GPU 使用率显示 满载 ,但训练一步要 29 分钟------正常应该是 10 秒(但是发现不对劲就停下训练开始检查)。
最后查找日志发现:GPU 显示在忙,其实在空转。
原因:我这张 RTX 5060 是 Blackwell 架构 ,而 bitsandbytes 的 4bit 量化算子在这张卡上没有 CUDA kernel,量化计算全被丢回 CPU 跑了。显卡满载空转,CPU 苦哈哈。
「之前就是修好了 GPU 结果发现跑 CPU 了」
这个坑,第二篇里的推理(卡在 CPU)这次直接重演了一遍。
解决方案是换框架:从 LLaMA-Factory 的 bitsandbytes 切到 unsloth (自带量化引擎,不依赖 bitsandbytes)。切完 10 秒/步------快了 约 176 倍。代价是在 LLaMA-Factory 上浪费了约 3 小时排查 8 个非核心 bug。
2.3 复盘:那三小时浪费在哪
事后复盘,那 8 个 bug 至少有一半是框架选型造成的,不是运气差:
- LLaMA-Factory 在 Windows + RTX 5060 Blackwell 上坑一堆------
--config的 YAML 不支持、Python/CUDA/transformers 版本敏感、终端编码和截断问题...... pip install -e .把 PyTorch 从 cu128 覆盖成 cpu------这是框架安装的副作用,但这个我该提前预警的。
也就是说,我一开始就没排查「这框架在这张卡上到底能不能跑」,直接闷头装了。等到一个个 bug 炸出来,才反应过来是选型的问题。
这个错我得认。换 unsloth 之后,10 秒/步,那些 bug 全都没了。
2.4 终端折腾名场面
中间还有个插曲。Windows 下贴长命令,CMD 直接截断、PowerShell 自动补全劫持输入------
「老子一粘贴好这一条,回车一下马上就是这一大段指令紧跟着」(当时快被这个逼疯了,咋都规避不了)
「我都要急哭了」
最后是用 Python 脚本绕过了终端限制。这段纯属折腾,但它说明了环境的问题有多磨人------耗了那么久,最后发现不是脚本问题、不是数据问题,是环境还没装正确。
2.5 这部分的协作分工
| 做什么 | 谁做 |
|---|---|
| 定位根因(GPU 版本被覆盖成 CPU / 量化在 CPU 跑) | 我主导判断 |
| LLaMA-Factory 那 8 个非核心 bug 的填坑 | agent |
| unsloth 切换、参数调整 | agent 执行 |
| 判断「这条路线对不对、要不要换」 | 我 |
三、训练:跑起来的那一刻
3.1 LoRA:不碰 7B 本体
7B 模型有 70 亿参数,全量微调想都别想------8G 显存的笔记本,根本装不下完整权重。所以用的是 LoRA(Low-Rank Adaptation):不动模型本体,只在一部分线性层旁边挂几个小矩阵,训练时只更新这几个小矩阵。
参数速查(实际用的这套):
makefile
LoRA: r=16, alpha=32, dropout=0, target=q/k/v/o_proj
精度: bf16(Qwen 是 bfloat16 模型,fp16 不匹配)
量化: 4bit(load_in_4bit)
Epochs: 3
batch: per_device=2 × grad_accum=2
lr: 2e-4(V2 续训降为 1e-4)
训练脚本长这样(unsloth 版,换数据集只改一个变量):
python
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="D:/models/Qwen2.5-7B-Instruct",
load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(
model, r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_alpha=32, lora_dropout=0,
)
3.2 V1 试训:验证「能跑」
第一次训练只用了 232 条代码数据,跑了 25 分钟,loss 从 1.66 降到 0.80。这一步的意义不是效果,是打通全流程------证明「这条路走得通,loss 在下降,训练起作用了」。
3.3 续训的坑:加载 V1 权重后,梯度断了
V2 要从 V1 接着训。结果加载权重的时候踩了个大坑------GPU 又不跑了。
这个问题的解释,是我询问智能体、由智能体给出的(机制层面我自己并不懂):
PeftModel.from_pretrained()加载 V1 权重后,梯度连接断了 。unsloth 的 4bit 量化模型必须通过get_peft_model()来正确设置requires_grad,直接加载会丢失梯度,GPU 空转不训练。
修复方式(智能体给的做法,当时照做了):
先用
get_peft_model()正常创建 LoRA 层(梯度全通),再手动把 V1 的权重数值拷进去------梯度设置不动。
这个坑修完,V2 才真正跑起来。
3.4 训练时长:预计 5 小时,实际 14 小时
V4 是最终版,2213 条数据,预计跑 4-5 小时。结果实际跑了 14 小时------从早上起来跑到晚上睡觉,8/4 23:56 才完成。

这个数字我自己后来想起来都后怕------这还只是两千多条数据的 LoRA 微调。当初要是真去下载 52G 数据集全量微调,这个训练周期根本遭不住。选垂直方向、控制数据量,是被现实验证过的正确决定。
3.5 这部分的协作分工
| 做什么 | 谁做 |
|---|---|
| 定训练参数、LoRA 配置 | 我定方向,agent 落地 |
| 训练脚本(run_unsloth.py) | agent |
| V1→V2 梯度断问题的排查与修复 | agent 排查,我确认 |
| 跑训练 / 监控 loss | agent |
| 判断「训练有没有在往对的方向走」 | 我 |
四、评测:最值钱的一课
4.1 广告泄漏:一条意外输出
训练跑完,当然要看看效果。结果 V1/V2 阶段,拿 agent 写的测试脚本一跑,输出里连代码随想录的广告链接都出来了。
4.2 顺着查:背答案,不是推理
一查,这个测试集出自训练集 ------模型不是在推理,它在背答案。
一条意外输出,我发现三件事:
- 评测方法错了------测试集出自训练集,等于开卷考试
- 数据脏了------抽样检查漏掉的污染
- 模型过拟合了------它不是学会了,是背下来了
一搜「过拟合」,对上了,就是这个概念。
4.3 立下纪律:评测题不能从训练集出
这件事之后,我立下一条铁律:
评测题不能从训练集出。
以后评测第一步,永远是 grep 去重------先确认要考的题一个都没在训练集里出现过,否则一切评测都是白搭。
4.4 怎么才叫「变好了」:V3 / V4 的评测设计
吸取教训之后,V3 评测我搞了 6 道「干净题」(全部 grep 验证不在训练集)。V4 沿用 V3 同款 6 题做公平对比(A组),另加 8 道全新的桥接概念题(B组,覆盖五个方向)。
A 组(解题,对比 V3):
| 题 | V3 | V4 |
|---|---|---|
| A1 极限 ln(3/2) | ✅ | ✅ 更简洁 |
| A2 特征值 (8,2,2) | ❌ 2,1,-4 | ❌ 2,6,7 |
| A3 Emax=2/3 | ❌ 答 1/2 | ✅ 完整推导 |
| A4 LC48 旋转 | ✅ | ✅ |
| A5 LC238 乘积 | ✅ 示例带乱码 | ✅ 干净 |
| A6 LC4 中位数 | ⚠️ 实现不全 | ⚠️ 实现不全 |
B 组(桥接讲解): 8 题全面优于 V3------721 条桥接数据塑形出「一、二、三」编号教学风格。
6 题中 5 题持平或优于 V3,零退化。 最关键的是 A3------概率题从 V3 答错(1/2)到 V4 完整推导出 2/3。
4.5 复盘:数据不只在「塞知识」,也可能带偏已有能力
base 本身就会概率题,V3 微调反而把它带偏成 1/2,V4 才矫正回 2/3。
数据不只是「往里塞知识」,也可能把模型已有的能力带偏。这个体感,只有亲手跑过才知道。
4.6 这部分的协作分工
| 做什么 | 谁做 |
|---|---|
| 测试脚本 | agent |
| 发现问题(广告泄漏、背答案) | 我亲手跑测试撞出 |
| 复盘把关、判断「数据该不该加/减/换」 | 我 |
| 评测执行(跑 14 题、记录结果) | agent |
五、部署:让模型真正能用
5.1 训练完的模型,怎么装进自己的电脑
训练产物是一个 LoRA adapter ------只有 40MB 的增量权重(那 7B 的本体还是原来的 Qwen)。要让模型真正能用,得把这两部分合并,再转成 Ollama 认得的格式:
sql
LoRA adapter (40MB)
→ merge:和 7B 本体合并成完整模型
→ 转 GGUF(q8_0 量化)
→ ollama create → v4-qwen
5.2 又踩了个量化版本的坑
转 GGUF 的时候又栽了一次:新版 llama.cpp 的转换脚本移除了 q4_k_m 输出(只支持 q8_0 等几个类型)。折腾半天 K-quants 要单独编译工具、GitHub 下载又失败,最后直接改用 q8_0------质量更高,8G 显存上也跑得动。
5.3 验证:这真的是我要的那个模型吗
部署完第一件事,拿之前的评测题去问它。对比 base(原版 Qwen2.5-7B)和 v4-qwen------这组对比是智能体跑出来给我看的:
| 测试 | v4-qwen | base |
|---|---|---|
| 概率题 Emax | ✅ 2/3 完整推导 | 据智能体的对比,base 其实也能答对 |
| 梯度下降讲解 | 六段式编号教学(训练塑形出来的风格) | 教科书式「### 1. 定义...」 |
概率题那个「base 本身就会、V3 带偏、V4 矫正」的关键发现,评测节已经讲过了,这里不重复。
5.4 为什么本地部署
说到这你可能会问:效果也就这样,那你图什么?
图的是本地部署本身的三件事:
- 私密------信息基本不会泄露。一些私密性质的信息处理,交给本地模型做没毛病。
- 离线------断网也能用,基本隔绝了网络问题。
- 私人化------自己拉权重、自己魔改,甚至改源码和架构,迎合自己的需求。
这是线上模型给不了的。你不可能把私密数据传上去、也不可能改别人的源码。本地模型虽然笨,但它是「你自己的」。
5.5 这部分的协作分工
| 做什么 | 谁做 |
|---|---|
| merge / 转 GGUF / ollama create | agent 执行 |
| 参数、Modelfile 配置 | agent 落地 |
| 部署验证(拿评测题去问) | 我验收 |
结尾
没懂的部分
写到这,得坦白一件事:整个过程,很多地方我是**「照葫芦画瓢」跑通的**。有几个机制到现在我也没完全搞懂:
- loss 到底是什么、为什么下降就是好?
- LoRA 权重(adapter)的本质,为什么微调完要「merge」才能用?
- 量化 q8_0 的原理,为什么能省显存?
这几个我按下不表,等补课了再回来补上。说明一下:这里的「不懂」和前面说的「模型上限」是两码事------上限是硬件和模型的物理天花板,砸多少钱都越不过;而这些是方法层面的原理,是可以靠补课弄明白的。前者限制效果,后者限制我自己的理解深度。
这次经历的一句话
回头看我这一周的尝试,最想说的是这句:
微调这件事,最值钱的几步------定方向、定数据标准、复盘把关------都是人做的。agent 是执行层,但你想要什么效果你自己必须清楚。
方向、数据标准、复盘,每一步都是我在表达「我到底要什么,要到具体什么效果」。agent 负责把它跑出来。也许这就是 你不必事事亲力亲为,但你必须清晰表达出你要什么。