ik_llama.cpp 逐层量化实践:用 llama-imatrix 层相似度(LIM)分数指导--custom-q按层量化
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
本文聚焦 ik_llama.cpp 仓库中的一项探索性工作:在计算重要性矩阵(imatrix)的同时,为每个张量/每个层计算 Layer Importance Modification(LIM)分数,并以此为据,通过
llama-quantize --custom-q为不同层分配不同的量化位宽。读完本文,你将掌握如何运行llama-imatrix --layer-similarity收集层间余弦相似度、如何解读"排序后的层重要性"输出,以及如何把它转化为一份可复制的按层量化正则配置,并在困惑度(PPL)与模型体积之间做权衡。
背景与动机:从均匀量化到按层量化
传统量化流程对模型所有层的同一类张量使用相同的量化类型,例如对整个模型统一执行q4_k_m。这种做法没有考虑一个事实:不同层、不同张量对量化误差的敏感程度并不相同。某些层被压得更狠,输出质量几乎不受影响;而另一些层稍有扰动,困惑度就会明显恶化。
ik_llama.cpp 的llama-quantize提供了--custom-q参数,允许用户用正则表达式为每一类张量指定独立的量化类型。PR #326(WIP Compute per layer LIM Scores during imatrix,作者 ubergarm,2025-04-13 创建、2025-04-16 关闭)的设想正是:
对给定张量的各层,按其对量化误差的敏感度进行排序,然后利用
--custom-q正则,决定哪些层量化得更狠、哪些层保留更多位宽,从而在保持生成质量(低困惑度)的同时,比"所有层统一量化类型"占用更少内存。
该 PR 自述为实验性质("This is mostly vibe code"),并在后续讨论中被更完善的实现取代——最终落地为llama-imatrix --layer-similarity(简写-lsim),在 examples/imatrix/imatrix.cpp 中实现。本文以该文档为骨架,结合当前仓库源码还原这一完整的技术路径。
LIM 方法原理:为什么用余弦相似度衡量层重要性
LIM 分数源自论文Layer-Wise Quantization: A Pragmatic and Effective Method for Quantizing LLMs Beyond Integer Bit-Levels(Dumitru 等人,arXiv:2406.17415)。其核心直觉是:
一个层对其接收的输入嵌入改变得越多,它就越重要。
在实现层面,PR #326 的初始尝试是:对同一个张量类型(例如ffn_down),计算相邻两个层的输入激活之间的余弦相似度,相似度越低说明该层对输入的"改造"越大,因而越重要。llama-imatrix天然具备收集激活的条件——它在回调解中能同时拿到每个矩阵乘的输入激活(src1->data)与输出激活(t->data)。
当前仓库 examples/imatrix/imatrix.cpp 中对应的数学实现非常直白:
static inline double cosine_similarity(int n, const float * x, const float * y) { double sumxy = 0, sumx2 = 0, sumy2 = 0; for (int j = 0; j < n; ++j) { sumxy += x[j]*y[j]; sumx2 += x[j]*x[j]; sumy2 += y[j]*y[j]; } double cos_sim = sumx2 > 0 && sumy2 > 0 ? sumxy/sqrt(sumx2*sumy2) : 0; return cos_sim; } static inline void collect_cos_similarity(int nrow, int n, const float * x, const float * y, std::pair<double, int>& p) { for (int row = 0; row < nrow; ++row) { p.first += cosine_similarity(n, x, y); p.second += 1; x += n; y += n; } }std::pair<double, int>的第一分量累加所有 token 行(nrow)上的余弦相似度之和,第二分量记录行数,最终平均值即该层的cos_sim分数。
收集路径:layer / attention / ffn 三组分数
在 collect_imatrix 回调解中,-lsim模式维护三组独立的统计量(见 examples/imatrix/imatrix.cpp 与 examples/imatrix/imatrix.cpp):
m_layer_sim:每个 block 的首个张量输入与上一层首个张量输入之间的相似度,度量"整个层"对输入的改变;m_attn_sim:每个 block 内 FFN 张量(名字含.ffn_)的输入与层首张量输入之间的相似度,度量 attention 子层的改变量;m_ffn_sim:当层内遇到 FFN 张量时,将上一时刻保存的m_ffn_input与当前输入对比,度量 FFN 子层的改变量。
从源码结构看,实现巧妙地复用了 imatrix 的回调机制:is_named_imatrix_tensor(examples/imatrix/imatrix.cpp)保证blk.*张量必然进入回调,而layer_index()负责从张量名(如blk.25.ffn_down_exps.weight)解析出层号,从而把分数挂到对应的层索引上。层号边界切换时还会做尺寸一致性检查("Oops: inconsistent ffn vs last_input size"),防止跨层误配。
实操第一步:运行 llama-imatrix 收集层相似度
启用方式
-lsim是llama-imatrix的专属参数,与主程序参数分离解析(见 examples/imatrix/imatrix.cpp 的独立参数循环),不会污染 common 参数解析。用法:
./build/bin/llama-imatrix \ --verbosity 1 \ --layer-similarity \ -m /path/to/Model-Q8_0.gguf \ -f calibration_data.txt \ -o imatrix-$(git rev-parse --short HEAD).dat \ --ctx-size 512 \ --numa numactl \ --threads 128文档中的真实运行示例(DeepSeek-V3-0324 Q8_0,671B 模型,61 个 block,3 个 dense 前导层 + 58 个 routed expert 层):
numactl -N 1 -m 1 \ ./build/bin/llama-imatrix \ --verbosity 1 \ --layer-similarity \ -m /mnt/ai/models/ubergarm/DeepSeek-V3-0324-GGUF/DeepSeek-V3-0324-Q8_0.gguf \ -f calibration_data_v5_rc.txt \ -o /mnt/ai/models/ubergarm/DeepSeek-V3-0324-GGUF/imatrix-ubergarm-DeepSeek-V3-0324-ik_llamacpp-$(git rev-parse --short HEAD).dat \ --ctx-size 512 \ --numa numactl \ --threads 128命令中涉及的关键参数说明:
| 参数 | 含义 |
|---|---|
-m | 输入模型路径,建议使用 f16 或 Q8_0 等近无损精度的 GGUF,激活统计才可靠 |
-f | 校准文本(如wiki.train.raw或自建语料),tokenize 后按 chunk 逐块前向计算 |
-o | imatrix 输出文件;文档示例中拼接git rev-parse --short HEAD以便区分版本 |
--layer-similarity/-lsim | 开启层相似度收集,结束后打印排序后的层重要性 |
--ctx-size 512 | 上下文长度,llama-imatrix默认即为 512 |
--numa numactl/--threads 128 | 多路服务器上的 NUMA 绑定与线程数,普通机器可省略 |
运行过程与输出解读
文档记录了一次完整运行的输出。模型加载后,计算阶段会逐 chunk 打印进度与即时困惑度:
compute_imatrix: tokenizing the input .. compute_imatrix: tokenization took 309.837 ms compute_imatrix: computing over 213 chunks with batch_size 512 compute_imatrix: 37.90 seconds per pass - ETA 2 hours 14.55 minutes [1]60.9619,[2]10.7701,[3]5.8724,[4]3.7883,[5]2.9691,[6]2.5089,[7]2.2199,[8]2.0199,[9]1.9095, ... [210]3.5342,[211]3.5134,[212]3.4930,[213]3.4727, Final estimate: PPL = 3.4727 +/- 0.03300值得注意的两点:
- MoE 专家覆盖警告:
save_imatrix: entry 'blk.60.ffn_down_exps.weight' has partial data (99.61%) 1 out of 256 experts are missing data Storing **but be aware**——对 256 专家的 DeepSeek MoE 层,某些专家在本次校准语料中未被路由激活,imatrix 会照常存储但明确告警,后续量化时这些张量的统计不完全。 - checkpoint 保存:每 10 个 chunk 自动保存一次
.dat文件,中途中断可结合--in-file续跑(见 examples/imatrix/README.md)。
排序后的层重要性输出
计算结束后,-lsim会打印三组排序结果,分别对应层整体、attention 子层、FFN 子层(打印逻辑见 print_layer_importance,按|cos_sim|升序排列,越靠前、cos_sim 越小 = 该层对输入的改变越大 = 越重要):
======================== sorted layer importances 0: Layer 0, <cos_sim> = 0.517453 1: Layer 60, <cos_sim> = 0.59436 2: Layer 8, <cos_sim> = 0.857555 3: Layer 3, <cos_sim> = 0.858137 ... 60: Layer 42, <cos_sim> = 0.971662 ======================== sorted attention importances 0: Layer 0, <cos_sim> = 0.13174 1: Layer 8, <cos_sim> = 0.516951 ... ======================== sorted ffn importances 0: Layer 7, <cos_sim> = 0.571293 1: Layer 10, <cos_sim> = 0.590428 ...从文档的实验记录看,DeepSeek-V3-0324 上排在"最需优先量化保护"前列的 routed expert 层(3–60 区间)为:3, 4, 5, 6, 7, 8, 15, 17, 22, 23, 24, 25, 53, 57, 58, 59, 60——这 17 个层被选作 A/B 实验中的"优先分配更多 bit"对象。
实操第二步:把层排序翻译成--custom-q正则
得到层排序后,下一步就是用llama-quantize --custom-q表达"哪些层多给 bit、哪些层少给 bit"。--custom-q的语法为regex1=type1,regex2=type2,...,对匹配不同正则的张量使用不同量化类型(参数解析见 examples/quantize/quantize.cpp 附近,帮助文本见 examples/quantize/quantize.cpp)。
文档提供了两组互为对照的完整配置,量化后模型体积完全一致,仅"哪 17 个 routed expert 层获得更多 bit"不同。
基线:FIRST-N-IQ3_K_R4(朴素取前 17 层)
# Routed Experts (3-60) (CPU) # Prioritize first 17 layers with larger quants blk\.[3-9]\.ffn_down_exps\.weight=iq5_k_r4 blk\.[1][0-9]\.ffn_down_exps\.weight=iq5_k_r4 blk\.[2-5][0-9]\.ffn_down_exps\.weight=iq4_k_r4 blk\.60\.ffn_down_exps\.weight=iq4_k_r4 blk\.[3-9]\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.[1][0-9]\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.[2-5][0-9]\.ffn_(gate|up)_exps\.weight=iq3_k_r4 blk\.60\.ffn_(gate|up)_exps\.weight=iq3_k_r4量化后的张量构成(llama_model_loader 输出):
llama_model_loader: - type f32: 361 tensors llama_model_loader: - type iq6_k: 1 tensors llama_model_loader: - type q6_0_r4: 61 tensors llama_model_loader: - type iq3_k_r4: 82 tensors llama_model_loader: - type iq4_k_r4: 75 tensors llama_model_loader: - type iq5_k_r4: 567 tensors对照组:COSSIM-IQ3_K_R4(按 cos_sim 排序选 17 层)
# Routed Experts (3-60) (CPU) # Prioritize quantizing 17 layers given by lowest cos similarity scores with larger bpw quant size blk\.3\.ffn_down_exps\.weight=iq5_k_r4 blk\.4\.ffn_down_exps\.weight=iq5_k_r4 blk\.5\.ffn_down_exps\.weight=iq5_k_r4 blk\.6\.ffn_down_exps\.weight=iq5_k_r4 blk\.7\.ffn_down_exps\.weight=iq5_k_r4 blk\.8\.ffn_down_exps\.weight=iq5_k_r4 blk\.15\.ffn_down_exps\.weight=iq5_k_r4 blk\.17\.ffn_down_exps\.weight=iq5_k_r4 blk\.22\.ffn_down_exps\.weight=iq5_k_r4 blk\.23\.ffn_down_exps\.weight=iq5_k_r4 blk\.24\.ffn_down_exps\.weight=iq5_k_r4 blk\.25\.ffn_down_exps\.weight=iq5_k_r4 blk\.53\.ffn_down_exps\.weight=iq5_k_r4 blk\.57\.ffn_down_exps\.weight=iq5_k_r4 blk\.58\.ffn_down_exps\.weight=iq5_k_r4 blk\.59\.ffn_down_exps\.weight=iq5_k_r4 blk\.60\.ffn_down_exps\.weight=iq5_k_r4 ## remainder blk\.[3-9]\.ffn_down_exps\.weight=iq4_k_r4 blk\.[1-5][0-9]\.ffn_down_exps\.weight=iq4_k_r4 # Same for gate/up blk\.3\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.4\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.5\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.6\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.7\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.8\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.15\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.17\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.22\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.23\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.24\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.25\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.53\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.57\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.58\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.59\.ffn_(gate|up)_exps\.weight=iq4_k_r4 blk\.60\.ffn_(gate|up)_exps\.weight=iq4_k_r4 ## remainder blk\.[3-9]\.ffn_(gate|up)_exps\.weight=iq3_k_r4 blk\.[1-5][0-9]\.ffn_(gate|up)_exps\.weight=iq3_k_r4 blk\.60\.ffn_(gate|up)_exps\.weight=iq3_k_r4从这两份配置可以提炼出几条可迁移的写作规则:
- 正则基于 GGUF 张量名,如
blk\.3\.ffn_down_exps\.weight,.必须转义; - 用
[3-9]、[1][0-9]、[1-5][0-9]等字符类表达层号区间,按规则出现的先后顺序匹配; ffn_(gate|up)_exps用括号分组一次覆盖ffn_gate_exps与ffn_up_exps;- 若同一张量被多条规则命中,需要自行确保规则顺序正确、互斥,避免歧义;
- 本例中"更重要的 17 层"用
iq5_k_r4(down)/iq4_k_r4(gate、up),其余层降档到iq4_k_r4/iq3_k_r4,模型总 bit 数不变。
A/B 实验:同一体积下按层分配带来的 PPL 差异
文档记录了一次完整的 A/B 对比,两个 quant 体积完全一致,唯一差异是"哪些 routed expert 层获得更高 bit 数":
FIRST-N:无脑给前 17 个 routed expert 层(3–19)更多 bit;COSSIM:根据--layer-similarity输出的 cos_sim 排序,给得分最低(最"重要")的 17 层更多 bit。
结果(V3-0324 上单次实验):
* FIRST-N Final estimate: PPL = 3.3193 +/- 0.01830 * COSSIM Final estimate: PPL = 3.3151 +/- 0.0182作者 ubergarm 的结论:
使用 PR#328 的
llama-imatrix --layer-similarity [-lsim]决定优先量化哪些层,在 V3-0324 的单次实验中比朴素地使用前 17 层获得了略好的困惑度。虽然差距仍在噪声范围内,但若进一步把分数应用到 attention 层量化上,可能还有提升空间(本次实验未涉及 attention 层)。
需要强调的诚实表述:这只是一次实验、差距在误差带内,不能据此断言"cos_sim 方法全面胜出";它证明了按层分配 bit 的量化管线在工程上是可行的,且分数来源可以替换为任何你认为更合理的判据。
讨论与局限性:方法学上的质疑
文档中最有信息量的部分,是项目维护者 ikawrakow 与 compilade 对初始 LIM 实现的逐条质疑,它们对理解该方法边界至关重要。
1. 反直觉结果:ffn_down竟然"最不重要"?
PR #326 初始版本按张量类型逐层计算输入余弦相似度后,输出显示ffn_down的 LIM 分数接近 0(例如 Layer 0 = -0.0005、Layer 1 = 0.0003),而attn_q_a/ffn_gate_shexp等却高达 -0.9 量级。ikawrakow 一针见血:
如果
ffn_down是最不重要的张量,那就太讽刺了,因为众所周知ffn_down的量化误差对可观测的量化质量影响最大。
2. 跨层比较在概念上的问题
ikawrakow 进一步指出,llama-imatrix收集到的"输入激活变化"并非由正在考察的那一个张量单独造成,而是该张量之后所有线性和非线性运算(GELU、SiLU、RoPE 变体、归一化组合等)共同作用的结果:
相邻两个层之间同一张量类型输入激活的差异,可以粗略估计整个层的重要性,但把它归因于这个特定张量类型的重要性,那就牵强了。
compilade 补充了同样的判断:这里实际计算的是模型各线性运算跨层输入的余弦相似度,其结果未必与张量相对重要性挂钩。
3. 更合理的替代方向:层内输入-输出对比
compilade 提出的改进思路是:利用llama-imatrix已经同时掌握的每个线性运算自己的输入(src1->data)与输出(t->data),在层内计算输入/输出点积并归一化来得到 LIM 分数,使每个层独立评估;但该方案只对输入输出同维的方阵矩阵乘直接成立,且跨 token 的聚合方式(大概率是平均)论文中并未明确。ikawrakow 也回应称,他曾在私有仓库中利用输入/输出激活推导过量化修正,但同样看不出如何据此得出张量重要性分数。
这些讨论直接促成了 PR #326 的关闭("Closing this in favor of implementation in PR#328"),也让最终落地在-lsim中的实现聚焦于**层级(layer / attention / ffn 三档)**的相似度排序,而不是张量类型级的 LIM 分数。读者若沿用此方法,应把分数当作"层相对重要性的经验排序",而非严格的理论依据。
进阶验证:用--show-statistics交叉检查 imatrix
文档最后提供了用主线上游实验性参数--show-statistics检查 imatrix 文件的方式,用于交叉验证不同层、不同张量的激活统计特征:
./build/bin/llama-imatrix \ --in-file /path/to/DeepSeek-V3-0324.imatrix \ --show-statistics输出为按张量名汇总的统计表,每一行给出某个层某张量的Σ(Bias)、Min、Max、μ、σ、% Active、样本数N、Entropy、E (norm)、ZD Score等指标。例如attn_kv_a_mqa在浅层(Layer 8)具有极高的 Max 值(30.78)与低熵(5.66),而深层(Layer 54)更平稳(Max 2.68、熵 11.81);ffn_down_exps在 Layer 60 出现异常大的 Σ(Bias)(1.4e9),对应文档前面"1 out of 256 experts are missing data"的告警。这类指标可以与 cos_sim 排序互相印证,辅助判断哪些层值得优先保护。
总结:从 LIM 探索到-lsim落地的完整链路
回顾整条技术路径,可以归纳为一条可复用的量化工作流:
- 准备:用 f16 / Q8_0 模型与有代表性的校准语料,运行
llama-imatrix --layer-similarity -m model.gguf -f train.txt -o imatrix.dat(examples/imatrix/imatrix.cpp 的-lsim分支会自动输出三组排序); - 决策:从
sorted layer/attention/ffn importances中挑出 cos_sim 最低(最重要)的层,确定每层的目标 bit 预算; - 表达:把选择翻译为
llama-quantize --custom-q的正则列表(语法见 examples/quantize/quantize.cpp); - 验证:用
llama-perplexity或llama-imatrix --in-file ... --show-statistics对比不同分配方案的 PPL 与统计特征。
同时应牢记文档与讨论揭示的边界:该方法最初是 WIP 实验(PR #326 已关闭),-lsim是其继承者(PR #328),余弦相似度提供的是经验性层排序;单个实验显示 COSSIM 方案与朴素 FIRST-N 方案 PPL 差距在噪声范围内;对于 MoE 模型,还要留意校准语料未覆盖全部专家导致的 partial data 告警。在把结论推广到其他模型(尤其非 DeepSeek 架构)之前,建议先用-lsim复跑一次本模型的分数,再决定量化预算的分配。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考