现代软件工程中,目录结构从来不仅限于文件的分类和组织,而是系统架构的物理映射。
llama.cpp 相对完整的目录结构如下图:
├── app
│ ├── CMakeLists.txt
│ ├── download.cpp
│ └── llama.cpp
├── benches
├── cmake
│ ├── arm64-apple-clang.cmake
│ ├── arm64-linux-clang.cmake
│ │ ......
│ └── x64-windows-llvm.cmake
├── CMakeLists.txt
├── CMakePresets.json
├── CODEOWNERS
├── common
│ ├── arg.cpp
│ ├── arg.h
│ ├── base64.hpp
│ ├── build-info.cpp
│ ├── build-info.cpp.in
│ ├── build-info.h
│ │ ......
│ ├── speculative.cpp
│ ├── speculative.h
│ ├── unicode.cpp
│ └── unicode.h
├── conversion
│ ├── __init__.py
│ │ ......
│ └── youtuvl.py
├── convert_hf_to_gguf_update.py
├── convert_hf_to_gguf.py
├── convert_llama_ggml_to_gguf.py
├── convert_lora_to_gguf.py
├── docs
├── examples
│ ├── batched
│ ├── batched.swift
│ │ ......
│ ├── sycl
│ ├── training
│ └── ts-type-to-grammar.sh
├── flake.nix
├── ggml
│ ├── cmake
│ ├── CMakeLists.txt
│ ├── include
│ └── src
├── gguf-py
├── grammars
├── include
│ ├── llama-cpp.h
│ └── llama.h
├── Makefile
├── media
├── models
│ ├── ggml-vocab-aquila.gguf
│ ├── ggml-vocab-baichuan.gguf
│ │ ......
│ ├── ggml-vocab-viking.gguf.inp
│ ├── ggml-vocab-viking.gguf.out
│ ├── templates
│ └── tokenizers
├── mypy.ini
├── pocs
│ ├── CMakeLists.txt
│ └── vdot
├── pyproject.toml
├── pyrightconfig.json
├── README.md
├── requirements
├── requirements.txt
├── scripts
│ ├── apple
│ ├── bench-models.sh
│ ├── build-info.sh
│ │ ......
│ ├── tool_bench.sh
│ ├── ui-assets.cmake
│ ├── verify-checksum-models.py
│ └── wc2wt.sh
├── SECURITY.md
├── skills
│ ├── add-new-model
│ └── code-review
├── src
│ ├── CMakeLists.txt
│ ├── ggml.c
│ ├── llama-adapter.cpp
│ ├── llama-adapter.h
│ ├── llama-arch.cpp
│ ├── llama-arch.h
│ ├── llama-batch.cpp
│ ├── llama-batch.h
│ ├── llama-chat.cpp
│ ├── llama-chat.h
│ ├── llama-context.cpp
│ ├── llama-context.h
│ ├── llama-cparams.cpp
│ ├── llama-cparams.h
│ ├── llama-ext.h
│ ├── llama-grammar.cpp
│ ├── llama-grammar.h
│ ├── llama-graph.cpp
│ ├── llama-graph.h
│ ├── llama-hparams.cpp
│ ├── llama-hparams.h
│ ├── llama-impl.cpp
│ ├── llama-impl.h
│ ├── llama-io.cpp
│ ├── llama-io.h
│ ├── llama-kv-cache-dsa.cpp
│ ├── llama-kv-cache-dsa.h
│ ├── llama-kv-cache-dsv4.cpp
│ ├── llama-kv-cache-dsv4.h
│ ├── llama-kv-cache-iswa.cpp
│ ├── llama-kv-cache-iswa.h
│ ├── llama-kv-cache.cpp
│ ├── llama-kv-cache.h
│ ├── llama-kv-cells.h
│ ├── llama-memory-hybrid-iswa.cpp
│ ├── llama-memory-hybrid-iswa.h
│ ├── llama-memory-hybrid.cpp
│ ├── llama-memory-hybrid.h
│ ├── llama-memory-recurrent.cpp
│ ├── llama-memory-recurrent.h
│ ├── llama-memory.cpp
│ ├── llama-memory.h
│ ├── llama-mmap.cpp
│ ├── llama-mmap.h
│ ├── llama-model-loader.cpp
│ ├── llama-model-loader.h
│ ├── llama-model-saver.cpp
│ ├── llama-model-saver.h
│ ├── llama-model.cpp
│ ├── llama-model.h
│ ├── llama-quant.cpp
│ ├── llama-quant.h
│ ├── llama-sampler.cpp
│ ├── llama-sampler.h
│ ├── llama-vocab.cpp
│ ├── llama-vocab.h
│ ├── llama.cpp
│ ├── models
│ ├── unicode-data.cpp
│ ├── unicode-data.h
│ ├── unicode.cpp
│ └── unicode.h
├── Testing
│ └── Temporary
├── tests
├── tools
│ ├── batched-bench
│ ├── cli
│ ├── CMakeLists.txt
│ ├── completion
│ ├── cvector-generator
│ ├── export-lora
│ ├── fit-params
│ ├── gguf-split
│ ├── imatrix
│ ├── llama-bench
│ ├── mtmd
│ ├── parser
│ ├── perplexity
│ ├── quantize
│ ├── results
│ ├── rpc
│ ├── server
│ ├── tokenize
│ ├── tts
│ └── ui
├── ty.toml
└── vendor
├── cpp-httplib
├── miniaudio
├── nlohmann
├── sheredom
└── stb
llama.cpp 的目录结构高度吻合其高性能 C/C++ 推理框架的定位,展现了克制、务实且高度模块化的架构设计。它没有盲目追求理论上的完美架构,而是针对 C/C++ 语言的特性、硬件加速的复杂性以及大模型推理的业务场景,做出了适度的权衡。
本系列的第三篇,采用自问自答的方式,深度剖析llama.cpp目录结构设计,并进一步阐述其背后折射出的核心软件架构思想。
一、 为什么这样划分
1. 为什么 GGML 被单独拆出来?
- GGML 是一个独立的子目录,有自己的构建系统和 API(事实上GGML本身也是
llama.cpp项目发起人 Georgi Gerganov 的独立开源项目)。 - 架构思想:基础设施与业务逻辑的彻底解耦 (Infrastructure vs. Domain Logic)
- 纯粹的计算引擎:GGML 的本质是一个张量计算引擎(Tensor Computation Engine)。它只关心如何高效地进行矩阵乘法、内存分配和计算图调度,它完全不知道什么是 Transformer,什么是注意力机制。
- 复用与边界:将其独立,意味着 GGML 变成了一个纯粹的底层基础设施(类似操作系统内核或数据库引擎)。它不仅服务于当前的推理框架,理论上还可以被其他任何需要张量计算的项目复用。这种设计严格划定了数学计算与模型业务逻辑的边界。
2. 为什么 tools 完全独立?
tools目录包含量化、模型格式转换(如 HuggingFace 转 GGUF)等独立可执行文件。- 架构思想:核心运行时与辅助工具链隔离 (Core Runtime vs. Toolchain Isolation)
- 保持核心库精简 (Keep Core Lean):工具链通常包含大量的辅助代码(如 Python 绑定、特定的文件 I/O 处理、第三方库依赖)。如果将它们混入核心推理库,会导致核心库体积膨胀,引入不必要的依赖。
- 生命周期与部署场景不同:核心库(
src)是需要在生产环境高并发、长时间运行的;而tools往往是开发者在本地一次性运行的脚本或程序。独立部署可以避免生产环境被开发/部署工具污染。
3. 为什么 examples 不引用 server?
examples目录下的代码只依赖核心推理 API,而不依赖server(HTTP 服务)模块。- 架构思想:严格的依赖方向控制与最小依赖原则 (Strict Dependency Direction & Minimal Dependencies)
- 防止概念污染与循环依赖:
server是一个具体的“应用层”实现,包含了网络协议、HTTP 路由、JSON 序列化等大量与“模型推理”无关的代码。如果examples引用了server,就意味着展示“如何调用模型”的示例代码,被迫引入了网络层的依赖。 - 依赖倒置的体现:
examples应该只依赖最底层的“核心推理抽象”。这保证了示例代码的极简性,让使用者一眼就能看懂核心 API 的用法,而不被外围的 HTTP/网络逻辑干扰。
- 防止概念污染与循环依赖:
4. 为什么 include 非常小?
- 对外暴露的
include头文件极少(通常只有llama.h,ggml.h等寥寥几个),内部结构体全部隐藏在src中。 - 架构思想:最小暴露原则与强封装 (Minimal API Surface & Strong Encapsulation)
- 对抗 C/C++ 的头文件地狱:在 C/C++ 中,暴露头文件意味着暴露了内部数据结构和编译依赖。
include越小,外部集成者的编译时间越短,认知负担越低。 - 面向接口编程:对外只暴露 C 语言的 API(Opaque Pointers,不透明指针),将具体的 C++ 实现细节(如各种复杂的模板、内部类)死死锁在
src目录中。这保证了 API 的 ABI(应用二进制接口)稳定性,外部调用者无需关心内部是如何实现的。
- 对抗 C/C++ 的头文件地狱:在 C/C++ 中,暴露头文件意味着暴露了内部数据结构和编译依赖。
5. 为什么后端计算设备(Backend)都是独立的?
- CUDA, Metal, Vulkan, SYCL 等硬件加速代码被独立放在
ggml/src/ggml-cuda等独立目录中。 - 架构思想:硬件抽象层 (HAL) 与可插拔的策略模式 (Pluggable Architecture & Strategy Pattern)
- 隔离硬件差异:不同硬件的编程模型(CUDA C++, Metal Shaders, Vulkan SPIR-V)差异巨大。将它们独立,是为了防止特定硬件的“脏代码”污染核心的计算图调度逻辑。
- 动态/静态插拔:Backend 独立使得系统可以按需编译(例如,没有 NVIDIA 显卡的机器可以不编译 CUDA backend)。核心推理逻辑只调用 GGML 定义的统一抽象接口,具体的硬件执行由后端的“策略”来实现。这赋予了系统极强的可移植性。
6. 为什么 parser 不放进 ggml?
- 模型文件解析(如 GGUF 解析)或分词器解析(Tokenizer/BPE)放在
src或独立模块,而不是放在ggml中。 - 架构思想:单一职责原则与领域边界清晰 (Single Responsibility Principle & Clear Domain Boundaries)
- 计算 vs. I/O与文本:GGML 的领域是“纯数学与内存计算”。而 Parser 的领域是“文件系统 I/O、二进制格式解析、文本与词表处理”。
- 防止底层被上层绑架:如果把 Parser 放进 GGML,GGML 就不得不依赖文件 I/O 库、字符串处理逻辑,甚至可能引入对特定模型格式的认知。这会破坏 GGML 作为纯粹计算引擎的抽象。Parser 负责把文件变成内存中的张量数据,然后交给 GGML 去计算,两者职责泾渭分明。
二、 这体现了怎样的软件架构思想
透过这些目录划分,我们可以提炼出该项目背后的三大核心架构哲学:
1. 洋葱架构 / 整洁架构的 C/C++ 变体
这个项目在物理目录上严格遵循了 “依赖指向内部” 的原则:
- 最内核:
ggml(纯数学计算,无业务概念)。 - 中间层:
src+include(模型推理逻辑,依赖 ggml,暴露极简 API)。 - 外围层:
server,tools,examples(具体应用、工具、示例,依赖中间层)。 这种设计确保了核心业务逻辑(模型推理)不依赖于任何外围细节(网络、UI、文件系统格式),使得核心逻辑极其稳定且易于测试。
2. 极致的关注点分离 (Separation of Concerns)
在 C/C++ 这种容易写出“面条代码”的语言中,该目录结构强行通过物理隔离来约束逻辑耦合:
- 硬件与软件分离(
backendvssrc) - 计算与数据分离(
ggmlvsparser) - 核心与边缘分离(
srcvstools/examples) 每一个目录都只解决一个特定维度的问题,绝不越界。
3. 实用主义与演进式设计的妥协
它没有采用 Java/Go 中常见的庞大单体包结构,也没有过度设计微服务。它充分考虑了 C/C++ 编译系统的痛点:
- 通过极小的
include减少编译依赖。 - 通过独立的
backend和tools实现按需编译,缩短开发者的编译等待时间。 - 通过 C API 暴露 (
include) 和 C++ 实现 (src) 的分离,兼顾了运行性能与跨语言兼容性。
结语
优秀的目录结构,是架构师留给后来者的一张导航图。
看到 ggml 独立、include 极小、backend 分离时,我们实际上是在阅读架构师的思考过程:他们如何抵御将不同关注点混为一谈的诱惑,如何在性能、扩展性、编译效率和代码可维护性之间找到那个最优雅的平衡点。 这不仅仅是代码的组织,更是对软件复杂性的深刻敬畏与有效控制。
参考链接