4、GGUF格式详解:文件结构、张量存储、元数据、分片机制

GGUF,全称是GPT-Generated Unified Format。说白了,它就是llama.cpp生态里最核心的模型文件格式。

我刚开始接触llama.cpp时,用的还是GGML格式。后来GGUF一出来,我立马就切过去了。为什么?因为GGUF解决了GGML很多让人头疼的问题——比如元数据扩展性差、张量对齐不统一、分片支持不友好等等。

今天咱们就把它拆开揉碎了讲。你理解了这个格式,以后做模型转换、量化、甚至自己写推理工具,都会顺手很多。

4.1 文件结构:从文件头到张量数据

GGUF文件的结构其实不复杂。我把它分成三大块:

  • 文件头(Header):固定长度,告诉你这个文件是什么版本、有多少张量、有多少元数据。
  • 元数据区(Metadata):KV键值对,存模型名称、架构、分词器、超参数等。
  • 张量数据区(Tensor Data):真正的权重数据,按顺序排列。

嗯,这里要注意:元数据区是变长的,张量数据区是严格对齐的。我见过有人自己手写解析器,结果忘了对齐,读出来的权重全是错的。

核心结构示意:

| 文件头 (Header) | 元数据 (Metadata) | 张量信息 (Tensor Infos) | 张量数据 (Tensor Data) |

文件头固定为28字节。我习惯用Python的struct模块去解析它:

import struct

# 读取文件头
header = f.read(28)
magic, version, n_tensors, n_kv = struct.unpack('<4sIIQ', header[:16])
# 注意:GGUF v3之后,文件头结构有微调

你想想看,如果版本号不对,后面的解析全白搭。所以我每次做转换工具,第一件事就是校验magic number——必须是GGUF

4.2 张量存储:对齐、类型与布局

张量数据是GGUF里最占体积的部分。它怎么存的?

每个张量在文件中有一个对应的张量信息条目,包含:

  • 张量名称(字符串)
  • 维度(n_dims)
  • 数据类型(如F32、F16、Q4_0等)
  • 偏移量(offset)

然后真正的数据在文件末尾,按偏移量对齐。对齐规则是:每个张量的起始地址必须是32字节对齐的

我曾经踩过的坑:有一次我手动修改了一个GGUF文件,把某个张量替换成量化后的版本。结果忘了重新计算偏移量,导致后面所有张量都读错了位置。排查了整整一个下午。所以,千万不要手动改GGUF文件,除非你完全理解对齐规则。

数据类型这块,GGUF支持很多种。我列个常用的:

类型名 类型ID 说明
F32 0 32位浮点,精度最高
F16 1 16位浮点,显存减半
Q4_0 2 4位量化,无分组
Q4_K_M 14 4位量化,中等分组,我常用这个
Q8_0 8 8位量化,精度损失小

我个人习惯:做推理服务时用Q4_K_M,做微调时用F16或Q8_0。为什么?Q4_K_M在速度和精度之间平衡得最好,我实测过很多模型,几乎感觉不到质量下降。

4.3 元数据:模型的身份证

元数据区是GGUF相比GGML最大的改进。它用KV键值对存储,键是字符串,值可以是多种类型:

  • 字符串(如模型名称)
  • 整数(如上下文长度)
  • 浮点数(如温度参数)
  • 数组(如分词器词汇表)
  • 布尔值(如是否使用RoPE)

举个例子,一个典型的GGUF元数据长这样:

{
  "general.name": "Qwen2.5-7B-Instruct",
  "general.architecture": "qwen2",
  "general.file_type": 14,  // Q4_K_M
  "general.quantization_version": 2,
  "qwen2.block_count": 28,
  "qwen2.context_length": 32768,
  "tokenizer.ggml.model": "gpt2",
  "tokenizer.ggml.tokens": ["<unk>", "<s>", "</s>", ...]
}

你想想看,有了这些元数据,llama.cpp在加载模型时就能自动识别架构、设置上下文长度、加载对应的分词器。完全不需要用户手动指定。

小技巧:如果你想查看一个GGUF文件的元数据,可以用llama.cpp自带的llama-model-info工具,或者用Python的gguf库。我个人写了个小脚本,专门用来批量检查模型文件的元数据是否完整。

4.4 分片机制:大模型怎么拆

大模型动辄几十GB,单个文件不好管理。GGUF支持分片,也就是把一个模型拆成多个文件。

分片的命名规则是:模型名-00001-of-00003.gguf,以此类推。

分片的核心逻辑是:

  • 每个分片都是一个完整的GGUF文件,包含完整的文件头和元数据
  • 张量数据按分片拆分,每个分片包含一部分张量
  • 加载时,llama.cpp会自动合并所有分片

我记得有一次,我需要把一个70B的模型部署到一台只有64GB内存的机器上。单文件太大了,加载就爆内存。后来我把它分成4个分片,每个分片约15GB,加载时逐个读取,内存占用降到了40GB左右。

分片的好处:

  • 方便传输和存储(单个文件不超过20GB)
  • 支持部分加载(比如只加载前几个分片做测试)
  • 便于分布式推理(不同分片可以放在不同节点上)

不过要注意:分片不是随便切的。每个分片必须包含完整的张量,不能把一个张量切到两个分片里。llama.cpp在分片时,会按张量的大小和顺序来分配,尽量让每个分片的大小接近。

4.5 知识体系总览

下面这张图,是我自己整理的GGUF格式核心逻辑。你看一眼,基本就全明白了:

GGUF 文件格式核心结构 文件头 (Header) - 28字节 Magic: GGUF | Version | Tensor数量 | KV元数据数量 元数据区 (Metadata) - KV键值对 模型名称 | 架构 | 上下文长度 | 分词器 | 量化参数 可扩展:任意添加自定义元数据 张量信息区 (Tensor Infos) - 每个张量一个条目 张量名称 | 维度 | 数据类型 (F32/F16/Q4_0/Q4_K_M等) | 偏移量 偏移量按32字节对齐 张量数据区 (Tensor Data) - 实际权重 按偏移量顺序排列 | 支持分片 (00001-of-00003) 每个分片包含完整文件头+元数据+部分张量 加载流程:读取Header → 解析Metadata → 定位Tensor Infos → 按偏移量读取Tensor Data

这张图把GGUF的四个核心部分串起来了。你从文件头开始,一步步往下走,就能完整解析一个GGUF模型。

我的建议:如果你想深入理解GGUF,最好的办法是自己写一个解析器。不用太复杂,能读出元数据和张量信息就行。我当初就是这么干的,写完之后对格式的理解完全不一样了。

好了,GGUF格式的核心内容就这些。文件结构、张量存储、元数据、分片机制,这四个点你掌握了,以后做模型转换、量化、部署,心里就有底了。


公众号:蓝海资料掘金营,微信deep3321