首页
/ llama.cpp gguf-split:GGUF 大模型文件的分片与合并工具实战解析

llama.cpp gguf-split:GGUF 大模型文件的分片与合并工具实战解析

2026-09-06 13:14:56作者:昌雅子Ethen

本篇围绕 tools/gguf-split/README.md 展开,讲解 llama.cpp 提供的 llama-gguf-split 命令行工具如何把一个体积庞大的 GGUF 模型文件按张量数或字节大小拆分为多个分片,以及如何将分片无损合并回单一文件。读完后,你将掌握 --split--merge--split-max-tensors--split-max-size 等全部参数的用法、分片文件的命名与元数据约定,以及从源码层面理解分片策略是如何制定和执行的。

为什么需要 GGUF 分片

GGUF 是 llama.cpp 采用的模型容器格式,一个文件内同时存放元数据(词表、架构参数等)和全部张量数据。对于大参数量的模型,单个文件可能达到几十 GB,这会在以下场景造成麻烦:

  • 目标文件系统对单文件有大小限制(如 FAT32 的 4GB 上限);
  • 需要把模型切分到多个存储介质,或按张量分布到不同设备上加载;
  • 分发大模型时希望用户按需下载部分分片。

llama-gguf-split 就是为这些场景设计的:README 将其概括为一句话——"CLI to split / merge GGUF files"(用于拆分/合并 GGUF 文件的命令行工具)。

构建与命令行总览

该工具在 tools/gguf-split/CMakeLists.txt 中定义,目标名为 llama-gguf-split,链接 llama-commonllama 库,启用 C++17:

set(TARGET llama-gguf-split)
add_executable(${TARGET} gguf-split.cpp)
target_link_libraries(${TARGET} PRIVATE llama-common llama ${CMAKE_THREAD_LIBS_INIT})

用法格式(来自 split_print_usage):

usage: llama-gguf-split [options] GGUF_IN GGUF_OUT
Apply a GGUF operation on IN to OUT.

两个位置参数分别是输入 GGUF(GGUF_IN)和输出路径(GGUF_OUT)。对于拆分操作,GGUF_OUT 是输出文件的前缀,工具会自动追加分片后缀;对于合并操作,GGUF_OUT 是最终合并产物的完整文件名。

命令行参数

README 列出的核心选项,结合 参数解析源码 补充的完整清单如下:

参数 说明 默认值
--split 拆分 GGUF 为多个 GGUF 文件(默认操作,可省略) 未指定操作时即为 split
--merge 合并多个 GGUF 分片为单个 GGUF。只需指定第一个分片路径与合并输出路径,工具会在同一目录内自动找到其余分片
--split-max-tensors N 每个分片最多包含的张量数 128
--split-max-size N(M|G) 每个分片的最大字节数,如 500M2G
--no-tensor-first-split 第一个分片不放任何张量(仅含元数据),默认关闭 关闭
--dry-run 只打印分片计划,不写出任何新文件 关闭
--delete-splits 合并完成后删除分片文件以释放磁盘空间。注意:源码注释明确警告,若合并中途失败将进入不可恢复状态 关闭
-h / --help--version 帮助与版本信息

参数解析中有两条互斥约束值得注意(split_params_parse_ex):

  1. --split--merge 不能同时指定;
  2. --split-max-tensors--split-max-size 不能同时指定——即"按张量数拆"和"按字节大小拆"两种策略二选一。

另外,M/G 单位在 split_str_to_n_bytes 中按十进制换算(1M = 1000×1000 字节,1G = 1000×1000×1000 字节),并非 1024 的倍数,且必须为正数,否则会抛出异常。

拆分(split):两种策略的制定与执行

分片策略如何制定

拆分的核心逻辑在 split_strategy 类中。构造时它会遍历输入文件中的每一个张量,根据当前策略判断是否开启新分片:

bool should_split(int i_tensor, size_t next_size) {
    if (params.mode == MODE_SIZE) {
        // split by max size per file
        return next_size > params.n_bytes_split;
    } else if (params.mode == MODE_TENSOR) {
        // split by number of tensors per file
        return i_tensor > 0 && i_tensor < n_tensors && i_tensor % params.n_split_tensors == 0;
    }
    GGML_ABORT("invalid mode");
}
  • 按张量数(MODE_TENSOR):当遍历到第 n_split_tensors 的整数倍位置(从第 1 个索引起)就切一刀。张量本身不会被截断——只有"整张量跨文件"的切分,因此分片数约为 ceil(总张量数 / 每片张量数)
  • 按字节大小(MODE_SIZE):累加张量大小(含 GGUF_DEFAULT_ALIGNMENT 对齐后的体积),一旦"当前分片已占大小 + 下一个张量大小"超过 --split-max-size 阈值,就在新分片放下一个张量。这保证了单个分片不会明显超出目标体积,但某个张量本身大于上限时无法避免单张量独占一个分片。

策略构建阶段还有一项校验:若某个分片最终张量为 0(上限设置得过于激进),程序直接报错退出(gguf-split.cpp#L232-L235)。

分片文件如何组织

三个关键元数据键定义在 common/common.h#L1120-L1122

const char * const LLM_KV_SPLIT_NO            = "split.no";
const char * const LLM_KV_SPLIT_COUNT         = "split.count";
const char * const LLM_KV_SPLIT_TENSORS_COUNT = "split.tensors.count";
  • split.no:当前分片的序号;
  • split.count:分片总数;
  • split.tensors.count:原始模型的张量总数。

全部元数据(KV)只写入第一个分片,其余分片只写 split.* 三个键(split_strategy 构造函数)。这也是为什么合并时只需第一个分片就能恢复完整元数据。

输出文件名由 llama_split_path 按固定格式生成:

static const char * const SPLIT_PATH_FORMAT = "%s-%05d-of-%05d.gguf";

<前缀>-00001-of-00012.gguf<前缀>-00002-of-00012.gguf……序号与总数均为 5 位补零。写文件时,每个张量数据按原偏移从输入文件读出,写入输出文件,并按 GGUF_DEFAULT_ALIGNMENT 补零对齐(write() 实现)。

拆分实战示例

来自仓库自带的 tests.sh,以 Qwen3-0.6B-Q8_0 模型为例:

# 每片最多 28 个张量,前缀为 ggml-model-split
llama-gguf-split --split-max-tensors 28 $WORK_PATH/Qwen3-0.6B-Q8_0.gguf $WORK_PATH/ggml-model-split

# 按 500MB 上限拆分
llama-gguf-split --split-max-size 500M $WORK_PATH/ggml-model-merge.gguf $WORK_PATH/ggml-model-split-500M

拆分前程序会先打印分片计划(n_split、每片的张量数与总大小,见 print_info),加 --dry-run 则只打印计划、不落盘,适合先评估拆分效果。

拆分出的第一个分片(...-00001-of-00012.gguf)本身就携带完整元数据,因此 llama.cpp 的模型加载器可以直接加载任意一个分片来加载整个模型——tests.sh 正是用 llama-completion 加载 ggml-model-split-00001-of-00012.gguf 来验证分片模型推理正常的(llama-model-loader.cpp 对 split.count/split.no 的读取印证了加载器对这些元数据键的依赖):

llama-completion -no-cnv --model ggml-model-split-00001-of-00012.gguf \
  -p "I believe the meaning of life is" --n-predict 32

合并(merge):从第一个分片还原完整文件

合并是拆分的逆操作。README 强调的关键点:只需给出第一个分片的名字,工具会自己推算并找到同目录下的其余分片。流程在 gguf_merge 中:

  1. 拒绝覆盖:若输出文件已存在,直接报错退出;
  2. 读第一个分片:校验其中必须存在 split.count 元数据并得到分片总数 n_split
  3. 反解前缀llama_split_prefix 从第一个分片路径中剥离 -NNNNN-of-MMMMM.gguf 后缀得到前缀,并以此验证文件名是否符合分片命名规范,之后逐片用 llama_split_path 拼出 -00002-of-...-00003-of-... 的路径;
  4. 重建元数据:把第一个分片的 KV 拷入新的输出上下文(同时把 split.count 置 0,避免这个合并产物再被当作分片去二次合并);
  5. 逐片搬运张量:按顺序从每个分片读取张量数据写入输出文件(同样做对齐补零);
  6. 回填元数据:由于元数据大小只有在张量列表构建完成后才确定,程序先写一段占位零,最后 seekp(0) 回到文件头写入真正的元数据;
  7. 可选清理:若指定 --delete-splits,每写完一个分片的数据即 std::remove 删除该分片。

合并示例(tests.sh 第 3 步):

llama-gguf-split --merge ggml-model-split-00001-of-00012.gguf ggml-model-merge.gguf

--no-tensor-first-split:元数据独占首片

--no-tensor-first-split 会让第一个分片不承载任何张量(构造 split_strategy 时连续创建两个输出上下文,gguf-split.cpp#L251-L254)。这种模式下,分片 1 只保存元数据,实际张量从分片 2 开始排布,常见于希望"头文件"与"权重分片"分离的加载方案。tests.sh 中的用法:

llama-gguf-split --split-max-tensors 32 --no-tensor-first-split \
  ggml-model-merge.gguf ggml-model-split-32-tensors

注意该模式下若总张量数为 128 的整数倍,由于首片被跳过,分片总数与常规模式可能差一(tests.sh 中该模型产生了 11 个分片而非 12 个)。

从源码看可靠性与限制

  • 只按张量边界切分:两种模式都只对整张量操作,不存在张量中途截断,因此拆分-合并是可逆的;
  • 缓冲拷贝:张量数据经内存缓冲区逐张量 seekg/read/writecopy_file_to_file),源码中留有 TODO,说明未来可能改用 copy_file_range() 提升性能;
  • --delete-splits 的风险:分片在合并过程中被即时删除,一旦中途出错(磁盘满、进程被杀),源分片已不可恢复——源码帮助信息中专门加粗了这条警告;
  • 合并产物不带分片标记:合并时把 split.count 清零,防止对输出文件再次执行合并;
  • 文件名强约束:分片必须遵循 %s-%05d-of-%05d.gguf 格式且全部位于同一目录,llama_split_prefix 解析失败会直接以 "unexpected input file name" 报错退出。

小结与典型工作流

llama-gguf-split 的完整闭环可以概括为:

# 1. 拆分(二选一)
llama-gguf-split --split-max-tensors 128 model.gguf model-split
llama-gguf-split --split-max-size 2G model.gguf model-split

# 2. 用任意一个分片直接加载验证
llama-completion --model model-split-00001-of-0000N.gguf -p "hi" --n-predict 32

# 3. 合并回单文件
llama-gguf-split --merge model-split-00001-of-0000N.gguf model-merged.gguf

工具全部实现集中在 tools/gguf-split/gguf-split.cpp(约 600 行),依赖 src/llama.cpp 提供的 llama_split_path/llama_split_prefix 命名辅助函数,并通过 split.no/split.count 元数据与 llama-model-loader.cpp 中的分片加载逻辑形成约定。需要强调的是:该工具只做字节层面的搬运与元数据重组,不改变任何张量内容,合并结果与源模型等价(可通过 tests.sh 中"分片/合并后分别跑一次推理验证"的流程自行确认)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389