大模型量化从0到1(十):bitsandbytes——一行配置就量化的最省心方案
bitsandbytes :为什么它能“加载即量化”,NF4、双重量化与 QLoRA 到底在做什么
GPTQ、AWQ 往往要先准备校准数据、执行量化、保存新权重;GGUF 通常还涉及格式转换与特定运行时。bitsandbytes 走的是另一条路:在 Transformers 加载模型时替换线性层并完成低比特权重转换,几行配置就能把模型以 8bit 或 4bit 形式放进设备内存。它不一定是推理最快的方案,也不是所有场景下精度最优的方案,但它把“量化”从一项专门工程,变成了普通开发者可以随手打开的模型加载选项。
这篇文章从零建立直觉,再逐层拆开 LLM.int8()、FP4、NF4、双重量化、计算精度和 QLoRA。读完后,你不仅会写配置,更应该能回答这些问题:
- bitsandbytes 到底在什么时候量化?
- 它为什么不需要校准数据?
- 8bit 为什么还要保留一条 FP16 计算支路?
- NF4 为什么比“均匀切成 16 格”更适合神经网络权重?
- 双重量化到底量化了什么,能省多少?
- “4bit 存、16bit 算”准确到什么程度?
- 为什么 4bit 模型不一定比 FP16 更快?
- QLoRA 为什么能反向传播,却又不更新 4bit 基座权重?
- bitsandbytes、GPTQ、AWQ、GGUF 应该怎样选?
阅读前先修正四个很常见、但不够准确的说法
在正式开始前,先把几个传播很广的简化说法校正一下。它们用来建立第一印象没问题,但写成教程时最好说完整。
第一,bitsandbytes 常见用法是“加载时量化”;保存能力要按位宽与版本区分
最常见的工作流确实是:从 Hugging Face 的 FP16/BF16 权重开始,from_pretrained() 加载时进行 8bit 或 4bit 转换。因此第一次使用时,你通常仍要下载原始高精度检查点。
“bitsandbytes 只省显存、绝对不能保存低比特权重”也不够准确。当前 Transformers 文档已经明确给出 8bit 模型保存、推送与重载的路径;4bit 检查点在近年的工具链中也存在保存与重载路径,但兼容性更依赖具体的 Transformers、bitsandbytes、模型架构、序列化格式和加载方式,不能脱离版本一概而论。更稳妥的表述是:
- 从原始模型按需加载时,本地缓存里往往仍保留原始高精度权重;
- 8bit 检查点已有较明确的官方保存与重载工作流;
- 4bit 检查点应在目标版本和目标架构上验证保存、冷启动重载与跨机器迁移;
- 即使能够保存,它的运行时依赖和跨生态可移植性,仍与 GGUF、AWQ、GPTQ 这类面向部署的预量化产物不同。[1]
第二,NF4 不是 BitsAndBytesConfig 的默认值
当前配置类中,bnb_4bit_quant_type 的默认值是 "fp4",不是 "nf4"。如果你想使用 NF4,应该明确写出:
bnb_4bit_quant_type="nf4"
官方文档尤其推荐在 4bit 基座模型训练,也就是 QLoRA 场景中使用 NF4。[1]
第三,“4bit 存、16bit 算”是常见配置,不是无条件事实
bnb_4bit_compute_dtype 默认通常是 FP32。只有当你显式把它设为 torch.bfloat16 或 torch.float16 时,才是常说的“4bit 存、16bit 算”。而且实现通常不会把整层权重永久展开成一份完整 BF16 副本;更常见的是在内核中按块读取、反量化并参与矩阵乘法。
第四,bitsandbytes 已不再是“只支持 Linux + NVIDIA CUDA”的代名词
截至本文修订时,官方文档列出了 NVIDIA GPU、CPU、Intel XPU、Intel Gaudi 等支持,并对 AMD ROCm、Apple Silicon 等平台持续推进或提供实验性支持。本文的代码仍以 NVIDIA CUDA 为主,因为这条路径最成熟、资料最多,但“CPU 基本用不了”“Windows 完全不支持”已经是旧印象。[2]
有了这四个修正,下面再建立完整框架。
一、先看它到底省心在哪
用 bitsandbytes 加载一个 4bit 模型,核心代码可以短到这样:
import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-1.5B-Instruct",
quantization_config=quant_config,
device_map="auto",
dtype=torch.bfloat16,
)
你没有单独写量化脚本,没有准备校准语料,没有逐层搜索量化参数,也没有先导出一种新格式。对于支持的 Transformers 模型,框架会把适合量化的线性层替换为 bitsandbytes 的低比特线性层,并在加载权重时完成转换。
这就是 bitsandbytes 最重要的产品价值:它把量化包装成了模型加载策略。
对初学者而言,这意味着你不必先成为量化专家,才能验证“这张显卡能否跑得动这个模型”;对研究和微调而言,这意味着你可以快速切换 FP16、8bit、FP4、NF4,而不用为每种实验维护一套离线产物;对 QLoRA 而言,这意味着 4bit 冻结基座可以直接接入 PEFT 和训练框架。
不过,配置简单不代表内部简单。bitsandbytes 的 8bit 与 4bit 其实是两套不同思路:
- 8bit 主要对应 LLM.int8():向量级缩放加离群特征的混合精度分解;
- 4bit 主要对应 分块权重量化:每个小块独立缩放,再用 FP4 或 NF4 码本表示权重;
- QLoRA 则在 4bit 冻结基座之上训练 LoRA,并结合 NF4、双重量化和分页优化器降低训练内存。
下面先从最容易被忽略的一件事开始:显存到底花到哪里去了。
二、先学会算显存账:量化主要压缩的是哪一部分
很多人第一次接触量化,会把“模型参数位宽”直接等同于“程序总显存”。例如:
- FP16 是 16bit;
- 4bit 是 FP16 的四分之一;
- 所以 4bit 模型总显存也应该只剩四分之一。
这个推理只对“被量化权重的理论裸存储量”近似成立,对整个推理进程并不成立。
2.1 只算权重时,估算很简单
设模型有 (N) 个参数,忽略所有额外信息:
- FP32 权重约占 (4N) 字节;
- FP16/BF16 权重约占 (2N) 字节;
- INT8 权重约占 (N) 字节;
- 4bit 权重约占 (0.5N) 字节。
以 7B 参数模型为例,使用十进制 GB 粗算:
| 权重形式 | 理论裸权重大小 |
|---|---|
| FP32 | 约 28 GB |
| FP16/BF16 | 约 14 GB |
| 8bit | 约 7 GB |
| 4bit | 约 3.5 GB |
这张表非常适合做第一轮容量判断,但不能拿来承诺实际显存。
2.2 真正运行时,还要加上这些开销
一个模型进程通常还包含:
- 没有被量化的模块。 LayerNorm、部分 embedding、输出头、偏置或不兼容量化的层,可能仍以 FP16、BF16 或 FP32 保存。
- 量化元数据。 每个块需要 scale、量化状态、码本信息;4bit 还需要把两个 4bit 编码打包进一个字节。
- CUDA 上下文与缓存分配器。 即使没有大张量,CUDA 运行时本身也会占用显存;PyTorch 还会保留已申请但暂未使用的显存块。
- 临时工作区。 矩阵乘法、反量化、注意力实现和采样过程可能申请临时 buffer。
- 激活。 推理时当前层的中间结果仍要存在;训练时还要为反向传播保留更多激活。
- KV Cache。 自回归生成会缓存每一层过去 token 的 Key 和 Value。上下文越长、批量越大,KV Cache 越可观。
- 训练专属内存。 梯度、LoRA 参数、优化器状态、损失计算和梯度检查点重算都会占内存。
因此,实际常见现象不是“4bit 总显存精确变成 FP16 的 25%”,而是:权重部分接近四分之一,总进程显存下降到原来的三分之一、二分之一或其他比例。 模型越大、上下文越短,权重压缩带来的总收益通常越明显;模型越小、上下文越长,固定开销和 KV Cache 占比越高。
2.3 量化权重不等于量化 KV Cache
这是长上下文场景里最重要的认知之一。
bitsandbytes 的 load_in_4bit=True 主要处理模型权重,默认并不会顺手把 KV Cache 也变成 4bit。于是你可能看到:
- 模型刚加载时只占 5 GB;
- 输入几万 token 后显存不断上升;
- 最后仍然 OOM。
这不是量化失效,而是你压缩了“模型静态资产”,却没有压缩“随序列长度增长的运行状态”。所以判断一张卡能否跑某个任务,至少要同时看:模型参数量、权重位宽、上下文长度、批量大小、KV Cache dtype 和注意力实现。
三、“加载即量化”到底发生了什么
把 bitsandbytes 简称为“在线量化”很方便,但最好知道这只是教学上的归纳,不是严谨到不能打破的分类。
3.1 GPTQ、AWQ 的典型路径:先量化,再部署
GPTQ 与 AWQ 都属于典型的后训练量化路线。常见流程是:
高精度模型
↓
准备代表性校准数据
↓
逐层统计或优化量化参数
↓
生成打包后的低比特权重
↓
保存为新的量化检查点
↓
部署时直接加载低比特产物
GPTQ 使用近似二阶信息逐步量化权重并补偿误差;AWQ 借助激活统计识别重要通道,再通过等价缩放降低显著权重的量化损失。两者都愿意在部署前多做一些工作,以换取更精细的量化决策和更适配推理内核的权重布局。[5][6]
3.2 bitsandbytes 的常见路径:加载时替换模块并转换权重
bitsandbytes 与 Transformers 集成后的常见流程更像:
读取模型结构
↓
把可量化的 torch.nn.Linear 替换为 Linear8bitLt / Linear4bit
↓
从检查点流式读取高精度权重
↓
按目标方案量化、打包并分发到设备
↓
得到可直接推理或接入 PEFT 的模型对象
这里有两个细节值得强调。
第一,框架通常会借助 Accelerate、meta device 和分片加载,避免先在 GPU 中完整放下一份 FP16 模型,再复制出低比特模型。否则“为了量化 14 GB 的模型,先需要 14 GB 显存”就失去意义了。
第二,所谓“加载时量化”不表示模型每次前向都重新量化。权重加载并转成 4bit/8bit 后,会以低比特形式驻留;前向过程中做的是读取低比特权重、按需反量化并完成矩阵运算,而不是每次从原始 FP16 文件重新生成低比特权重。
3.3 为什么它不需要校准数据
GPTQ、AWQ 的量化决策会参考一批代表性输入:
- GPTQ 需要从层输入估计与误差相关的二阶信息;
- AWQ 需要收集激活统计来判断哪些通道更重要。
bitsandbytes 的基础 4bit 权重量化主要依据当前权重块自身的数值范围与固定码本;LLM.int8() 的离群处理则在运行时根据隐藏状态阈值进行混合精度分解。它不需要先拿一批语料跑完整校准流程,因此通用性和启动速度都更好。
代价也很直观:它没有利用你的目标任务分布去精修每一层的量化参数。于是它的优势是低门槛和可训练性,而不是保证在每个模型、每个硬件、每个吞吐目标上都胜过离线量化。
3.4 在线与离线不是“高级”和“低级”的区别
不要把两条路线理解成:
- 在线量化简单,所以技术含量低;
- 离线量化复杂,所以一定更好。
它们优化的是不同目标。
bitsandbytes 优化的是:
- 任意兼容模型快速试验;
- 不准备校准集;
- 与 PyTorch、Transformers、PEFT 直接衔接;
- 让低比特冻结基座参与训练。
GPTQ/AWQ 优化的是:
- 预先付出一次量化成本;
- 产出可反复部署的低比特检查点;
- 尽量降低量化误差;
- 配合面向推理的打包格式和专用内核。
真正的工程选型从来不是“谁更先进”,而是“谁更匹配你的瓶颈”。
四、8bit 原理:LLM.int8() 为什么不是普通 INT8
很多教程把 bitsandbytes 8bit 简化成“把 FP16 权重除以 scale,四舍五入到 INT8”。如果只是这样,LLM.int8() 就不会成为一篇单独的论文。
它解决的核心问题是:大模型隐藏状态中会出现少量、系统性的极端特征维度。 这些特征很少,却可能对注意力和预测结果非常重要。若用一个粗糙的统一尺度把它们和普通值一起压到 INT8,极端值会占满动态范围,普通值就只能挤在很少的整数刻度上。
4.1 先理解最朴素的 absmax 量化
给定一组浮点数 (x),对称 INT8 的编码范围通常取 ([-127,127])。定义:
[
s = \frac{127}{\max |x|}
]
量化:
[
q = \operatorname{round}(s x)
]
反量化:
[
\hat{x} = \frac{q}{s}
]
问题在于,假设绝大多数值在 ([-1,1]),但其中一个值是 20,那么 scale 必须照顾 20。结果是大量普通值只落在零附近很少几个整数上,量化误差明显变大。
4.2 第一层改进:vector-wise quantization
矩阵乘法可以看作很多行向量与列向量的内积。与其整张矩阵只用一个 scale,不如:
- 对输入矩阵的每一行使用独立缩放;
- 对权重矩阵的每一列使用独立缩放;
- INT8 乘法后,再用行 scale 与列 scale 的外积恢复尺度。
这叫向量级量化。它比整张张量共用一个 scale 精细得多,因为局部极值只影响对应行或列。
4.3 第二层改进:把离群特征维度拆出去
仅有向量级缩放还不够。LLM.int8() 论文发现,在足够大的 Transformer 中,极端值不是随机散落,而是集中在少数隐藏特征维度中。论文提出混合精度分解:
设输入隐藏状态为 (X),权重为 (W),原始计算是:
[
Y = XW
]
把特征维度分成普通集合 (R) 与离群集合 (O):
[
Y = X_R W_R + X_O W_O
]
其中:
- (X_R W_R) 使用向量级 INT8 路径;
- (X_O W_O) 使用 FP16 高精度路径;
- 最后把两部分结果相加。
论文报告,超过 99.9% 的值仍可在 8bit 路径中计算,而关键离群特征保留 16bit,从而在其评测模型上保持接近高精度基线的表现。[3]
4.4 llm_int8_threshold 判断的是隐藏状态,不是“权重大不大”
这一点非常容易讲错。
llm_int8_threshold=6.0 不是在加载时扫描权重,把绝对值大于 6 的权重留在 FP16;它对应的是前向过程中隐藏状态离群值的阈值。超过阈值的特征会触发高精度处理。
默认 6.0 通常是合理起点。不要因为看到“阈值”就机械调小或调大:
- 不同模型的隐藏状态分布不同;
- 阈值会同时影响质量、混合精度路径比例和速度;
- 某些实现对特殊值还有优化或快捷路径;
- 最终应通过任务指标与吞吐共同验证。
4.5 LLM.int8() 不是纯粹的 W8A8
部署资料里常用 W8A8 表示权重和激活都以 8bit 参与计算。但 LLM.int8() 更准确的描述是:
- 大部分权重与特征走 8bit 矩阵乘法;
- 离群特征走 16bit;
- INT8 乘积通常以更高位整数累加,再恢复到浮点输出;
- 整个算子是混合精度的。
因此,它的目标不是“所有数都必须是 INT8”,而是“把绝大多数工作放到低比特路径,同时保护少数高敏感特征”。这是它比朴素 INT8 更稳的关键。
4.6 8bit 什么时候值得用
8bit 很适合这些场景:
- 你希望尽量接近 FP16/BF16 的模型表现;
- 显存只差大约一倍,4bit 没必要那么激进;
- 模型或任务对 4bit 误差比较敏感;
- 你想先用最保守的低比特方案验证流程;
- 需要使用 8bit CPU offload 等机制装下更大模型。
它不一定适合:
- 显存非常紧张,必须把权重压到接近 4bit;
- 你追求某个专用推理引擎上的极致吞吐;
- 模型很小,量化内核和混合精度分解的固定开销占比太高。
五、4bit 的基本结构:不是把每个权重粗暴塞进 0~15
bitsandbytes 4bit 的核心是分块量化 + 4bit 码本 + 按块缩放。
5.1 为什么必须分块
假设一整层有几百万个权重,只用一个全局最大值做 scale。只要其中有少数极端权重,整层的量化刻度就会被拉宽,普通权重的分辨率下降。
于是把权重展平并切成小块,每个块独立计算绝对最大值。bitsandbytes 的 4bit functional API 常见默认 block size 是 64,也就是每 64 个权重共享一个缩放常数。[8]
块越小:
- 每个 scale 负责的数更相近,量化通常更精确;
- 但 scale 数量更多,元数据开销更大。
块越大:
- 元数据更少;
- 但局部离群值影响范围更大。
这正是后面双重量化要解决的矛盾:为了精度把块切小,却因此存了很多 scale。
5.2 一个 4bit 块如何编码
对某个权重块 (B={w_i}),先计算:
[
a = \max_i |w_i|
]
再归一化:
[
u_i = \frac{w_i}{a}
]
此时 (u_i) 大致位于 ([-1,1])。然后准备一个只有 16 个值的码本:
[
C = {c_0,c_1,\ldots,c_{15}}
]
每个权重只保存最接近码本值的索引:
[
q_i = \arg\min_j |u_i-c_j|
]
因为索引只有 0~15,所以 4bit 就够。反量化时:
[
\hat{w_i}=a\cdot c_{q_i}
]
真正保存的是:
- 每个权重的 4bit 索引;
- 每个块的 scale,也就是上面的 (a);
- 量化类型和其他状态。
两个 4bit 索引可以打包进一个 uint8,因此 bnb_4bit_quant_storage=torch.uint8 并不意味着权重变成了 8bit 精度;uint8 只是装两个半字节的容器。
5.3 4bit 最大的问题不是范围,而是只有 16 个代表值
16 个值非常少。关键问题就变成:这 16 个点放在哪里?
- 如果均匀分布在 ([-1,1]),实现简单,但可能浪费刻度;
- 如果采用 4bit 浮点码本,能表达不同数量级,但码点布局未必最适合神经网络权重;
- 如果根据权重常见分布设计非均匀码本,就可能把有限刻度用在“最常出现”的区域。
这就是 NF4 的出发点。
六、NF4:它不是“更神奇的 4bit”,而是更匹配分布的 16 个码点
NF4 全称 NormalFloat 4。它来自 QLoRA 工作,设计目标是为近似零均值正态分布的预训练权重提供更合适的 4bit 表示。[4]
6.1 为什么均匀量化会浪费刻度
想象一组接近正态分布的权重:
- 大量权重密集在 0 附近;
- 越靠近两端,权重越少;
- 真正接近最大绝对值的元素只占很小比例。
若把 16 个码点均匀铺在 ([-1,1]),0 附近相邻码点的间距和边缘一样大。但数据在 0 附近最密集,恰恰最需要更细分辨率;两端数据少,却占用了同样密度的刻度。
NF4 的思路是:按照标准正态分布的分位点构造非均匀码本,让每个量化区间在理想分布下承担近似相等的概率质量。 结果就是:
- 0 附近码点更密;
- 两端码点更疏;
- 同样只有 16 个值,却更符合权重的常见分布。
6.2 “按正态分布排刻度”更严谨的说法
常见比喻是“正态分布哪里数据多,哪里刻度就密”。这个直觉没错,但正式一点可以这样描述:
- 从标准正态分布 (N(0,1)) 的分位函数中取得一组代表点;
- 把码本归一化到 ([-1,1]);
- 为了让零值可以无误差表示,对正负部分的构造做非对称处理,并保留精确的 0;
- 每个实际权重块先按 absmax 归一化,再映射到这组固定码点。
所以 NF4 不是每个块都重新统计 16 个分位点,也不是对整个模型做昂贵的分布拟合。码本本身是固定的,块内只需要缩放和最近码点映射。
6.3 NF4 为什么适合 QLoRA
QLoRA 训练时,4bit 基座被冻结。LoRA 需要在这个近似基座上学习任务增量。如果基座量化误差过大,LoRA 一部分容量就会被迫用来“补量化误差”,而不是学习任务。
QLoRA 论文在多种模型与任务上比较了 4bit 格式,报告 NF4 的量化精度优于所比较的 FP4 和 INT4 形式;论文的消融也显示,NF4 + 双重量化可以在其设置中恢复接近 16bit LoRA 的表现。[4]
因此,对 QLoRA 来说,NF4 不是单纯为了“推理输出更好看”,而是为了给适配器提供一个误差更小的冻结基座。
6.4 NF4 是否永远比 FP4 好
不应把论文结论扩大成无条件定律。
NF4 的优势依赖于一个关键先验:被量化权重块接近零均值正态分布。现实模型中:
- 不同层分布不同;
- 某些权重经过特殊训练、裁剪或合并后不再近似正态;
- 不同实现、块大小和任务会改变最终差距;
- 纯推理中的感知差异可能比微调场景小。
稳妥做法是:
- QLoRA 默认优先 NF4;
- 纯推理也可优先尝试 NF4;
- 但把 FP4 作为对照,通过困惑度或任务指标验证,而不是只比较一条聊天回答。
6.5 一个必须避免的混淆:FP4 不等于均匀 INT4
bitsandbytes 的 "fp4" 是 4bit 浮点式码本,不是简单的 16 个均匀整数刻度。因此更准确的对比是:
- FP4:采用 4bit 浮点风格的离散值布局;
- NF4:采用面向正态分布分位点设计的离散值布局;
- INT4:通常指整数码点,还要结合对称/非对称、group size、zero point 等具体定义。
不要把所有“4bit”当成同一种格式。位数只告诉你有多少编码空间,不告诉你码点如何安排、scale 如何分组、内核如何计算。
七、双重量化:量化的不是权重第二遍,而是第一遍产生的 scale
“双重量化”这个名字很容易让人误解成:
权重先量化成 8bit,再量化成 4bit
实际不是。权重仍然只做一次 4bit 编码;第二次被量化的是每个权重块对应的量化常数。
7.1 为什么 scale 也会变成一笔账
假设每 64 个权重为一块,每块需要一个 FP32 scale。
- 一个 scale 占 32bit;
- 由 64 个权重共同承担;
- 平均到每个权重,就是:
[
\frac{32}{64}=0.5\text{ bit/parameter}
]
也就是说,权重编码本身是 4bit,但只算第一层 scale,平均有效开销已经接近 4.5bit/参数,还没加其他状态。
7.2 第二次量化如何降低这笔开销
QLoRA 的双重量化把第一层 scale 再按块量化。例如论文中的设计:
- 第一层权重块大小为 64;
- 第一层 scale 用 8bit 保存;
- 每 256 个第一层 scale 再共享一个 FP32 二级 scale。
于是平均每个参数承担的 scale 开销变成:
[
\frac{8}{64}+\frac{32}{64\times256}
=0.127\text{ bit/parameter}
]
相较于原来的 0.5bit,约节省:
[
0.5-0.127=0.373\text{ bit/parameter}
]
论文把它概括为平均节省约 0.37bit/参数;对 65B 模型,大约是 3GB 量级。[4]
7.3 0.37bit 看起来很小,为什么仍然重要
对单个参数,0.37bit 几乎可以忽略;乘上几十亿参数后就不是小数目。
粗略估算:
- 7B:(7\times10^9\times0.373/8),约 0.33GB;
- 13B:约 0.61GB;
- 33B:约 1.54GB;
- 65B:约 3.03GB。
在显存富余的服务器上,这可能只是锦上添花;在 16GB、24GB、48GB 这种“差几百 MB 就装不下”的边界场景里,它可能决定实验能不能启动。
7.4 双重量化真的完全没有代价吗
官方文档和 QLoRA 论文都把它描述为额外节省内存且没有观察到明显性能退化。[1][4] 工程上更稳妥的表述是:
- 它通常对质量影响极小;
- 会增加一点量化状态和反量化逻辑复杂度;
- 是否有可测速度差异取决于内核、硬件和工作负载;
- 对极端敏感任务,仍应做对照评测。
因此,QLoRA 和显存紧张的 4bit 实验通常建议打开:
bnb_4bit_use_double_quant=True
但它不是魔法,也不应替代实际基准测试。
八、计算精度:把“存储位宽”和“计算位宽”彻底分开
理解 bitsandbytes 最重要的能力之一,是同时区分四种 dtype 概念。
8.1 量化类型:NF4 或 FP4
由 bnb_4bit_quant_type 控制。它决定 4bit 编码的码本布局。
bnb_4bit_quant_type="nf4"
这回答的是:“一个 4bit 编码 0~15 分别代表什么值?”
8.2 量化存储容器:通常是 uint8
由 bnb_4bit_quant_storage 控制。默认通常为 torch.uint8。两个 4bit 值打包在一个字节里。
这回答的是:“这些 4bit 编码放在哪种 PyTorch 张量容器中?”
不要看到权重张量底层是 uint8,就误以为它是 8bit 权重量化。
8.3 计算 dtype:FP32、BF16 或 FP16
由 bnb_4bit_compute_dtype 控制。当前配置默认通常是 FP32;为了速度与内存,常改为 BF16 或 FP16。[1]
bnb_4bit_compute_dtype=torch.bfloat16
这回答的是:“低比特权重参与矩阵运算时,反量化结果与算术采用什么浮点精度?”
8.4 非量化模块 dtype
from_pretrained() 中的 dtype,旧版接口常见 torch_dtype,会影响没有被 4bit 替换的模块,例如 LayerNorm、embedding 或输出头。
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=quant_config,
dtype=torch.bfloat16,
)
这回答的是:“模型中仍保持浮点的部分用什么类型?”
8.5 为什么 BF16 常被推荐
BF16 与 FP32 一样有 8 位指数,因此动态范围大;它的尾数精度低于 FP16,但比 FP16 更不容易因为数值范围过窄而溢出。对支持 BF16 的现代加速器,BF16 往往兼顾:
- 16bit 存储与带宽;
- 比 FP16 更宽的数值范围;
- 比默认 FP32 更快的计算。
但“推荐 BF16”有前提:硬件和 PyTorch 后端真的支持。CUDA 环境可以先检查:
import torch
print(torch.cuda.is_bf16_supported())
若不支持,可用 FP16;若模型数值不稳定或后端对 FP16 支持不理想,再考虑 FP32。
8.6 “反量化后再算”不等于完整生成一份 BF16 权重副本
为了建立直觉,常说:
4bit 权重 → 反量化成 BF16 → 矩阵乘法
这很有用,但容易让人误以为每次前向都要在显存里额外放下一整层 BF16 权重。
高效实现通常会把反量化与矩阵乘法融合:按 tile 或 block 读取打包权重,把所需部分在寄存器、共享内存或临时缓冲中恢复,再立即参与运算。完整高精度权重不会长期驻留。否则 4bit 的显存优势会被一份完整 BF16 副本抵消。
所以更准确的说法是:
权重以 4bit 形式长期存储;计算内核按需把局部权重恢复到指定计算精度,并完成乘加与累积。
8.7 为什么 4bit 不一定更快
从理论上看,4bit 权重更小,显存带宽压力更低,理应更快。但真实速度取决于两股力量:
可能加速的部分:
- 从显存读取的权重字节更少;
- 自回归 decode 常常受显存带宽限制;
- 更大的模型可以留在单卡,减少跨卡通信或 CPU offload。
可能拖慢的部分:
- 解包和反量化需要额外指令;
- 4bit 内核未必充分利用你的 GPU;
- 小模型、小 batch 时固定开销占比高;
- 某些硬件对 FP16/BF16 Tensor Core 极其优化,原生半精度已经很快;
- prompt prefill 与单 token decode 的瓶颈不同;
device_map="auto"若触发 CPU offload,PCIe 传输可能压倒量化收益。
结论不是“4bit 会变慢”,也不是“4bit 一定加速”,而是:bitsandbytes 首先是内存工具,速度必须在目标硬件和目标负载上实测。
九、BitsAndBytesConfig 参数逐个拆解
下面把常用参数分成“入门必会”和“进阶按需”。
9.1 load_in_8bit
BitsAndBytesConfig(load_in_8bit=True)
启用 LLM.int8(),把支持的线性层替换为 8bit 线性层。适合希望接近半精度质量、同时把权重显存大致减半的场景。
9.2 load_in_4bit
BitsAndBytesConfig(load_in_4bit=True)
启用 4bit 线性层。与 load_in_8bit 互斥,不要同时设为 True。
9.3 bnb_4bit_quant_type
bnb_4bit_quant_type="nf4" # 或 "fp4"
决定 4bit 码本。
"fp4":配置默认值;"nf4":面向近似正态权重设计,QLoRA 推荐显式使用。
9.4 bnb_4bit_compute_dtype
bnb_4bit_compute_dtype=torch.bfloat16
决定 4bit 线性层内部计算精度。
- 默认 FP32:兼容与稳定性较好,但更慢;
- BF16:硬件支持时通常是优先选择;
- FP16:更普遍,但动态范围更窄。
9.5 bnb_4bit_use_double_quant
bnb_4bit_use_double_quant=True
把第一层量化的 scale 再压缩,进一步节省显存。QLoRA 和显存边界场景通常建议开启。
9.6 bnb_4bit_quant_storage
bnb_4bit_quant_storage=torch.uint8
控制打包后权重的存储张量 dtype。普通单卡推理一般保持默认即可。它在 FSDP-QLoRA 等分布式场景中更重要,因为通信与分片系统可能要求量化存储 dtype 与其他参数 dtype 协调。
9.7 llm_int8_threshold
llm_int8_threshold=6.0
LLM.int8() 隐藏状态离群阈值。默认 6.0 是常见起点。除非有明确的速度或质量评测,不建议随手改。
9.8 llm_int8_skip_modules
llm_int8_skip_modules=["lm_head"]
明确指定不转成 8bit 的模块。某些多头结构或数值敏感输出层量化后可能不稳定,可以保留高精度。
9.9 llm_int8_enable_fp32_cpu_offload
llm_int8_enable_fp32_cpu_offload=True
允许把部分模块放到 CPU,以装下超出单卡显存的模型。要注意:放在 CPU 的权重通常以 FP32 保存,不是在 CPU 上做 INT8 计算。因此它是容量补救方案,不是免费扩容;速度可能明显受 PCIe 和 CPU 限制。
9.10 llm_int8_has_fp16_weight
这个参数主要面向需要保留 16bit 主权重的特殊训练场景,使前后向不必反复在主权重表示之间转换。普通 8bit 推理通常保持默认。
9.11 device_map 不是量化参数,但往往决定你是否真的省心
常见写法:
device_map="auto"
它让 Accelerate 根据可用设备分配模块。对推理很方便,但要注意:
- 如果单卡装不下,可能自动把部分模块放到 CPU;
- 这会让显存下降,却也可能大幅降低速度;
- 做严格单卡基准时,应使用明确的设备映射,例如
device_map={"": 0}; - 官方文档提醒,
device_map="auto"主要用于推理,训练应交给 Accelerate、Trainer、FSDP 等训练编排机制。[1]
9.12 一套更严谨的 4bit 推荐配置
import torch
from transformers import BitsAndBytesConfig
if not torch.cuda.is_available():
raise RuntimeError("本示例以 CUDA 为目标,请改用与你后端匹配的设备配置。")
compute_dtype = (
torch.bfloat16
if torch.cuda.is_bf16_supported()
else torch.float16
)
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
它不是对所有任务的数学最优解,但作为 QLoRA 和显存优先实验的起点,比“不写任何 4bit 参数”更可靠。
十、上手实测:从最小代码到可复现基准
10.1 安装
本文以较新的 PyTorch、Transformers、Accelerate 和 bitsandbytes 为例:
pip install -U torch transformers accelerate bitsandbytes
做 QLoRA 再安装:
pip install -U peft datasets trl
当前官方安装文档已经覆盖 NVIDIA GPU、CPU、Intel XPU、Intel Gaudi 等后端;不同平台的最低版本和 wheel 支持变化较快,遇到安装问题应以你所用 bitsandbytes 版本的安装矩阵为准。[2]
10.2 先检查运行环境
import torch
import transformers
import bitsandbytes as bnb
print("PyTorch:", torch.__version__)
print("Transformers:", transformers.__version__)
print("bitsandbytes:", bnb.__version__)
print("CUDA available:", torch.cuda.is_available())
print("PyTorch CUDA:", torch.version.cuda)
if torch.cuda.is_available():
print("GPU:", torch.cuda.get_device_name(0))
print("BF16 supported:", torch.cuda.is_bf16_supported())
如果出现 no kernel image available、CUDA 运行库不匹配等错误,优先排查:
- PyTorch 自带的 CUDA runtime 版本;
- 驱动是否支持;
- 系统是否同时暴露多个冲突的 CUDA 路径;
- GPU compute capability 是否满足所用功能;
- bitsandbytes wheel 是否覆盖当前平台。
10.3 8bit 最小示例
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model_8bit = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=BitsAndBytesConfig(load_in_8bit=True),
device_map="auto",
dtype="auto",
)
print(f"模型占用估算: {model_8bit.get_memory_footprint() / 1024**2:.1f} MiB")
10.4 NF4 + 双重量化最小示例
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
compute_dtype = (
torch.bfloat16
if torch.cuda.is_available() and torch.cuda.is_bf16_supported()
else torch.float16
)
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model_4bit = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=quant_config,
device_map="auto",
dtype=compute_dtype,
)
print(f"模型占用估算: {model_4bit.get_memory_footprint() / 1024**2:.1f} MiB")
若你的 Transformers 版本尚未接受 dtype,可把它替换为 torch_dtype。这属于版本接口差异,不是量化逻辑差异。
10.5 统一生成函数
import torch
def generate_once(model, tokenizer, prompt: str) -> str:
messages = [{"role": "user", "content": prompt}]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(text, return_tensors="pt")
# 单卡模型通常可用 model.device;若模型被 device_map 分片,
# 输入应放到输入 embedding 所在设备。
input_device = model.get_input_embeddings().weight.device
inputs = {k: v.to(input_device) for k, v in inputs.items()}
with torch.inference_mode():
output = model.generate(
**inputs,
max_new_tokens=96,
do_sample=False,
use_cache=True,
)
new_tokens = output[0, inputs["input_ids"].shape[1]:]
return tokenizer.decode(new_tokens, skip_special_tokens=True)
print(generate_once(
model_4bit,
tokenizer,
"请用一个生活类比解释大模型量化,并指出类比不准确的地方。",
))
10.6 如何确认模型真的用了 4bit 层
不要只看 model.dtype。一个 4bit 模型的 model.dtype 可能显示 BF16、FP16 或 FP32,因为它反映的往往是部分浮点参数,而不是每个量化权重的实际编码。
可以检查模块类型:
import bitsandbytes as bnb
count = 0
for name, module in model_4bit.named_modules():
if isinstance(module, bnb.nn.Linear4bit):
count += 1
if count <= 5:
print(name, type(module).__name__)
print("Linear4bit 层数量:", count)
也可以查看量化配置:
print(model_4bit.quantization_config)
以及模型内存估算:
print(model_4bit.get_memory_footprint())
10.7 为什么不建议在同一 Python 进程里连续测五种模式
很多示例会:
加载 FP16 → del model → empty_cache → 加载 8bit → 再加载 4bit
这对快速演示可以,但严格比较容易被这些因素污染:
- Python 引用未完全释放;
- PyTorch 缓存分配器保留显存块;
- CUDA 内核与工作区已被初始化;
- 文件系统缓存使后面的加载更快;
- tokenizer、生成输出或 hook 仍持有对象;
memory_allocated不包含所有驱动层开销。
更可靠的方法是每种模式启动独立进程。下面给出一份可直接使用的基准脚本。
十一、一份更可靠的显存与速度基准脚本
将下面内容保存为 bnb_bench.py:
from __future__ import annotations
import argparse
import json
import time
from typing import Any
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
def to_mib(value: int) -> float:
return value / 1024**2
def choose_compute_dtype(name: str) -> torch.dtype:
if name == "fp32":
return torch.float32
if name == "fp16":
return torch.float16
if name == "bf16":
if not torch.cuda.is_bf16_supported():
raise RuntimeError("当前 CUDA 设备不支持 BF16。")
return torch.bfloat16
if name == "auto":
return (
torch.bfloat16
if torch.cuda.is_bf16_supported()
else torch.float16
)
raise ValueError(f"未知 compute dtype: {name}")
def build_quant_config(
mode: str,
compute_dtype: torch.dtype,
) -> BitsAndBytesConfig | None:
if mode == "fp16":
return None
if mode == "int8":
return BitsAndBytesConfig(load_in_8bit=True)
if mode == "fp4":
return BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="fp4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=False,
)
if mode == "nf4":
return BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=False,
)
if mode == "nf4-dq":
return BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
raise ValueError(f"未知模式: {mode}")
def load_model_compat(
model_id: str,
kwargs: dict[str, Any],
):
"""兼容新旧 Transformers 中 dtype / torch_dtype 的参数名。"""
try:
return AutoModelForCausalLM.from_pretrained(model_id, **kwargs)
except TypeError as exc:
if "dtype" not in kwargs:
raise
fallback = dict(kwargs)
fallback["torch_dtype"] = fallback.pop("dtype")
try:
return AutoModelForCausalLM.from_pretrained(model_id, **fallback)
except Exception:
raise exc
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
default="Qwen/Qwen2.5-1.5B-Instruct",
)
parser.add_argument(
"--mode",
choices=["fp16", "int8", "fp4", "nf4", "nf4-dq"],
required=True,
)
parser.add_argument(
"--compute-dtype",
choices=["auto", "fp16", "bf16", "fp32"],
default="auto",
)
parser.add_argument("--device", type=int, default=0)
parser.add_argument("--max-new-tokens", type=int, default=128)
args = parser.parse_args()
if not torch.cuda.is_available():
raise RuntimeError("这份基准脚本要求 CUDA。")
torch.cuda.set_device(args.device)
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats(args.device)
compute_dtype = choose_compute_dtype(args.compute_dtype)
quant_config = build_quant_config(args.mode, compute_dtype)
tokenizer = AutoTokenizer.from_pretrained(args.model)
load_kwargs: dict[str, Any] = {
# 明确放在单卡,避免 auto 悄悄 CPU offload,污染比较。
"device_map": {"": args.device},
"low_cpu_mem_usage": True,
}
if quant_config is None:
load_kwargs["dtype"] = torch.float16
else:
load_kwargs["quantization_config"] = quant_config
# 控制未量化模块的 dtype。
load_kwargs["dtype"] = compute_dtype
start = time.perf_counter()
model = load_model_compat(args.model, load_kwargs)
model.eval()
torch.cuda.synchronize(args.device)
load_seconds = time.perf_counter() - start
allocated_after_load = torch.cuda.memory_allocated(args.device)
reserved_after_load = torch.cuda.memory_reserved(args.device)
peak_during_load = torch.cuda.max_memory_allocated(args.device)
model_footprint = model.get_memory_footprint()
prompt = "请用两段话解释模型量化,并说明它为什么不一定加速。"
messages = [{"role": "user", "content": prompt}]
rendered = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
inputs = tokenizer(rendered, return_tensors="pt")
input_device = model.get_input_embeddings().weight.device
inputs = {k: v.to(input_device) for k, v in inputs.items()}
# 预热一次,减少首次内核编译与初始化干扰。
with torch.inference_mode():
_ = model.generate(
**inputs,
max_new_tokens=8,
do_sample=False,
use_cache=True,
)
torch.cuda.synchronize(args.device)
torch.cuda.reset_peak_memory_stats(args.device)
start = time.perf_counter()
with torch.inference_mode():
output = model.generate(
**inputs,
max_new_tokens=args.max_new_tokens,
do_sample=False,
use_cache=True,
)
torch.cuda.synchronize(args.device)
generation_seconds = time.perf_counter() - start
new_token_count = int(
output.shape[1] - inputs["input_ids"].shape[1]
)
tokens_per_second = new_token_count / generation_seconds
peak_during_generation = torch.cuda.max_memory_allocated(args.device)
response_tokens = output[0, inputs["input_ids"].shape[1]:]
response = tokenizer.decode(response_tokens, skip_special_tokens=True)
result = {
"model": args.model,
"mode": args.mode,
"compute_dtype": str(compute_dtype),
"load_seconds": round(load_seconds, 3),
"model_footprint_mib": round(to_mib(model_footprint), 1),
"allocated_after_load_mib": round(to_mib(allocated_after_load), 1),
"reserved_after_load_mib": round(to_mib(reserved_after_load), 1),
"peak_during_load_mib": round(to_mib(peak_during_load), 1),
"peak_during_generation_mib": round(
to_mib(peak_during_generation), 1
),
"new_tokens": new_token_count,
"generation_seconds": round(generation_seconds, 3),
"tokens_per_second": round(tokens_per_second, 2),
}
print(json.dumps(result, ensure_ascii=False, indent=2))
print("\n回答预览:")
print(response[:300])
if __name__ == "__main__":
main()
分别用独立进程运行:
for mode in fp16 int8 fp4 nf4 nf4-dq; do
python bnb_bench.py --mode "$mode"
done
11.1 看哪些数字
这份脚本同时输出:
model_footprint_mib:模型参数与 buffer 的框架估算;allocated_after_load_mib:PyTorch 当前仍在使用的 CUDA 显存;reserved_after_load_mib:PyTorch 向 CUDA 申请并保留的显存;peak_during_load_mib:加载阶段峰值;peak_during_generation_mib:生成阶段峰值;tokens_per_second:本次固定生成设置下的粗略吞吐。
不要只截一项就下结论。比如:
model_footprint适合看权重压缩;peak_during_generation更接近“实际任务会不会 OOM”;reserved高不等于内存泄漏,可能只是缓存分配器行为;nvidia-smi看到的进程显存还会包含框架外与驱动层开销。
11.2 不要把示例数字当成标准答案
同一模型的结果会受到以下因素影响:
- GPU 架构;
- bitsandbytes、PyTorch、Transformers 版本;
- CUDA runtime 与驱动;
- 注意力实现;
- 是否启用 FlashAttention;
- prompt 长度与生成长度;
- 是否 CPU offload;
- 模型是否 tied embeddings;
- dtype 和量化跳过模块;
- batch size。
因此文章里最负责任的写法不是“1.5B 模型一定占 1010MB”,而是:
在给定软件栈、硬件、输入长度和设备映射下,本次测得某个数;其他环境应以本地结果为准。
11.3 一条回答不能证明量化质量
聊天输出“看起来通顺”只能证明模型没有完全坏掉,不能证明量化几乎无损。更可靠的比较至少包含一种:
- 固定验证集上的 perplexity;
- 与任务相关的准确率、F1、Pass@k 等;
lm-evaluation-harness上的多个标准任务;- 足够规模的人工盲评;
- 对关键业务样本的回归集;
- 多随机种子、多 prompt 模板测试。
还要固定:tokenizer、chat template、生成参数、上下文长度、评测脚本与随机种子。量化误差可能只在长文本、数学、代码、低资源语言或少数边缘样本中暴露。
十二、QLoRA:bitsandbytes 最重要的训练场景
bitsandbytes 被大量开发者认识,并不只是因为“推理时省显存”,更因为它成为了 QLoRA 的关键基础设施。
12.1 先理解普通全量微调为什么贵
全量微调不仅要保存模型权重,还要保存:
- 每个可训练参数的梯度;
- Adam 一阶动量;
- Adam 二阶动量;
- 某些混合精度方案中的 FP32 master weights;
- 反向传播所需激活;
- 临时计算 buffer。
因此,一个 FP16 权重本身约 14GB 的 7B 模型,全量训练远不止 14GB。模型越大,参数相关状态线性增长;序列越长,激活相关内存也快速增长。
12.2 LoRA 做了什么
对一个线性层:
[
Y=XW
]
LoRA 不直接更新大矩阵 (W),而是加入一个低秩增量:
[
Y=XW+sXAB
]
其中:
- (A\in\mathbb{R}^{d_{in}\times r});
- (B\in\mathbb{R}^{r\times d_{out}});
- rank (r) 远小于原矩阵维度;
- (s) 常由
lora_alpha / r决定。
训练时冻结 (W),只更新小矩阵 (A) 与 (B)。这样梯度和优化器状态只为少量适配器参数保存。
12.3 QLoRA 比 LoRA 多了哪一步
普通 LoRA 的基座权重通常仍以 FP16/BF16 保存。QLoRA 则把冻结基座以 4bit 保存:
高精度预训练模型
↓ 加载时 4bit 量化
NF4 冻结基座
+
BF16/FP16 LoRA 适配器
↓
只更新 LoRA
这样同时减少:
- 基座权重显存;
- 需要训练的参数数量;
- 适配器的梯度与优化器状态。
QLoRA 原论文的三项核心内存技术是:
- NF4;
- 双重量化;
- 分页优化器,用统一内存在内存峰值时把优化器页在 CPU 与 GPU 之间迁移。[4]
12.4 “梯度穿过 4bit 权重”是什么意思
这是 QLoRA 最容易被误解的地方。
前向:
[
Y=X\hat{W}_{4bit}+sXAB
]
其中 (\hat{W}_{4bit}) 表示从 4bit 存储按计算 dtype 恢复出的近似权重。
反向时,需要计算损失对输入和 LoRA 参数的梯度,因此计算图会经过基座矩阵乘法。但基座参数被设置为:
requires_grad = False
所以:
- 会利用基座参与前向和反向链式法则;
- 不会为基座保存可更新梯度;
- 不会让优化器修改 4bit 权重;
- 真正更新的是 LoRA 参数。
“通过量化模型反向传播”不等于“直接训练量化权重”。当前 Transformers 文档也明确说明,4bit/8bit 训练支持的是额外参数,而非把所有量化权重当作普通可训练参数。[1]
12.5 prepare_model_for_kbit_training() 做什么
PEFT 提供:
from peft import prepare_model_for_kbit_training
model = prepare_model_for_kbit_training(model)
这个函数不是装饰性调用。它会执行一组为 k-bit 训练准备的操作,包括冻结基座参数、把部分非量化参数提升到更稳定的精度、处理梯度检查点和输入梯度等。官方 PEFT 源码文档明确指出,它会冻结所有基座层。[7]
这也解释了为什么调用后看到部分层变成 FP32,不必立刻怀疑“4bit 失效”:
- 大线性权重仍是 4bit;
- LayerNorm、输出相关模块等可能为了稳定性保留或提升为高精度;
- 总内存不是纯粹的 4bit 裸权重大小。
12.6 一套清晰的 QLoRA 模型准备代码
import torch
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
)
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
compute_dtype = (
torch.bfloat16
if torch.cuda.is_available() and torch.cuda.is_bf16_supported()
else torch.float16
)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
dtype=compute_dtype,
)
# 训练时通常关闭 KV Cache,避免与梯度检查点冲突并节省内存。
model.config.use_cache = False
model = prepare_model_for_kbit_training(
model,
use_gradient_checkpointing=True,
)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
# QLoRA 风格通常尽量覆盖 Transformer 中的线性层。
target_modules="all-linear",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
12.7 为什么 target_modules="all-linear" 经常优于只放 q_proj、v_proj
早期 LoRA 配方常只给注意力中的 query、value 投影加适配器,参数更少。但 QLoRA 论文发现,为了匹配更强的 16bit 基线,在所有 Transformer 线性层上放 LoRA 很关键。[4]
这不表示任何任务都必须 all-linear:
- 适配器越多,训练参数与计算会增加;
- 不同架构模块命名不同;
- 输出头是否包含在内要看 PEFT 规则与任务;
- 小数据集可能需要更强正则。
但对于“我要尽量复现 QLoRA 的高保真思路”,all-linear 比只写 q_proj、v_proj 更接近原始设计。
12.8 分页优化器和 8bit 优化器不是一回事
bitsandbytes 还提供 8bit 优化器,例如 Adam8bit。它压缩的是优化器状态,不是模型权重。
几个概念应分开:
load_in_4bit=True:压缩基座权重;- LoRA:减少可训练参数;
paged_adamw_*:利用分页机制应对内存峰值;adamw_8bit:压缩优化器状态;- gradient checkpointing:少存激活,反向时重算。
它们可以组合,但每项解决不同内存来源。由于 LoRA 参数本来就少,8bit 优化器对适配器状态的绝对节省可能不如基座 4bit 那么夸张;分页机制在长序列、峰值明显时更有价值。
12.9 QLoRA 不是“任何显卡都能微调任何模型”
量化基座解决了大头,但训练仍受这些因素影响:
- 序列长度;
- micro batch size;
- 梯度累积;
- LoRA 覆盖层数与 rank;
- 激活检查点;
- attention kernel;
- 数据 packing;
- 优化器;
- 多模态模型额外编码器;
- GPU 是否支持目标 compute dtype。
因此,“一张消费级显卡能微调 7B”只能作为经验级描述。更专业的说法应包含显卡容量、序列长度、batch、rank、是否 checkpointing 等完整条件。
12.10 训练后保存了什么
常见 PEFT 保存:
model.save_pretrained("my-qlora-adapter")
通常保存的是:
- LoRA adapter 权重;
- PEFT 配置;
- 可能的额外可训练模块。
它不是完整基座。部署时需要:
- 加载与训练时对应的基座模型;
- 使用兼容的量化配置;
- 再加载 adapter。
如果想合并成独立模型,通常要考虑:
- 是否先在高精度基座中 merge;
- merge 后是否重新量化;
- 当前 PEFT、Transformers、量化后端是否支持直接合并;
- 合并和再量化是否引入额外误差。
不要把“LoRA 可以 merge”理解成“任何 4bit 模型都能无损原地合并”。生产部署前应单独验证。
十三、bitsandbytes、GPTQ、AWQ、GGUF 横向对比
先给一张更不容易误导的表。
| 维度 | bitsandbytes | GPTQ | AWQ | GGUF / llama.cpp 生态 |
|---|---|---|---|---|
| 常见量化时机 | 通常在加载时按需量化;部分低比特检查点可保存并重载,兼容性需按版本验证 | 部署前离线量化 | 部署前离线量化 | 转换或量化后生成 GGUF 文件 |
| 是否需要校准数据 | 通常不需要 | 通常需要 | 需要激活统计 | 取决于量化类型;经典流程可不需要,部分高级方案会用重要性矩阵 |
| 核心思路 | LLM.int8 混合精度;4bit 分块 FP4/NF4 | 近似二阶误差补偿的逐层权重量化 | 激活感知的显著通道保护与缩放 | 文件格式 + 多种 CPU/GPU 量化与内核实现 |
| 上手门槛 | 很低 | 中等 | 中等 | 使用预制文件很低,自行转换有一定门槛 |
| 训练/微调 | QLoRA 主流路径,PEFT 集成成熟 | 主要面向推理,部分 PEFT 流程可用 | 主要面向推理,部分生态支持适配器 | 主要面向本地推理,不是主流训练格式 |
| 推理速度 | 依硬件与内核,不能保证最快 | 专用内核下常有优势 | 专用内核下常有优势 | CPU 与 CPU/GPU 混合推理成熟 |
| 磁盘产物 | 默认加载路径常保留高精度缓存;8bit 保存路径较明确,4bit 重载兼容性需按版本实测 | 紧凑预量化检查点 | 紧凑预量化检查点 | 单文件或分片 GGUF,便于本地分发 |
| 生态重心 | PyTorch、Transformers、PEFT | GPU 推理与服务 | GPU/边缘推理与服务 | llama.cpp 及本地应用 |
| 最典型用途 | 快速实验、显存受限推理、QLoRA | 重复部署、追求 GPU 推理效率 | 重复部署、追求质量与硬件友好 | CPU、本地桌面、跨平台离线运行 |
13.1 质量不能简单排成 AWQ > GPTQ > NF4
不同量化方案的误差排名受这些因素影响:
- 模型架构与规模;
- 校准数据是否匹配;
- group size;
- 是否量化输出头;
- 权重位宽和激活位宽;
- 任务类型;
- 推理内核是否严格对应量化格式;
- 评测指标。
GPTQ 与 AWQ 通过校准获得更有针对性的量化参数,往往在部署精度上有优势;NF4 利用权重分布先验,不需数据,且非常适合 QLoRA。不能脱离模型和任务给出永久总排名。
13.2 速度也不能简单写成“AWQ/GPTQ 一定更快”
专用部署格式经常有更积极的权重打包、kernel fusion 和硬件适配,所以在对应引擎里可能明显快于 bitsandbytes。但速度受:
- GPU 架构;
- batch size;
- prompt prefill 与 token decode 比例;
- 并发;
- 内核版本;
- 是否跨卡;
- 是否 CPU offload;
- attention kernel;
- 模型尺寸。
最稳妥的选型流程是:先根据生态缩小候选,再在目标机器上测吞吐、首 token 延迟、单 token 延迟和显存。
13.3 “省磁盘”也要区分缓存与发布产物
如果你这样用 bitsandbytes:
from_pretrained("原始模型", quantization_config=...)
Hugging Face 缓存里通常有原始模型文件,所以本地总磁盘并没有自动减少。
在工具链明确支持且已完成重载验证的前提下,把量化模型保存成独立检查点并清理不再需要的原始缓存,磁盘占用可能下降。但此时还应考虑:
- 目标环境能否加载这种 bitsandbytes 序列化格式;
- 是否依赖特定 Transformers/bitsandbytes 版本;
- 服务框架是否对该格式有高性能路径;
- 是否需要跨语言、跨平台运行。
GGUF、AWQ、GPTQ 的优势往往不只是“文件小”,而是整个部署生态围绕预量化格式建立。
十四、怎样选:用问题而不是口号做决策
场景一:我要做 LoRA/SFT 微调
优先考虑:bitsandbytes NF4 + 双重量化 + PEFT。
原因不是它必然拥有最高推理吞吐,而是它把 4bit 冻结基座、反向传播和 adapter 训练连成了成熟工作流。
场景二:我只想快速试一个 Hugging Face 模型能否放进显卡
优先考虑 bitsandbytes:
- 先 8bit,观察质量与显存;
- 不够再上 NF4 4bit;
- 显存仍不够,再用
device_map="auto"、CPU offload 或更小模型。
场景三:我要长期部署同一个模型,追求吞吐和稳定延迟
不要直接把实验期的 bitsandbytes 配置搬到生产。应对比:
- AWQ;
- GPTQ;
- 目标服务框架支持的其他低比特格式;
- FP8、INT8 或厂商原生路径;
- bitsandbytes 作为基线。
生产选型更关注并发吞吐、P99 延迟、内核稳定性、监控和版本锁定。
场景四:我要在 CPU 或普通笔记本上本地跑
当前 bitsandbytes 已有 CPU 等后端支持,但若目标是成熟的 CPU 本地推理、量化文件分发和桌面工具兼容,GGUF/llama.cpp 生态通常仍更顺手。这里比较的是生态成熟度和内核路线,不是“bitsandbytes 在 CPU 上绝对不能运行”。
场景五:我最在意磁盘和模型分发
优先考虑已有的预量化产物或把目标格式显式保存。不要只打开 load_in_4bit 就以为下载目录会自动缩小。
场景六:我最在意质量
先不要从位宽猜质量。做三件事:
- 建立 FP16/BF16 基线;
- 至少对比 8bit、NF4 4bit 和一个校准式 4bit 方案;
- 用业务回归集而不是聊天观感做决定。
场景七:我最在意长上下文
除了权重量化,还要评估:
- KV Cache dtype;
- GQA/MQA 架构;
- batch 与并发;
- sliding window;
- attention kernel;
- context length 对吞吐的影响。
只压缩权重,可能仍无法解决长上下文 OOM。
十五、常见误区与避坑
误区 1:bitsandbytes 每次前向都会重新量化权重
不对。常见的是加载时完成权重转换;前向时按需反量化并计算。重新启动并从原始高精度检查点加载时,才可能再次执行加载量化。
误区 2:使用 bitsandbytes 后,磁盘模型一定不变小
不够准确。若只从原始模型按需加载,本地缓存仍有高精度文件;当前 8bit 保存与重载已有明确文档路径,而 4bit 还要按具体版本、架构和序列化方式验证。是否省磁盘,最终取决于你的保存、缓存、兼容性和分发工作流。
误区 3:bnb_4bit_quant_type 默认就是 NF4
错误。默认通常是 FP4。要用 NF4,应显式配置。
误区 4:4bit 模型的所有参数都是 4bit
不对。常被替换的是线性层权重;LayerNorm、embedding、输出头、偏置或跳过模块可能保持高精度。
误区 5:model.dtype 显示 BF16,说明模型没有 4bit
不对。model.dtype 不能完整代表混合量化模型。应检查 Linear4bit 模块、量化配置和内存占用。
误区 6:4bit 就是把 FP16 显存精确除以四
只对理想裸权重近似成立。scale、非量化模块、KV Cache、激活、CUDA 上下文和临时 buffer 都不会按同样比例下降。
误区 7:双重量化把权重又量化了一次
不对。它主要量化第一层量化产生的 scale/统计量。
误区 8:双重量化能把 4bit 再变成 2bit
不对。权重编码仍是 4bit;它只降低元数据的平均位开销。
误区 9:compute_dtype 不写也会自动用 BF16
不对。默认通常是 FP32。要使用 BF16/FP16 应显式指定,并确认硬件支持。
误区 10:BF16 在任何显卡上都比 FP16 好
不对。BF16 需要硬件与后端支持;不支持时可能报错、回退或性能不理想。推荐值必须带硬件前提。
误区 11:4bit 推理一定比 FP16 快四倍
不对。权重字节数缩小不等于总计算量缩小四倍。反量化、内核效率、batch、模型大小和瓶颈类型都会影响速度。
误区 12:量化后还 OOM,说明 bitsandbytes 没生效
不一定。可能是 KV Cache、长序列激活、生成临时 buffer、CPU/GPU 设备映射或训练状态导致。
误区 13:QLoRA 会直接更新 4bit 基座权重
不对。标准 QLoRA 冻结基座,只训练 LoRA 等额外参数。梯度经过基座计算图,不代表基座被优化器更新。
误区 14:LoRA 参数很少,所以训练内存几乎只剩 adapter
不对。冻结基座仍要驻留,前向和反向仍有激活;长序列时激活可能非常可观。
误区 15:device_map="auto" 永远是最佳设置
不对。它适合快速推理,但可能把模块放到 CPU,造成速度骤降;训练也不应无脑依赖它。
误区 16:一条 prompt 回答正常,就说明量化无损
不对。生成具有冗余和偶然性,少数样本看不出细微退化。至少要有固定回归集或标准评测。
误区 17:bitsandbytes 只能用于 NVIDIA CUDA
这是旧信息。当前官方文档已列出多个后端;但不同功能、平台和版本的成熟度仍有差异,不能反过来理解成“所有后端完全等价”。
误区 18:8bit optimizer 和 8bit 模型是一回事
不对。前者压缩优化器状态,后者压缩模型线性层权重并改变矩阵乘法路径。两者可独立使用。
误区 19:调用 dequantize() 能恢复原始权重
不对。量化已经丢失信息。dequantize() 只是把低比特近似值展开到高精度 dtype,不会找回量化前被舍弃的小数。
误区 20:torch.cuda.empty_cache() 会释放所有模型显存
不对。它只释放缓存分配器中当前没有活跃张量引用的块。只要 Python 对象仍引用张量,显存就不会被释放。
十六、常见报错与排查路径
16.1 No kernel image is available for execution on the device
常见原因:
- GPU 架构不在 wheel 编译目标中;
- CUDA wheel 与设备能力不匹配;
- 安装了错误平台的包;
- 使用了过旧 GPU 执行要求更高的功能。
排查:
- 查看 GPU 型号与 compute capability;
- 查看
torch.version.cuda; - 对照 bitsandbytes 当前版本安装矩阵;
- 更新 wheel 或按官方说明源码编译。
16.2 CUDA 版本看起来对,但仍提示找不到库
系统可能同时存在:
- PyTorch wheel 自带 CUDA runtime;
/usr/local/cuda-*;- Conda 环境里的
cudart; LD_LIBRARY_PATH中的旧库。
要区分:
nvidia-smi显示的是驱动可支持的 CUDA 上限;torch.version.cuda是 PyTorch 构建所用 CUDA 版本;nvcc --version是本机 Toolkit 编译器版本。
三者不必完全相同,但库解析冲突会导致 bitsandbytes 加载错误。
16.3 BF16 报错或输出异常
先改成:
bnb_4bit_compute_dtype=torch.float16
若仍不稳定,再尝试 FP32,确认问题是否来自精度。还要检查模型原生 dtype、GPU 支持和 attention kernel。
16.4 模型加载后速度极慢
检查 hf_device_map:
print(model.hf_device_map)
若大量层在 cpu 或 disk,瓶颈很可能是 offload。显存节省不等于高性能。
16.5 输入和模型不在同一设备
单卡可这样处理:
inputs = tokenizer(text, return_tensors="pt").to(model.device)
分片模型更稳妥的做法是把输入放到 embedding 所在设备:
input_device = model.get_input_embeddings().weight.device
inputs = {k: v.to(input_device) for k, v in inputs.items()}
16.6 加载没 OOM,生成时 OOM
优先缩减:
- 输入长度;
max_new_tokens;- batch size;
- beam 数;
- 并发请求数。
然后检查 KV Cache 和注意力实现。权重量化只解决一部分。
16.7 QLoRA 第一个 backward 就 OOM
优先调整:
- 把 micro batch 降到 1;
- 用 gradient accumulation 保持有效 batch;
- 开 gradient checkpointing;
- 缩短序列;
- 开数据 packing,减少 padding 浪费;
- 降低 LoRA rank;
- 检查是否意外让基座参数
requires_grad=True; - 检查是否启用了
use_cache=True; - 选择分页优化器;
- 确认没有同时保留多份模型。
16.8 训练时没有可训练参数
检查调用顺序:
加载量化模型
→ prepare_model_for_kbit_training
→ get_peft_model
→ print_trainable_parameters
如果在注入 LoRA 后又执行某个冻结函数,可能把 adapter 也冻结。
16.9 训练完成但保存目录很小
这通常是正常的:你保存的是 adapter,不是完整基座。部署时仍需原始基座和 PEFT 配置。
16.10 保存量化模型后无法在另一台机器加载
检查:
- Transformers 与 bitsandbytes 版本;
- 目标后端是否支持相同量化类型;
quantization_config.json是否完整;- 权重是否使用兼容的 safetensors 序列化;
- 自定义模型代码是否需要
trust_remote_code=True; - 模型架构是否在目标版本中受支持。
量化检查点比普通 FP16 检查点更依赖运行时契约,生产环境应锁定版本并做冷启动测试。
十七、进阶理解:几个经常被忽略的工程问题
17.1 小模型可能看不到理想压缩比
对于 1B 甚至更小模型:
- CUDA 上下文占比高;
- embedding 和输出头占比可能较高;
- 量化元数据相对显眼;
- kernel 启动与反量化开销更难摊薄。
因此 4bit 可能只让总显存下降 50%~70%,而不是 75%。模型越大,权重主导越明显,理论压缩比越容易体现。
17.2 tied embeddings 会影响显存统计
一些模型输入 embedding 与输出 lm_head 共享权重。不同加载、量化、保存路径对共享参数的处理可能影响:
- 是否重复计数;
- 某一层是否保持高精度;
get_memory_footprint()与参数数目换算是否完全一致。
看到估算与理论不符时,不要先假设库出错,先检查权重共享与未量化模块。
17.3 多 GPU 下,量化不自动解决通信瓶颈
把模型分到多卡后:
- 权重更小,单卡容量压力下降;
- 但层间传输、张量并行 all-reduce 或 pipeline bubble 仍存在;
- 不同卡上的量化内核支持可能不同;
- 自动 device map 通常优化“装得下”,不一定优化“跑得快”。
生产多卡服务应使用明确的并行策略和服务框架,而不是把 device_map="auto" 当成最终方案。
17.4 量化误差和采样随机性不是一回事
即使固定 prompt,使用 do_sample=True 时,两次 FP16 输出也可能不同。比较量化模型时:
- 先用 greedy 或固定随机种子;
- 记录 logits、困惑度或任务分数;
- 不要把采样差异误判为量化退化。
但即使 greedy,低比特误差也可能在某一步改变 top-1 token,随后整条生成路径分叉。这是自回归模型的放大效应,不表示每一步误差都很大。
17.5 量化的“质量”不只是一项指标
至少要区分:
- 语言模型 perplexity;
- 指令跟随;
- 事实问答;
- 数学与代码;
- 长上下文检索;
- 多语言;
- 安全与拒答行为;
- 校准与置信度。
一个方案在平均困惑度上接近基线,不保证业务边缘行为完全一致。
17.6 QLoRA 的优势不代表最终部署也必须用 bitsandbytes
很常见的生命周期是:
训练:bitsandbytes NF4 + QLoRA
↓
得到 LoRA adapter
↓
在高精度基座上合并或保留 adapter
↓
按生产后端重新量化为 AWQ / GPTQ / GGUF / 其他格式
训练格式与部署格式可以不同。bitsandbytes 的核心价值在训练阶段,最终服务可以选择更适合吞吐和平台的格式。
17.7 “无校准”不等于“无数据依赖”
量化过程不需要校准集,但你仍然需要数据来:
- 判断质量是否可接受;
- 选择 8bit 还是 4bit;
- 调整 LoRA;
- 构建回归测试;
- 发现特定领域退化。
不需要校准数据,只是减少量化前置步骤,不是免除评测责任。
十八、FAQ
Q1:新手第一次应该先试 8bit 还是 4bit?
显存允许时先试 8bit,建立接近半精度的保守基线;显存紧张或要做 QLoRA,直接试 NF4 4bit。最终以任务指标和显存为准。
Q2:纯推理也推荐 NF4 吗?
可以把 NF4 作为优先候选,尤其当模型权重分布接近其设计假设时。但纯推理中的差距可能比 QLoRA 训练场景小,最好和 FP4 做实测。
Q3:双重量化应该永远打开吗?
显存紧张、QLoRA、大模型场景通常值得打开。若你在做极端低延迟基准或对任何数值变化都敏感,可保留一个关闭版本作对照。
Q4:4bit 会不会明显伤害中文能力?
不能只按语言判断。影响取决于模型、量化格式、任务和评测。中文、代码、数学、长文本都应放进回归集,而不是引用其他模型的结论。
Q5:量化 tokenizer 有意义吗?
没有。tokenizer 主要是词表、规则和少量 CPU 数据结构,不是显存大头。模型量化处理的是神经网络权重和计算。
Q6:能不能把 4bit 模型全部参数都训练起来?
标准 bitsandbytes + QLoRA 流程不这样做。官方支持的主流方式是训练额外参数。要更新低比特基座,需要专门的量化感知训练或其他算法,不能把普通 Linear4bit 当作 FP16 参数直接全量优化。
Q7:QLoRA adapter 能否加载到 FP16 基座上?
通常 adapter 针对同一个基座模型结构,可以加载到高精度基座上,但它训练时看到的是量化近似权重,最终表现可能与 4bit 基座略有差异。无论是否 merge,都应重新评测。
Q8:为什么 4bit 模型加载时间有时比 FP16 更长?
因为除了磁盘读取,还要替换模块、量化、打包和建立量化状态。在工具链能够直接重载对应低比特检查点的组合中,重复转换成本可能降低;是否成立要以实际版本的冷启动测试为准。
Q9:为什么 8bit 只省了 35%,没有省一半?
因为总显存还包括高精度模块、CUDA 上下文、KV Cache、临时 buffer 等。8bit 主要把可量化权重从约 2 字节/参数降到约 1 字节/参数。
Q10:为什么 NF4 + 双重量化和 NF4 显存几乎一样?
模型较小时,双重量化节省的几百 MB 以内差异可能被固定开销、统计口径和 allocator 粒度掩盖。对几十 B 模型,差异更明显。
Q11:为什么 memory_allocated() 比 nvidia-smi 小?
前者主要统计 PyTorch 当前管理的活跃分配;后者看到整个进程的设备内存,包括 CUDA context、库工作区及其他分配。
Q12:为什么 memory_reserved() 比 memory_allocated() 大很多?
PyTorch 缓存分配器会保留已申请显存以便后续复用,减少频繁向驱动申请的成本。这不等于都有活跃张量。
Q13:量化后可以用 FlashAttention 吗?
权重量化与注意力实现是不同层面,很多模型可以组合使用,但兼容性取决于模型、dtype、GPU 和软件版本。要分别验证。
Q14:bitsandbytes 能替代 vLLM、TensorRT-LLM 之类的服务框架吗?
不能直接类比。bitsandbytes 提供低比特层、算子和优化器;服务框架还负责批处理、KV Cache 管理、调度、并行和网络接口。它们可能集成,也可能使用其他量化内核。
Q15:怎样判断是否该从 bitsandbytes 换成 AWQ/GPTQ?
当模型和任务已经稳定、会被大量重复调用,并且吞吐、延迟或部署文件成为主要瓶颈时,就值得做离线量化与生产基准。实验阶段则通常没必要太早增加产物管理成本。
十九、最后用一张心智模型收束全文
可以把 bitsandbytes 理解成三层能力。
第一层:低比特存储
- 8bit 把大部分线性权重压到 1 字节量级;
- 4bit 用两个半字节编码塞进一个
uint8; - scale、码本、量化状态负责把离散编码还原成近似浮点权重。
第二层:可用的低比特计算
- LLM.int8() 用向量级量化保护普通特征;
- 把关键离群特征单独送入 16bit 路径;
- 4bit 内核按块反量化并完成矩阵运算;
- compute dtype 决定局部恢复和计算采用 FP32、BF16 还是 FP16。
第三层:与训练生态结合
- Transformers 在加载时替换模块;
- Accelerate 负责大模型分发;
- PEFT 冻结基座并插入 LoRA;
- NF4 减少量化误差;
- 双重量化压缩 scale;
- 分页优化器和梯度检查点继续削减训练峰值。
把这三层连起来,就能理解它为什么“看起来只加几行配置”,却同时涉及量化格式、CUDA 内核、模型加载和参数高效微调。
二十、小结:真正应该记住的十句话
- bitsandbytes 的最大优势不是绝对最快,而是把量化变成加载配置。
- 它通常不需要校准数据,因此适合快速实验和广泛模型兼容。
- LLM.int8() 不是朴素 INT8,而是向量级量化加离群特征的 16bit 支路。
- 4bit 的核心是分块缩放与 16 值码本,不同 4bit 格式并不等价。
- NF4 用正态分布分位点设计码本,尤其适合 QLoRA 的冻结预训练权重。
- NF4 不是配置默认值,想用必须显式写
bnb_4bit_quant_type="nf4"。 - 双重量化压缩的是 scale,不是把权重从 4bit 再压一遍。
- “4bit 存、16bit 算”需要显式设置 compute dtype;默认通常是 FP32。
- QLoRA 让梯度穿过量化基座,但只更新 LoRA 等额外参数。
- 实验和微调优先考虑 bitsandbytes;生产推理、CPU 本地运行或特定吞吐目标,应与 AWQ、GPTQ、GGUF 等方案实测比较。
如果只记一套 QLoRA 起步配置,可以记住:
BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16, # 硬件支持时
bnb_4bit_use_double_quant=True,
)
但比“黄金配置”更重要的是知道它每一项在解决什么问题:
load_in_4bit解决基座权重容量;nf4解决 16 个码点如何更合理地表示常见权重;compute_dtype解决运算速度和数值范围;double_quant解决大量 scale 的元数据开销。
理解到这里,你就不再是“照抄四行参数”,而是真正掌握了 bitsandbytes 的工作方式、适用边界和工程代价。
参考资料
[1] Hugging Face Transformers 文档:Bitsandbytes quantization;BitsAndBytesConfig API。
[2] Hugging Face bitsandbytes 文档:Installation Guide;Hardware Compatibility。
[3] Tim Dettmers, Mike Lewis, Younes Belkada, Luke Zettlemoyer. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. NeurIPS 2022, arXiv:2208.07339。
[4] Tim Dettmers, Artidoro Pagnoni, Ari Holtzman, Luke Zettlemoyer. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314。
[5] Elias Frantar, Saleh Ashkboos, Torsten Hoefler, Dan Alistarh. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. ICLR 2023, arXiv:2210.17323。
[6] Ji Lin et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. MLSys 2024, arXiv:2306.00978。
[7] Hugging Face PEFT 文档与源码:Quantization developer guide;prepare_model_for_kbit_training()。
[8] bitsandbytes Functional API:quantize_4bit()、dequantize_4bit(),默认 block size 与量化状态说明。
更多推荐



所有评论(0)