解释EXL2格式的分数bpw(如4.65bpw)不是营销手法而是层间不同整数宽度的真实平均,允许在2-8bit间连续调节,使24GB显卡刚好装下一个模型而非浪费或溢出。
其他 4-bit 格式给你的就是完整的 4 个 bit。EXL2 文件标注的是 4.65bpw 或 3.0bpw 这样的数字,小数部分不是营销上的四舍五入——而是对各层分别量化到不同整数宽度后的真实平均值。这个设计让你能够精确填满一张特定的显卡,而不是只能在太大和太小之间二选一。
在本地运行模型时,真正起约束作用的不是位宽,而是你拥有的内存总量,减去 KV cache 在你期望的上下文长度下所占的用量,再减去运行时自身的开销。剩下的是权重可用的 gigabyte 数值,这个数字本质上永远不会刚好等于整个模型 4-bit 量化或 5-bit 量化的大小。
整数位宽格式逼你在向下取整后浪费掉中间的差距。24 GB 的显卡放一个 4-bit 的 34B 模型,会有好几个 GB 用不掉,而且无处可用。EXL2 的解决方案是把比特率做成一个连续可调的旋钮:ExLlamaV2 支持 2、3、4、5、6 和 8-bit 量化,并在模型内部混合使用,以达到 2 到 8 之间任意平均 bits per weight。其背后的量化方法与 GPTQ explained 中描述的 GPTQ 风格误差补偿重建相同;新颖之处在于分配方式。
平均比特率是对参数做加权平均,而非对层做平均,权重是各层的参数量。假设一个模型在可量化线性层中有 7.0e9 个参数,且分配器将其中 20% 放在 6-bit、60% 放在 4-bit、20% 放在 3-bit:
bpw = 0.20*6 + 0.60*4 + 0.20*3
= 1.2 + 2.4 + 0.6
= 4.2 bits per weight
weights on disk = 7.0e9 * 4.2 / 8 bytes
= 3.675e9 bytes
= 3.675 GB (3.42 GiB)
关于这个数字有两点需要注意,两者都很重要。首先,它只计算可量化的线性权重:embeddings、输出 head 和 norm 参数是单独存储的,而且在 ExLlamaV2 中 head 通常比主体保持更高的精度,所以文件比公式算出来的要大。其次,EXL2 checkpoint 标注的 bpw 已经包含了每组 scale 的开销,这就是为什么"4.0bpw"的 EXL2 文件和 group size 为 128 的 4-bit GPTQ 文件大小相近,而不是 EXL2 的更小——详见 group size arithmetic,那里解释了这些额外的几分之一个 bit 从何而来。
把同样的算式倒过来估算一次量化。如果在预留 cache 后空闲 22 GiB,想要放一个 34B 模型:
target_bytes = 22 * 1024**3 = 2.362e10
bpw = target_bytes * 8 / 34e9
= 5.56 bits per weight
这就是你传给转换器的数字——再减去 head、embeddings 和碎片化的余量。
给分配器一个目标平均值只有在它知道 bit 应该投向哪里时才有意义。因此 ExLlamaV2 的转换分两遍完成,第一遍纯粹就是为了回答这个问题。
测量 pass 会对每个线性层进行多次量化,每个候选设置量化一次——不同的位宽、不同的 group size——并记录每个设置相对于校准数据产生的重建误差。结果是每层的误差曲线:对于这个矩阵,从 4-bit 降到 3-bit 代价这么多;对于那个矩阵,代价则大得多。这些曲线被写入工作目录下的 measurement.json,第二遍则针对这些曲线来解决分配问题——把 bit 预算花在曲线最陡的地方。
实际意义是:测量 pass 是整个过程中代价昂贵的那一半,而且可以复用。它取决于模型和校准数据,而非目标比特率,所以对一个模型做五种不同比特率的量化,意味着一次测量和五次廉价的转换。ExLlamaV2 直接暴露了这一点:-om 写入测量结果后退出,-m 读取已有的测量结果。
Flag 名称、默认值和支持的位宽集合属于具体项目的 CLI,且在 ExLlamaV2 各版本间有过变化。请查阅你安装版本的转换文档,而不是相信从某篇博客里抄来的 flag。
获取完整精度的权重。许多强大的开源模型托管在 Hugging Face 上且需要先在模型页面接受许可协议才能下载;那道门槛就是许可协议在执行,这是正确的路径。
安装 ExLlamaV2 并准备一个空闲空间充足的工作目录——转换器会在磁盘上保存中间状态,临时占用空间会超过输出结果。
运行一次测量 pass 并写出结果:python convert.py -i /models/base -o /work -om /work/measure.json。这是耗时较长的步骤。
针对保存的测量结果运行每个比特率:python convert.py -i /models/base -o /work -m /work/measure.json -cf /out/4.65bpw -b 4.65。每个目标比特率重复运行一次,换用不同的 -b 和 -cf。
加载结果,检查你实际需要的上下文长度能否与之并存。一份没有给 KV cache 留空间的量化,是模型在对话进行到第三轮时就跑不动的量化。
关于质量在哪个点开始下降,老实的说法是:没有人发布过一条你可以信赖的、针对你的工作负载的 per-model 曲线,而且校准文本上的重建误差也不等同于你的任务变差。从格式本身结构能说出来的要窄得多,但也更有用:
边际 bit 在不同地方价值不相同。从 4.0 → 4.65 让分配器有预算去提升误差曲线最陡的层,这和把每个层统一提升 0.65 bit 是不同且更好的做法。这与 GGUF 的 K-quant 混合所采用的原理相同,只不过用的是固定配方而非求解出来的方案。
低于大约 3 bits per weight 时,格式在做分诊。此时大多数层处于 2-bit,只有少数层受到分配器保护。这是否可接受取决于你的任务,唯一验证方法是自己在两种量化结果上跑评估。
比特率换的是上下文,而不只是质量。更低比特率释放的内存进入 KV cache。一个 4.0bpw 的模型配上 16k 上下文,可能远比一个只能到 4k 的 5.5bpw 模型更有用,格式把这种权衡显式呈现出来,而整数格式不会。
Mixed-Precision Quantization: Keeping Some Layers at Higher Bits
GPTQ Explained: How Post-Training Calibration Works
What K, M and S Mean in a GGUF Quant Name