ggml后端架构简要分析
后端系统概述
GGML后端系统主要提供如下功能:
- 统一接口: 不同硬件平台使用相关的API。
- 自动选择:根据硬件自动选择最优后端。
- 灵活切换:可以在运行时切换后端。
- 扩展性:易于添加的新后端。
后端主要涉及如下概念:
- Backend:执行计算的抽象层(CPU,CUDA, Metal等)
- Buffer:后端管理的内存缓冲区。
- Buffer Type:缓冲区的类型(主机内存、GPU内存等)
- Graph Plan:计算图的执行。
- Devcice:物理设备(CPU,GPU等)
后端接口
核心类型
typedef struct ggml_backend * ggml_backend_t; // 后端句柄
typedef struct ggml_backend_buffer * ggml_backend_buffer_t; // 缓冲区句柄
typedef struct ggml_backend_buffer_type * ggml_backend_buffer_type_t; // 缓冲区类型
主要的API
(1)后端管理
// 获取后端名称
const char * ggml_backend_name(ggml_backend_t backend);
// 释放后端
void ggml_backend_free(ggml_backend_t backend);
// 获取默认缓冲区类型
ggml_backend_buffer_type_t ggml_backend_get_default_buffer_type(ggml_backend_t backend);
(2)缓冲区的操作
// 分配缓冲区
ggml_backend_buffer_t ggml_backend_alloc_buffer(ggml_backend_t backend, size_t size);
// 释放缓冲区
void ggml_backend_buffer_free(ggml_backend_buffer_t buffer);
// 获取缓冲区基址
void * ggml_backend_buffer_get_base(ggml_backend_buffer_t buffer);
(3)Tensor操作
// 设置 tensor 数据
void ggml_backend_tensor_set(struct ggml_tensor * tensor,
const void * data,
size_t offset,
size_t size);
// 获取 tensor 数据
void ggml_backend_tensor_get(const struct ggml_tensor * tensor,
void * data,
size_t offset,
size_t size);
(4)计算图执行
// 同步执行
enum ggml_status ggml_backend_graph_compute(ggml_backend_t backend,
struct ggml_cgraph * cgraph);
// 异步执行
enum ggml_status ggml_backend_graph_compute_async(ggml_backend_t backend,
struct ggml_cgraph * cgraph);
后端注册机制
注册流程
位置: src/ggml-backend-reg.cpp
初始化时注册:
struct ggml_backend_registry {
std::vector<ggml_backend_reg_entry> backends;
ggml_backend_registry() {
#ifdef GGML_USE_CUDA
register_backend(ggml_backend_cuda_reg());
#endif
#ifdef GGML_USE_METAL
register_backend(ggml_backend_metal_reg());
#endif
#ifdef GGML_USE_CPU
register_backend(ggml_backend_cpu_reg());
#endif
// ... 其他后端
}
};
后端注册结构
struct ggml_backend_reg {
const char * name; // 后端名称
ggml_backend_t (*init_fn)(void); // 初始化函数
size_t (*dev_count_fn)(void); // 设备数量
// ... 其他函数指针
};
动态加载
支持从动态库加载后端
ggml_backend_reg_t ggml_backend_load(const char * path);
4. demo.c vs demo_backend.c 对比
demo.c (传统模式)
(1)特点
- 使用 ggml_init()直接分配内存
- 所有 tensor 从 context 分配
- 使用 ggml_graph_compute_with_ctx()执行
- 简单直接,适合 CPU 计算
(2)代码流程
// 1. 初始化 context
ctx = ggml_init(params);
// 2. 创建 tensor (从 context 分配)
tensor = ggml_new_tensor_2d(ctx, ...);
// 3. 构建计算图
gf = ggml_new_graph(ctx);
ggml_build_forward_expand(gf, result);
// 4. 执行 (使用 context 的工作内存)
ggml_graph_compute_with_ctx(ctx, gf, n_threads);
(3)内存管理
- Context 管理所有内存
- 预分配内存池
- 运行时不再分配
demo_backend.c (后端模式)
(1)特点
- 使用后端系统管理内存
- 支持 GPU 等后端
- 使用 ggml_gallocr 分配内存
- 更灵活,适合生产环境
(2)代码流程
// 1. 初始化后端
backend = ggml_backend_cpu_init(); // 或 CUDA, Metal 等
// 2. 初始化 context (no_alloc = true)
ctx = ggml_init(params_no_alloc);
// 3. 创建 tensor (不分配内存)
tensor = ggml_new_tensor_2d(ctx, ...);
// 4. 后端分配内存
buffer = ggml_backend_alloc_ctx_tensors(ctx, backend);
// 5. 设置数据
ggml_backend_tensor_set(tensor, data, ...);
// 6. 构建计算图
gf = ggml_new_graph(ctx_cgraph);
ggml_build_forward_expand(gf, result);
// 7. 分配计算图内存
allocr = ggml_gallocr_new(...);
ggml_gallocr_alloc_graph(allocr, gf);
// 8. 后端执行
ggml_backend_graph_compute(backend, gf);
(3)内存管理
- 后端管理内存分配
- 支持不同内存类型 (主机内存、GPU 内存)
- 使用 ggml_gallocr优化分配
关键差异
| 特性 | demo.c | demo_backend.c |
|---|---|---|
| 内存管理 | Context 直接管理 | 后端系统管理 |
| GPU 支持 | 否 | 是 |
| 内存类型 | 仅主机内存 | 支持多种类型 |
5. 内存分配器
ggml_gallocr (Graph Allocator) 用于
- 优化计算图的内存分配
- 减少内存碎片
- 支持内存复用
Tensor分配
适用场景:你希望某些 tensor 放到特定的内存/设备(比如 Spacemit/特殊 CPU buffer),而另一些 tensor 仍放在默认 CPU buffer。核心 API 是:
- ggml_backend_buft_get_alloc_size():算出某个 buft 下该 tensor 需要的实际分配大小
- ggml_backend_buft_alloc_buffer():分配一块后端 buffer
- ggml_backend_buffer_get_base():拿到 buffer 的 base 地址
- ggml_backend_tensor_alloc(buffer, tensor, base + offset):把 tensor “绑定”到这块 buffer 的地址上
典型流程(精简版):
// 0) 构建 tensor 时建议 ctx.no_alloc = true(只建元数据,不在 ctx 内部分配)
struct ggml_tensor * a = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_A, rows_A);
struct ggml_tensor * b = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, cols_B, rows_B);
// 1) 为 a 选择特殊 buft(如 Spacemit),为 b 选择默认 CPU buft
ggml_backend_buffer_type_t a_buft = ggml_backend_cpu_riscv64_spacemit_buffer_type();
ggml_backend_buffer_type_t cpu_buft = ggml_backend_get_default_buffer_type(backend);
// 2) 分配 buffer 并把 tensor 绑定进去
size_t a_size = ggml_backend_buft_get_alloc_size(a_buft, a);
ggml_backend_buffer_t a_buf = ggml_backend_buft_alloc_buffer(a_buft, a_size);
ggml_backend_tensor_alloc(a_buf, a, ggml_backend_buffer_get_base(a_buf));
size_t b_size = ggml_backend_buft_get_alloc_size(cpu_buft, b);
ggml_backend_buffer_t b_buf = ggml_backend_buft_alloc_buffer(cpu_buft, b_size);
ggml_backend_tensor_alloc(b_buf, b, ggml_backend_buffer_get_base(b_buf));
// 3) 写入/读取 tensor 数据(后端负责必要的数据搬运)
ggml_backend_tensor_set(a, host_ptr_A, 0, ggml_nbytes(a));
ggml_backend_tensor_set(b, host_ptr_B, 0, ggml_nbytes(b));
// ... graph compute ...
ggml_backend_tensor_get(out, host_ptr_out, 0, ggml_nbytes(out));
要点:
- buffer 的生命周期必须覆盖计算阶段(结束后再 ggml_backend_buffer_free())。
- 这种方式本质上是“你负责 placement”,适合做 demo/异构内存验证/特殊平台适配。
Graph分配
适用场景:你希望把“中间 tensor + 输出 tensor”的内存分配交给 allocator 统一规划,减少碎片、提升复用率。前提通常是:
- 用于建图的 ctx 设置.no_alloc = true(tensor 先不分配)
- 图 gf 已经 ggml_build_forward_expand()完整展开
典型流程:
// 1) 创建分配器:通常用 backend 的默认 buffer type
ggml_gallocr_t allocr = ggml_gallocr_new(
ggml_backend_get_default_buffer_type(backend)
);
// 2) 为计算图分配内存(会给图里需要的 tensor 规划并分配后端 buffer)
ggml_gallocr_alloc_graph(allocr, gf);
// 3) 执行计算图
ggml_backend_graph_compute(backend, gf);
// 4) 释放分配器(以及其内部持有的资源)
ggml_gallocr_free(allocr);
可选优化(大模型/多次重复执行同结构图时更常见):
- ggml_gallocr_reserve(allocr, gf):先“规划/预留”一次,再根据 ggml_gallocr_get_buffer_size()观察峰值需求(见一些大 example 的用法)。
Workflow分配
适用场景:你有多个后端(例如主后端 + CPU fallback),希望由 scheduler/allocator 完成:
- 图的 tensor 分配
- 跨后端的 placement/拷贝(如果需要)
simple/simple-backend.cpp 的核心思路是:
- 建图时 .no_alloc = true
- 每次计算前调用 ggml_backend_sched_alloc_graph()触发分配
- 用 ggml_backend_tensor_set/get在 host 与后端 tensor 之间传数据
典型流程(精简版):
// 1) 初始化多个 backend,并创建 sched
ggml_backend_t backend = ggml_backend_init_best();
ggml_backend_t cpu_backend = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, NULL);
ggml_backend_t backends[] = { backend, cpu_backend };
ggml_backend_sched_t sched = ggml_backend_sched_new(backends, NULL, 2, GGML_DEFAULT_GRAPH_SIZE,
/*pipeline_parallel=*/false,
/*graph_copy=*/true);
// 2) 建图(ctx.no_alloc=true;图结构固定后可重复执行)
struct ggml_cgraph * gf = ...; // ggml_new_graph + ggml_build_forward_expand
// 3) 分配 + 执行
ggml_backend_sched_reset(sched);
ggml_backend_sched_alloc_graph(sched, gf);
ggml_backend_tensor_set(a, host_ptr_A, 0, ggml_nbytes(a));
ggml_backend_tensor_set(b, host_ptr_B, 0, ggml_nbytes(b));
ggml_backend_sched_graph_compute(sched, gf);
struct ggml_tensor * out = ggml_graph_node(gf, -1);
ggml_backend_tensor_get(out, host_ptr_out, 0, ggml_nbytes(out));
要点:
- 这条路线更接近“生产用法”:分配/拷贝/执行由 backend 系统统一处理,你只维护图和输入输出。
- 如果你需要“像 demo_spacemit.c 那样手动把某个输入 tensor 固定到特定 buft”,就不要完全依赖 scheduler 的自动分配;通常应采用tensor 级手动分配或者在更高层做 placement 策略(取决于后端能力)。