乐于分享
好东西不私藏

llama.cpp 源码分析:后端管理与路由

llama.cpp 源码分析:后端管理与路由

llama.cpp 源码分析:后端管理与路由

在上一篇中,我们写了一个最小可运行的聊天程序。其中第一行有效代码就是 ggml_backend_load_all()——它看似简单,却决定了你的模型能不能用到 Apple Silicon 上的 Metal、NVIDIA 上的 CUDA,或者只能跑在 CPU 上。本文深入 llama.cpp 的后端子系统,讲清楚“后端”是怎么被发现、注册、调度和协同工作的。

从一个调用开始

ggml_backend_load_all();

这行代码在 examples/simple/simple.cpp、llama-cli 以及我们自己的 demo 里都会出现。它背后的故事是:

扫描并加载所有编译进来的后端插件(Metal、CUDA、BLAS、CPU 等)

把它们注册到一个全局 registry

后续 llama_context 根据模型参数,挑选合适的 backend 创建 scheduler

scheduler 在每次 llama_decode() 时,把计算图拆成多块,分发到不同后端执行

2. 后端的注册:静态 + 动态

2.1 入口:ggml_backend_load_all()

源码位置:ggml/src/ggml-backend-reg.cpp:555-586

void ggml_backend_load_all() {ggml_backend_load_all_from_path(nullptr);}void ggml_backend_load_all_from_path(const char * dir_path) {// ...ggml_backend_load_best("blas", silent, dir_path);ggml_backend_load_best("cuda", silent, dir_path);ggml_backend_load_best("metal", silent, dir_path);ggml_backend_load_best("vulkan", silent, dir_path);// ...ggml_backend_load_best("cpu", silent, dir_path); // 最后加载 CPU}

注意加载顺序:CPU 被故意放在最后。这是因为 scheduler 在后端列表里把 CPU 当作“兜底”后端——前面 GPU/ACCEL 后端处理不了的算子,最后交给 CPU。

2.2 静态注册

源码位置:ggml/src/ggml-backend-reg.cpp:115-167

ggml_backend_registry() {#ifdefGGML_USE_CUDAregister_backend(ggml_backend_cuda_reg());#endif#ifdefGGML_USE_METALregister_backend(ggml_backend_metal_reg());#endif// ...#ifdefGGML_USE_CPUregister_backend(ggml_backend_cpu_reg());#endif}

编译时通过宏控制哪些后端被静态链接。你在 macOS 上编译 llama.cpp,只要检测到 Metal framework,GGML_USE_METAL 就会被定义,Metal 后端就自动注册。

2.3 动态加载

ggml_backend_load_best() 会尝试 dlopen 对应的后端共享库(如 libggml-metal.dylib),找到它的注册函数并加入 registry。这对插件化分发很有用。

后端的抽象:ggml_backend

每个后端都实现了一套统一的接口(vtable),核心能力包括:

分配/释放 buffer

把 tensor 数据从 host 拷贝到 device

判断自己是否支持某个 op(supports_op)

执行某个 op(compute_forward)

源码位置:ggml/include/ggml-backend.h:240-353

struct ggml_backend_i {const char * (*get_name)(...);void (*free)(...);// bufferggml_backend_buffer_type_t (*get_default_buffer_type)(...);// tensorvoid (*set_tensor_async)(...);void (*get_tensor_async)(...);// computebool (*supports_op)(...);enumggml_status (*graph_compute)(...);// ...};

CPU 后端实现:ggml/src/ggml-cpu/ggml-cpu.cpp:690-700

Metal 后端实现:ggml/src/ggml-metal/ggml-metal.mm

CUDA 后端实现:ggml/src/ggml-cuda/ggml-cuda.cu:5572+

llama_context 如何挑选后端

4.1 模型加载阶段:llama_prepare_model_devices()

源码位置:src/llama.cpp:100-276

llama_model_load_from_file() 在加载模型时,会调用 llama_prepare_model_devices() 来收集可用设备:

遍历所有已注册的后端

筛选出 GPU 设备(GGML_BACKEND_DEVICE_TYPE_GPU)

处理 split_mode:NONE(单 GPU)、LAYER(按层切分)、ROW/TENSOR(张量并行)

对 LLAMA_SPLIT_MODE_TENSOR,创建一个 meta device

4.2 上下文创建阶段

源码位置:src/llama-context.cpp:268-295

// 1. GPU 后端for (const auto & dev : model.devices) {ggml_backend_t backend = ggml_backend_dev_init(dev.dev, nullptr);backends.emplace_back(backend);}// 2. ACCEL 后端(BLAS)for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {ggml_backend_dev_t dev = ggml_backend_dev_get(i);if (ggml_backend_dev_type(dev) == GGML_BACKEND_DEVICE_TYPE_ACCEL) {backends.emplace_back(ggml_backend_dev_init(dev, nullptr));}}// 3. CPU 兜底backend_cpu = ggml_backend_init_by_type(GGML_BACKEND_DEVICE_TYPE_CPU, nullptr);backends.emplace_back(backend_cpu);

关键规则:CPU 必须放在 backend 列表的最后。因为 scheduler 在分配算子时,优先让前面的 GPU/ACCEL 后端认领,最后没人要的才给 CPU。

调度器:ggml_backend_sched

5.1 创建

源码位置:src/llama-context.cpp:462, 618

sched.reset(ggml_backend_sched_new(backend_ptrs.data(),backend_buft.data(),backend_ptrs.size(),max_nodes,cparams.pipeline_parallel,cparams.op_offload));

ggml_backend_sched_new() 的定义:ggml/src/ggml-backend.cpp:1727

它接收一个 backend 数组、buffer type 数组、最大图节点数、是否启用 pipeline 并行、是否允许 op offload。

5.2 图切分与路由:5-pass 算法

源码位置:ggml/src/ggml-backend.cpp:1014-1487

每次调用 ggml_backend_sched_alloc_graph() 或 ggml_backend_sched_graph_compute() 时,scheduler 会执行一个 5-pass 算法,把整棵计算图切成若干 ggml_backend_sched_split:

Pass 作用

Pass 1 已经预分配在特定 backend 上的输入/权重,保持不动

Pass 2 向上/向下扩展 GPU backend 的赋值范围

Pass 3 如果某个高优先级 backend 能执行某个 op,就把该 op 搬过去

Pass 4 为剩余的 src tensor 从 dst/view_src 推断 backend

Pass 5 把图切成连续的 backend chunk,标记跨 backend 拷贝

关键函数 ggml_backend_sched_backend_id_from_cur():ggml-backend.cpp:878-933

static int ggml_backend_sched_backend_id_from_cur(...) {// 1. 已预分配的 tensor 保持原 backend// 2. 权重 tensor 跟随其所在 backend// 3. op_offload 开启时,CPU 上的权重可能被搬到 GPU}

5.3 op_offload 的含义

cparams.op_offload 控制是否把本应在 CPU 上执行的 op 尽量 offload 到 GPU。它会影响 Pass 3 的决策:

if (sched->op_offload && src_backend_id == sched->n_backends - 1 &&ggml_backend_buffer_is_host(src->buffer)) {for (int b = 0; b < src_backend_id; b++) {if (ggml_backend_supports_op(sched->backends[b], tensor) &&ggml_backend_offload_op(sched->backends[b], tensor)) {return b; // offload 到 GPU}}}

5.4 执行:同步与异步

// 同步ggml_backend_sched_graph_compute(sched, gf);// 异步(llama_context 实际使用)ggml_backend_sched_graph_compute_async(sched, gf);llama_context::graph_compute():src/llama-context.cpp:2416-2444auto status = ggml_backend_sched_graph_compute_async(sched.get(), gf);return status;

异步执行时,scheduler 会通过 event 同步跨 backend 的数据拷贝。对于 Metal/CUDA,这意味着 CPU 可以提前准备下一个 batch,而不必等 GPU 算完。

Pipeline 并行

源码位置:src/llama-context.cpp:366-391

当存在多张 GPU 且 split_mode == LLAMA_SPLIT_MODE_LAYER、n_gpu_layers 足够大时,llama.cpp 会尝试 pipeline 并行:

bool pipeline_parallel =model.n_devices() > 1 &&model.n_gpu_layers() > model.hparams.n_layer_all &&model.split_mode() == LLAMA_SPLIT_MODE_LAYER &&cparams.offload_kqv &&!model.has_tensor_overrides();

这要求所有设备都支持 async 和 events。启用后,scheduler 可以同时维护多个 graph 副本(GGML_SCHED_MAX_COPIES = 4),通过事件机制重叠不同 batch 的计算。

小结

ggml_backend_load_all()注册 Metal/CUDA/BLAS/CPU 后端(CPU 最后)llama_prepare_model_devices() 按 split_mode 选设备llama_context 创建 backend 列表(GPU → ACCEL → CPU)ggml_backend_sched_new() 构造 scheduler每次 llama_decode():build_graph() → sched_alloc_graph() → split_graph() → graph_compute_async()

理解这套机制后,你就能解释很多现象:

为什么 M5 MacBook Pro 上日志会显示 ggml_metal_init: loaded Metal library?因为 Metal 后端被注册并选中。

为什么 -ngl 0 会慢很多?因为所有层都被分配到 CPU backend,scheduler 的 5-pass 优化空间骤减。

为什么多卡时要设置 split_mode?因为 llama_prepare_model_devices() 和 scheduler 需要根据这个策略切分层/张量。

参考

  • ggml/include/ggml-backend.h:后端公共 API

  • ggml/src/ggml-backend-reg.cpp:555-586:ggml_backend_load_all()

  • ggml/src/ggml-backend.cpp:1014-1487:scheduler 图切分

  • src/llama-context.cpp:268-295:上下文后端初始化

  • src/llama-context.cpp:2416-2444:llama_context::graph_compute()

  • llama.cpp 官方后端文档