第4章:llama.cpp源码获取与结构分析
4.1 克隆源码——第一关就踩坑
拿到llama.cpp的第一步当然是把它拉到本地。你可能会想,不就是git clone吗?别急,这事儿其实有讲究。
我个人习惯用这条命令:
git clone --recurse-submodules https://github.com/ggerganov/llama.cpp.git
为什么要加--recurse-submodules?因为llama.cpp依赖了ggml这个子模块,如果不带这个参数,你拉下来会发现ggml目录是空的。编译的时候会报一堆符号未定义的错误。我以前第一次踩过这个坑,折腾了半小时才反应过来——嗯,你不会想重蹈覆辙。
如果你已经克隆完了才发现没拉子模块,也别慌,手动补上:
cd llama.cpp
git submodule update --init --recursive
wget下载压缩包,但子模块得单独处理。我建议还是在开发机上完成克隆,然后通过NFS或scp同步到目标板。
4.2 看一眼目录——东西放得挺清楚
拉下来之后,先看看顶层结构长什么样。我列出最核心的那几项:
| 目录/文件 | 作用 | 我的重点关注 |
|---|---|---|
llama.h | 主头文件,定义推理API | 全量接口声明,必须仔细读 |
llama.cpp | 核心实现,模型加载、推理逻辑 | 理解函数调用链的关键 |
common/ | 工具函数:参数解析、采样、日志 | 快速跑demo的必备模块 |
ggml/ | 底层张量计算库,子模块 | 算子的实现,性能调优要盯这里 |
examples/ | 示例程序:main, server, batched等 | 直接拿来改,省时间 |
CMakeLists.txt | CMake构建入口 | 控制编译选项,交叉编译靠它 |
说实话,ggml这个子模块才是真正的计算引擎。我在做嵌入式移植时,有90%的精力都花在理解ggml的量化操作上——因为它直接决定了模型能不能在只有256MB内存的开发板上跑起来。
4.3 核心模块深度解码
llama.h —— 你能调用的所有东西都在这里
打开llama.h,你会看到一套干净利落的C接口。我重点提几个:
llama_init():加载模型和分词器,返回一个上下文句柄。坑点:嵌入式上得手动指定n_threads,否则默认开满CPU核,内存会炸。llama_eval():执行一次前向推理。输入token ID序列,输出logits。我在项目中遇到过一个问题——输入长度超过上下文窗口时,这个函数不会报错,只是默默丢掉前面的token,导致结果不对。所以调用前一定要检查token数。llama_token_to_piece():把token ID转回字符串,用于解码输出。这个函数在中文场景下特别容易产生乱码,我建议你每次都先调用llama_tokenize()验证一下。
llama.cpp —— 五脏六腑都在里面
这个文件超过8000行,但核心逻辑其实可以分成几块:
- 模型加载:解析GGUF格式的权重,填充内部结构体。我刚开始看的时候被
llama_model_load里的各种fread绕晕了。后来自己写了个迷你loader才搞明白——其实就是按magic number、头部信息、张量数据顺序读。 - 前向推理:把token输入组装成batch,调用ggml计算图。这部分大量用了回调函数,你如果不懂C函数指针,看起来会有点吃力。
- 采样:
llama_sample_*系列函数,从logits中选出下一个token。默认是贪心,也可以选top_k、top_p等。
说实话,刚接触时不建议深入逐行读llama.cpp,先把接口用起来,遇到性能瓶颈再回来抠细节。我自己的做法是先跑通main示例,然后慢慢往llama.h里加自己的日志。
common/ —— 给开发者的“救生圈”
这个目录里的代码,说白了就是帮你省去重复劳动。最常用的是:
common.h / common.cpp:定义了gpt_params结构体,把模型路径、上下文大小、线程数、采样参数等全打包在一起。你在嵌入式上需要固定参数时,直接修改这个结构体,省得每次传一堆命令行参数。sampling.h / sampling.cpp:封装了采样策略,包括重复惩罚、温度缩放等。我建议不要直接用里面的默认值,嵌入式场景下温度0.6~0.8比较稳。log.h:日志系统,默认输出到stderr。在嵌入式上我习惯把它重定向到串口或者syslog,不然DEBUG信息很容易跟模型输出混在一起。
<filesystem>。如果你用的是老旧交叉编译器(比如gcc 7以前),可能会编译不过。解决办法是把路径处理换成realpath()或者纯字符串操作,我在树莓派Zero上就这么干过。
4.4 构建系统——CMake别乱选
llama.cpp的CMake配置写得很完善,支持多种后端。在嵌入式Linux上,我个人推荐这样配置:
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DLLAMA_OPENBLAS=ON \ # 板子上有BLAS库就开
-DLLAMA_STATIC=ON \ # 静态链接,减少运行时依赖
-DLLAMA_NATIVE=OFF \ # 交叉编译时必须关掉
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++
你想想看,如果LLAMA_NATIVE开着,cmake会探测当前机器的CPU指令集,编译出来的二进制在ARM板上跑直接就非法指令了。我刚开始交叉编译时就忘了关这个选项,查了三天才发现……真是血的教训。
另外几个常用选项:
| 宏开关 | 作用 | 嵌入式上要注意的 |
|---|---|---|
LLAMA_METAL | 启用Apple Metal加速 | 嵌入式Linux不可用,别开 |
LLAMA_CUBLAS | 启用NVIDIA CUDA加速 | 如果你有Jetson之类带GPU的板子,可以考虑 |
LLAMA_CLBLAST | 启用OpenCL加速 | 部分ARM Mali GPU支持,但效率一般 |
LLAMA_NO_ACCELERATE | 禁用所有加速库 | 嵌入式神器——纯CPU模式,兼容性最好 |
构建成功后,你会在build/bin/下看到main、server等可执行文件。拿main跑一个最简单的推理例子:
./bin/main -m /path/to/model.gguf -p "Hello, how are you?" -n 64
如果它正常输出了回复,恭喜,你已经成功在嵌入式Linux上把llama.cpp跑起来了!
4.5 一图读懂源码结构
下面这张SVG图帮你把整个关系串起来——从你克隆仓库到最终的可执行文件,每一步对应什么模块、什么文件,一目了然。
图中可以看出,整个项目的骨架其实不复杂:上层API(llama.h)声明了你能调用的函数,核心(llama.cpp)实现了推理逻辑,底层依赖ggml做计算,而common和examples让你能快速跑起来。构建系统则通过CMake把这一切串成可执行文件。