欢迎来到 ggml
的世界!这是我们系列教程的第一章。在这里,我们将一起探索 ggml
的核心概念,从最基础的构建块开始。准备好了吗?让我们开始吧!
什么是张量?为什么我们需要它?
在机器学习领域,我们处理的一切几乎都是数字。比如,一张图片可以表示为像素值的网格,一段文字可以转换成数字向量,而模型的"知识"则存储为大量的参数,这些也都是数字。
我们需要一种高效的方式来组织和操作这些数字。这就是张量 (Tensor) 发挥作用的地方。
核心思想 :你可以把张量想象成一个多维的数字网格。
一维张量 就像一个简单的列表或向量:
[1, 2, 3, 4]
二维张量 就像一个 Excel 表格或矩阵:
lua[[1, 2, 3], [4, 5, 6]]
三维张量就像一本有多张工作表的 Excel 工作簿,或者一张彩色图片(高 x 宽 x 颜色通道)。
以此类推,我们可以有更高维度的张量。
在 ggml
中,ggml_tensor
是所有数据的基础结构。无论是模型的权重、你的输入(比如一句话的编码),还是计算过程中的中间结果,都用张量来表示。
张量的核心要素
一个 ggml_tensor
不仅仅是原始的数字数据,它还包含了一些"元数据"(metadata),用来描述这些数据。把这些元数据想象成贴在数据盒子上的标签,告诉我们盒子里装的是什么,以及如何排列。
主要有四个核心要素:
- 类型 (Type) : 张量中每个数字的格式是什么?是标准的32位浮点数 (
GGML_TYPE_F32
),还是半精度浮点数 (GGML_TYPE_F16
),或者是经过压缩的4位整数(一种量化类型)? - 形状 (Shape) : 张量的维度是怎样的?它有多少个维度,每个维度的大小是多少?这由一个名为
ne
(number of elements) 的数组表示。 - 步长 (Strides) : 在内存中,要跳过多少字节才能到达下一个维度的元素?这对于高效计算至关重要,由一个名为
nb
(number of bytes) 的数组表示。 - 数据指针 (Data) : 一个指向内存中真正存储数字数据位置的指针 (
void *data
)。
让我们用一个 2x3 的二维张量(2行3列)来形象化理解这些概念:
(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
内部会执行以下步骤:
这个过程确保了每个张量既有描述自己的元数据,也有存储实际数值的内存空间,并且这一切都在 上下文 (ggml_context) 的统一管理之下。
总结
在本章中,我们学习了 ggml
的基石------张量 (ggml_tensor)。
- 张量是用于表示所有数据的多维数组。
- 每个张量都有类型 、形状 、步长 和指向实际数据的指针这几个核心要素。
- 我们使用
ggml_new_tensor_...
系列函数在一个ggml_context
中创建张量。
现在你已经理解了数据在 ggml
中是如何表示的。然而,在处理像大型语言模型这样动辄数十亿参数的模型时,使用标准的32位浮点数会消耗巨大的内存和显存。有没有办法让我们的张量"瘦身"呢?
当然有!下一章,我们将探讨一个非常关键的技术:第 2 章:量化 (Quantization),学习如何用更少的数据位来表示数字,从而大幅减小模型大小和内存占用。