上一篇文章发布后,我又仔细琢磨了一下,觉得深度不够,所以这次再发一遍,加强一些内容的解释。
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 ”ggml: use BLAS” ${GGML_BLAS_DEFAULT})option(GGML_CUDA ”ggml: use CUDA” OFF)option(GGML_METAL ”ggml: use Metal” ${GGML_METAL_DEFAULT})option(GGML_VULKAN ”ggml: use Vulkan” OFF)option(GGML_RPC ”ggml: use 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()
两种构建模式:
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-metalggml-metal.cppggml-metal-device.mggml-metal-device.cppggml-metal-common.cppggml-metal-context.mggml-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); // 最后加载 CPUconst 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_CUDAregister_backend(ggml_backend_cuda_reg());#endif#ifdef GGML_USE_METALregister_backend(ggml_backend_metal_reg());#endif// ...#ifdef GGML_USE_CPUregister_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_t) dl_get_sym(handle.get(), ”ggml_backend_score”);auto backend_init_fn = (ggml_backend_init_t) dl_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 _WIN32return fs::u8path(”ggml-”);#elsereturn fs::u8path(”libggml-”);#endif}static fs::path backend_filename_extension(){#ifdef _WIN32return fs::u8path(”.dll”);#elsereturn 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 | ||
ggml_backend_dev_t | ||
ggml_backend_t |
源码位置: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)(...);// 是否支持 opbool (*supports_op)(...);// 初始化 backend 实例ggml_backend_t (*init_backend)(...);// ...};
4.4 backend_t:真正执行计算
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)(...);// computeenumggml_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:
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.txt:Metal 后端 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:上下文后端初始化
夜雨聆风