第二章:GGUF 文件结构概览——文件头、元数据区、张量数据区的整体布局

GGUF 这玩意儿,说白了就是一个精心打包的“三明治”。

最上面一层是文件头,告诉你这是个什么文件、版本号多少、后面的数据怎么组织。中间是元数据区,存了模型的名字、参数量、词表大小这些“身份证信息”。最下面是张量数据区,所有神经网络的实际权重就躺在那里。

我个人习惯把 GGUF 的结构记成:头部 + 字典 + 大字节数组。当然,实际细节比这个粗糙说法要多不少。今天我们就把它完全拆开,看看每块到底长什么样。

2.1 从文件头开始:GGUF 的身份证

文件头是 GGUF 的起点,固定位于文件偏移量 0 处。长度是 固定的 40 字节,不多不少。

我记得第一次手动 hexdump 一个 GGUF 文件时,看到前四个字节是 47 47 55 46,也就是 ASCII 的 "GGUF",当时就觉得这设计挺直率的——不像某些格式还用魔数藏个隐晦含义。

字段 类型 字节大小 说明
magic uint32 4 魔数,固定为 0x46554747 ("GGUF")
version uint32 4 版本号,目前主流是 3
tensor_count uint64 8 文件中的张量总数(含量化变体)
metadata_kv_count uint64 8 元数据键值对的总数
alignment uint32 4 张量数据的对齐字节数,通常是 32
padding[4] uint8[4] 4 保留填充字节
小提示: version 字段很重要。我在项目中遇到过解析器没检查版本号,直接把 version=2 的旧文件当成 version=3 来读,结果 tensor_count 解析位置错了,整个模型加载失败。所以,解析前一定先读 version,根据版本调整后续布局规则。

2.2 元数据区:模型信息的“字典”

文件头之后紧跟着的就是元数据区。这部分说白了就是一个键值对(KV)列表,长度由 header 里的 metadata_kv_count 决定。

每个 KV 项的结构统一如下:

  • key:以 null 结尾的 UTF-8 字符串。读取时先读一个 uint64 表示字符串长度(不含 null 结尾符),再读对应字节数的字符数据,最后跳过一个 null 字节。
  • value_type:uint32,指示值的类型。取值范围从 0 到 10 左右,每种类型对应不同的序列化方式。
  • value:根据 value_type 解释的具体数据。可以是整数、浮点数、布尔、字符串、数组、甚至嵌套字典。

常见的 value_type 枚举如下:

枚举值 类型名 序列化长度
0uint81 字节
1int81 字节
2uint162 字节
3int162 字节
4uint324 字节
5int324 字节
6float324 字节
7bool1 字节(0/1)
8stringuint64 长度 + 数据 + null
9arrayuint32 类型 + uint64 数量 + 各元素
注意避坑: 我曾经在一个大模型导出工具里,忘记处理 string 类型后的 null 字节对齐。结果后一个 KV 的 value_type 被多读了一个字节,整条元数据链崩了。调试了两天才发现是少跳了一个 null。

元数据区里通常会包含 general.name、general.architecture、llama.vocab_size、llama.embedding_length 等关键键。这些信息供加载器决定如何初始化模型结构。

2.3 张量数据区:真正的“血肉”

元数据区结束后,就到了最核心的张量数据区。这里存放的是所有模型参数——权重、偏置、词嵌入等。

不过要注意:张量数据区并不是直接一个接一个地塞进去的。GGUF 做了一个很聪明的设计——使用 张量信息索引 和 张量数据块 分离存储。

具体布局顺序:

  1. 张量信息数组(tensor infos):紧跟在元数据区之后。每个张量信息包含:
    • name:字符串(方式同元数据 key)
    • n_dims:uint32,维度数量(通常 1 或 2)
    • dims[0..n_dims-1]:uint64 数组,每个维度的大小
    • data_offset:uint64,该张量在数据块部分相对于张量数据区起始的偏移量
    • type:uint32,张量数据类型(如 F32、F16、Q4_0 等)
  2. 对齐填充:在 tensor_infos 结束后,文件指针可能会 pad 到 alignment 的倍数,保证后续数据对齐。
  3. 张量数据块:所有张量的实际数据按顺序堆叠。每个张量从 data_offset 指定位置开始,长度为 element_count * element_size(具体数值由 type 决定)。
为什么要分离索引和数据? 这样你可以先读完索引,知道每个张量的大小和位置,然后可以并行读入或只读部分张量(比如只加载指定层)。我经常用这个特性来做局部模型测试——只加载前几层权重,大幅减少内存占用。

2.4 整体结构图(一图胜千言)

下面我画了一张完整的 GGUF 文件结构图,把三块区域和内部细节都标了出来。你仔细看箭头走向,就能明白解析器是怎么一步步跳着读的。

GGUF 文件整体布局 文件头 (40 字节) magic | version | tensor_count | metadata_kv_count | alignment | padding 元数据区 (KV 列表) key (字符串) | value_type | value 例如: "general.architecture" -> "llama", "llama.vocab_size" -> 32000 实际长度 = metadata_kv_count 个 KV 项之和 张量信息数组 (tensor infos) 每个:name | n_dims | dims[] | data_offset | type 张量数据块 (实际权重) 从 data_offset 开始,按类型长度读取 常见类型:F32 (4B)、F16 (2B)、Q4_0 (0.5B/参数) ━━━━━ 数据按 tensor_count 个张量顺序排列 ━━━━━

2.5 对齐与填充:一个容易踩的坑

GGUF 要求张量数据块的起始地址是 alignment(文件头里的字段)的整数倍。默认 alignment 是 32 字节,但有些工具会改成 64 甚至 256。

我踩过这个坑:有一次在内存中 mmap 一个 GGUF 文件,直接用 data_offset 做指针偏移,没检查对齐,结果后面读张量时 CPU 发出 SIGBUS 信号——因为对齐访问导致段错误。

解决方案: 解析时,读完所有 tensor info 后,下一个张量数据块起始位置必须是 alignment 的整数倍。如果不足,需要填充 null 字节。具体公式:pad_len = (alignment - (current_offset % alignment)) % alignment。

2.6 实战小练习:用 C 读文件头

光讲理论没意思,我写个简单的 C 代码片段,你看一眼就明白如何 parse 前 40 字节:

#include <stdint.h>
#include <stdio.h>

typedef struct {
    uint32_t magic;
    uint32_t version;
    uint64_t tensor_count;
    uint64_t metadata_kv_count;
    uint32_t alignment;
    uint8_t  padding[4];
} gguf_header_t;

int read_header(FILE *f, gguf_header_t *hdr) {
    if (fread(hdr, sizeof(gguf_header_t), 1, f) != 1) return -1;
    if (hdr->magic != 0x46554747) {
        fprintf(stderr, "Bad magic = 0x%X\n", hdr->magic);
        return -1;
    }
    printf("version: %u\n", hdr->version);
    printf("tensor_count: %lu\n", hdr->tensor_count);
    printf("metadata_kv_count: %lu\n", hdr->metadata_kv_count);
    printf("alignment: %u\n", hdr->alignment);
    return 0;
}

这个结构体正好 40 字节,编译器打包可以不用特意调对齐(x86 上默认对齐就 OK)。但如果你在 ARM 或 RISC-V 上跨平台编译,建议加上 __attribute__((packed)) 以兼容。

2.7 小结

GGUF 的三段式布局看起来简单,但细节不少。文件头提供导航信息,元数据区描述模型属性,张量数据区才是真正的模型重量。解析时务必注意对齐、null 结尾、版本兼容这三件事。

下次你手头有个 GGUF 文件,直接 hexdump 看前几十字节,对照上面的表格应该能一眼认出结构了。嗯,有了这个基础,下一件有趣的事就是怎么反序列化元数据里的各种值了——不过那是第三章的内容,今天先说这么多。