如果只是为了"把大模型跑起来",现在的答案多到溢出来:装个 vLLM、llama.cpp、Ollama,几行命令就能出结果。那为什么还有人(对,就是我们)会从 28 个 C 文件开始,手写矩阵乘法、线程池、HTTP 服务器,甚至自己实现 SM2 椭圆曲线签名?答案要先看一个反差的数字:我们的推理引擎,RK3588 板端 Release 产物是 818,872 字节(约 0.8 MB),冷启动 2.01 秒。而常规做法要带多大的环境、预热多久,跑过的人都懂。
1. 知识点:需求把方案逼出来的三条因果链
手搓不是情怀,是被场景逼出来的工程决策。三个因果链:
链一:设备决定"尺寸"。 RK3588 是开发板,不是服务器。内存 4–16GB、eMMC 存储、还可能断网、会重启。在这种设备上,"引擎 = Python 解释器 + PyTorch + CUDA 栈"动辄几 GB 的方案天生不成立——能省的每一字节都是收益,于是你开始在意"我的可执行文件多大"。
链二:边缘决定"启动"。 边缘盒子经常要重启(断电、升级、看门狗拉起)。重启一次服务要 30 秒和要 2 秒,是"能忍"和"无感"的区别。于是你开始在意"加载流程里有没有一秒都没法省的浪费"——这正是后面我们要读的 mmap 权重格式的由来。
链三:可信决定"自研"。 医疗、法务场景要回答"这个结果是不是这台机器用这套权重算的"。这需要把密码学原语(SM3/SM4/SM2)编进引擎。引入 OpenSSL?还是自己写并用国密标准向量逐字节验证? 我们选了后者(第 23–27 天专题),这也是"零依赖"的另一个动机。
所以请记住这个结论:"800KB、零依赖"不是起点,是被场景逼出来的结果;本系列 30 天,就是把这条因果链反着走一遍,逐环还原给你看。
2. 对应代码:先在仓库里找到这三个证据
打开 https://gitee.com/pei-xiaoguang/kestrel-llm ,做三件事:
- 读 README.md 顶部的「核心亮点」表——里面的每个数字后面都会被拆成文章;
- 看根目录 CMakeLists.txt 第二行
project(vllm_kestrel C)——一个 target、纯 C,没有第三方库声明; - 数一下源码规模:构建清单里是 28 个
.c文件(main + core 11 + common 2 + serve 6 + model 6 + npu 2,即 CMakeLists.txt 里VLLM_SOURCES的完整列表)。注意磁盘上src/实际有 30 个.c:src/media/下另有h264_dec.c、vllm_mp4.c两个解码器是独立模块,默认不参与构建(这也呼应了 README 里"H.264 模块独立且默认不启用"的口径)。
2.4 关键代码:CMakeLists 骨架,第一次读
把 CMakeLists.txt 从头扫一遍,你会发现整个构建文件就是"八个决定"(编号是我加的,源码无此编号):
project(vllm_kestrel C) # (1) 纯 C 工程,没有 C++
set(CMAKE_C_STANDARD 11) # (2) 强制 C11,编译器必须支持
set(CMAKE_C_STANDARD_REQUIRED ON)
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
if(NOT VLLM_MARCH)
message(FATAL_ERROR
"vllm_kestrel targets aarch64 (RK3588) only; found ...") # (3) 构建层单平台守卫
endif()
endif()
if(NOT VLLM_MARCH)
set(VLLM_MARCH "armv8.2-a+dotprod+fp16") # (4) march 旋钮:不传参就用默认值
endif()
option(VLLM_STATIC "Build a fully static (zero .so dependency) binary" OFF) # (5)
set(CMAKE_C_FLAGS_RELEASE "-O2 -mcpu=cortex-a76 -march=${VLLM_MARCH} -ffast-math -fopenmp -D_GNU_SOURCE") # (6)
set(VLLM_SOURCES src/main.c src/core/vllm_l3.c ... ) # (7) 28 个 .c,功能布局排列
add_executable(vllm_kestrel ${VLLM_SOURCES} ${VLLM_HEADERS}) # (8) 单 target、单产物
逐点看懂:
- (2)
CMAKE_C_STANDARD 11:从构建系统层面锁死"C11",配合代码里大量_Static_assert、for (int i...)等 C11 特性。这解释了为什么 README 敢写"纯 C11"——不是口头约定,是构建系统强制。 - (3) 构建层也有一道平台守卫:1-2 你会在
vllm_platform.h里看到#error(编译 C 代码时的守卫),这里message(FATAL_ERROR)是更早一层的守卫——cmake 配置阶段就拒绝。两道守卫 = "错误前移"做了两次。 - (4)
VLLM_MARCH旋钮:这就是为什么 1-2 能用一个cmake -DVLLM_MARCH=...就复现翻车——架构串是变量,不是写死的。 - (5)
VLLM_STATIC:默认 OFF;打开后走-static链接(CMakeLists 第 39–44 行),产物ldd报 not a dynamic executable。 - (7) 源文件清单:这就是 1-1 开头数的"28 个 C 文件"的官方出处。留意排列是按功能布局(main / core 推理内核 / common / serve / model / npu),且
vllm_l3.c(1-3 的对拍主角)和vllm_ckks.c / vllm_fhe.c / vllm_ntt.c(同态加密研究内核)都在同一个 target 里编译——一个二进制里既有明文引擎也有 FHE 研究代码,是刻意的共存设计。 - (8) 单
add_executable:全工程只有一个产物。这行决定了"800KB 单文件"不是靠打包,而是真的只链出一个可执行文件。
小结:读懂 CMakeLists 的顺序 = 读懂引擎"边界"的顺序:语言(C11)→ 平台(aarch64)→ 指令集(march 旋钮)→ 依赖(静态/动态)→ 组成(28 个文件谁是谁)。第 2–3 天会沿着这条线逐层深入。
3. 改动后果:一个真实的教训——"不用心维护运行产物"会怎样
这一篇先讲一个不改代码也会踩的坑。看 build_rk3588.sh 第 75–79 行的注释,它记录了仓库的真实事故:
# Keep the repo-root ./vllm_kestrel in lockstep with the build artifact. Every
# ./vllm_kestrel invocation on the board then runs the LATEST kernel code -
# without this, stale binaries silently invalidate perf/quality runs
# (2026-08-22: a 00:05 M4b binary at the repo root invalidated all M4c runs).
意思是:构建脚本特意把产物同步到仓库根 ./vllm_kestrel,就是为了防止你手滑运行了旧的二进制。某次实验里,根目录躺着一个旧版本二进制,导致一整天性能数据全部作废。后果是:代码没改错,但"跑错了东西",结果比不跑还糟。这是贯穿整个系列的第一条工程纪律:实验前先确认你在跑哪个二进制。build_rk3588.sh 的"根同步"正是为此而存在。
4. 学员调试任务(本篇为观察任务)
- A 档:
git clone仓库到你的板子,执行ls src、ls include,用file查看 README 里提到的产物(若还没有可先跳过),并把 CMakeLists.txt 第 37 行那串编译旗标抄下来——下一篇要逐字拆它。 - B 档:同上 clone + 数文件 + 读 CMakeLists.txt,记录你对"零第三方依赖"的疑问清单,后面每天会逐条回答。
预期输出:能说出"引擎由哪几层组成、构建入口在哪、为什么脚本要 root-sync 二进制"。
收尾
本篇源码点名:CMakeLists.txt、build_rk3588.sh、README「核心亮点」表。
- 开源仓库:Kestrel-LLM (Gitee)(源码可得双许可:学习 / 学术研究免费)
下篇预告: ARM交叉编译踩坑实录带你看一个真实的 -march 翻车现场——编译期报错,是引擎在救你。