ggml后端架构简要分析

🕒 2026-01-29 📁 推理框架 👤 laumy 🔥 96 热度

后端系统概述

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 策略(取决于后端能力)。

发表你的看法

\t