llama.cpp gguf-split:GGUF 大模型文件的分片与合并工具实战解析
本篇围绕 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-common 与 llama 库,启用 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) |
每个分片的最大字节数,如 500M、2G |
— |
--no-tensor-first-split |
第一个分片不放任何张量(仅含元数据),默认关闭 | 关闭 |
--dry-run |
只打印分片计划,不写出任何新文件 | 关闭 |
--delete-splits |
合并完成后删除分片文件以释放磁盘空间。注意:源码注释明确警告,若合并中途失败将进入不可恢复状态 | 关闭 |
-h / --help、--version |
帮助与版本信息 | — |
参数解析中有两条互斥约束值得注意(split_params_parse_ex):
--split与--merge不能同时指定;--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 中:
- 拒绝覆盖:若输出文件已存在,直接报错退出;
- 读第一个分片:校验其中必须存在
split.count元数据并得到分片总数n_split; - 反解前缀:llama_split_prefix 从第一个分片路径中剥离
-NNNNN-of-MMMMM.gguf后缀得到前缀,并以此验证文件名是否符合分片命名规范,之后逐片用llama_split_path拼出-00002-of-...、-00003-of-...的路径; - 重建元数据:把第一个分片的 KV 拷入新的输出上下文(同时把
split.count置 0,避免这个合并产物再被当作分片去二次合并); - 逐片搬运张量:按顺序从每个分片读取张量数据写入输出文件(同样做对齐补零);
- 回填元数据:由于元数据大小只有在张量列表构建完成后才确定,程序先写一段占位零,最后
seekp(0)回到文件头写入真正的元数据; - 可选清理:若指定
--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/write(copy_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 中"分片/合并后分别跑一次推理验证"的流程自行确认)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00