GGUF 文件格式全解析
在机器学习领域,模型的存储和部署一直是关键环节。随着大语言模型 (LLM) 的广泛应用,如何高效地存储和加载这些复杂的模型成为一个亟待解决的问题。GGUF(GGML Universal Format)作为一种新兴的二进制文件格式,旨在解决传统 GGML 及其衍生格式(如 GGMF 和 GGJT)的局限性,为模型推理提供更高效、更灵活的解决方案。
官方介绍:https://github.com/ggml-org/ggml/blob/master/docs/gguf.md
GGUF 文件格式全解析
- 一、关于 GGUF
- 二、GGUF 规范
- 三、GGUF 命名约定
- 四、GGUF 文件结构
一、关于 GGUF
GGUF 是一种用于存储模型以便与 GGML 及其基于 GGML 的执行器进行推理的文件格式。GGUF 是一种二进制格式,设计目的是为了快速加载和保存模型,并便于读取。模型通常使用 PyTorch 或其他框架开发,然后转换为 GGUF 以在 GGML 中使用。
它是 GGML、GGMF 和 GGJT 的后继文件格式,旨在通过包含加载模型所需的所有信息来消除歧义,同时设计为可扩展,以便在不破坏兼容性的情况下为模型添加新信息。
二、GGUF 规范
GGUF 是一种基于现有 GGJT 格式的格式,但对其进行了一些更改,使其更具扩展性和易用性。以下是期望的功能:
- 单文件部署:易于分发和加载,不需要额外的外部文件提供附加信息。
- 可扩展性:可以在不破坏现有模型兼容性的情况下,为基于 GGML 的执行器添加新功能或为 GGUF 模型添加新信息。
- mmap 兼容性:模型可以使用 mmap 加载,以实现快速加载和保存。
- 易于使用:通过少量代码即可轻松加载和保存模型,无需外部库,且不受所用语言限制。
- 完整信息:加载模型所需的所有信息都包含在模型文件中,用户无需提供额外信息。
GGJT 与 GGUF 的关键区别在于,超参数(现称为元数据)使用了键值结构,而不是无类型的数值列表。这允许在不破坏现有模型兼容性的情况下添加新元数据,并为模型添加可能对推理或识别模型有用的附加信息。
三、GGUF 命名约定
GGUF 遵循 .gguf 的命名约定,其中每个组件如果存在则由 - 分隔。最终目的是让人类能够一目了然地了解模型的最重要细节。由于现有 GGUF 文件名的多样性,这一命名约定并非旨在实现完美的字段解析。
组件包括:
- BaseName:模型基础类型或架构的描述性名称。
- 可从 GGUF 元数据 general.basename 派生,将空格替换为短划线。
- SizeLabel:参数权重类别(适用于排行榜),表示为 x。
- 可从 GGUF 元数据 general.size_label 派生(若存在),或在缺失时计算。
- 支持带单个字母比例前缀的四舍五入小数点计数,以辅助显示浮点指数,如下所示:
- Q:千万亿参数。
- T:万亿参数。
- B:十亿参数。
- M:百万参数。
- K:千参数。
- 可根据需要附加额外的 - 来表示其他感兴趣的属性。
- FineTune:模型微调目标的描述性名称(例如 Chat、Instruct 等)。
- 可从 GGUF 元数据 general.finetune 派生,将空格替换为短划线。
- Version:(可选)表示模型版本号,格式为 v.。
- 如果模型缺少版本号,则假定为 v1.0(首次公开发布)。
- 可从 GGUF 元数据 general.version 派生。
- Encoding:表示应用于模型的权重编码方案。内容、类型混合和排列由用户代码决定,可能因项目需求而异。
- Type:表示 GGUF 文件的类型及其预期用途。
- 如果缺失,则文件默认是一个典型的 GGUF 张量模型文件。
- LoRA:GGUF 文件是一个 LoRA 适配器。
- vocab:仅包含词汇表数据和元数据的 GGUF 文件。
- Shard:(可选)表示模型已被分成多个分片,格式为 -of-。
- ShardNum:此模型中的分片位置,必须为 5 位数字,用零填充。
- 分片编号始终从 00001 开始(例如,第一个分片始终为 00001-of-XXXXX,而不是 00000-of-XXXXX)。
- ShardTotal:此模型中的分片总数,必须为 5 位数字,用零填充。
- ShardNum:此模型中的分片位置,必须为 5 位数字,用零填充。
验证上述命名约定
至少,所有模型文件应包含 BaseName、SizeLabel 和 Version,以便轻松验证其是否符合 GGUF 命名约定。例如,如果省略 Version,则 Encoding 可能被误认为是 FineTune。
可以使用以下正则表达式进行验证:^(?<BaseName>[A-Za-z0-9\s]*(?:(?:-(?:(?:[A-Za-z\s][A-Za-z0-9\s]*)|(?:[0-9\s]*)))*))-(?:(?<SizeLabel>(?:\d+x)?(?:\d+\.)?\d+[A-Za-z](?:-[A-Za-z]+(\d+\.)?\d+[A-Za-z]+)?)(?:-(?<FineTune>[A-Za-z0-9\s-]+))?)?-(?:(?<Version>v\d+(?:\.\d+)*))(?:-(?<Encoding>(?!LoRA|vocab)[\w_]+))?(?:-(?<Type>LoRA|vocab))?(?:-(?<Shard>\d{5}-of-\d{5}))?\.gguf$
,它将检查 BaseName、SizeLabel 和 Version 是否按照正确顺序存在。
例如:
- Mixtral-8x7B-v0.1-KQ2.gguf:
- 模型名称:Mixtral
- 专家数量:8
- 参数数量:7B
- 版本号:v0.1
- 权重编码方案:KQ2
- Hermes-2-Pro-Llama-3-8B-F16.gguf:
- 模型名称:Hermes 2 Pro Llama 3
- 专家数量:0
- 参数数量:8B
- 版本号:v1.0
- 权重编码方案:F16
- 分片:无
- Grok-100B-v1.0-Q4_0-00003-of-00009.gguf:
- 模型名称:Grok
- 专家数量:0
- 参数数量:100B
- 版本号:v1.0
- 权重编码方案:Q4_0
- 分片:共 9 个分片中的第 3 个
四、GGUF 文件结构
GGUF 文件结构如下。它们使用由 general.alignment 元数据字段指定的全局对齐(下文称为 ALIGNMENT)。必要时,文件会用 0x00 字节填充至 general.alignment 的下一个倍数。
字段(包括数组)按顺序写入,除非另有说明,否则不进行对齐。
模型默认使用小端序(little-endian)。它们也可以使用大端序(big-endian)以适应大端序计算机;在这种情况下,所有值(包括元数据值和张量)也将是大端序。目前无法确定模型是否为大端序,这一问题可能在未来版本中得到修正。如果未提供额外信息,则假定模型为小端序。
enum ggml_type: uint32_t {
GGML_TYPE_F32 = 0,
GGML_TYPE_F16 = 1,
GGML_TYPE_Q4_0 = 2,
GGML_TYPE_Q4_1 = 3,
// GGML_TYPE_Q4_2 = 4, 支持已移除
// GGML_TYPE_Q4_3 = 5, 支持已移除
GGML_TYPE_Q5_0 = 6,
GGML_TYPE_Q5_1 = 7,
GGML_TYPE_Q8_0 = 8,
GGML_TYPE_Q8_1 = 9,
GGML_TYPE_Q2_K = 10,
GGML_TYPE_Q3_K = 11,
GGML_TYPE_Q4_K = 12,
GGML_TYPE_Q5_K = 13,
GGML_TYPE_Q6_K = 14,
GGML_TYPE_Q8_K = 15,
GGML_TYPE_IQ2_XXS = 16,
GGML_TYPE_IQ2_XS = 17,
GGML_TYPE_IQ3_XXS = 18,
GGML_TYPE_IQ1_S = 19,
GGML_TYPE_IQ4_NL = 20,
GGML_TYPE_IQ3_S = 21,
GGML_TYPE_IQ2_S = 22,
GGML_TYPE_IQ4_XS = 23,
GGML_TYPE_I8 = 24,
GGML_TYPE_I16 = 25,
GGML_TYPE_I32 = 26,
GGML_TYPE_I64 = 27,
GGML_TYPE_F64 = 28,
GGML_TYPE_IQ1_M = 29,
GGML_TYPE_COUNT,
};
enum gguf_metadata_value_type: uint32_t {
// 值是 8 位无符号整数。
GGUF_METADATA_VALUE_TYPE_UINT8 = 0,
// 值是 8 位有符号整数。
GGUF_METADATA_VALUE_TYPE_INT8 = 1,
// 值是 16 位无符号小端整数。
GGUF_METADATA_VALUE_TYPE_UINT16 = 2,
// 值是 16 位有符号小端整数。
GGUF_METADATA_VALUE_TYPE_INT16 = 3,
// 值是 32 位无符号小端整数。
GGUF_METADATA_VALUE_TYPE_UINT32 = 4,
// 值是 32 位有符号小端整数。
GGUF_METADATA_VALUE_TYPE_INT32 = 5,
// 值是 32 位 IEEE754 浮点数。
GGUF_METADATA_VALUE_TYPE_FLOAT32 = 6,
// 值是布尔值。
// 1 字节值,其中 0 表示假,1 表示真。
// 其他任何值均无效,应视为模型无效或读取器有错误。
GGUF_METADATA_VALUE_TYPE_BOOL = 7,
// 值是 UTF-8 非空终止字符串,前置长度。
GGUF_METADATA_VALUE_TYPE_STRING = 8,
// 值是其他值的数组,前置长度和类型。
// 数组可以嵌套,数组长度是元素数量而非字节数。
GGUF_METADATA_VALUE_TYPE_ARRAY = 9,
// 值是 64 位无符号小端整数。
GGUF_METADATA_VALUE_TYPE_UINT64 = 10,
// 值是 64 位有符号小端整数。
GGUF_METADATA_VALUE_TYPE_INT64 = 11,
// 值是 64 位 IEEE754 浮点数。
GGUF_METADATA_VALUE_TYPE_FLOAT64 = 12,
};
// GGUF 中的字符串。
struct gguf_string_t {
// 字符串的字节长度。
uint64_t len;
// UTF-8 非空终止字符串。
char string[len];
};
union gguf_metadata_value_t {
uint8_t uint8;
int8_t int8;
uint16_t uint16;
int16_t int16;
uint32_t uint32;
int32_t int32;
float float32;
uint64_t uint64;
int64_t int64;
double float64;
bool bool_;
gguf_string_t string;
struct {
// 任何值类型均有效,包括数组。
gguf_metadata_value_type type;
// 元素数量而非字节数
uint64_t len;
// 值数组。
gguf_metadata_value_t array[len];
} array;
};
struct gguf_metadata_kv_t {
// 元数据的键。它是一个标准的 GGUF 字符串,但有以下限制:
// - 必须是有效的 ASCII 字符串。
// - 必须是层次键,每段为 `lower_snake_case`,由 `.` 分隔。
// - 长度最多为 2^16-1/65535 字节。
// 不遵循这些规则的键均无效。
gguf_string_t key;
// 值的类型。
// 必须是 `gguf_metadata_value_type` 中的一个值。
gguf_metadata_value_type value_type;
// 值。
gguf_metadata_value_t value;
};
struct gguf_header_t {
// 魔数,表明这是一个 GGUF 文件。
// 在字节级别必须是 `GGUF`:`0x47` `0x47` `0x55` `0x46`。
// 您的执行器可能使用小端字节序,因此可能需要检查 0x46554747 并让字节序抵消。
// 在此建议非常明确地指定字节序。
uint32_t magic;
// 实现的格式版本。
// 对于本规范中描述的版本必须为 `3`,该版本引入了大端序支持。
// 仅在格式结构发生变化时才应增加此版本。
// 不影响文件结构的变化应通过更新元数据来表示。
uint32_t version;
// 文件中的张量数量。
// 明确包含此项,而不是将其放入元数据,以确保加载张量时始终存在。
uint64_t tensor_count;
// 元数据键值对的数量。
uint64_t metadata_kv_count;
// 元数据键值对。
gguf_metadata_kv_t metadata_kv[metadata_kv_count];
};
uint64_t align_offset(uint64_t offset) {
return offset + (ALIGNMENT - (offset % ALIGNMENT)) % ALIGNMENT;
}
struct gguf_tensor_info_t {
// 张量的名称。它是一个标准的 GGUF 字符串,但限制最多为 64 字节长。
gguf_string_t name;
// 张量的维度数。
// 当前最多为 4,但未来可能会改变。
uint32_t n_dimensions;
// 张量的维度。
uint64_t dimensions[n_dimensions];
// 张量的类型。
ggml_type type;
// 张量数据在此文件中的字节偏移量。
//
// 此偏移量相对于 `tensor_data`,而不是文件的开头,以方便写入器编写文件。
// 读取器应考虑将此偏移量暴露为相对于文件开头的值,以方便读取数据。
//
// 必须是 `ALIGNMENT` 的倍数。即 `align_offset(offset) == offset`。
uint64_t offset;
};
struct gguf_file_t {
// 文件头部。
gguf_header_t header;
// 张量信息,可用于定位张量数据。
gguf_tensor_info_t tensor_infos[header.tensor_count];
// 填充至最接近的 `ALIGNMENT` 倍数。
//
// 即,如果 `sizeof(header) + sizeof(tensor_infos)` 不是 `ALIGNMENT` 的倍数,则添加此填充。
//
// 可计算为 `align_offset(position) - position`,其中 `position` 是 `tensor_infos` 末尾的位置(即 `sizeof(header) + sizeof(tensor_infos)`)。
uint8_t _padding[];
// 张量数据。
//
// 这是对应模型权重的任意二进制数据。这些数据应与原始模型文件中的数据接近或相同,但由于量化或其他推理优化可能有所不同。任何此类偏差应记录在元数据或架构定义中。
//
// 每个张量的数据必须存储在此数组中,并通过其 `tensor_infos` 条目定位。每个张量数据的偏移量必须是 `ALIGNMENT` 的倍数,张量之间的空间应填充至 `ALIGNMENT` 字节。
uint8_t tensor_data[];
};