ik_llama.cpp 离线 Repack 实战指南:将已有 GGUF 重新打包为行交错 R4 变体,摆脱 --run-time-repack 并启用 mmap
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
导读
本文围绕 ik_llama.cpp 社区讨论 #323 的核心议题展开:如何将一个已经量化好的 GGUF(例如 DeepSeek-V3-0324 的 Q4_K_XL 量化版)**离线重新打包(repack)**为行交错(row-interleaved)的_R4变体,从而在加载模型时不再依赖--run-time-repack(-rtr)选项,进而重新获得mmap内存映射加载能力。读完本文,你将掌握llama-quantize --repack与--repack-pattern正则的完整用法、--override-tensor与 repack 正则之间的对应关系、多部分 GGUF 的处理方式,以及混合 CPU/GPU 部署下专家张量(exps 系列)放置与性能调试的实战经验。
背景:-rtr与 mmap 的取舍
在 ik_llama.cpp 中,--run-time-repack(-rtr)会在模型加载时把所有张量就地(in-place)重打包为对应的行交错变体(如果存在的话)。这种做法带来的直接代价可以在 common/common.cpp 的参数解析逻辑中看到:
if (arg == "-rtr" || arg == "--run-time-repack") { params.repack_tensors = true; params.use_mmap = false; return true; }也就是说,一旦启用-rtr,mmap会被强制关闭。这正是讨论发起人 Lissanro 面临的两难处境:
- 使用 Unsloth 发布的
DeepSeek-V3-0324-GGUF-UD-Q4_K_XL配合-rtr运行时,在 EPYC 7763(64 核)+ 1TB DDR4-3200 内存 + 4×RTX 3090 的机器上可以获得7+ tokens/s的生成速度; - 但
-rtr禁用 mmap 后,每次重新加载模型都需要大量计算(每次加载都要重新做一遍 repack),而他的工作流需要频繁切换模型(例如先用 72B 视觉模型处理图片,再切换回 DeepSeek-V3),切换成本被显著放大。
于是核心诉求就变成了:能不能把-rtr在加载时做的 repack 一次性离线完成、固化到一个新的 GGUF 文件里,这样加载时无需-rtr、可以继续使用 mmap、还能保住同样的性能。
核心概念:行交错(Row-Interleaved)R4 量化变体
要理解 repack,先要理解 ik_llama.cpp 特有的行交错量化类型。这类类型以_R4、_R8(以及更高阶的_R16)后缀命名,可以从 examples/quantize/quantize.cpp 中的QUANT_OPTIONS列表一览全貌,例如:
IQ2_K_R4("IQ2_K repacked")、IQ4_K_R4、IQ4_KS_R4、IQ5_KS_R4Q2_K_R4、Q3_K_R4、Q4_K_R4、Q5_K_R4、Q6_K_R4、Q8_K_R8IQ1_S_R4、IQ1_M_R4、IQ2_XXS_R4、IQ2_XS_R4、IQ3_XXS_R4、IQ3_S_R4Q4_0_R8、Q8_0_R8、Q8_KV_R8、BF16_R16、MXFP4_R8等
这些类型的共同点是:把若干行(row)的数据交错打包在一起,从而让内存布局更有利于 CPU 侧的矩阵乘法(特别是配合 MoE 专家张量在 CPU 上执行时),实现比传统同 bpw 类型更高的吞吐。也正因如此,它们通常被当作"运行在 CPU 内存上的张量"的首选格式。
从 src/llama-quantize.cpp 的repacked_ftype()映射表可以看出,每个传统量化类型都有且仅有一个对应的行交错变体,例如:
| 原类型 | repack 后类型 |
|---|---|
| Q4_K_S / Q4_K_M | Q4_K_R4 |
| Q5_K_S / Q5_K_M | Q5_K_R4 |
| Q6_K | Q6_K_R4 |
| Q8_K | Q8_K_R8 |
| IQ2_K / IQ3_K / IQ4_K / IQ5_K | IQ2_K_R4 / IQ3_K_R4 / IQ4_K_R4 / IQ5_K_R4 |
| IQ4_XS | IQ4_XS_R8 |
| Q4_0 / Q8_0 | Q4_0_R8 / Q8_0_R8 |
| BF16 | BF16_R16 |
重要限制:repack 只能把某个类型转换到它唯一的对应行交错变体,不能随意改造成别的量化类型。正如作者 ikawrakow 在讨论中明确回应的:"No. The repacking is only to the corresponding row-interleaved type. Repacking to something else would result in quality loss."(不行,repack 只针对对应的行交错类型,改成别的类型会导致质量损失。)例如IQ4_XS只能变成IQ4_XS_R4或IQ4_XS_R8(视映射而定),不会变成Q4_K_R4。
离线 Repack 实操:llama-quantize --repack
基础命令
离线 repack 正是由llama-quantize工具完成的,核心参数是--repack与--repack-pattern。作者 ikawrakow 给出的最简命令如下:
./bin/llama-quantize --repack \ --repack-pattern exps \ ~/models/DeepSeek-V3-0324-GGUF-UD-Q4_K_XL/DeepSeek-V3-0324-UD-Q4_K_XL-00001-of-00009.gguf \ repacked_model_file_name q4_k_r4逐项解读:
--repack:进入"仅 repack"模式,等价于把 examples/quantize/quantize.cpp 中的params.only_repack置为true,此时工具不做重新量化,只对匹配到的张量做类型重打包;--repack-pattern:逗号分隔的正则表达式列表,只有张量名匹配其中任一正则的张量才会被 repack(参数解析见 quantize.cpp)。exps会匹配 DeepSeek 这类 MoE 模型中以ffn_down_exps、ffn_up_exps、gate_exps结尾的专家张量;q4_k_r4:目标类型参数,这里表示把匹配到的张量转换为Q4_K_R4(大小写不敏感,工具内部会统一转大写后查表);- 输出文件:必须与输入模型是不同路径。命令不会覆盖原模型,因此磁盘上需要同时容纳新旧两份模型的空间(对于 DeepSeek-V3 这类数百 GB 的模型,务必提前确认磁盘余量)。
repack 正则与 --override-tensor 的对应关系
讨论中最实用的一条技巧是:--repack-pattern的正则可以直接从--override-tensor参数里抄过来,去掉=CPU部分即可。因为两者使用的都是张量名正则匹配机制。
例如,服务器启动参数中的:
--override-tensor "ffn_down_exps=CPU, ffn_up_exps=CPU, gate_exps=CPU"等价于更简洁的写法(--override-tensor本身支持正则):
--override-tensor exps=CPU而与之对应的 repack 命令则是:
./bin/llama-quantize --repack --repack-pattern "ffn_down_exps,ffn_up_exps,gate_exps" ... q4_k_r4即:你打算在 CPU 上运行的张量,就写进--repack-pattern;你打算放在 GPU 上的张量,就不要 repack(保持非 R4 类型)。这是整个 repack 部署策略的核心准则。
多部分 GGUF 的处理
DeepSeek-V3 / R1 的 GGUF 往往被拆成多个分片(例如-00001-of-00009.gguf)。ikawrakow 坦言自己从未 repack 过多部分 GGUF,不确定llama-quantize是否能正确加载全部分片;讨论参与者 saood06 则补充了关键细节:按 gguf-split 方式拆分的文件,必须用 gguf-split 工具合并,不能直接用cat拼接。因此,如果遇到多分片 repack 失败的情况,合理的做法是先合并分片得到单一 GGUF 文件,再执行 repack。
另外值得注意的是,Lissanro 在转换日志中看到llama_model_loader: additional 8 GGUFs metadata loaded.,说明在部分场景下加载器确实能够读取到其余分片,但为稳妥起见,仍建议优先保证单一文件输入。
实战案例:DeepSeek-R1 / V3 的专家张量离线 Repack
用户的最终 repack 命令
经过多轮调试,Lissanro 最终用于 R1 的离线 repack 命令如下:
~/pkgs/ik_llama.cpp/build/bin/llama-quantize --repack \ --repack-pattern "(^blk\.[7-9]|\d\d).ffn_(up|gate)_exps|ffn_down_exps" \ /mnt/secondary/neuro/DeepSeek-R1-GGUF_Q4_K_M-163840seq/DeepSeek-R1-Q4_K_M-00001-of-00011.gguf \ /home/lissanro/neuro/DeepSeek-R1-GGUF_Q4_K_M-163840seq/DeepSeek-R1-GGUF_Q4_K_M_R4.gguf \ q4_k_r4这个正则(^blk\.[7-9]|\d\d).ffn_(up|gate)_exps|ffn_down_exps的含义是:
(^blk\.[7-9]|\d\d).ffn_(up|gate)_exps:匹配第 7~9 层以及所有两位数的层(即 10 层及以上)的ffn_up_exps和ffn_gate_exps张量;ffn_down_exps:匹配所有ffn_down_exps张量。
这样精心构造的意图是:第 0~6 层(含一位数编号的层)的ffn_up_exps/ffn_gate_exps要保留在 GPU 上执行,因此不 repack;其余专家张量全部转为Q4_K_R4交给 CPU 处理。正如 ubergarm 所建议的:先规划好哪些层要放 GPU,把对应张量排除在 repack 之外,只把剩余的路由专家层 repack 成 CPU 友好格式。
配套的服务器启动命令
repack 之后,加载模型就无需-rtr,可以正常享受 mmap。Lissanro 的启动命令为:
taskset -c 0-63 ~/pkgs/ik_llama.cpp/build/bin/llama-server \ --model /home/lissanro/neuro/DeepSeek-R1-GGUF_Q4_K_M-163840seq/DeepSeek-R1-GGUF_Q4_K_M_R4.gguf \ --ctx-size 73728 --n-gpu-layers 62 --tensor-split 25,25,25,25 -mla 2 -fa -ctk q8_0 -amb 1024 -fmoe \ -ot "blk\.3\.ffn_up_exps=CUDA0, blk\.3\.ffn_gate_exps=CUDA0" \ -ot "blk\.4\.ffn_up_exps=CUDA1, blk\.4\.ffn_gate_exps=CUDA1" \ -ot "blk\.5\.ffn_up_exps=CUDA2, blk\.5\.ffn_gate_exps=CUDA2" \ -ot "blk\.6\.ffn_up_exps=CUDA3, blk\.6\.ffn_gate_exps=CUDA3" \ -ot "ffn_down_exps=CPU, ffn_up_exps=CPU, gate_exps=CPU" \ --threads 64 --host 0.0.0.0 --port 5000这里有两条非常实用的经验:
-ot(--override-tensor)支持正则,例如blk\.3\.ffn_up_exps=CUDA0中的\.是转义后的字面点号;正则匹配同样适用于 repack,因此--repack-pattern可以直接复用这些正则(去掉=设备部分)。- CPU 覆盖规则必须放在最后:Lissanro 实测发现,如果先写
ffn_down_exps=CPU, ffn_up_exps=CPU, gate_exps=CPU,再写各 GPU 的覆盖,CUDA 覆盖会不生效;把 CPU 兜底规则放在最后一条-ot,各 GPU 的逐层覆盖才真正生效。 - 由于单个
-ot参数无法表达多行格式,可以用多个-ot参数,每个参数一行,便于在脚本中组织可读性。
专家张量放置策略:up/gate 优先上 GPU
关于"剩余 VRAM 该放哪些张量",ikawrakow 给出了明确的策略性建议:在 VRAM 有富余时,优先把若干层的ffn_up_exps和ffn_gate_exps成对放进 GPU——这比只放某一个专家张量、或把三个专家张量都放进去收益更大,尤其是在开启-fmoe时。他在低配硬件(Ryzen 5975WX + RTX 4080)上实验 Llama-4-Scout 时使用的配置是一个很好的模板:
-ot "blk\.[0-9]\.ffn_up_exps=CUDA0,blk\.[0-9]\.ffn_gate_exps=CUDA0,blk\.1[0-9]\.ffn_up_exps=CUDA0,blk\.1[0-9]\.ffn_gate_exps=CUDA0,exps=CPU" -ngl 100其含义是:所有 attention 与共享专家张量放 GPU,前 20 层(blk.[0-9]与blk.1[0-9])的ffn_up_exps/ffn_gate_exps也放 GPU,其余专家(exps)全部留在 CPU。
至于具体放多少层,取决于剩余 VRAM 与张量大小——Lissanro 在 4×24GB VRAM、72K 上下文的约束下,最终只能在每张卡上各放一层(blk.3~blk.6)的 up/gate 专家对。
性能调试实录:mmap 与页缓存问题
离线 repack 与在线 repack 是否等价?
ikawrakow 明确表示:"The offline repacking command should produce a result that is 100% equivalent to what happens with online repacking."(离线 repack 命令的结果应当与在线 repack 100% 等价。)但他同时指出,两种运行方式下内存的分配与张量归属方式不同,因此性能表现可能不完全一致——只是在他的硬件上差异从未达到 Lissanro 报告的那么大。
Lissanro 实测对比(同一 prompt,多次运行取计时行):
- 原版 Unsloth 量化 +
-rtr:约7.26 / 7.29 / 7.56 tokens/s - 离线 repack 后(不带
-rtr):约4.28 / 5.07 / 3.96 tokens/s
他观察到 EPYC 7763 的 64 核在两种场景下都接近满载,怀疑转换后的量化在 CPU 侧存在额外瓶颈。
排查步骤:drop_caches、--no-mmap 与 BIOS
ikawrakow 建议先尝试清空页缓存再加载离线 repack 的模型:
echo 3 | sudo tee /proc/sys/vm/drop_cachesLissanro 照做后(系统 1TB 内存、无 swap,模型约 378GB,加载后仍有 300+GB 空闲),性能不升反降(从接近 7.5 掉到不足 4 tokens/s);随后又发现:
- 给 repack 后的模型加上
-rtr再跑,性能又恢复到 7.3~7.5 tokens/s(说明 repack 操作本身没问题); - 用
--no-mmap显式关闭 mmap、不带-rtr运行,性能恢复到 7.34 tokens/s——由此定位到问题出在 mmap 而非量化文件。
ubergarm 则补充了两点重要的 mmap 基准测试常识:
- mmap 模式下模型是懒加载进页缓存的,第一轮完整运行的计时通常会偏慢,需要先"预热"页缓存再统计;可以借助
btop观察磁盘 I/O 与cached来确认。而关闭 mmap 时启动更慢(要把整个模型读入 RAM),但后续运行更快。 - 关闭 mmap 时,系统可能自动利用透明大页(transparent huge pages),可以用
numastat -m -p $(pidof llama-server)(或 llama-bench 等进程)检查,该因素对性能的影响因系统而异。
最终 Lissanro 通过重置 BIOS、只保留必要设置解决了 mmap 下的性能问题——他怀疑是 BIOS 中关于内存吞吐的性能调优项影响了 mmap 访问效率。修复后,mmap 模式下 repack 模型的性能为:
- 上下文填充约 2.5K token 时:7.86 tokens/s;
- 32K 填充时:5.09 tokens/s;
- 64K+ 填充时:略高于 3 tokens/s,输入处理约 50~80 tokens/s;
- mmap 加载模型耗时约 45 秒(相比
-rtr每次重新计算 repack,切换模型的成本大幅下降)。
源码级原理支撑
1. repack 的目标类型由映射表决定
离线 repack 时,llama-quantize会读取输入模型的ftype,通过repacked_ftype()(见 src/llama-quantize.cpp)查出整模型的 repack 目标类型;对每个张量则通过iqk_repacked_type()判断是否存在行交错变体(相关逻辑见 src/llama-quantize.cpp)。若张量类型没有对应 R4/R8 变体,或张量名不匹配任何--repack-pattern正则,则保持原样写入新文件。
2. --repack 与 --repack-pattern 的参数解析
两个参数在 examples/quantize/quantize.cpp 中解析:--repack将params.only_repack置为true;--repack-pattern把逗号分隔的正则列表拆分为std::vector<std::string>。工具的 usage 帮助文本(quantize.cpp)也明确写道:
--repack Repack all tensors to the corresponding _r4/8 variant if available. --repack-pattern Comma separated list of regexs to use for matching tensor names to be repacked.3. -rtr 与 mmap 的绑定关系
如前文所述,common/common.cpp 中-rtr同时设置params.repack_tensors = true与params.use_mmap = false,这就是"启用-rtr必然失去 mmap"的代码根源。此外,src/llama-reload.cpp 也指出-rtr会在加载时把宿主张量就地 repack(类型变为行交错变体),且热切换(hotswap)恢复的张量无法复现 repack 后的状态——这意味着需要频繁切换模型的场景下,每次切换都要付出重新 repack 的计算代价,进一步凸显离线 repack 的价值。
总结与适用建议
- 一句话方案:用
llama-quantize --repack --repack-pattern <正则> <输入.gguf> <输出.gguf> <目标R4类型>把要留在 CPU 上的张量离线转成行交错变体,加载时去掉-rtr,即可恢复 mmap。 - 正则设计:
--repack-pattern的正则直接取自--override-tensor(去掉=设备后缀);只 repack CPU 侧张量,GPU 侧张量保持非 R4 类型。 - 类型限制:repack 只能转换为该类型唯一的对应行交错变体(如
Q4_K_M → Q4_K_R4),不能跨类型转换,否则会有质量损失。 - 多分片模型:gguf-split 拆分的分片应先用 gguf-split 合并成单文件再 repack。
- 性能验证:离线 repack 与在线 repack 理论上 100% 等价,但实际运行时的内存分配、页缓存状态、透明大页乃至 BIOS 设置都可能造成显著差异;mmap 场景下基准测试应预热页缓存后再统计。
- GPU 放置优先级:VRAM 富余时,优先把
ffn_up_exps与ffn_gate_exps成对放 GPU(配合-fmoe收益更大),ffn_down_exps可留 CPU;多-ot时 CPU 兜底规则要放在最后。
进一步参考:llama-quantize 工具源码、量化核心实现、命令行参数解析 与 工具使用文档,可在当前仓库中继续深入阅读。
【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考