本文是QLoRA 4bit训练全链路 dtype 通俗拆解(基座大权重 + LoRA小权重分开讲)
全程分三段:硬盘原始文件 → 加载进显存训练(前向/反向计算)→ 训练结束写回硬盘
先统一配置(标准QLoRA参数,行业通用):
python
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # 存储4bit NF4
bnb_4bit_compute_dtype=torch.bfloat16, # 计算统一BF16
bnb_4bit_use_double_quant=True # 双重量化,scale存INT8
)
两个核心对象:
- 基座大模型权重W:几十亿参数,全程冻结不更新;
- LoRA适配器A/B小矩阵:几万~几百万参数,唯一可训练、有梯度的部分。
一、基座大模型权重 W:完整 dtype 流转(硬盘 ↔ 显存)
阶段1:硬盘上的原始基座文件(未加载前)
网上下载的大模型(Llama/Qwen/Mistral)原始权重:
- dtype:BF16(也有FP16版本,现在主流BF16)
- 存储:safetensors/bin,每个权重数字占2字节16位浮点数
- 大小示例:7B BF16 ≈13GB硬盘文件
阶段2:加载进显存(硬盘 → VRAM,量化转换发生在这里)
代码执行from_pretrained(..., quantization_config)时,底层bitsandbytes做两步转换:
- 硬盘读取BF16权重到CPU内存;
- 按64个权重分一组,量化压缩成 NF4 4bit;
- 打包存入GPU显存。
显存里基座权重的真实存储结构(混合dtype)
- 主体权重数值:NF4 4bit(塞进uint8字节里,1个字节存2个4bit数字);
- 每组缩放系数scale(用来反量化还原浮点数):双重量化开启 → INT8;不开则FP32;
- 这部分基座永久冻结,不会产生梯度,全程不修改。
显存占用直观对比7B:
原生BF16 LoRA:13GB;QLoRA 4bit NF4:3~4GB。
阶段3:训练每一步前向/反向计算(显存内实时类型转换,最关键)
显存里存的是4bit压缩格式,但GPU不能直接拿4bit做矩阵乘法,每轮计算临时反量化:
- 取当前层NF4 4bit权重 + INT8 scale;
- 实时反量化,还原成配置指定的 BF16 (
bnb_4bit_compute_dtype); - 用BF16做矩阵乘、激活、损失计算;
- 计算完成后,立刻丢弃这份临时BF16副本,显存里依旧只保留4bit压缩版基座。
重点:
基座全程没有BF16常驻显存,只有计算瞬间临时生成、用完即删;基座永远不会更新,不存在梯度溢出风险。
阶段4:训练结束保存模型(显存 → 硬盘,两种保存方式)
方式A:只保存LoRA适配器(99%微调场景用,不存基座)
执行model.save_pretrained("./lora_output")
- 基座4bit权重完全不保存,硬盘不会生成基座文件;
- 只存LoRA小矩阵,和基座无关。
方式B:合并LoRA,导出完整基座模型(部署前才做merge_and_unload())
合并流程dtype变化:
- 读取显存4bit基座,完整反量化还原为FP32全精度临时权重;
- 把训练好的BF16 LoRA增量叠加到FP32基座上;
- 你可以手动转dtype再存:
merged_model.to(torch.bfloat16).save_pretrained()→ 硬盘得到完整BF16权重文件;merged_model.to(torch.float16).save_pretrained()→ 硬盘得到完整FP16推理权重;
- 合并后硬盘文件是标准16bit浮点,不再带4bit量化信息。
关键点:不执行merge,就永远不会把基座写到硬盘;QLoRA训练过程不会修改原始基座硬盘文件。
二、LoRA适配器小权重 A、B:完整 dtype 流转(独立一套流程)
LoRA是附加小矩阵,和基座完全隔离,全程不做4bit量化。
阶段1:硬盘(训练前无LoRA文件,随机初始化)
加载完4bit基座后,代码随机新建A、B矩阵,直接分配到GPU显存。
阶段2:显存常驻 dtype(训练全程不变)
- A、B 存储 dtype:BF16(和compute dtype统一);
- 梯度、优化器(AdamW)状态:全部BF16高精度;
- 只有这部分参数会被反向传播更新,所以必须高精度防止微调效果崩盘。
显存占用极小:7B模型r=8时,LoRA仅几十~两百MB,完全不占显存。
阶段3:前向/反向计算
LoRA本身就是BF16常驻显存,不需要反量化;
计算时直接和基座临时还原出的BF16权重相加运算,全程统一BF16,无类型转换开销。
阶段4:训练结束保存LoRA到硬盘
model.save_pretrained()输出adapter文件夹:
- safetensors里A/B权重 dtype:BF16;
- 文件体积很小(几十MB),不包含任何基座信息;
- 后续推理时,必须搭配原始完整基座文件(BF16/FP16/4bit都可)一起加载使用。
三、一条完整时间线汇总(分开基座/ LoRA)
基座大权重W
- 硬盘原始:BF16
- 加载进显存:量化压缩为 NF4 4bit + INT8 scale(常驻显存)
- 每轮计算:临时反量化为BF16运算,算完删除临时BF16
- 训练结束不merge:不写基座到硬盘。
LoRA小权重A/B
- 训练初始化:显存直接创建BF16矩阵
- 全程训练:显存常驻BF16,梯度、优化器都是BF16
- 训练保存:硬盘写入BF16格式LoRA适配器文件。
四、为什么训练 BF16,推理就变成了 FP16 ?
- QLoRA训练不会改动原始基座硬盘文件,原始基座永远是BF16;
- 训练产出只有BF16格式LoRA小文件;
- 部署时两条路切换精度,不需要"自动变身",全是人工可控转换:
- 方案1(不合并):加载原始BF16基座 + BF16 LoRA,代码
model.half()全局转FP16推理,显存实时转换,不改动硬盘原文件; - 方案2(合并导出):merge完整BF16模型后,转FP16另存一份独立硬盘文件,以后推理直接加载FP16。
- 方案1(不合并):加载原始BF16基座 + BF16 LoRA,代码
五、大白话类比
- 基座好比一本厚书:硬盘原版是清晰16开彩印(BF16);装进显卡前压缩成微型缩印卡片(4bit NF4)存显存;每次要看某一页(计算),临时放大成清晰16开(BF16),看完扔掉放大图;想永久存一本适配推理的薄版,就完整还原彩印再转成省墨的16开(FP16)保存。
- LoRA好比一张修正贴纸:全程高清彩印(BF16)贴在缩印书上;训练只修改贴纸,最后只保存这张小贴纸到硬盘。