这一篇把它拆开,讲明白 ARM 交叉开发的三个概念:原生 vs 交叉编译、-march/-mcpu 是什么、dotprod 扩展为什么不能少。顺带带你复现一个编译报错现场——那个错误是引擎在保护你,不是在刁难你。
1. 知识点
1.1 原生编译 vs 交叉编译
| 原生编译 | 交叉编译 | |
|---|---|---|
| 在哪编 | 目标设备上(RK3588 板子本地) | 性能强的主机(x86)上,编出 aarch64 产物 |
| 工具链 | 板子的 gcc(aarch64-linux-gnu) | x86 主机装 gcc-aarch64-linux-gnu |
| 优点 | 环境最真、可直接跑 | 编译快几十倍,适合大工程 |
| 缺点 | 板子 CPU 弱,全量编译慢 | 环境差异偶尔坑你;仍要拷回板子验证 |
本仓库两种都支持:./build_rk3588.sh 是板端原生;./build_rk3588.sh --cross 走 cmake/toolchain-aarch64-rk3588.cmake。
工程教训:交叉编译产物永远要在真机重跑自检——这正是本仓库用 PASS/FAIL 自检门的原因(下一篇)。
1.2 -mcpu vs -march:写给编译器的"这台 CPU 有什么"
-mcpu=cortex-a76:按具体核优化(调度、流水线),RK3588 大核就是 Cortex-A76。注意:cortex-a76 这颗核出厂就带 dotprod + fp16,-mcpu是按"这颗核一定有什么"来声明的——第 3 节实验 A 会看到它带来的"兜底"效应;-march=armv8.2-a+dotprod+fp16:按指令集版本 + 扩展声明能力。armv8.2-a:ARMv8.2 架构基线;+dotprod:打开 INT8 点积指令扩展——量化点积与手写 GEMM 全靠它(vdotq_s32/vdotq_laneq_s32,第 6 天细讲);+fp16:打开半精度浮点扩展。
你再看 CMakeLists.txt 第 17–27 行:架构名没有写死,允许覆盖:
cmake -B build-rk3588 -DVLLM_MARCH=armv8.2-a+dotprod+fp16
默认值在第 25–27 行定义(armv8.2-a+dotprod+fp16),VLLM_MARCH 是留给换板用的旋钮。注意措辞是"声明能力":你告诉编译器"大胆用 dotprod",编译器就真的会用——如果目标 CPU 没有这个扩展,跑起来会非法指令。所以 -march 永远要诚实。
1.3 单平台纪律:vllm_platform.h 的"一票否决"
include/common/vllm_platform.h 开头就写明定位,并且在非 aarch64 上直接编译报错:
#if defined(__aarch64__) || defined(_M_ARM64)
#define ST_ARCH_ARM64 1
...
#define ST_HAVE_NEON 1
#else
#error "vllm_kestrel targets aarch64 (RK3588) only"
#endif
这是"专注边缘设备、不再维护 x86/Windows 分支"的取舍:宁可编译期拒绝,也不在运行期崩。你后面会看到大量 NEON 代码,它们假定 ST_HAVE_NEON == 1——平台层在源头上保证了这份假设一定成立。
2. 对应代码:构建脚本全流程
build_rk3588.sh 做了四件事(第 64–79 行):
cmake -B build-rk3588 -DCMAKE_BUILD_TYPE=Release(默认原生 + Release);cmake --build ... -j$(nproc)并行编译;- 把
admin.html/chat.html拷进构建目录(服务端内嵌页,第 22 天细讲); - 把产物同步到仓库根
./vllm_kestrel(防陈旧二进制,1-1 讲过)。
顺带一提 -ffast-math:它允许编译器做"不严格符合 IEEE"的浮点优化,是性能档的一部分;代价是极端数值下结果可能与教科书不同——本引擎的确定性红线靠"参考对拍 + 固定编译参数"来兜底(1-3 与第 5 天展开)。
2.1 关键代码逐行:那串 Release 旗标
把第 37 行拆成表,每一段都对应一种"编译器授权":
set(CMAKE_C_FLAGS_RELEASE "-O2 -mcpu=cortex-a76 -march=${VLLM_MARCH} -ffast-math -fopenmp -D_GNU_SOURCE")
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "-s") # 非静态档:strip 符号表
| 旗标 | 含义 | 改掉/去掉的后果 |
|---|---|---|
-O2 |
优化档。CMakeLists 注释里记着一次实测:-O2 比 -Os 快约 5%(2306→2194ms),但体积差异在板端存储下无意义,故选速度 |
换 -O0 → 手写 asm GEMM 不受影响,但 norm/attention 等 C 代码明显变慢 |
-mcpu=cortex-a76 |
按 A76 的流水线/调度优化(RK3588 大核) | 换保守 cpu → 调度不贴合,吞吐小幅回落 |
-march=${VLLM_MARCH} |
指令集能力声明,默认 armv8.2-a+dotprod+fp16 |
去掉 +dotprod:Debug 档(无 -mcpu 兜底)→ 编译报错(第 3 节实验 B);Release 档有 -mcpu=cortex-a76 兜底 → 不报错,但 dotprod 内核被静默关闭、量化路径回退标量(第 3 节实验 A) |
-ffast-math |
放开 IEEE 严格性,允许快速浮点重排 | 去掉 → 部分热区变慢;保持它 + 固定工具链是位级一致的前提之一 |
-fopenmp |
链接 OpenMP(libgomp 是 gcc 自带的) | 引擎核心并行走自研线程池(第 4 天),OpenMP 只在 NPU 直驱(vllm_npu_direct.c)里服务少量批量并行区 |
-D_GNU_SOURCE |
暴露 GNU/Linux 扩展接口(如 posix_memalign) |
去掉 → 平台头里的 posix_memalign 不可见,编译报错 |
-s(链接档) |
strip 符号表 | 去掉 → 产物多几百 KB(0.8MB 的"瘦"也来自这里) |
注意 -ffast-math 那个后果很关键:"快"与"确定"在这里不是矛盾的——引擎用"固定编译参数 + 参考对拍 + 确定性自检"把不确定性锁在外面(第 1-3 篇与第 5 天会反复出现这条纪律)。
2.2 关键代码:平台头里还有哪些守护
除了你看到的 #error,vllm_platform.h 还集中了所有"跨平台本会散落各处"的东西——把这份文件通读一遍,你就知道"单平台专注"省了多少事:
#if defined(__aarch64__) || defined(_M_ARM64)
#define ST_ARCH_ARM64 1
#define ST_HAVE_NEON 1
#include <arm_neon.h>
#else
#error "vllm_kestrel targets aarch64 (RK3588) only"
#endif
#define ST_PREFETCH(p) __builtin_prefetch((p), 0, 3) /* temporal prefetch */
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) {
... clock_gettime(CLOCK_MONOTONIC, &ts); ... }
解读三个设计意图:
ST_ARCH_X86 / ST_HAVE_AVX512_VNNI保留为 0(头注释:便于逐文件迁移后删除)——团队承认 x86 分支曾是历史,现在不维护,但用宏"软删除"而不是物理删光,方便迁移期对照。读到这类"历史遗迹"注释时,别当成代码坏味道,它是工程演进的脚印。posix_memalign+st_aligned_free:NEON 的vld1q/vst1q对 16B 对齐有要求,跨行访问还涉及 cache line;统一走 64B 对齐分配(第 6 天讲 GEMM 时你会看到为什么对齐是性能下限)。st_now_sec用CLOCK_MONOTONIC:单调时钟不受校时/NTP 跳变影响,测时间才可信。头注释写明纪律:"计时仅用于报告(reporting only),不进入计算路径"——即时间戳永不影响数值结果,这是位级确定性的另一条保障。
3. 改动后果:复现一次"缺了 dotprod"的翻车
这是我们在板端真实踩过的坑(ASan 排查时也复现过一次),你完全可以亲手复现。先看两个实验,它们揭示了 -mcpu 与 -march 在"兜底"上的差异。
实验 A:Release 档去掉 +dotprod——被 -mcpu 兜底,静默回退
在 Release 档(默认带 -mcpu=cortex-a76)把 VLLM_MARCH 里的 +dotprod 去掉,重新编译:
cmake -B build-rk3588 -DCMAKE_BUILD_TYPE=Release -DVLLM_MARCH=armv8.2-a+fp16
cmake --build build-rk3588 -j$(nproc)
结果:编译不报错。因为 -mcpu=cortex-a76 声明了"这颗核出厂就带 dotprod",编译器据此仍然允许使用 vdotq_s32。但注意——-march 里没有 +dotprod,编译器会认为"你不想用这个扩展",于是 dotprod 内核被静默关闭,量化路径回退到标量实现。产物能跑,但 INT8 点积的加速没了,性能明显回落。
这是最隐蔽的坑:没有报错,没有警告,只有性能悄悄变差。所以 Release 档下,光看"能不能编过"是不够的,还要确认 -march 里确实带着 +dotprod。
实验 B:Debug 档去掉 +dotprod——编译期报错,保护你
在 Debug 档(没有 -mcpu 兜底)去掉 +dotprod:
cmake -B build-rk3588 -DCMAKE_BUILD_TYPE=Debug -DVLLM_MARCH=armv8.2-a+fp16
cmake --build build-rk3588 -j$(nproc)
你会看到类似这样的报错:
error: '__builtin_neon_vdotq_s32' requires ARMv8.2-A or later
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: you can enable this extension with '-march=armv8.2-a+dotprod'
这个报错不是编译器在刁难你,恰恰相反——是编译器在保护你。它发现你的代码里用了 vdotq_s32(INT8 点积指令),但当前 -march 没有声明 dotprod 扩展。如果编译器硬着头皮编过去,产物在 RK3588 上跑起来就会触发非法指令(SIGILL),而且是在运行时才崩,排查成本高得多。
收尾:三条纪律
这一篇的翻车现场,其实浓缩成三条纪律,写代码和配构建时都值得贴在显示器上:
-march永远要诚实:它声明的是目标 CPU 的真实能力,不是你的愿望。声明了不存在的扩展,运行时 SIGILL 等着你;漏声明了真实存在的扩展,性能悄悄回退。- Release 档的"能编过"不等于"用上了":
-mcpu=cortex-a76会兜住 dotprod,让编译通过,但-march里没有+dotprod时内核被静默关闭。验证性能前,先确认旗标真的带上了扩展。 - 编译期报错是朋友,不是敌人:Debug 档的
#error和requires ARMv8.2-A都在编译期把问题拦下来,比运行期 SIGILL 好排查一百倍。平台头的"一票否决"也是同一套哲学。
仓库:https://gitee.com/pei-xiaoguang/kestrel-llm (源码可得双许可:学习 / 学术研究免费)
下篇预告:我们进入确定性测试门:推理引擎的自检机制,没有模型,怎么用 PASS/FAIL 确定性测试把这类"静默回退"当场抓出来。