训完之后有三件收尾的事:存什么、怎么发、怎么跑快。 三件事的顺序不能颠倒——先合并再量化,先量化就没法合并了。 这一节把合并、量化、缓存各验一次,最后回头把第 3 章的五节串一遍。
上一节验完了模型,这一节把它交付出去。 服务怎么起、并发怎么量, 模型部署 讲过; 这一节讲的是同一套服务在加载了一个十几 GB 的模型之后,哪些地方会变成瓶颈。
一、保存:存的是四样东西
权重、配置、分词器、训练参数。
少一样都还原不回来,而最常被忘的是分词器——权重一样、分词器不一样,同一个提示会切出不同的 token,
输出就完全不同。
权重格式优先用 safetensors。它只存张量,不执行反序列化代码,加载也快;
老的 .bin 用 pickle,加载时会执行代码,来源不明的文件不要碰。
只训了 LoRA 的话,存的是适配器,几十 MB。
这时候必须在文件名或 README 里写清基座版本——挂错基座的 LoRA 不会报错,只会输出一堆乱码。
训练状态(优化器矩、学习率调度器、当前步数)要不要存,看你还打不打算接着训。 不存就不能续训,只能从头来。
二、合并 LoRA:先合并,再量化
合并就是把 B 乘 A 乘上缩放系数,直接加进主权重,然后只留一个大矩阵。 演示里两条路的输出最大差 1.67e-16——差在浮点舍入上,不是算法差异。 合并之后推理不再有额外开销,也不再需要 peft 这类库。代价是失去「一份基座配多个 LoRA」的灵活性。 顺序不能反。 量化之后的权重是整数,加不了浮点的 LoRA。所以流程是:合并成 fp16、再量化成 4 bit、最后导出。
另有一条路是量化着训(QLoRA):基座按 4 bit 加载,LoRA 单独用 fp16 训,训完合并回量化的基座。 这样显存省得更多,代价是慢一些。
三、导出格式:三种,三种活法
GGUF 是 llama.cpp 那一系的格式,能在 CPU 和苹果芯片上跑,量化档位很多。 显存不够但内存够的机器,这条路常常是唯一能走通的。 GPTQ 是 GPU 上的 4 bit 量化,按每一列对输出的影响决定舍入方向,需要一份校准数据。 加载快、显存省,是显卡推理的常用选择。 AWQ 的思路是按激活值的分布给权重通道加权,保护重要的通道。 它和 GPTQ 是同一档的竞品,选哪个看你的推理框架支持哪个。
三者都不是「更先进」的关系,是生态关系。选格式等于选推理框架,选框架等于选部署环境。
四、推理加速:四个方向,各有各的代价
第一是 KV 缓存。不用缓存,每生成一个 token 都要把前面所有 token 重算一遍; 用了缓存只算新来的那一个。演示里长度 1024 时差 683 倍。 第二是批处理。把多个请求攒成一批一次算完,吞吐上去了,但排在前面的请求要等后面的凑齐。 连续批处理(vLLM 那一套)进一步做到了「谁先算完谁先走」。 第三是注意力算子。FlashAttention 这类实现不改变数学结果,只是把中间矩阵分块算,少读写几遍显存。 它省的是显存带宽,不是乘加次数。
第四是投机解码:用小模型先猜几个 token,大模型一次验证。猜对就白赚,猜错就退回。 它适合「小模型和大模型风格接近」的场景,不适合就纯亏。
五、本章小结
这一章五节,绕着一个问题转:怎么用几百条自己的数据,把一个通用模型改成能干活的模型。 3.1 讲数据:口径比标注员重要,模板会把 token 撑大两三倍,训练前必须体检。 3.2 讲加载:精度决定结果、量化省显存不省算力,4 bit 是工程上的常用落点。
3.3 讲训练:全参微调的账在优化器状态上,LoRA 能成立是因为权重的谱是衰减的。 3.4 讲评估:分维度看丢分,一次只改一处,改口径不算优化。 3.5 讲交付:先合并再量化,缓存省算力占显存。 这五件事里,最容易做错的是 3.1 和 3.4——它们不涉及任何新算法, 却决定了整个项目是「白训一场」还是「真能用」。
六、动手 Demo:合并 LoRA、量化、缓存:三件收尾的事各验一次
三件事各验一次:把 LoRA 合并回主权重并比对输出、把合并后的权重量化并量输出误差、 按公式算 KV 缓存省了多少次乘加。 用的是 64 维的小矩阵,真机上是 4096 维。维度小了,结论的方向不变,只是数变小。
W = 主权重,A、B 是低秩矩阵,r 是秩,alpha 是缩放超参
scale = alpha / r
# 分开算:先乘 W,再乘 B、A
y_sep = x @ W + (x @ B) @ A * scale
# 合并:把 B @ A 乘上缩放直接加进 W
Wm = W + (B @ A) * scale
y_merge = x @ Wm
print(np.abs(y_sep - y_merge).max()) # 1.67e-16
# KV 缓存省了多少:不用缓存是 sum((t+1)^2),用缓存是 sum(t+1)
for n in (64, 256, 1024):
no_cache = sum((t + 1) ** 2 for t in range(n))
cache = sum(t + 1 for t in range(n))
print(n, no_cache / float(cache))
实跑输出:
--------------------------------------------------------------------
合并 LoRA、量化、缓存:三件收尾的事各验一次
--------------------------------------------------------------------
① 把 LoRA 合并回主权重
--------------------------------------------------------------------
分开算:先乘 W,再乘 B、A,最后按 alpha/r 缩放。
合并后:把 B @ A 乘上缩放系数直接加进 W,只留一个矩阵。
两条路的输出最大差 1.67e-16——差在浮点舍入上,不是算法差异。
! 合并之后推理不再有额外开销,也不再需要 peft 这类库。
代价是失去「一份基座配多个 LoRA」的灵活性。
② 合并前后的体积
--------------------------------------------------------------------
| 状态 | 参数个数 | fp16 体积 |
|---|---:|---:|
| 合并前(基座 + LoRA) | 4096 + 1024 = 5120 | 10.0 KiB |
| 合并后(只有主权重) | 4096 | 8.0 KiB |
这个矩阵上,LoRA 只占 25%;放到真的 4096 维上,占比会掉到百分之一以下。
! 所以 LoRA 文件通常只有几十 MB,而基座是十几个 GB——这就是为什么社区里传的是「一个 LoRA」,
不是「一个模型」。
③ 量化:把合并后的权重压下去
--------------------------------------------------------------------
| 精度 | 输出相对误差 | 体积 |
|---|---:|---:|
| 8 bit | 0.654% | 4.0 KiB |
| 4 bit | 11.297% | 2.0 KiB |
注意这里量的是「输出」的误差,不是「权重」的误差——后者在 3.2 节量过。
输出误差才是真正影响结果的那个。
! 两个数是两种口径:3.2 节量权重,用的是「绝对误差占满量程的比例」,4 bit 时百分之二十;
这里量输出,用的是「相对欧氏距离」,同一个 4 bit 是百分之十一。
口径不同,别拿这两个数直接比大小。
! 但方向是一致的:权重粗了,输出就偏。
真正决定能不能用的是「输出偏到什么程度还认得出来」,
所以量化最后总要跑一遍任务评测——权重误差只是参考,任务分数才是判据。
④ 推理加速:KV 缓存省了多少次乘加
--------------------------------------------------------------------
| 生成长度 | 不用缓存 | 用缓存 | 差多少 |
|---:|---:|---:|---:|
| 64 | 0.00 亿 | 0.00 亿 | 43 倍 |
| 256 | 0.06 亿 | 0.00 亿 | 171 倍 |
| 1024 | 3.58 亿 | 0.01 亿 | 683 倍 |
这是按「注意力矩阵的元素个数」算的,不是实测时间。
不用缓存时,每生成一个 token 都要把前面所有 token 重算一遍,总代价随长度立方涨;
用缓存只算新来的那一个,总代价随长度平方涨。
! 倍数随长度线性长——长度翻四倍,倍数也翻四倍。所以上下文越长,缓存越值钱。
这也解释了为什么长上下文推理特别吃显存:缓存省的是算力,占的是显存。
⑤ 真机上这三件事怎么落
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| # 1 合并
| model = PeftModel.from_pretrained(base, 'lora-out')
| merged = model.merge_and_unload()
| merged.save_pretrained('merged-out')
|
| # 2 量化(llama.cpp 那条路)
| python convert.py models/llama-2-7b --outtype f16
| python quantize models/llama-2-7b/ggml-model-f16.gguf \
| models/llama-2-7b/ggml-model-q4_0.gguf q4_0
前一段要 peft,后一段要 llama.cpp,本沙箱都没有,不执行。
顺序很重要:先合并,再量化。先量化就没法合并了——量化后的权重加不了浮点的 LoRA。
! 还有一种做法是量化着训(QLoRA),那是另一条路:基座量化着加载,LoRA 单独用 fp16 训,
训完再合并回量化的基座。第 5 章会碰到。
①两条路的输出最大差 1.67e-16,这是双精度浮点的舍入误差量级。 说明合并是数学等价的,不是近似。合并之后推理没有额外开销,也不再需要 peft。 ②看体积:合并前基座加 LoRA 是 5120 个参数,合并后 4096 个。 这个 64 维的例子里 LoRA 占 25%,放到真机的 4096 维上会掉到百分之一以下。 ③量的是输出的相对误差,不是权重的误差——后者在 3.2 节量过。 两个数口径不同,别直接比大小。4 bit 在这里是 11.297%,在那边是百分之二十,一个是向量的距离, 一个是数值占满量程的比例。 方向是一致的:权重粗了,输出就偏。 但真正决定能不能用的是「偏到什么程度还认得出来」,所以量化最后总要跑一遍任务评测。 权重误差只是参考,任务分数才是判据。 ④是缓存的价值:长度 64 时差 43 倍,256 时 171 倍,1024 时 683 倍。 倍数随长度线性涨——长度翻四倍,倍数也翻四倍。 这也解释了为什么长上下文推理特别吃显存:缓存省的是算力,占的是显存。省下来的时间,是拿显存换的。 ⑤那两段真机代码本沙箱跑不了,要看的是顺序:先合并、再量化。 量化后的权重是整数,加不了浮点的 LoRA——顺序反了就得从头再来。
30 秒实验
- 把④的生成长度加到 2048,看倍数涨到多少。再算一笔:按你这个倍数,长上下文服务该不该默认开缓存?
- 把③的精度从 8、4 改成 3、2,看输出误差怎么变。 和 3.2 节那张权重误差表对比,两边的退化速度一样吗?
- 把①里的
scale从 alpha 除秩改成 1,再看两条路的差。 差应该还是 1e-16 量级——这说明缩放系数不影响「合并等价」这件事,只影响修正项的强度。
常见误区
- 先量化再合并。量化后的权重是整数,加不了浮点的 LoRA。顺序一定是合并、量化、导出。
- 只存 LoRA 不写基座版本。 挂错基座不会报错,只会输出一堆看起来像话但意思全错的句子,而且极难查——因为每一步都没报错。
- 以为 KV 缓存不占显存。它占的还不少:7B 在 4k 上下文下是 2 GiB,长上下文按长度线性涨。 省算力的东西,往往费显存。
自测三问(点开对答案)
1. 为什么必须先合并 LoRA 再量化? —— 因为量化把权重变成整数格点上的值,浮点的 LoRA 修正项加不进去。 反过来先合并成 fp16 再量化,量化器面对的是完整的权重,误差一次算清。 2. KV 缓存省的是算力还是显存? —— 省算力,费显存。不用缓存要重算前面所有 token 的键值,代价随长度立方涨; 用了缓存只算新的那一个,代价随长度平方涨。但缓存本身要按长度线性占显存。 3. LoRA 文件为什么只有几十 MB,而基座是十几 GB? —— 因为它只存两个瘦长矩阵 B 和 A,参数占比不到百分之一。 基座那 67 亿参数一个都没存——它们还在原来的地方,LoRA 只是叠在上面的一层修正。第 3 章到此结束。下一章专攻一个具体的毛病:多轮对话里忘事。 那是 Llama 2 上线之后被抱怨最多的地方。
- ☐ (本篇完,下一篇 4.1 定制多轮对话数据集和构造方法)
相关笔记
- 17 课 编程原理:缓存为什么能省掉重复计算
- 微调策略:LoRA 的账怎么算
- 模型加载:4 bit 的权重误差
- 🦙 Llama 大模型实践 · 索引