llama.cpp 源码解析(二):GGUF 格式与模型加载——从磁盘文件到内存张量

这是 llama.cpp 源码系列的第二篇。上一篇我们拆解了推理引擎 GGML,理解了张量、Arena、延迟执行、Galloc、KV Cache、线程模型的完整骨架。

但还有一个问题没回答:模型文件本身是怎么设计的? 从磁盘上的一个文件,到内存里一组可用的 ggml_tensor,中间经历了什么?

这就是本文要讲的内容。读完你会理解 GGUF 的二进制格式、mmap 为什么能做到”瞬间”加载、以及一个 HuggingFace 模型怎么转成 GGUF 用 llama.cpp 跑起来。

引言:先说清楚这篇文章讲什么——以及为什么需要它

第一章:模型文件格式的本质——把”一堆数字”和”它们是什么”存进磁盘

抛开所有框架、所有工具,一个模型文件最底层到底存了什么?两样东西:数据和元数据。

  • 数据就是权重——训练结束后冻结下来的那几十亿个浮点数。
  • 元数据告诉你这些数字怎么用——它们按什么形状排列、属于哪一层、叫什么名字、模型总共多少层、用什么 tokenizer。

就这么简单。所有的格式之争,本质上都是在争”数据和元数据怎么组织、怎么打包、怎么读取“这三个问题的不同答案。

1.1 格式一:PyTorch pickle(.pt / .pth / .bin)——最古老,也最危险

PyTorch 从 2016 年诞生起就用 Python 的 pickle 做序列化。Pickle 的设计初衷是”把任意 Python 对象存到磁盘,以后原样读回来”——包括类定义、函数、甚至 lambda。

怎么存的torch.save(model.state_dict(), "model.pt") → Python 的 pickle 模块把整个 state_dict(一个 OrderedDict,key 是层名,value 是 tensor)序列化成二进制。

问题在哪:pickle 的反序列化过程会执行 pickle 数据中的任意 Python 代码。这意味着一个恶意构造的 .pt 文件可以在你加载时删掉你的文件、窃取你的数据、安装后门。HuggingFace 早期大量使用这种格式,后来因为安全问题逐步迁移到 safetensors。

为什么推理不能用它

  • 需要 Python + PyTorch 运行时(几百 MB)
  • 不支持 mmap(必须一次性读到内存)
  • 不支持量化(权重以 FP32/FP16 存储)
  • 不是自描述的(需要额外代码定义模型类才能加载)

1.2 格式二:Safetensors——安全替代 pickle

HuggingFace 在 2022 年推出了 safetensors。它的核心卖点是:纯数据,不能执行代码

怎么存的:文件头 8 字节是 JSON 元数据的长度(uint64),后面紧跟一段 JSON 描述每个 tensor 的名字、形状、数据类型、在文件中的偏移量,再后面就是原生的 tensor 二进制数据。

Safetensors 文件布局:
┌──────────────────────────────────────────────┐
│ Header (8 bytes): JSON 部分的长度              │
├──────────────────────────────────────────────┤
│ JSON Metadata:                               │
│   "token_embd.weight": {shape:[768,6400],    │
│     dtype:"F16", offsets:[0, 9830400]}       │
│   "blk.0.attn_q.weight": {...}               │
│   ...                                        │
├──────────────────────────────────────────────┤
│ Tensor Data (raw bytes):                     │
│   [9.8MB of token_embd] [1.1MB of attn_q] ...│
└──────────────────────────────────────────────┘

为什么它安全:JSON 不是图灵完备的——解析 JSON 不能执行代码。Tensor 数据是纯二进制,直接 memcpy 到目标地址。

为什么推理不能直接用:它只解决了”安全存储”,没有解决”高效推理”。Safetensors 的元数据(JSON)在文件最前面,但这只是 tensor 级别的元数据——没有模型架构信息(多少层、什么 attention)、没有 tokenizer没有推理超参数。而且它不支持量化格式内嵌——权重总是以 FP32/FP16/BF16 存储。

1.3 格式三:ONNX——跨框架的”通用语言”

ONNX(Open Neural Network Exchange)是微软和 Facebook 在 2017 年发起的。它的目标是:用 PyTorch 训练,用 TensorRT 推理,中间用 ONNX 做桥梁。

ONNX 用 Protobuf 定义了一个完整的计算图——不仅有权重,还有操作节点(Conv、MatMul、ReLU…)。这比 safetensors 更进一步:safetensors 只知道”有这么些 tensor”,ONNX 知道”这些 tensor 怎么用”。

为什么 LLM 推理不用它:ONNX 是为视觉模型(固定输入大小、静态图)设计的。LLM 需要动态形状(batch 可变、序列长度可变)、KV Cache、autoregressive 循环——这些都是 ONNX 的弱项。加上 onnxruntime 的依赖链仍然很重。

1.4 格式四:AWQ / GPTQ——GPU 服务端的量化格式

这两个是专门为 GPU 服务端推理设计的量化格式,不是独立的文件格式——它们通常以 PyTorch checkpoint 或 safetensors 为载体,内部包含量化后的权重和量化参数。

  • AWQ(Activation-aware Weight Quantization):按激活值的大小分配量化精度,重要通道保留更高精度
  • GPTQ(GPT Post-Training Quantization):用 Hessian 矩阵逐层优化量化方案,质量接近原始

它们的问题:必须配合特定的推理引擎(vLLM、TGI、TensorRT-LLM),不能在 llama.cpp 中直接使用。因为它们的量化方案(per-channel scaling、分组量化)和推理逻辑是耦合的。

1.5 格式五:GGUF——专为本地推理而生

GGUF 的诞生动机很纯粹:前面四种格式没有一个适合”在用户的破笔记本上跑大模型”:

  • PyTorch pickle 不安全 + 需要 Python
  • Safetensors 安全但没有模型级元数据 + 需要 Python 库
  • ONNX 不适合动态 LLM
  • AWQ/GPTQ 绑定了 GPU 服务端推理引擎

GGUF 的设计目标一句话:打开文件,一切就绪。 不需要 config.json,不需要 tokenizer.json,不需要 Python,不需要 GPU。一个文件,纯 C 读取,mmap 瞬间加载,量化格式内嵌,跨平台。

第二章:GGUF 的二进制格式——五段结构

2.1 规范就在源码注释里

打开 ggml/include/gguf.h,前 31 行就是完整的文件格式规范。不用翻文档,不用查 wiki——源码注释就是规范本身。这很 GGML——把复杂的事情说得尽可能简单。

1. File magic "GGUF" (4 bytes)                     ← 文件头,固定 0x47475546
2. File version (uint32_t)                          ← 当前是 3
3. Number of ggml tensors in file (int64_t)         ← 有多少个权重张量
4. Number of key-value-pairs in file (int64_t)      ← 有多少个元数据键值对
5. For each KV pair:                                ← 遍历所有 KV 对
   1. The key (string)
   2. The value type (gguf_type)
   3a. If array: type + count + binary of elements
   3b. Otherwise: binary of value
6. For each ggml tensor:                            ← 遍历所有张量
   1. The tensor name (string)
   2. The number of dimensions (uint32)
   3. For each dimension: size (int64)
   4. The tensor data type (int32)
   5. The tensor data offset in tensor data blob (uint64)
7. The tensor data binary blob (optional, aligned)  ← 实际权重数据

画成图就是五段:

┌──────────────────────────────────────────────┐
│ 1. Magic  "GGUF"  (4 bytes)                  │
├──────────────────────────────────────────────┤
│ 2. Header                                     │
│    version     = 3        (uint32)            │
│    n_tensors   = 90      (int64)              │
│    n_kv        = 33      (int64)              │
├──────────────────────────────────────────────┤
│ 3. KV Metadata (33 pairs, variable length)    │
│    "general.architecture"  → "qwen3"          │
│    "qwen3.block_count"     → 8                │
│    "qwen3.embedding_length"→ 768              │
│    "tokenizer.ggml.tokens" → ["<|endoftext|>",│
│                               "<|im_start|>", │
│                               ...]            │
│    ... (30 more)                               │
├──────────────────────────────────────────────┤
│ 4. Tensor Info Array (90 entries)              │
│    [0] "token_embd.weight"                    │
│        dims=[768, 6400], type=F16, offset=0   │
│    [1] "blk.0.attn_norm.weight"               │
│        dims=[768], type=F32, offset=9.8M      │
│    [2] "blk.0.attn_q.weight"                  │
│        dims=[768, 768], type=F16, offset=...  │
│    ... (87 more)                              │
│   [89] "output_norm.weight"                   │
│        dims=[768], type=F32, offset=127.8M    │
├──────────────────────────────────────────────┤
│ 5. Tensor Data (aligned to 32 bytes)           │
│    实际的权重值,按 Tensor Info 的 offset 定位    │
│    总大小 = sum of all tensor sizes            │
└──────────────────────────────────────────────┘

2.2 存储类型系统

GGUF 的 KV 元数据支持 12 种基本类型(ggml/include/gguf.h:53-68):

enum gguf_type {
    GGUF_TYPE_UINT8   = 0,   // 无符号 8 位整数
    GGUF_TYPE_INT8    = 1,
    GGUF_TYPE_UINT16  = 2,
    GGUF_TYPE_INT16   = 3,
    GGUF_TYPE_UINT32  = 4,
    GGUF_TYPE_INT32   = 5,   // 枚举用这个
    GGUF_TYPE_FLOAT32 = 6,   // 浮点数
    GGUF_TYPE_BOOL    = 7,   // 布尔值(存储为 int8)
    GGUF_TYPE_STRING  = 8,   // 字符串(长度 uint64 + 内容)
    GGUF_TYPE_ARRAY   = 9,   // 数组(类型 + 数量 + 元素列表)
    GGUF_TYPE_UINT64  = 10,
    GGUF_TYPE_INT64   = 11,  // 大整数
    GGUF_TYPE_FLOAT64 = 12,
};

字符串的存储方式是 string_length (uint64) 后跟 C 字符串(无 null 终止符)。这样读取时知道要读多少字节,不用扫描 null。

第三章:一个完整的加载流程——从命令行到你第一个 token

把上面所有内容串起来。当你在终端输入:

llama-cli -m minimind-f16.gguf -p "你好"

发生的完整流程:

llama_model_load_from_file("minimind-f16.gguf")
  │
  ├─► gguf_init_from_file("minimind-f16.gguf")
  │     → 读 Header: version=3, n_tensors=90, n_kv=33
  │     → 读 33 个 KV 对 → gguf_context.kv
  │     → 读 90 个 Tensor Info → gguf_context.info
  │     → mmap 数据区 → gguf_context.data = 0x7f...
  │
  ├─► llama_model_loader 构造
  │     → 读 "general.architecture" = "qwen3"
  │     → 读 超参数: n_layer=8, n_embd=768, n_head=8, ...
  │     → 读 tokenizer: 6400 个 token, BPE merges
  │
  ├─► 确定架构 → LLM_ARCH_QWEN3 → llm_build_qwen3
  │
  ├─► 在 model_ctx(Arena, no_alloc=true)创建 90 个 ggml_tensor
  │     token_embd, output_norm, output
  │     blk.0.attn_q, blk.0.attn_k, blk.0.attn_v, blk.0.attn_output
  │     blk.0.attn_q_norm, blk.0.attn_k_norm
  │     blk.0.ffn_gate, blk.0.ffn_down, blk.0.ffn_up
  │     ... (8层)
  │     全部 data=NULL
  │
  ├─► ggml_backend_alloc_ctx_tensors_from_buft(model_ctx, cpu_buft)
  │     → 计算总大小 = 122 MB
  │     → mmap 映射: 每个 tensor->data 直接指向 GGUF 文件内对应偏移
  │     → 或 malloc: 分配 122MB, memcpy 从文件读取
  │
  └─► 返回 llama_model *
         model->layers[0].wq->data → 0x7f... (GGUF 文件内偏移)
         model->layers[0].wq->ne   = [768, 768]
         model->hparams.n_layer    = 8
         model->vocab.n_tokens     = 6400

llama_init_from_model(model, params)
  → 创建 llama_context, KV Cache, sampler, ...

llama_decode(ctx, batch)
  → 第一篇讲的完整推理流程

每个阶段都干净利索,没有多余步骤。这就是 GGUF + mmap + GGML 三者组合的威力——从磁盘文件到跑出第一个 token,中间没有一次完整的数据拷贝。

下图是整个加载流程的五阶段全景图(含各阶段实测耗时):

llama.cpp 模型加载完整流程:从 GGUF 文件到开始推理的 5 个阶段


   转载规则


《llama.cpp 源码解析(二):GGUF 格式与模型加载——从磁盘文件到内存张量》 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
llama.cpp RPC 局域网分布式推理验证 llama.cpp RPC 局域网分布式推理验证
一、RPC 分布式推理的核心原理llama.cpp 的 RPC 模式基于一个非常朴素的想法:大模型是一层一层叠起来的,不同层可以在不同机器上算。 Transformer 模型的每一层计算相对独立——上一层算完把结果传给下一层。这就给了分布式
2026-01-11
下一篇 
llama.cpp 源码解析(一):GGML——大模型推理引擎是如何设计的 llama.cpp 源码解析(一):GGML——大模型推理引擎是如何设计的
几周前我开始看 llama.cpp 的源码。想看看”为什么我这台本地的笔记本也能跑得动 7B 甚至 14B 模型”,结果发现——这个项目太有意思了。所以我准备尝试写篇系列文章(又开新坑😂)。 从推理引擎开始,如果你也想知道 llama.c
2026-01-01
  目录