乐于分享
好东西不私藏

llama.cpp 源码分析:后端管理与路由(深入篇)

llama.cpp 源码分析:后端管理与路由(深入篇)

上一篇文章发布后,我又仔细琢磨了一下,觉得深度不够,所以这次再发一遍,加强一些内容的解释。

1. 从一个调用开始

ggml_backend_load_all();

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

CMake 阶段根据平台/选项决定编译哪些后端,并定义对应的 GGML_USE_* 宏

编译阶段把每个后端编译成 ggml-base + ggml 内部链接,或独立的 libggml-*.so/.dylib

运行时 ggml_backend_load_all() 把静态注册的后端,以及磁盘上找到的动态后端库,统一加入 registry

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

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

2. CMake 构建链路:从 option 到编译宏

2.1 顶层 option

源码位置:ggml/CMakeLists.txt:95-269

if(APPLE) set(GGML_METAL_DEFAULT ON) set(GGML_BLAS_DEFAULT ON) set(GGML_BLAS_VENDOR_DEFAULT ”Apple”)else() set(GGML_METAL_DEFAULT OFF) set(GGML_BLAS_DEFAULT OFF)endif()option(GGML_CPU ”ggml: enable CPU backend” ON)option(GGML_BLAS ”ggmluse BLAS” ${GGML_BLAS_DEFAULT})option(GGML_CUDA ”ggmluse CUDA” OFF)option(GGML_METAL ”ggmluse Metal” ${GGML_METAL_DEFAULT})option(GGML_VULKAN ”ggmluse Vulkan” OFF)option(GGML_RPC ”ggmluse RPC” OFF)// ...

在 macOS 上,GGML_METAL默认就是 ON。你在 Linux 上想启用 CUDA,就要显式 -DGGML_CUDA=ON。

2.2 添加后端子目录并定义 GGML_USE_*

源码位置:ggml/src/CMakeLists.txt:312-323

function(ggml_add_backend backend) string(TOUPPER ”GGML_${backend}” backend_id) if (${backend_id}) string(TOLOWER ”ggml-${backend}” backend_target) add_subdirectory(${backend_target}) message(STATUS ”Including ${backend} backend”) if (NOT GGML_BACKEND_DL) string(TOUPPER ”GGML_USE_${backend}” backend_use) target_compile_definitions(ggml PUBLIC ${backend_use}) endif() endif()endfunction()

这是关键转换:

CMake option GGML_METAL=ON → 进入 ggml/src/ggml-metal/CMakeLists.txt

非动态后端模式下,给 ggml 目标定义 GGML_USE_METAL 宏

于是 ggml/src/ggml-backend-reg.cpp 里#ifdefGGML_USE_METAL 成立,Metal 后端被静态注册

2.3 后端被编译成什么目标

源码位置:ggml/src/CMakeLists.txt:265-310

function(ggml_add_backend_library backend) if (GGML_BACKEND_DL) add_library(${backend} MODULE ${ARGN}) set_target_properties(${backend} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) target_compile_definitions(${backend} PRIVATE GGML_BACKEND_DL) add_dependencies(ggml ${backend}) else() add_library(${backend} ${ARGN}) target_link_libraries(ggml PUBLIC ${backend}) endif() target_link_libraries(${backend} PRIVATE ggml-base)endfunction()

两种构建模式:

模式
CMake 选项
效果
静态链接(默认)
GGML_BACKEND_DL=OFF
后端代码直接编译进 libggml.so,通过 GGML_USE_* 宏注册
动态后端
GGML_BACKEND_DL=ON
每个后端编译成独立的 libggml-*.so,运行时 dlopen 加载

默认情况下我们用的都是静态链接模式。GGML_BACKEND_DL 主要用于减小主库体积、支持插件式后端。

2.4 以 Metal 为例看 CMake 链路

源码位置:ggml/src/ggml-metal/CMakeLists.txt:1-20

find_library(FOUNDATION_LIBRARY Foundation REQUIRED)find_library(METAL_FRAMEWORK Metal REQUIRED)find_library(METALKIT_FRAMEWORK MetalKit REQUIRED)message(STATUS ”Metal framework found”)ggml_add_backend_library(ggml-metal ggml-metal.cpp ggml-metal-device.m ggml-metal-device.cpp ggml-metal-common.cpp ggml-metal-context.m ggml-metal-ops.cpp)target_link_libraries(ggml-metal PRIVATE ${FOUNDATION_LIBRARY} ${METAL_FRAMEWORK} ${METALKIT_FRAMEWORK})

所以当你在 Mac 上 cmake -B build:

GGML_METAL=ON

(Apple 默认)

ggml_add_backend(METAL)

被调用

进入 ggml/src/ggml-metal/

编译目标 ggml-metal

在非动态模式下,target_link_libraries(ggml PUBLIC ggml-metal) 把它链进 libggml

target_compile_definitions(ggml PUBLIC GGML_USE_METAL)

启用静态注册

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

3.1 入口:ggml_backend_load_all()

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

voidggml_backend_load_all_from_path(constchar * 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(”cpu”, silent, dir_path); // 最后加载 CPU const char * backend_path = std::getenv(”GGML_BACKEND_PATH”); if (backend_path) { ggml_backend_load(backend_path); }}

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

3.2 静态注册:条件编译 + 全局 registry

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

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}

这里的 ggml_backend_*_reg() 就是每个后端提供的“注册函数”。它返回一个 ggml_backend_reg_t,里面包含:

后端名字

设备数量

获取设备函数

get_proc_address

:暴露后端特有 API

注册后,registry 会遍历该后端的所有 device,调用 register_device() 加入全局设备列表。

3.3 动态加载:dlopen + 符号查找

当 GGML_BACKEND_DL=ON 时,每个后端被编译成独立模块,并导出两个符号:

源码位置:ggml/src/ggml-backend-impl.h:240-271

#define GGML_BACKEND_DL_IMPL(reg_fn) \ extern ”C” { \ GGML_BACKEND_API ggml_backend_reg_t ggml_backend_init(void); \ } \ ggml_backend_reg_t ggml_backend_init(void) { \ return reg_fn(); \ }

例如 ggml-cpu.cpp:703、 ggml-metal.cpp:950 都有:

GGML_BACKEND_DL_IMPL(ggml_backend_cpu_reg)GGML_BACKEND_DL_IMPL(ggml_backend_metal_reg)

运行时 ggml_backend_load_best() 会:

按命名规则 [lib]ggml-{name}-*.[so|dll] 搜索文件

dlopen加载

dlsym查找 ggml_backend_score(可选)和 ggml_backend_init

调用 ggml_backend_init() 拿到 reg,注册进全局 registry

源码位置:ggml/src/ggml-backend-reg.cpp:213-257

ggml_backend_reg_tload_backend(const fs::path & path, bool silent){ dl_handle_ptr handle { dl_load_library(path) }; auto score_fn = (ggml_backend_score_tdl_get_sym(handle.get(), ”ggml_backend_score”); auto backend_init_fn = (ggml_backend_init_tdl_get_sym(handle.get(), ”ggml_backend_init”); ggml_backend_reg_t reg = backend_init_fn(); register_backend(reg, std::move(handle)); return reg;}

3.4 .so/.dylib 搜索路径

源码位置:ggml/src/ggml-backend-reg.cpp:457-489

static fs::path backend_filename_prefix(){#ifdef _WIN32 return fs::u8path(”ggml-”);#else return fs::u8path(”libggml-”);#endif}static fs::path backend_filename_extension(){#ifdef _WIN32 return fs::u8path(”.dll”);#else return fs::u8path(”.so”);#endif}

搜索路径(ggml/src/ggml-backend-reg.cpp:479-489):

如果编译时定义了 GGML_BACKEND_DIR,先搜这里

可执行文件所在目录(get_executable_path())

当前工作目录

如果调用 ggml_backend_load_all_from_path(path),则只搜指定目录

环境变量 GGML_BACKEND_PATH 指定的额外后端

所以你把 libggml-metal.dylib 和可执行文件放同一目录,运行时就能自动加载。

4. 后端接口:从抽象到真实实现

4.1 三层对象

llama.cpp 的后端抽象有三层:

对象
类型
作用
ggml_backend_reg_t
backend registration
一个“后端插件”,包含多个 device
ggml_backend_dev_t
backend device
一个具体设备(如一张 GPU)
ggml_backend_t
backend instance
一个实际可执行计算的后端实例

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

4.2 reg_t:后端注册信息

struct ggml_backend_reg_i { const char * (*get_name)(...); size_t (*get_device_count)(...); ggml_backend_dev_t (*get_device)(...); void * (*get_proc_address)(...);};struct ggml_backend_reg { int api_version; struct ggml_backend_reg_i iface; void * context;};

get_proc_address 用于暴露后端特有 API,例如 CPU 的 NUMA 控制、线程池设置等。

4.3 dev_t:设备能力描述

struct ggml_backend_device_i { const char * (*get_name)(...); const char * (*get_description)(...); enumggml_backend_dev_type (*get_type)(...)// CPU/GPU/ACCEL // 内存查询 void (*get_memory)(...); // 是否支持 op bool (*supports_op)(...); // 初始化 backend 实例 ggml_backend_t (*init_backend)(...); // ...};

4.4 backend_t:真正执行计算

struct ggml_backend_i { const char * (*get_name)(...); void (*free)(...); // buffer ggml_backend_buffer_type_t (*get_default_buffer_type)(...); // tensor void (*set_tensor_async)(...); void (*get_tensor_async)(...); // compute enumggml_status (*graph_compute)(...); // ...};

llama_context 最终持有的是 ggml_backend_t 列表。

4.5 以 CPU 后端为例看真实实现

源码位置:ggml/src/ggml-cpu/ggml-cpu.cpp

ggml_backend_cpu_reg()

:ggml-cpu.cpp:690-700,初始化 CPU feature detection,返回 reg

ggml_backend_cpu_reg_i

:包含 get_name、get_device_count、get_device、get_proc_address

ggml_backend_cpu_device_i

:包含 init_backend、内存查询、supports_op 等

ggml_backend_cpu_get_proc_address()

:ggml-cpu.cpp:600-680,暴露 ggml_backend_cpu_set_n_threads、ggml_backend_cpu_numa_init 等特有函数

真正的计算在 ggml_backend_cpu_graph_compute() 里,最终调用 ggml_graph_compute()

4.6 以 Metal 后端为例

源码位置:ggml/src/ggml-metal/ggml-metal.cpp

ggml_backend_metal_reg()

:ggml-metal.cpp:908-948,创建 Metal device,返回 reg

ggml_backend_metal_device_i

:设备接口

ggml_backend_metal_i

:backend 接口,其中 graph_compute 最终调用 Metal command buffer 提交 kernel

Metal shader 代码在 ggml/src/ggml-metal/ggml-metal.metal 中

4.7 以 CUDA 后端为例

源码位置:ggml/src/ggml-cuda/ggml-cuda.cu

ggml_backend_cuda_reg()在 cu文件中

CUDA kernel 定义在同目录的 .cuh/.cu 文件中

通过 cudaStream、cublas、flash attention 等实现算子

5. llama_context 如何挑选后端

5.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、LAYER、ROW、TENSOR

对 LLAMA_SPLIT_MODE_TENSOR,创建 meta device

5.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 必须放在最后,这样 scheduler 分配算子时,GPU/ACCEL 优先认领。

6. 调度器:ggml_backend_sched

6.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));

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

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

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 拷贝

6.3 op_offload

cparams.op_offload 控制是否把 CPU 上的 op 尽量 offload 到 GPU。代码在 ggml-backend.cpp:919-925。

6.4 执行

auto status = ggml_backend_sched_graph_compute_async(sched.get(), gf);

异步执行,通过 event 同步跨 backend 拷贝。

7. Pipeline 并行

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

多 GPU + split_mode=LAYER + 全部层卸载时,llama.cpp 会尝试 pipeline 并行,用 GGML_SCHED_MAX_COPIES=4 个 graph 副本重叠执行。

8. 小结

CMake 阶段: GGML_METAL=ON / GGML_CUDA=ON / GGML_CPU=ON ↓ ggml_add_backend(METAL/CUDA/CPU) ↓ 编译目标 ggml-metal / ggml-cuda / ggml-cpu ↓ 定义 GGML_USE_METAL / GGML_USE_CUDA / GGML_USE_CPU静态注册(默认): ggml/src/ggml-backend-reg.cpp #ifdef GGML_USE_* register_backend(ggml_backend_*_reg())动态后端(GGML_BACKEND_DL): 编译生成 libggml-*.so 运行时 dlopen → dlsym(ggml_backend_init) → register_backend()运行时: llama_context 收集 GPU/ACCEL/CPU backend ↓ ggml_backend_sched_new() 创建 scheduler ↓ llama_decode() → build_graph() → split_graph() → graph_compute_async()

理解这套机制后,你就能解释:

为什么 Mac 上日志会显示 ggml_metal_init: loaded Metal library?因为 CMake 默认 GGML_METAL=ON, Metal 后端被编译并注册。

为什么 -DGGML_CUDA=ON 后代码路径会不同?因为 ggml_add_backend(CUDA) 会进入 ggml/src/ggml-cuda/,并定义 GGML_USE_CUDA。

为什么 -ngl 0 会慢?因为所有层都在 CPU backend,scheduler 没机会把 op 发到 GPU。

为什么可以把 libggml-metal.dylib 单独分发?因为 GGML_BACKEND_DL=ON 时它是独立模块,ggml_backend_load_all() 会在可执行文件目录找到它。

参考

  • ggml/CMakeLists.txt:95-269:后端 CMake option

  • ggml/src/CMakeLists.txt:265-323:ggml_add_backend_library()、ggml_add_backend()

  • ggml/src/ggml-metal/CMakeLists.txtMetal 后端 CMake 示例

  • ggml/src/ggml-backend-reg.cpp:115-167:静态注册

  • ggml/src/ggml-backend-reg.cpp:213-257:动态加载

  • ggml/src/ggml-backend-reg.cpp:457-586:搜索路径与 ggml_backend_load_all()

  • ggml/src/ggml-backend-impl.h:240-271:GGML_BACKEND_DL_IMPL

  • ggml/src/ggml-cpu/ggml-cpu.cpp:690-700:CPU 后端注册

  • ggml/src/ggml-metal/ggml-metal.cpp:908-950:Metal 后端注册

  • ggml/include/ggml-backend.h:backend 接口定义

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