掘金 人工智能 12小时前
ggml 介绍(4) 计算图 (ggml_cgraph)
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本章深入讲解了ggml的核心执行机制——计算图(ggml_cgraph)。计算图被比喻为模型推理的“菜谱”,它详细定义了数据(张量)之间的操作步骤和依赖关系。文章通过一个简单的线性方程y = a * x + b的例子,演示了如何从准备上下文和张量,到定义计算步骤,再到构建和执行计算图的全过程。ggml的“定义”与“执行”分离的设计哲学,使得计算图能够实现高效执行、自动求导和跨平台优化。理解计算图是掌握ggml高效能计算的关键。

🎯 **计算图是模型推理的“菜谱”**:ggml_cgraph 就像一份详细的“菜谱”,它记录了模型计算所需的全部操作步骤(如乘法、加法)以及数据(张量)之间的依赖关系和先后顺序,但不直接执行计算,体现了“延迟执行”的核心思想。

🛠️ **构建计算图的流程**:通过调用如 `ggml_mul`、`ggml_add` 等函数来定义操作和依赖关系,这些函数仅在内存中记录计算逻辑,并不立即执行。随后,使用 `ggml_new_graph` 创建计算图对象,并利用 `ggml_build_forward_expand` 从最终输出张量反向追踪,构建出完整的、可执行的计算流程图。

🚀 **高效执行与优化**:计算图的“定义”与“执行”分离,使得ggml能够先全局规划,优化计算步骤(如并行处理、内存管理),并支持自动求导和跨平台(CPU/GPU)优化,从而实现高效的模型推理。

💡 **“定义”与“执行”分离的优势**:这种设计哲学允许计算图在被执行前进行全面的分析和优化,为模型训练(自动求导)和跨不同硬件平台的高效运行奠定了基础,是ggml强大功能的核心来源。

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

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

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

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

你不会一口气就算出 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 的一个核心设计哲学,它带来了巨大的好处:

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

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

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

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

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

#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 的操作函数来“画”出我们的计算图。记住,这些函数调用不会立即执行计算

    // 步骤 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);

这两行代码做了什么?

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

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

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

    // 1. 创建一个空的计算图对象    struct ggml_cgraph * gf = ggml_new_graph(ctx);    // 2. 从最终的目标张量 y 开始,反向追踪并构建图    ggml_build_forward_expand(gf, y);

第 4 步:执行计算

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

    // 为输入张量设置具体数值    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);

第 5 步:获取结果并清理

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

    // 从 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;}

输出应该是:

y = a * x + b = 3*2 + 4 = 10.0

深入幕后:ggml_cgraph 的内部结构

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

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

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

ggml 计算图 模型推理 张量 延迟执行
相关文章