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 官方后端文档
夜雨聆风