ggml 介绍(5) GGUF 上下文 (gguf_context)

在前一章 计算图 (ggml_cgraph) 中,我们学会了如何定义一系列计算步骤并执行它们,就像拥有了一本"菜谱"。但是,在真实世界中,我们很少从零开始"发明"一个像 Llama 这样复杂的模型。更常见的场景是,我们下载一个由社区训练好并打包的模型文件,然后加载它来使用。

这就引出了一个新问题:一个包含了数十亿参数(张量)和各种配置信息的大型模型,应该如何被高效地存储、加载和分享呢?ggml 生态系统为此设计了一个标准化的解决方案:GGUF (GGML Universal Format) 文件格式。

而我们与 GGUF 文件打交道的工具,就是本章的主角:gguf_context

什么是 GGUF 上下文?

核心思想 :你可以把 gguf_context 想象成一个图书馆员,专门负责管理 GGUF 格式的书籍(模型文件)。这位管理员知道如何解读书籍的索引(元数据),并能根据你的需要快速找到并取出书中的具体内容(张量权重)。

简单来说,gguf_context 是一个解析 GGUF 文件的工具。当你给它一个 GGUF 文件时,它会读取并理解文件的所有内容,让你能轻松地访问模型的配置信息(比如"这是一个 Llama 架构的模型")和权重数据。它是加载和使用 ggml 模型的第一步。

GGUF 文件:一个精心打包的模型盒子

在我们使用"图书馆员"之前,先来看看他管理的"书籍"------GGUF 文件到底是什么样的。一个 GGUF 文件就像一个精心打包的模型工具箱,里面主要包含三个部分:

  1. 元数据 (Metadata):工具箱的"说明书"。它以"键值对"的形式存储了关于模型的所有信息,比如模型架构、层数、上下文长度、分词器词汇表等。
  2. 张量信息 (Tensor Info) :工具箱的"零件清单"。它详细列出了模型中每一个张量(权重)的名称、形状、数据类型(比如 F16 或 量化过的 Q4_K)以及它在数据区的具体位置。
  3. 张量数据 (Tensor Data):工具箱里所有的"实际零件"。这是一个巨大的、连续的二进制数据块,包含了模型所有张量的权重数据。

下面是 GGUF 文件结构的简化示意图:

graph TD subgraph "GGUF 文件 (llama.gguf)" direction TB A["头部信息
版本号, 张量数量, KV数量"] B["1. 元数据 (键值对)
architecture: 'llama'
context_length: 4096
..."] C["2. 张量信息 (索引)
张量 'T1': 名称, 形状, 类型, 偏移量
张量 'T2': 名称, 形状, 类型, 偏移量
..."] D["3. 张量数据 (二进制块)
|--- 张量 T1 的数据 ---|--- 张量 T2 的数据 ---|..."] end A --> B --> C --> D

这种结构非常巧妙,因为它将"描述信息"和"海量数据"分开了。我们可以只读取前面的元数据和张量信息来快速了解模型,而无需加载后面庞大的张量数据。

动手实践:用 gguf_context 打开模型盒子

现在,让我们请出我们的"图书馆员" gguf_context,来帮我们打开并检查一个 GGUF 模型文件。

第 1 步:初始化 GGUF 上下文

我们的第一步是创建一个 gguf_context 并让它从文件中读取信息。

c 复制代码
#include "ggml.h"
#include "gguf.h"
#include <stdio.h>

int main(void) {
    // 假设我们有一个名为 "tinyllama.gguf" 的模型文件
    const char * model_path = "tinyllama.gguf";

    // 1. 设置初始化参数
    // 我们暂时只想读取元数据,并不想立即加载庞大的张量数据
    struct gguf_init_params params = {
        .no_alloc = true, // 关键:告诉 gguf 不要为张量分配内存
        .ctx      = NULL,
    };

    // 2. 从文件初始化 GGUF 上下文
    struct gguf_context * gctx = gguf_init_from_file(model_path, params);
    if (!gctx) {
        fprintf(stderr, "无法加载模型: %s\n", model_path);
        return 1;
    }

    // ... 后续操作 ...

代码解释:

  • gguf_init_params 告诉 gguf_init_from_file 我们的意图。
  • .no_alloc = true 是一个非常重要的设置。它指示 gguf_context 只读取文件的元数据和张量信息,而不要为张量数据分配任何内存或加载它们。这使得我们可以快速"窥探"一个模型文件的内部,而无需消耗大量内存。
  • gctx 现在就是我们的"图书馆员",他已经读完了"索引卡",准备好回答我们的问题了。

第 2 步:查询元数据

我们可以向 gctx 查询模型的基本信息。比如,这个模型的架构是什么?

c 复制代码
    // 查找名为 "general.architecture" 的键
    const int key_idx = gguf_find_key(gctx, "general.architecture");
    if (key_idx < 0) {
        fprintf(stderr, "未找到模型架构信息\n");
        // ... 清理并退出 ...
    }
    const char * arch = gguf_get_val_str(gctx, key_idx);
    printf("模型架构: %s\n", arch);

代码解释:

  • gguf_find_key 在元数据中搜索指定的键,并返回其索引。
  • gguf_get_val_str 根据索引获取该键对应的值(这里是一个字符串)。
  • 通过这种方式,我们可以查询到 GGUF 文件中存储的任何元数据。

第 3 步:检查张量信息

接下来,让我们看看模型的"零件清单"。

c 复制代码
    // 获取模型中的张量总数
    const int n_tensors = gguf_get_n_tensors(gctx);
    printf("模型共有 %d 个张量。\n", n_tensors);

    // 查找一个特定张量的信息,例如 "output.weight"
    const int tensor_idx = gguf_find_tensor(gctx, "output.weight");
    if (tensor_idx < 0) {
        fprintf(stderr, "未找到 'output.weight' 张量\n");
        // ... 清理并退出 ...
    }
    const char * name = gguf_get_tensor_name(gctx, tensor_idx);
    enum ggml_type type = gguf_get_tensor_type(gctx, tensor_idx);

    printf("找到张量: %s, 类型: %s\n", name, ggml_type_name(type));

代码解释:

  • gguf_get_n_tensors 返回模型中张量的总数。
  • gguf_find_tensor 允许我们按名称搜索特定的张量。
  • gguf_get_tensor_namegguf_get_tensor_type 则可以获取该张量的具体信息。

第 4 步:真正加载模型权重

到目前为止,我们只读取了描述信息。现在,我们要做的是将所有张量权重加载到内存中,准备进行计算。为此,我们需要一个 ggml_context来存放这些张量。

c 复制代码
    // 在上一步之后... 先释放只读的 gctx
    gguf_free(gctx);

    // 准备一个新的 ggml_context 来存放张量
    struct ggml_context * mctx = NULL;

    // 重新设置参数,这次我们要加载张量
    struct gguf_init_params params_full = {
        .no_alloc = false, // false 表示要为张量分配内存和数据
        .ctx      = &mctx, // 传入 mctx 的地址,gguf 会为我们创建它
    };

    // 再次调用,这次会完整加载模型
    gctx = gguf_init_from_file(model_path, params_full);
    if (!gctx) { /* 错误处理 */ }

    // ...
    // 执行到这里,mctx 就已经是一个包含了模型所有权重的 ggml_context 了!
    // 我们可以用它来构建计算图并进行推理。
    // ...

代码解释:

  • 这次,我们将 .no_alloc 设为 false,并把一个 ggml_context 指针的地址 (&mctx) 传给 .ctx
  • gguf_init_from_file 看到这些参数时,它不仅会读取元数据,还会:
    1. 在内部创建一个新的 ggml_context (mctx)。
    2. 将 GGUF 文件中的整个张量数据块读入 mctx
    3. 为每一个张量创建一个 ggml_tensor 结构,并将其 data 指针指向数据块中正确的位置。

最后,别忘了清理所有东西:

c 复制代码
    // 任务完成,释放所有资源
    gguf_free(gctx);
    ggml_free(mctx);
    return 0;
}

深入幕后:gguf_context 的内部结构

gguf_context 结构体(定义在 src/gguf.cpp 中)清晰地反映了 GGUF 文件的结构:

c 复制代码
// 来自 src/gguf.cpp 的简化版结构
struct gguf_context {
    uint32_t version;

    // 存储所有键值对元数据 ("说明书")
    std::vector<struct gguf_kv> kv;

    // 存储所有张量的信息 ("零件清单")
    std::vector<struct gguf_tensor_info> info;

    size_t alignment; // 数据对齐方式
    size_t offset;    // 张量数据块在文件中的起始位置
    size_t size;      // 张量数据块的总大小

    // 指向内存中张量数据块的指针 (当数据被加载时)
    void * data;
};
  • kv 向量存储了所有的键值对。
  • info 向量存储了每个张量的详细描述,包括一个 ggml_tensor 结构体(但不含 data 指针)和一个 offset 字段,该字段记录了此张量数据相对于整个数据块开头的偏移量。
  • data 指针则指向被完整加载到内存中的庞大张量数据。

gguf_init_from_file 被调用时,其内部流程大致如下:

sequenceDiagram participant User as 用户代码 participant GGUF as gguf_init_from_file participant File as GGUF 文件 participant GGML as ggml 库 User->>GGUF: 调用 gguf_init_from_file(params) GGUF->>File: 打开文件 GGUF->>File: 读取头部信息 (版本, 数量等) GGUF->>File: 循环读取元数据 (KV) GGUF->>File: 循环读取张量信息 (Tensor Info) Note over GGUF: 此时,元数据和张量信息
已存入 gguf_context alt params.no_alloc == false GGUF->>GGML: 调用 ggml_init() 创建 ggml_context (mctx) GGML-->>GGUF: 返回 mctx GGUF->>File: 读取整个张量数据块到 mctx GGUF->>GGML: 循环创建 ggml_tensor, 并设置其 data 指针 end GGUF-->>User: 返回 gguf_context, (如果需要) mctx 也已就绪

这个过程清晰地展示了 gguf_context 如何充当文件和 ggml 核心数据结构之间的桥梁。

总结

在本章中,我们探索了 ggml 生态系统的文件格式标准 GGUF,以及与之交互的工具 gguf_context

  • GGUF 是一个通用文件格式,用于打包模型的元数据张量信息张量数据
  • gguf_context 就像一个"图书馆员",负责解析 GGUF 文件
  • 我们可以使用 gguf_context 快速检查模型属性 而无需加载所有数据(通过 .no_alloc = true)。
  • 我们也可以用它来将模型的全部权重加载到一个 ggml_context中,为后续的计算做准备。

现在我们已经知道如何将一个完整的、预先训练好的模型加载到内存中了。但是,我们的计算任务究竟是在哪里执行的呢?是在 CPU 上,还是可以利用强大的 GPU?ggml 如何管理不同的计算硬件呢?

相关推荐
汤永红1 小时前
week1-[循环嵌套]画正方形
数据结构·c++·算法
重启的码农2 小时前
ggml 介绍(4) 计算图 (ggml_cgraph)
c++·人工智能
R-G-B2 小时前
OpenCV Python——报错AttributeError: module ‘cv2‘ has no attribute ‘bgsegm‘,解决办法
人工智能·python·opencv·opencv python·attributeerror·module ‘cv2‘·no attribute
Seeklike2 小时前
diffusers学习--stable diffusion的管线解析
人工智能·stable diffusion·diffusers
数据知道3 小时前
机器翻译:模型微调(Fine-tuning)与调优详解
人工智能·自然语言处理·机器翻译
悠哉清闲3 小时前
C++ #if
c++
Hard but lovely3 小时前
C++:stl-> list的模拟实现
开发语言·c++·stl·list
沫儿笙4 小时前
焊接机器人保护气体效率优化
人工智能·机器人
青岛前景互联信息技术有限公司4 小时前
应急救援智能接处警系统——科技赋能应急,筑牢安全防线
人工智能·物联网·智慧城市