ggml介绍 (1) 张量 (ggml_tensor)

欢迎来到 ggml 的世界!这是我们系列教程的第一章。在这里,我们将一起探索 ggml 的核心概念,从最基础的构建块开始。准备好了吗?让我们开始吧!

什么是张量?为什么我们需要它?

在机器学习领域,我们处理的一切几乎都是数字。比如,一张图片可以表示为像素值的网格,一段文字可以转换成数字向量,而模型的"知识"则存储为大量的参数,这些也都是数字。

我们需要一种高效的方式来组织和操作这些数字。这就是张量 (Tensor) 发挥作用的地方。

核心思想 :你可以把张量想象成一个多维的数字网格

  • 一维张量 就像一个简单的列表或向量:[1, 2, 3, 4]

  • 二维张量 就像一个 Excel 表格或矩阵:

    lua 复制代码
    [[1, 2, 3],
     [4, 5, 6]]
  • 三维张量就像一本有多张工作表的 Excel 工作簿,或者一张彩色图片(高 x 宽 x 颜色通道)。

  • 以此类推,我们可以有更高维度的张量。

ggml 中,ggml_tensor 是所有数据的基础结构。无论是模型的权重、你的输入(比如一句话的编码),还是计算过程中的中间结果,都用张量来表示。

张量的核心要素

一个 ggml_tensor 不仅仅是原始的数字数据,它还包含了一些"元数据"(metadata),用来描述这些数据。把这些元数据想象成贴在数据盒子上的标签,告诉我们盒子里装的是什么,以及如何排列。

主要有四个核心要素:

  1. 类型 (Type) : 张量中每个数字的格式是什么?是标准的32位浮点数 (GGML_TYPE_F32),还是半精度浮点数 (GGML_TYPE_F16),或者是经过压缩的4位整数(一种量化类型)?
  2. 形状 (Shape) : 张量的维度是怎样的?它有多少个维度,每个维度的大小是多少?这由一个名为 ne (number of elements) 的数组表示。
  3. 步长 (Strides) : 在内存中,要跳过多少字节才能到达下一个维度的元素?这对于高效计算至关重要,由一个名为 nb (number of bytes) 的数组表示。
  4. 数据指针 (Data) : 一个指向内存中真正存储数字数据位置的指针 (void *data)。

让我们用一个 2x3 的二维张量(2行3列)来形象化理解这些概念:

graph TD subgraph logical_view ["逻辑视图 (一个 2x3 矩阵)"] direction LR A1(1.0) --- A2(2.0) --- A3(3.0) B1(4.0) --- B2(5.0) --- B3(6.0) end subgraph metadata ["元数据 (ggml_tensor 结构体中的信息)"] C["形状 ne: [3, 2, 1, 1]
(3个元素在第0维, 2个在第1维)"] D["类型 type: GGML_TYPE_F32
(32位浮点数)"] E["步长 nb:
nb[0] = 4字节 (移动到下一列)
nb[1] = 12字节 (移动到下一行)"] end subgraph physical_layout ["物理内存布局 (连续存储)"] direction LR M1[1.0] --> M2[2.0] --> M3[3.0] --> M4[4.0] --> M5[5.0] --> M6[6.0] end metadata -- "描述" --> logical_view logical_view -- "如何存储" --> physical_layout

如图所示,ggml 按"行主序"存储数据,即先把第一行的所有元素存完,再存第二行,以此类推。nb 数组告诉我们如何在这一长串内存中导航:

  • nb[0]:移动到同一行 的下一个元素需要跳过多少字节(这里是4字节,即一个 float 的大小)。
  • nb[1]:移动到下一行的同一个位置需要跳过多少字节(这里是 3列 * 4字节/列 = 12字节)。

动手创建你的第一个张量

理论足够了,让我们来写点代码!在 ggml 中创建张量非常简单。不过,在创建任何张量之前,我们需要一个"工作空间"来存放它们。这个工作空间由一个叫做 上下文 (ggml_context) 的东西管理。现在你只需要知道,所有的张量都必须在上下文中创建。

首先,让我们初始化一个上下文:

c 复制代码
#include "ggml.h"

// ... 在你的 main 函数中 ...

// 1. 设置初始化参数,分配 16MB 内存作为工作空间
struct ggml_init_params params = {
    .mem_size   = 16 * 1024 * 1024, // 16 MB
    .mem_buffer = NULL,             // ggml 会自动为我们分配内存
    .no_alloc   = false,
};

// 2. 初始化上下文
struct ggml_context * ctx = ggml_init(params);
if (!ctx) {
    fprintf(stderr, "ggml_init() 失败\n");
    return 1;
}

这段代码创建了一个内存池,我们之后创建的张量都会从这里申请空间。

创建一个一维张量(向量)

现在我们有了一个上下文 ctx,可以创建张量了。让我们创建一个包含 4 个元素的一维浮点数张量。

c 复制代码
// 创建一个一维张量,类型为 F32,包含 4 个元素
struct ggml_tensor * my_vector = ggml_new_tensor_1d(
    ctx,
    GGML_TYPE_F32,
    4
);

// 为它命名,方便调试
ggml_set_name(my_vector, "my_vector");

就是这么简单!ggml_new_tensor_1d 为我们处理了所有细节,包括计算步长和从上下文中分配内存。

创建一个二维张量(矩阵)

接下来,创建一个 2x3 的二维浮点数张量,就像我们上面图示的那样。

c 复制代码
// 创建一个二维张量,类型为 F32,形状为 3x2 (ne0=3, ne1=2)
struct ggml_tensor * my_matrix = ggml_new_tensor_2d(
    ctx,
    GGML_TYPE_F32,
    3, // 第 0 维的大小 (列数)
    2  // 第 1 维的大小 (行数)
);

// 命名
ggml_set_name(my_matrix, "my_matrix");

注意 ggml 中维度的顺序:ne[0] 通常是最低维(在矩阵中是列),ne[1] 是更高一维(行),以此类推。

创建完张量后,记得在程序结束时释放上下文,以避免内存泄漏。

c 复制代码
// 释放上下文和所有在其中创建的张量
ggml_free(ctx);

深入幕后:ggml_tensor 结构体

为了更好地理解张量,让我们看看它在 ggml 头文件 ggml.h 中的定义。这是一个简化版的结构体,展示了我们讨论过的核心要素:

c 复制代码
// 来自 include/ggml.h 的 ggml_tensor 结构体定义
struct ggml_tensor {
    // 1. 类型
    enum ggml_type type;

    // ... 其他字段 ...

    // 2. 形状 (Number of elements)
    int64_t ne[GGML_MAX_DIMS];

    // 3. 步长 (Stride in bytes)
    size_t  nb[GGML_MAX_DIMS];

    // ... 其他字段 ...

    // 4. 数据指针
    void * data;

    char name[GGML_MAX_NAME];

    // ... 其他字段 ...
};

当你调用 ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 3, 2) 时,ggml 内部会执行以下步骤:

sequenceDiagram participant User as 用户代码 participant ggml as ggml 库 participant ctx as ggml_context (内存管理器) User->>ggml: 调用 ggml_new_tensor_2d(ctx, F32, 3, 2) ggml->>ggml: 计算所需的元数据大小 (sizeof(struct ggml_tensor)) ggml->>ggml: 计算所需的数据大小 (3 * 2 * sizeof(float)) ggml->>ctx: 请求分配总内存 ctx-->>ggml: 返回分配好的内存块指针 ggml->>ggml: 在内存块前端初始化 ggml_tensor 结构体 ggml->>ggml: 设置 type = GGML_TYPE_F32 ggml->>ggml: 设置 ne = [3, 2, 1, 1] ggml->>ggml: 计算并设置 nb = [4, 12, 12, 12] ggml->>ggml: 将 data 指针指向元数据之后的内存区域 ggml-->>User: 返回指向 ggml_tensor 结构体的指针

这个过程确保了每个张量既有描述自己的元数据,也有存储实际数值的内存空间,并且这一切都在 上下文 (ggml_context) 的统一管理之下。

总结

在本章中,我们学习了 ggml 的基石------张量 (ggml_tensor)

  • 张量是用于表示所有数据的多维数组。
  • 每个张量都有类型形状步长 和指向实际数据的指针这几个核心要素。
  • 我们使用 ggml_new_tensor_... 系列函数在一个 ggml_context 中创建张量。

现在你已经理解了数据在 ggml 中是如何表示的。然而,在处理像大型语言模型这样动辄数十亿参数的模型时,使用标准的32位浮点数会消耗巨大的内存和显存。有没有办法让我们的张量"瘦身"呢?

当然有!下一章,我们将探讨一个非常关键的技术:第 2 章:量化 (Quantization),学习如何用更少的数据位来表示数字,从而大幅减小模型大小和内存占用。

相关推荐
重启的码农39 分钟前
ggml介绍 (2) 量化 (Quantization)
人工智能·神经网络
失散133 小时前
深度学习——03 神经网络(4)-正则化方法&价格分类案例
深度学习·神经网络·正则化
cwn_3 小时前
计算机视觉CS231n学习(9)
人工智能·深度学习·神经网络·学习·计算机视觉
失散139 小时前
深度学习——03 神经网络(2)-损失函数
人工智能·深度学习·神经网络·损失函数
deephub15 小时前
Dots.ocr:告别复杂多模块架构,1.7B参数单一模型统一处理所有OCR任务22
人工智能·深度学习·神经网络·ocr
巫婆理发22218 小时前
浅层神经网络
人工智能·深度学习·神经网络
失散1321 小时前
深度学习——03 神经网络(3)-网络优化方法
网络·深度学习·神经网络
xw33734095641 天前
《卷积神经网络(CNN):解锁视觉与多模态任务的深度学习核心》
人工智能·pytorch·深度学习·神经网络·cnn
人工智能培训网1 天前
对于深度神经网络,为了使得训练过程更少遇到极小点,应该采用何种权重初始化策略?
人工智能·神经网络·dnn