本站内容由 AI 生成,可能存在错误。如发现问题,欢迎到 GitHub Issues 反馈。

工具全景与 GGUF 二进制解析

工具全景与 GGUF 二进制解析

更新于 2026-04-23

系列定位:本文是 llama.cpp 源码精读系列 #1,聚焦 llama.cpp 工具生态和 GGUF 格式的 C++ 实现细节。如果你还没有阅读 系列总览,建议先建立全局地图再进入本章。

Part A:工具全景

GGUF 工具选择决策树选择合适的 GGUF 工具需要格式转换?Yesconvert_hf_to_gguf.pyHuggingFace → GGUF (FP16/BF16)No需要量化?Yesllama-quantizeFP16 → Q4_K_M / Q8_0 等No需要查看/编辑?Yesgguf-py读取/修改 GGUF 元数据和 tensor

llama.cpp 在 tools/ 目录下提供了多个可执行程序。最核心的三个是 llama-completionllama-clillama-bench,它们面向不同的使用场景,架构也截然不同。

架构差异

llama.cpp 三个核心工具的架构对比
llama-completion(传统工具)
completion.cpp
主入口
sampling.h
直接调用采样 API
llama.h
直接调用底层 C API
llama-cli(新一代工具)
cli.cpp
主入口
server-context.h
嵌入式 Server
server-task.h
Task/Response 异步模式
llama.h
llama-bench(基准测试)
llama-bench.cpp
主入口
llama.h
只测 pp/tg 速度

llama-completion 是最早的文本生成工具(原名 main),直接调用 llama.hsampling.h 的底层 C API,手动管理 context、KV cache 和 sampling。代码结构清晰,适合理解 llama.cpp 底层机制。

llama-cli 是新一代的交互式聊天客户端。它在内部嵌入了一个 server 实例(引入 server-context.hserver-task.h),用 task/response reader 模式异步处理请求。有 ASCII logo、spinner 加载动画,支持多模态(图片/音频)和推测解码(speculative decoding)。

llama-bench 是纯性能基准测试工具。它不生成有意义的文本,只测量 prompt processing(pp)和 token generation(tg)的吞吐量,输出结构化的性能数据。

功能对比

特性llama-completionllama-clillama-bench
文本生成 / 补全
交互式聊天✅ 基础✅ 完善
Prompt Cache--prompt-cache
Self-Extend--grp-attn-n/w
--in-prefix/suffix
Speculative Decoding--draft
多模态(图片/音频)--image/audio
Grammar / JSON Schema
全部采样参数
性能测量(pp/tg t/s)

--in-prefix/suffix 只注册在 LLAMA_EXAMPLE_COMPLETION 中;--grp-attn-n 注册在 LLAMA_EXAMPLE_COMPLETIONLLAMA_EXAMPLE_PASSKEY 中,--grp-attn-w 只注册在 LLAMA_EXAMPLE_COMPLETION 中(见 common/arg.cpp)。llama-cli 和 llama-bench 均不支持这些参数。

选择建议

  • 日常聊天llama-cli:体验最好,功能最新,支持多模态和推测解码
  • 精细控制 / 脚本化批处理llama-completion:prompt cache、Self-Extend、自定义前后缀
  • 硬件 / 后端性能评估llama-bench:纯跑 pp 和 tg 吞吐量

Part B:GGUF 逐字段解析

llama.cpp 使用 GGUF(GGML Universal File Format)作为模型文件格式。理解 GGUF 的结构是理解后续所有加载流程的基础。

文件物理布局

一个 GGUF 文件从头到尾由以下部分顺序排列:

GGUF 文件物理布局
Header(固定字段)
Magic: 'GGUF'(4B) | Version: uint32(v3) | n_tensors: int64 | n_kv: int64
KV Metadata(变长)
key-value 对: architecture, chat_template, ...
Tensor Info Array(变长)
每个 tensor: name + n_dims + ne[] + type + offset
Alignment Padding
填充到 alignment 边界(默认 32 bytes)
Tensor Data Blob(数 GB)
所有 tensor 的二进制权重数据,按 offset 顺序排列

关键结论:只需读 header 就能获取所有 tensor 的元数据(name、shape、type),不需要读取 data blob。 对于一个 7B 模型,header 通常只有几 MB,而 data blob 有数 GB。

解析流程

GGUF 的解析入口是 gguf_init_from_file_ptr()ggml/src/gguf.cpp),它按以下顺序读取:

// 1. 验证 magic
char magic[4];
read(magic);  // 必须是 "GGUF"

// 2. 读版本号
uint32_t version;
read(version);  // 当前支持 v2, v3

// 3. 读 tensor 数量和 KV 数量
int64_t n_tensors, n_kv;
read(n_tensors);
read(n_kv);

// 4. 解析所有 KV metadata
for (int64_t i = 0; i < n_kv; i++) {
    // 读 key (string) + value type + value
}

// 5. 解析所有 tensor info(不读 data!)
for (int64_t i = 0; i < n_tensors; i++) {
    read(name);          // tensor 名称
    read(n_dims);        // 维度数
    read(ne[0..3]);      // 每个维度的元素数(shape)
    read(type);          // 量化类型: Q4_0, Q4_K, F16 等
    // 根据 type 计算 stride
    nb[0] = ggml_type_size(type);
    nb[1] = nb[0] * (ne[0] / ggml_blck_size(type));
    read(offset);        // 在 data blob 中的偏移
}

// 6. 跳到 data section 起始位置(alignment padding 之后)
fseek(file, GGML_PAD(current_pos, alignment), SEEK_SET);
ctx->offset = ftell(file);  // 记录 data section 起始偏移

注意第 5 步中,每个 tensor info 只包含元数据,不包含实际权重数据。解析完所有 tensor info 后,整个 header 就结束了。后续是否读取 data blob 取决于调用者传入的 no_alloc 参数。

Tensor Info 结构

每个 tensor 的元数据对应以下 C 结构体:

// gguf.cpp
struct gguf_tensor_info {
    struct ggml_tensor t;   // 内含 type, ne[4], nb[4], name
    uint64_t offset;        // 在 data blob 中的字节偏移
};

// ggml.h(简化)
struct ggml_tensor {
    enum ggml_type type;            // 量化类型: GGML_TYPE_Q4_K, GGML_TYPE_F16 等
    int64_t ne[GGML_MAX_DIMS];      // shape: 每个维度的元素数
    size_t  nb[GGML_MAX_DIMS];      // byte stride: 每个维度的字节步长
    char name[GGML_MAX_NAME];       // tensor 名称
    void * data;                    // 数据指针(解析 header 时为 NULL)
};

KV Metadata

KV metadata 存储模型的各种元信息,常见的 key 包括:

Key类型说明
general.architecturestring模型架构名,如 "llama", "qwen3"
general.namestring模型显示名
tokenizer.chat_templatestringJinja2 chat template(后续章节详解)
{arch}.context_lengthuint32训练时的最大 context 长度
{arch}.embedding_lengthuint32embedding 维度
{arch}.block_countuint32transformer 层数
{arch}.attention.head_countuint32attention head 数量

llama.cpp 在加载模型时,首先从 general.architecture 确定模型架构,再据此读取对应的超参数。

Part C:量化 Block 结构

量化块位布局量化块位布局: Q4_0 vs Q8_0Q4_0scale (d)FP16, 2Bq0|q1q8|q9q16|q17q24|q2516 bytes: 32 个 4-bit 值 (每 byte 装 2 个)18 B / 32 = 4.5 bits/权重Q8_0scale (d)FP16, 2B32 bytes: 32 个 int8 值 (每 byte 1 个)34 B / 32 = 8.5 bits/权重每个 block 都是 32 个权重; 区别在于每个值占用的 bits 数反量化: w = d × (q - offset)

GGUF 中的量化类型决定了权重的存储方式。每种量化类型定义了一个固定大小的 block,包含一组量化后的权重值及其反量化所需的 scale/min 参数。

Q4_0:最简单的 4-bit 量化

#define QK4_0 32  // 每个 block 32 个元素
typedef struct {
    ggml_half d;           // delta (scale), f16 格式
    uint8_t qs[QK4_0 / 2]; // 16 bytes: 每个 byte 存 2 个 4-bit 量化值
} block_q4_0;
// sizeof = 2 + 16 = 18 bytes → 32 个权重 → 4.5 bits/weight

反量化公式:w=d(q8)w = d \cdot (q - 8),其中 qq 是 4-bit 无符号整数(0~15),减去 8 得到有符号值。

Q4_K:K-quant 系列的 4-bit 量化

#define QK_K 256         // super-block: 256 个元素
#define K_SCALE_SIZE 12  // scales 的字节数

typedef struct {
    union {
        struct {
            ggml_half d;    // super-block scale(用于量化 scales)
            ggml_half dmin; // super-block scale(用于量化 mins)
        };
        ggml_half2 dm;
    };
    uint8_t scales[K_SCALE_SIZE]; // 12 bytes: 8 组 sub-block 的 scale 和 min,6-bit 量化
    uint8_t qs[QK_K/2];           // 128 bytes: 256 个 4-bit 量化值
} block_q4_K;
// sizeof = 4 + 12 + 128 = 144 bytes → 256 个权重 → 4.5 bits/weight

Q4_K 采用两层量化结构:

Q4_K 两层量化结构
Super-block(256 个权重)
d, dmin
2 个 f16 全局 scale
scales[12]
8 组 sub-block 的 scale/min(6-bit 量化)
qs[128]
256 个 4-bit 量化值
反量化一个权重
1. 从 scales[] 解出 sub-block 的 scale 和 min
2. scale = d * raw_scale, min = dmin * raw_min
3. weight = scale * q - min

反量化公式:w=draw_scaleqdminraw_minw = d \cdot \text{raw\_scale} \cdot q - d_{\min} \cdot \text{raw\_min},其中 raw_scale 和 raw_min 从 scales[] 的 6-bit 编码中解出。dminraw_mind_{\min} \cdot \text{raw\_min} 作为零点偏移被减去。

Q8_0:8-bit 量化

#define QK8_0 32  // 每个 block 32 个元素
typedef struct {
    ggml_half d;       // delta (scale), f16 格式
    int8_t qs[QK8_0];  // 32 bytes: 32 个 int8 量化值
} block_q8_0;
// sizeof = 2 + 32 = 34 bytes → 32 个权重 → 8.5 bits/weight

Q8_0 通常不直接作为模型存储格式,而是作为运行时中间格式——在执行 dot product 时,激活值会被量化为 Q8_0/Q8_K 与权重做点积。

Dot Product Pairing

量化矩阵乘法时,两个操作数的量化类型需要配对:

权重类型配对的 dot product 类型说明
Q4_0Q8_04-bit 权重 × 8-bit 激活
Q4_KQ8_KK-quant 4-bit × K-quant 8-bit
Q5_KQ8_KK-quant 5-bit × K-quant 8-bit
Q6_KQ8_KK-quant 6-bit × K-quant 8-bit
F16F16半精度权重 × 半精度激活

执行矩阵乘法时,backend 根据权重 tensor 的 type 查表(type_traits_cpu[])找到对应的 vec_dot 函数和 vec_dot_type。如果激活值的类型不匹配 vec_dot_type,会先分配一个临时 buffer 做类型转换。

小结

本章覆盖了 llama.cpp 的两个基础层面:

  1. 工具生态llama-completion(传统、精细控制)、llama-cli(新一代、功能最全)、llama-bench(纯性能测量)三个工具各有定位,选择取决于使用场景
  2. GGUF 格式:Header → KV Metadata → Tensor Info → Padding → Data Blob 的五段式布局,通过 gguf_init_from_file_ptr() 一次顺序读取完成解析
  3. 量化 Block:Q4_0(简单 scale + 4-bit)、Q4_K(两层量化 super-block/sub-block)、Q8_0(运行时中间格式)的 struct 定义和反量化公式,以及 dot product pairing 的查表机制

下一篇 模型加载 将追踪 GGUF 文件解析完成后,权重如何通过 mmap 或 buffer 上传进入 GPU/CPU 内存。