现代软件工程中,目录结构从来不仅限于文件的分类和组织,而是系统架构的物理映射

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(应用二进制接口)稳定性,外部调用者无需关心内部是如何实现的。

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++ 这种容易写出“面条代码”的语言中,该目录结构强行通过物理隔离来约束逻辑耦合:

  • 硬件与软件分离backend vs src
  • 计算与数据分离ggml vs parser
  • 核心与边缘分离src vs tools/examples) 每一个目录都只解决一个特定维度的问题,绝不越界。

3. 实用主义与演进式设计的妥协

它没有采用 Java/Go 中常见的庞大单体包结构,也没有过度设计微服务。它充分考虑了 C/C++ 编译系统的痛点:

  • 通过极小的 include 减少编译依赖。
  • 通过独立的 backendtools 实现按需编译,缩短开发者的编译等待时间。
  • 通过 C API 暴露 (include) 和 C++ 实现 (src) 的分离,兼顾了运行性能与跨语言兼容性。

结语

优秀的目录结构,是架构师留给后来者的一张导航图。

看到 ggml 独立、include 极小、backend 分离时,我们实际上是在阅读架构师的思考过程:他们如何抵御将不同关注点混为一谈的诱惑,如何在性能、扩展性、编译效率和代码可维护性之间找到那个最优雅的平衡点。 这不仅仅是代码的组织,更是对软件复杂性的深刻敬畏与有效控制。


参考链接

https://github.com/ggml-org/llama.cpp

https://github.com/ggml-org/ggml