真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
一句话导读:平台层契约收敛:针对 NEON、mmap、绑核等大量带平台前提的能力,把架构判断收进 vllm_platform.h 一处供给,讲清 ST_HAVE_NEON 的宏契约与 #error 门闩,落点在有守卫与无守卫的编译对照实验。
引言:钉死内存布局之后,谁来钉死"我活在什么平台上"
2-1 说"内存布局要钉死",钉死之后呢?引擎里到处是 float32x4_t、vld1q_s8、mmap、sched_setaffinity……这些全都有平台前提:必须是 aarch64、必须带 NEON。如果每个源文件各自写一份"我是不是 aarch64"的判断,迟早有人写错、有人漏写。本仓库的做法是:把"活在哪台机器上"的所有前提,收进一个头文件——include/common/vllm_platform.h,让"平台契约"只有一处定义、四处引用。
1. 知识点:平台层的职责是什么
"单平台(RK3588/aarch64)"不代表不需要平台层。恰恰相反——正因为只支持一种平台,你更需要在一处把它的全部属性说清楚,让业务代码不用重复判断。数一下 vllm_platform.h 管了哪几摊事(按文件内分区):
| 分区 | 职责 | 典型成员 |
|---|---|---|
| 编译器工具宏 | 统一的 inline / 线程局部 / 对齐语法 | ST_INLINE / ST_THREAD / ST_ALIGN(n) |
| 架构开关 | "我是谁、我有什么指令"的唯一真相 | ST_ARCH_ARM64 / ST_HAVE_NEON / ST_ARCH_X86=0 |
| 对齐分配 | NEON 访问的 64B 对齐分配 | st_aligned_alloc / st_aligned_free |
| 计时 | 单调时钟(仅上报,不进计算) | st_now_sec / st_now_ticks |
| 大文件 I/O | 64 位寻址 / mmap 张量直挂 | ST_FSEEK / st_mmap_open |
| CPU 亲和 | 绑核(A76 簇优先) | st_bind_cpu |
| 栈 | 主线程栈提升到 16MB | st_raise_stack_limit |
| 页守卫 | 调试期堆越界 triage | st_pg_alloc / st_pg_free |
这背后是一种工程模式,可以叫"宏契约":业务代码不写 #if defined(__aarch64__) && ... 这种长条件,而是问平台层"NEON 在不在?":
#if ST_HAVE_NEON
/* 大胆写 NEON */
#endif
ST_HAVE_NEON 的值在平台层只有一处定义(aarch64 分支里 = 1,其它情况为 0),业务代码只消费这个答案。全库直接书写 NEON 向量类型(int8x16_t / float32x4_t / vld1q_*)的 .c 只有 4 个(vllm_safetensors.c、vllm_l3.c、vllm_vision.c、vllm_npu_direct.c),显式 include 平台头的有 10 个 .c——你对照一下就能看出:NEON 代码面小而集中,平台前提由一处供给。
2. 对应代码:平台头里最关键的一段
架构开关是整份头的"门闩"(第 46–59 行):
#if defined(__aarch64__) || defined(_M_ARM64)
#define ST_ARCH_ARM64 1
#define ST_ARCH_X86 0
#define ST_HAVE_NEON 1
#define ST_HAVE_AVX512_VNNI 0
#include <arm_neon.h>
#else
#error "vllm_kestrel targets aarch64 (RK3588) only"
#endif
/* 遗留别名:现有源码沿用两个拼写。 */
#ifndef ST_HAVE_NEON
#define ST_HAVE_NEON 0
#endif
三个细节值得品:
#include <arm_neon.h>全库只有这一处。任何文件想要 NEON 类型,都得先经过平台头——这就是"类型只从一道门进来"。ST_ARCH_X86/ST_HAVE_AVX512_VNNI保留为 0:引擎不再维护 x86 分支(1-2 提过的"软删除"),但宏位还留着,便于迁移期对照。#error之前是"无声的 0":就算你硬把#error去掉,非 aarch64 下ST_HAVE_NEON也只是安静地变 0——然后第 3 节你会看到,那些没写守卫的 NEON 调用点会怎样。
再往下看分配器与计时(第 67–98 行),它们是"确定性红线"的物理前提:
ST_INLINE void *st_aligned_alloc(size_t size, size_t align) {
void *p = NULL;
if (align < sizeof(void *)) align = sizeof(void *);
if (posix_memalign(&p, align, size ? size : 1) != 0) return NULL;
return p;
}
/* ... */
ST_INLINE double st_now_sec(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts); /* 单调时钟:不受 NTP 跳变影响 */
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
平台头注释里写死的纪律是:计时仅用于报告(reporting only),不进入计算路径——时间戳一旦混进数值计算,位级确定性就没了。
3. 改动后果:把"非 aarch64 就报错"放开,看会发生什么
#error 是平台层的"最后一道闸"。这一节我们做两次编译,量化对比有守卫 vs 无守卫。技巧:在 aarch64 的 gcc 上可以用 -U__aarch64__ 临时撤掉内置宏,模拟"非 aarch64 编译"(板端实测可行,gcc 11.4)。
实验 A(有守卫):整个库只报一行错
gcc -c -U__aarch64__ -Iinclude/common -x c -o /dev/null - <<'EOF'
#include "vllm_platform.h"
int main(void){ return 0; }
EOF
真实输出(RK3588 / gcc 11.4 / 2026-09):
In file included from <stdin>:1:
include/common/vllm_platform.h:53:6: error: #error "vllm_kestrel targets aarch64 (RK3588) only"
干净、明确、就一行。这就是"错误前移"的样子:平台不符,第一时间用一句话告诉你。
实验 B(无守卫):NEON 代码全线崩
把第 53 行的 #error 注释掉(模拟"把报错放开"),再编译一个真实的热路径文件 vllm_safetensors.c:
gcc -c -U__aarch64__ -D_GNU_SOURCE -fopenmp -Iinclude/common -Iinclude/core -Iinclude/serve \
-Iinclude/model -Iinclude/npu -Iinclude/npu/rk3588 src/model/vllm_safetensors.c -o /dev/null
板端实测:46 个 error。第一个错误长这样:
src/model/vllm_safetensors.c: In function 'vllm_q8_matvec_b_neon_worker':
src/model/vllm_safetensors.c:6990:9: error: unknown type name 'float32x4_t'; did you mean 'float_t'?
注意那个"善意"的提示 did you mean 'float_t'?——这是编译器在误导你。真实原因根本不是类型写错,而是 #include <arm_neon.h> 被平台头的非 aarch64 分支跳过了,NEON 类型集体失踪。没有平台头做中介的话,这种错误会在每个 NEON 使用点各炸一次,且每个都像"我代码写错了"——排查成本陡增。
对比:有守卫 = 1 行人话;无守卫 = 46 行让人怀疑人生的报错。这就是为什么平台层宁可 #error 也不让编译继续——让错误在最友好的位置、以最友好的形态发生。
B 档学员说明:以上实验在板端用
-U__aarch64__模拟;如果你手头真有 x86 Linux,动作完全一样——去掉-U直接编即可,报错内容同源(真实 x86 上arm_neon.h压根不存在,NEON 类型同样缺失)。实验 B 之后请务必把#error还原,并顺手git diff确认平台头没有残留改动。
4. 学员调试任务
A 档(板端动手)
- 复现实验 A 与实验 B,截图对比"1 行 vs 46 行";
grep -rn "ST_HAVE_NEON" src/ | head,数一数守卫宏都在哪些文件被消费;- 还原后运行
./build_rk3588.sh,确认整树编译如常。
B 档(纯读源码)
读 vllm_platform.h 全文,把第 1 节那张"职责地图"的每一行对应到真实代码段(行号 26–37 / 46–59 / 67–75 / 81–98 / 128–148 / 后续行号按实际文件补全),并回答:为什么 ST_HAVE_NEON 的"无声的 0"分支要放在 #error 之后?
小结:平台契约,一处定义、四处引用
回到开头的问题:钉死内存布局之后,谁来钉死"我活在什么平台上"?答案是 vllm_platform.h。它把 NEON、mmap、绑核、栈、页守卫这些带平台前提的能力,全部收敛到一处供给,业务代码只消费 ST_HAVE_NEON 这类宏答案,不重复判断。
这一篇的三个落点,值得带走:
- 宏契约:业务代码问"NEON 在不在?",平台层给唯一答案;NEON 代码面小而集中(4 个
.c),平台前提由一处供给。 - 门闩
#error:平台不符,第一时间用一句话告诉你,而不是让 46 行"像代码写错了"的报错淹没你。 - 纪律:计时仅用于报告,不进计算路径;位级确定性是这条红线的物理前提。
关键词:平台层、vllm_platform.h、NEON、宏契约、aarch64
- 开源仓库:Kestrel-LLM (Gitee)(源码可得双许可:学习 / 学术研究免费)
下篇预告:设备画像实战:识别RK3588而不是靠猜CPU型号,我们继续看平台层如何把"我是谁"从编译期延伸到运行期。