Skip to content

GGML 张量库基础 — 代码走读

ggml/include/ggml.h — 公共 API

这是 GGML 的主要头文件,导出了所有张量操作。

张量创建

c
// 创建上下文(内存池)
struct ggml_context * ggml_init(struct ggml_init_params params);

// 创建张量
struct ggml_tensor * ggml_new_tensor_1d(struct ggml_context * ctx,
    enum ggml_type type, int64_t ne0);
struct ggml_tensor * ggml_new_tensor_2d(struct ggml_context * ctx,
    enum ggml_type type, int64_t ne0, int64_t ne1);
struct ggml_tensor * ggml_new_tensor_4d(struct ggml_context * ctx,
    enum ggml_type type, int64_t ne0, int64_t ne1, int64_t ne2, int64_t ne3);

算术运算

c
// 逐元素运算
struct ggml_tensor * ggml_add(ctx, a, b);
struct ggml_tensor * ggml_mul(ctx, a, b);
struct ggml_tensor * ggml_div(ctx, a, b);

// 矩阵乘法
struct ggml_tensor * ggml_mul_mat(ctx, a, b);  // b @ a^T

// 激活函数
struct ggml_tensor * ggml_gelu(ctx, a);
struct ggml_tensor * ggml_silu(ctx, a);
struct ggml_tensor * ggml_relu(ctx, a);

计算图执行

c
// 构建计算图
struct ggml_cgraph * ggml_new_graph(struct ggml_context * ctx);
void ggml_build_forward_expand(struct ggml_cgraph * graph, struct ggml_tensor * tensor);

// 计算
void ggml_graph_compute(struct ggml_cgraph * graph);

ggml/src/ggml.c — 核心实现

关键实现细节:

张量操作注册

每个张量操作由一个 ggml_op 枚举值标识(定义在 ggml/include/ggml.h 约 492 行,例如 GGML_OP_ADDGGML_OP_MUL_MAT 等,当前共 101 个,GGML_OP_COUNT 为其上界)。ggml_tensor::op 字段就保存这个枚举值。前向与反向(自动微分)的计算逻辑并不放在结构体里,而是通过 switch 分发实现在 ggml/src/ggml.cggml_compute_backward(以及后端层的对应前向计算)中。真正的后端执行则由独立的 ggml_backend 接口负责,并不是 ggml_op 的某个字段。

上游先前的窗口新增了面向稀疏注意力的算子:GGML_OP_LIGHTNING_INDEXER(DeepSeek 的 lightning indexer,取 f16 mask)与 DeepSeek V4 的三个融合超连接算子 GGML_OP_DSV4_HC_PRE / _COMB / _POST(对应构造函数 ggml_dsv4_hc_pre/comb/post());张量类型 GGML_TYPE_Q2_0 (=42):block QK2_0=64,布局为 block_q2_0 { ggml_half d; uint8_t qs[QK2_0/4] }(18 字节,2 bit/元素),对应文件类型 LLAMA_FTYPE_MOSTLY_Q2_0 = 41。另有检查「内层维度」连续性的辅助函数 ggml_is_contiguous_to_{1,2,3}()(均为 GGML_API)。

本窗口(GGML 0.17.0 → 0.23.0)算子枚举与张量类型均无增删GGML_OP_COUNT 仍 101、GGML_TYPE_COUNT 仍 43),但公共 API 有一批语义级变化:

  • ggml_clamp 拆分 — 现在默认非原地(返回 dup 结果张量),原地版本改叫 ggml_clamp_inplaceggml.c:4127/4135
  • ggml_rope_set_offset(a, n_offs) — 新 setter:让 RoPE 的前 n_offs 维不旋转(写 op_params[15],vision RoPE 显式 assert 不支持)
  • ggml_ssm_scan 签名增加第 9 参 int64_t K — 请求后端在最终态之外多返回 K−1 份循环态回滚快照(服务 recurrent 模型的推测解码回滚)
  • ggml_swiglu_clamp(ctx, a, b, limit) — 新的 GLU 子类型 GGML_GLU_OP_SWIGLU_CLAMP(不是新 ggml_op,走 GGML_OP_GLU 的 glu_op 参数)
  • ggml_prec_set_acc / ggml_prec_set_src — 精度等级通用 setter(详见概念页的 ggml_prec 小节)
  • ggml_flash_attn_ext_set_n_kv_max稀疏 Flash Attention 入口(CUDA/Metal):mask 中的有限项被当作稀疏 K/V 集合,CUDA 端会把 mask 压缩为索引表
  • ggml_build_forward_order(graph, tensor) — 只插入节点但不置 compute 标志的图收集变体(ggml_build_forward_expand 会把祖先全标记为 compute)

内存布局

GGML 使用行主序 (row-major) 存储,nb[0] 是最小步长(单个元素的字节数):

对于一个 shape [ne0, ne1] 的张量:
nb[0] = ggml_type_size(type)                              // 单个元素的字节数
nb[1] = nb[0] * (ne0 / ggml_blck_size(type)) + padding    // 一行(含可选 padding)

其中 ggml_blck_size(type) 是该类型的块大小。对未量化(块大小为 1)类型,nb[1] 退化为 nb[0] * ne0;对量化类型,每个 block 包含多个元素,需先除以块大小再乘以单元素字节数。更高维步长则按 nb[i] = nb[i-1] * ne[i-1] 递推。

算子的构造与图分发

理解 GGML 的关键是:创建算子张量时不执行任何计算。以 ggml_mul_mat(ctx, a, b) 为例(ggml/src/ggml.c:~3341),它只做三件事:

cpp
// 简化示意(真实见 ggml/src/ggml.c)
struct ggml_tensor * result = ggml_new_tensor(ctx, ...);
result->op        = GGML_OP_MUL_MAT;   // 记录算子类型
result->src[0]    = a;                  // 记录输入(图边)
result->src[1]    = b;
result->op_params[0] = ...;             // 记录参数(如是否转置)
return result;                          // 尚未计算!

真正的计算要等到图执行阶段,分三步走:

  1. 构建ggml_mul_mat 等构造函数只填 op / src[] / op_params,返回一个「待计算」的张量节点
  2. 收集ggml_build_forward_expand(graph, output) 从输出张量反向遍历 src[],把整条 DAG 收进 ggml_cgraph
  3. 执行ggml_backend_graph_compute(backend, graph) 按拓扑序遍历节点,对每个 op 调用后端的实现

注意职责分离:算子的「定义」(GGML_OP_* 枚举 + 构造函数)在 ggml.c前向计算的参考实现在 CPU 后端(ggml/src/ggml-cpu/),各硬件后端再各自实现;而**反向(自动微分)**的图构建在 ggml.cggml_compute_backward:~6723)。同一个 GGML_OP_MUL_MAT 在头文件声明一次,多个后端各自实现——这就是为什么新算子总是「先有枚举与 CPU 参考,再逐后端补 kernel」。

内存池(arena)分配

GGML 的张量不从系统 malloc 逐个分配,而是从一个 ggml_context 内存池(arena)里线性 bump 分配

c
// ggml/include/ggml.h:~677
struct ggml_init_params {
    size_t   mem_size;    // 内存池大小
    void   * mem_buffer;  // 调用方提供,或 NULL 让 GGML 自己 mmap/malloc
    bool     no_alloc;    // true 时仅记账不分配数据
};

struct ggml_context * ctx = ggml_init(params);

ggml_new_tensorggml_new_tensor_implggml/src/ggml.c:~1766)从池中划出一块,tensor->data 直接指向 arena 内的偏移。优点:

  • 零碎片 — 线性分配、整批释放,无内存碎片
  • 一次性释放ggml_free(ctx) 释放池里所有张量,无需逐个 free
  • 可托管no_alloc 模式下只建立张量元数据,实际数据由后端 buffer 接管(推理时权重即如此)

这也解释了为何一次推理的「临时计算图」要用独立 context:图执行完 ggml_free 一次回收所有中间张量,干净利落。

关键函数索引

函数说明
ggml_init创建上下文内存池
ggml_new_tensor_*d创建指定维度张量(从池中分配)
ggml_mul_mat矩阵乘法(核心运算,注意 b @ a^T 参数顺序;仅构造节点不计算)
ggml_build_forward_expand把输出张量的依赖收集成计算图
ggml_new_graph创建计算图
ggml_graph_compute执行计算图
ggml_backend_graph_compute在指定后端上执行计算图
ggml_set_param标记可训练参数
ggml_build_backward_expand构建反向传播图