ggml 介绍(4) 计算图 (ggml_cgraph)

在上一章 上下文 (ggml_context) 中,我们学习了如何搭建一个高效的"工作台"来管理所有张量 (ggml_tensor)的内存。现在,我们有了原材料(张量)和工作空间(上下文),但我们还缺少最关键的一样东西:一份菜谱,告诉我们如何一步步处理这些原材料,最终做出一道美味的"大餐"(比如一个模型的推理结果)。

这本菜谱,在 ggml 中就是计算图 (ggml_cgraph)

什么是计算图?为什么需要它?

想象一下,你想计算一个简单的线性方程:y = a * x + b

  • a, x, b 是你的输入数据(原材料)。
  • * (乘法) 和 + (加法) 是你需要执行的操作步骤。
  • y 是你想要的最终结果。

你不会一口气就算出 y。你会先计算 a * x,得到一个中间结果,然后再将这个中间结果与 b 相加。这个先后顺序和依赖关系,就可以用一张图来表示:

graph TD subgraph "输入 (原材料)" a[张量 a] x[张量 x] b[张量 b] end subgraph "计算步骤 (菜谱)" op1(乘法 *) --> res1[中间结果 a*x] op2(加法 +) --> y[最终结果 y] end a --> op1 x --> op1 res1 --> op2 b --> op2

这张图就是计算图

核心思想ggml_cgraph 就像一张菜谱。菜谱上记录了做一道菜需要的所有步骤(操作)和原材料(张量),以及它们之间的先后顺序。仅仅看着菜谱并不会做出菜。ggml_cgraph 记录了你定义的所有张量操作(比如加法、乘法),形成了一个操作流程图。这个图本身不执行计算,但它详细地告诉了 ggml "如何"进行计算。

这种"只记录,不执行"的模式(也称为"延迟执行")是 ggml 的一个核心设计哲学,它带来了巨大的好处:

  1. 高效执行ggml 可以先拿到完整的"菜谱",然后通盘考虑如何最高效地安排烹饪步骤,比如哪些步骤可以并行处理,如何最节省地使用内存。
  2. 自动求导 :对于模型训练,ggml 可以沿着这张图反向追溯,自动计算出每个参数的梯度,这是训练神经网络的关键。
  3. 跨平台优化 :同一张计算图可以被不同的后端(CPU, GPU)解释和执行,ggml 可以为不同硬件生成最优的执行计划。

动手实践:构建并执行你的第一个计算图

让我们用代码来实现 y = a * x + b 这个过程。

第 1 步:准备工作(创建上下文和张量)

首先,我们需要一个上下文 (ggml_context) 作为我们的工作台,并创建输入张量 a, x, b

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

int main(void) {
    // 1. 准备工作台
    struct ggml_init_params params = { 16 * 1024 * 1024, NULL, false };
    struct ggml_context * ctx = ggml_init(params);

    // 2. 准备原材料 (所有张量都是 1x1 的标量)
    struct ggml_tensor * a = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    struct ggml_tensor * x = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);
    struct ggml_tensor * b = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1);

    // ... 后续步骤将在这里继续 ...

第 2 步:定义计算步骤(构建依赖关系)

接下来,我们调用 ggml 的操作函数来"画"出我们的计算图。记住,这些函数调用不会立即执行计算

c 复制代码
    // 步骤 1: a * x
    struct ggml_tensor * ax = ggml_mul(ctx, a, x);

    // 步骤 2: (a * x) + b
    struct ggml_tensor * y = ggml_add(ctx, ax, b);

这两行代码做了什么?

  • ggml_mul(ctx, a, x) 创建了一个新的张量 axax 的内部记录着:"我是由 GGML_OP_MUL 操作产生的,我的输入是 ax"。
  • ggml_add(ctx, ax, b) 创建了最终的张量 yy 的内部记录着:"我是由 GGML_OP_ADD 操作产生的,我的输入是 axb"。

至此,一个描述 y = a * x + b 的依赖关系网络已经在内存中形成了。

第 3 步:创建并构建计算图

现在,我们需要把这个内存中的依赖关系,翻译成一个实际的、可执行的计划,也就是 ggml_cgraph 对象。

c 复制代码
    // 1. 创建一个空的计算图对象
    struct ggml_cgraph * gf = ggml_new_graph(ctx);

    // 2. 从最终的目标张量 y 开始,反向追踪并构建图
    ggml_build_forward_expand(gf, y);
  • ggml_new_graph(ctx) 创建了一个空的"菜谱本"。
  • ggml_build_forward_expand(gf, y) 是一个神奇的函数。它会从 y 开始,像侦探一样沿着 y 的输入 (axb),再沿着 ax 的输入 (ax),一路回溯,找出所有参与计算的节点,并把它们按照正确的执行顺序(比如先算乘法再算加法)记录到 gf 这个"菜谱本"里。

第 4 步:执行计算

菜谱已经准备好了,现在是时候"开火做菜"了!在执行前,我们需要为输入张量赋予具体的值。

c 复制代码
    // 为输入张量设置具体数值
    ggml_set_f32(a, 3.0f); // a = 3
    ggml_set_f32(x, 2.0f); // x = 2
    ggml_set_f32(b, 4.0f); // b = 4

    // 使用 4 个线程执行计算图中的所有操作
    ggml_graph_compute_with_ctx(ctx, gf, 4);
  • ggml_set_f32 用来给张量赋值。
  • ggml_graph_compute_with_ctx 是真正执行计算的地方。它会严格按照 gf 中记录的步骤,一个接一个地执行,比如先执行 a*x,然后把结果加上 b。计算出的结果会直接存放在对应张量(axy)的 data 指针所指向的内存中。

第 5 步:获取结果并清理

计算完成后,我们可以从 y 张量中读取结果。

c 复制代码
    // 从 y 张量中获取计算结果
    float result = ggml_get_f32_1d(y, 0);
    printf("y = a * x + b = 3*2 + 4 = %.1f\n", result);

    // 释放所有资源
    ggml_free(ctx);

    return 0;
}

输出应该是:

css 复制代码
y = a * x + b = 3*2 + 4 = 10.0

深入幕后:ggml_cgraph 的内部结构

ggml_cgraph 结构体(在 ggml-impl.h 中定义)本质上是一个节点列表,它存储了所有需要计算的张量。

c 复制代码
// 来自 ggml-impl.h 的简化版结构
struct ggml_cgraph {
    int n_nodes; // 图中有多少个计算节点
    int n_leafs; // 图中有多少个输入节点 (叶子节点)

    struct ggml_tensor ** nodes; // 指向计算节点的张量指针数组
    struct ggml_tensor ** leafs; // 指向叶子节点的张量指针数组

    // ... 其他用于自动求导等的字段 ...
};

当我们调用 ggml_build_forward_expand(gf, y) 时,ggml 内部会进行一次"图遍历":

sequenceDiagram participant User as 用户代码 participant ggml as ggml_build_forward_expand participant gf as ggml_cgraph User->>ggml: ggml_build_forward_expand(gf, y) ggml->>ggml: 访问节点 y, 检查是否已处理 Note over ggml: 未处理, 递归访问 y 的源节点: ax, b ggml->>ggml: 访问节点 ax, 检查是否已处理 Note over ggml: 未处理, 递归访问 ax 的源节点: a, x ggml->>ggml: 访问节点 a, 检查是否已处理 Note over ggml: 未处理, a 是叶子节点 (无源) ggml->>gf: 将 a 添加到 leafs 列表 ggml->>ggml: 访问节点 x, 检查是否已处理 Note over ggml: 未处理, x 是叶子节点 ggml->>gf: 将 x 添加到 leafs 列表 Note right of ggml: 返回到 ax 的处理 ggml->>gf: 将 ax 添加到 nodes 列表 ggml->>ggml: 访问节点 b, 检查是否已处理 Note over ggml: 未处理, b 是叶子节点 ggml->>gf: 将 b 添加到 leafs 列表 Note right of ggml: 返回到 y 的处理 ggml->>gf: 将 y 添加到 nodes 列表 ggml-->>User: 图构建完成

这个过程(专业上称为拓扑排序 )确保了 gf->nodes 数组中的张量是按照正确的依赖顺序排列的。ggml_graph_compute_with_ctx 只需要从头到尾遍历 gf->nodes 数组,对每个张量执行其对应的 op 操作,就能完成整个计算。

总结

在本章中,我们学习了 ggml 的执行核心------计算图 (ggml_cgraph)

  • 计算图是计算任务的**"菜谱"或"蓝图"**,它定义了操作的流程和依赖关系。
  • ggml 中,我们先定义图,再执行计算 。调用 ggml_mulggml_add 等函数只是在构建图的依赖关系,并不会立即计算。
  • 我们通过 ggml_new_graphggml_build_forward_expand 来创建和构建一个可执行的计算图。
  • 最后使用 ggml_graph_compute_with_ctx 来真正地执行计算。
  • 这种将"定义"与"执行"分离的设计,为 ggml 带来了高效、可优化和功能强大的特性。

现在,我们已经掌握了 ggml 中从数据表示到内存管理再到计算执行的整个核心流程。但是,在实际应用中,我们很少会像这样从零开始手动构建一个大型神经网络。我们更常见的做法是加载一个别人已经训练好的模型文件。

相关推荐
汤永红1 小时前
week1-[循环嵌套]画正方形
数据结构·c++·算法
重启的码农2 小时前
ggml 介绍(5) GGUF 上下文 (gguf_context)
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 小时前
应急救援智能接处警系统——科技赋能应急,筑牢安全防线
人工智能·物联网·智慧城市