Appearance
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_ADD、GGML_OP_MUL_MAT 等,当前共 101 个,GGML_OP_COUNT 为其上界)。ggml_tensor::op 字段就保存这个枚举值。前向与反向(自动微分)的计算逻辑并不放在结构体里,而是通过 switch 分发实现在 ggml/src/ggml.c 的 ggml_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):blockQK2_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_inplace(ggml.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; // 尚未计算!真正的计算要等到图执行阶段,分三步走:
- 构建 —
ggml_mul_mat等构造函数只填op/src[]/op_params,返回一个「待计算」的张量节点 - 收集 —
ggml_build_forward_expand(graph, output)从输出张量反向遍历src[],把整条 DAG 收进ggml_cgraph - 执行 —
ggml_backend_graph_compute(backend, graph)按拓扑序遍历节点,对每个op调用后端的实现
注意职责分离:算子的「定义」(
GGML_OP_*枚举 + 构造函数)在ggml.c;前向计算的参考实现在 CPU 后端(ggml/src/ggml-cpu/),各硬件后端再各自实现;而**反向(自动微分)**的图构建在ggml.c的ggml_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_tensor → ggml_new_tensor_impl(ggml/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 | 构建反向传播图 |