基本介绍
为什么 CPU 的 SIMD 很重要
现代 CPU 的计算瓶颈往往不在于时钟频率,而在于数据吞吐。以 DDR5-6400 内存为例,其理论带宽约 51.2 GB/s,而一块 Intel Sapphire Rapids 处理器单核的 L1 带宽可达 2 TB/s 以上。如果只靠标量指令(每次处理一个操作数),大量的寄存器带宽和执行单元会被闲置。
SIMD(Single Instruction, Multiple Data)正是为此而生:用一条指令同时操作一组打包在同一宽寄存器中的数据元素。其收益在以下场景尤为突出:
| 场景 | 标量瓶颈 | SIMD 收益 |
|---|---|---|
| 大块内存复制/置零 | 每次 8 字节 | AVX-512 每次可处理 64 字节,显著减少循环与访存指令数 |
| 浮点密集计算(矩阵乘、卷积) | 每条指令只覆盖少量元素 | AVX-512 FMA 可同时覆盖 16 个 FP32 lane |
| 字符串处理(查找、比较) | 逐字节 | SSE4.2 pcmpestri 16 字节/次 |
| 向量量化推理(LLM int8/int4) | 逐元素乘加 | VNNI/VPDPBUSD 可把多组 INT8 乘加折叠进一条指令 |
从性能模型角度看,SIMD 带来的提升本质是提高了每条指令承载的数据级并行度,并常常间接改善有效 IPC。对于内存密集型代码,SIMD 还能减少循环次数,降低分支预测压力和循环控制开销,使前端(Front-End)更不容易成为瓶颈。
此外,现代编译器(GCC、Clang、MSVC)的自动向量化能力有限,面对复杂控制流或不规则内存访问时往往束手无策,手写 intrinsic 或内联汇编是榨干硬件性能的最后一公里。
端侧/PC 大模型推理中的 CPU SIMD
随着 LLM 向端侧和 PC 迁移,CPU SIMD 的地位愈发关键。这类场景有一个共同特征:显存/内存容量是第一约束,算力利用率退居其次。Decode 阶段 batch size 通常为 1,矩阵乘退化为矩阵-向量乘(GEMV),访存带宽完全决定 token 生成速度。这正是 CPU 配合高带宽内存(或统一内存)发挥 SIMD 价值的黄金场景。
llama.cpp — 跨平台 SIMD 内核的集大成者
llama.cpp 是目前端侧推理生态中影响力最大的项目,其核心文件 ggml-cpu.c / ggml-quants.c 中手写了覆盖以下平台的 SIMD kernel:
| 平台 | 指令集 | 典型 kernel |
|---|---|---|
| x86-64 | AVX2、AVX-512、VNNI | ggml_vec_dot_q4_K_q8_K |
| ARM AArch64 | NEON、DOTPROD、I8MM(部分平台再加 SVE) | ggml_vec_dot_q4_0_q8_0 |
| Apple Silicon | NEON(系统库内部可能进一步利用私有矩阵单元) | CPU/GPU 协同 |
| RISC-V | RVV 1.0 | 正在合并中 |
以 Q4_K 量化格式的 GEMV 为例,llama.cpp 的 AVX2 内核可以概括为下面这种结构化简化伪代码:
1 | // 结构化伪代码:省略了 super-block scale/min 解码、补偿项等细节 |
关键技巧:
- nibble extraction:先把 4-bit 权重拆成低/高两个 nibble,再进入
_mm256_maddubs_epi16这类“分组乘加”路径 - super-block 元数据:Q4_K 并不是“单纯 4-bit 权重 + 一个简单 scale”这么直接,真实实现里还有块级 scale/min 等元数据来平衡压缩率与反量化开销
- 有无 AVX-512VNNI 的差异:有 VNNI 时直接用
_mm512_dpbusd_epi32单指令完成 4 字节点积,吞吐进一步翻倍
实际吞吐通常在“几十 token/s 或更低”的量级波动,强依赖线程数、上下文长度、量化格式、内存频率以及是否混合 GPU offload,因此更适合给出方法论而不是固定数字。
KTransformers — 稀疏专家 CPU 卸载的 AMX 利用
KTransformers 是清华大学团队开发的 MoE 模型高效推理框架,核心思路是将 MoE 的冷专家权重卸载到 CPU 内存,利用 CPU 执行稀疏 Expert GEMM,GPU 专注于 Attention 和激活专家的计算。
CPU 侧的关键优化路径:
AMX 加速专家 GEMM:在 Intel Sapphire Rapids 上,专家 FFN 的 BF16/INT8 矩阵乘可以映射到 AMX Tile 指令,单位核吞吐通常明显优于纯 AVX-512 实现。
AVX-512 量化 GEMV:对于未激活的冷专家,KTransformers 预先将权重量化为 Q4/Q8 格式存储在 CPU 内存,Decode 时动态 dequant + GEMV,同样依赖 AVX-512VNNI 加速 int8 点积。
NUMA-aware 内存布局:多路服务器上,专家权重按 NUMA 节点分布,SIMD kernel 通过
numactl绑核 + 本地内存访问,避免跨 NUMA 带宽损失。
这类异构方案的实际速度高度依赖专家命中率、量化方式、NUMA 绑定和 GPU/CPU 分工,公开结果通常只能作为量级参考,不能简单外推到其他机器。
llama.cpp Metal 后端 — CPU SIMD 与 GPU 的协同边界
llama.cpp 的 Metal 后端(ggml-metal.m)会把大量算子卸载到 Apple GPU,但 CPU SIMD 仍承担一部分 host-side 工作:
- 调度与采样:logits 后处理、sampling 等小粒度、延迟敏感工作常保留在 CPU
- 内存搬运与格式转换:部分小张量 copy、layout 调整、量化相关辅助步骤仍可能落在 CPU 上
- 回退路径:未卸载层或不适合 GPU 的小算子,依然可能由 NEON/AVX2 路径执行
这种 CPU+GPU 协同模式揭示了一个重要工程原则:SIMD 不与 GPU 竞争,而是负责 GPU 不擅长的小批量、控制流复杂、延迟敏感的数据处理任务。
核心结论
上述三个框架共同印证了 CPU SIMD 在端侧 LLM 推理中的价值矩阵:
| 场景 | SIMD 角色 | 关键指令集 |
|---|---|---|
| 纯 CPU 推理(llama.cpp) | 量化 GEMV 主力计算 | AVX2 maddubs、AVX-512VNNI dpbusd、NEON sdot |
| MoE CPU 卸载(KTransformers) | 专家 GEMM 加速 | AMX tdpbf16ps、AVX-512 VNNI |
| CPU+GPU 混合(Metal 后端) | 数据处理与格式转换 | NEON、AVX2 |
| 边缘设备(MCU/NPU 旁路) | 轻量 token 处理 | RVV、NEON |
当模型无法完整装入 GPU 显存,或目标设备没有足够强的独立加速器时,CPU SIMD 往往是最通用、最可控的一条性能路径之一。这使得掌握 SIMD 编程对于端侧部署 LLM 的工程师来说非常重要。
AVX、AVX2、AVX-512、AMX
ISA 演进时间线
1 | SSE (1999, 128-bit XMM) |
AVX / AVX2
官方参考:Intel Intrinsics Guide(AVX/AVX2 过滤) | Intel 64 and IA-32 Architectures Software Developer’s Manual Vol.2(指令集参考)
AVX 将寄存器从 128-bit(SSE XMM)扩展到 256-bit YMM,支持 8 个 float 或 4 个 double 同时运算。AVX2 在此基础上补全了整数向量运算(256-bit 整数加减乘、移位、打包等),并引入了 gather 指令(_mm256_i32gather_epi32),允许以向量索引做非连续内存读取。
关键特性:
- 3-operand 编码(VEX prefix):
vaddps ymm0, ymm1, ymm2,避免了 SSE 的破坏性二元操作 - 上半部分清零(VZEROUPPER):当 AVX 代码路径即将进入仍使用 legacy SSE 编码的函数时,常在边界执行
_mm256_zeroupper()以避免 AVX-SSE transition penalty;如果全程都使用 VEX/EVEX 编码,这个问题会小得多 - FMA(Fused Multiply-Add):FMA3 支持
_mm256_fmadd_ps,把乘法和加法融合为一条指令;在乘加密集内核里通常能明显提高吞吐
AVX-512
官方参考:Intel Intrinsics Guide(AVX-512 过滤) | Intel Architecture Instruction Set Extensions and Future Features Programming Reference
AVX-512 是一族扩展的统称,核心特性:
| 子集 | 描述 |
|---|---|
| AVX-512F(Foundation) | 512-bit ZMM 寄存器,16 个 float 或 8 个 double |
| AVX-512BW | 字节/字宽度整数操作 |
| AVX-512DQ | 64-bit 整数与双精度扩展 |
| AVX-512VNNI | INT8 点积加速(用于量化推理) |
| AVX-512BF16 | BF16 浮点支持(Cooper Lake+) |
| AVX-512FP16 | FP16 浮点支持(Sapphire Rapids) |
掩码寄存器(opmask) 是 AVX-512 最重要的设计之一:每条向量指令均可附带一个 k 寄存器(k0–k7)作掩码,按位控制哪些 lane 参与运算或写回,彻底取代了过去繁琐的 blend+compare 组合,并能实现**零掩码(zeroing)或合并掩码(merging)**语义:
1 | // 仅对 mask 为 1 的 lane 写结果,其余 lane 置零 |
在 x86-64 下,向量寄存器数量也从 16 个扩展到 32 个(ZMM0–ZMM31),大幅缓解寄存器压力。
频率偏移问题(AVX Frequency Offset):不少处理器在持续执行重型 256-bit/512-bit 向量指令时都会出现不同程度的频率下降;服务器 SKU 通常更可控,但是否比 AVX2 更划算仍要实测。cpuid 只能告诉你“是否支持”,不能替代吞吐与频率测试。
AMX(Advanced Matrix Extensions)
官方参考:Intel AMX 编程参考(ISA Extensions) | Intel AMX 开发者指南 | Intrinsics Guide AMX 过滤
AMX 是 Intel 在 Sapphire Rapids(第四代至强)引入的矩阵加速单元,专为深度学习推理/训练设计。
架构要素:
- Tile 寄存器:8 个 Tile 寄存器(tmm0–tmm7),每个最大 1 KB(16 行 × 64 字节)
- TMUL 执行单元:专用矩阵乘法硬件,执行
TDPBSSD(INT8 点积)或TDPBF16PS(BF16 累加) - 操作语义:
C_tile += A_tile × B_tile,一条指令完成整块 tile 的矩阵乘累加
1 | // 伪代码:AMX BF16 矩阵乘加 |
实际代码在执行前还需要配置 tile 形状(例如 _tile_loadconfig),并在 Linux 上通过 arch_prctl(ARCH_REQ_XCOMP_PERM, XFEATURE_XTILEDATA) 申请 Tile 状态权限。AMX 在 BF16/INT8 GEMM 上通常比纯 AVX-512 更高效,但最终收益仍受 tile 配置、访存和线程绑定影响。
RVV(RISC-V Vector Extension)
官方参考:RISC-V V 扩展规范 v1.0(riscv-v-spec) | RISC-V V Intrinsic API 规范 | GCC/Clang RVV Intrinsic 参考
RVV 是 RISC-V 的官方向量扩展,1.0 版本于 2021 年冻结。与 x86 SIMD 的固定宽度设计不同,RVV 采用了向量长度无关(Vector-Length Agnostic, VLA) 的设计哲学。
核心概念
VLEN(Vector Register Length):硬件实现决定向量寄存器的实际位宽,软件无需关心。当前公开实现常见范围从 128-bit 到 512-bit 不等,未来也可以继续扩展。
SEW(Selected Element Width):当前操作的元素宽度(8/16/32/64 bit),由 vsetvli 动态配置。
LMUL(Length MULtiplier):将多个物理寄存器合并为一个逻辑向量寄存器组,取值 1/8、1/4、1/2、1、2、4、8。LMUL=8 时逻辑寄存器宽度 = 8 × VLEN。
vl(vector length):vsetvli 指令返回本次循环实际处理的元素个数,硬件自动处理尾部边界,无需手写 tail 处理代码。
编程模型
RVV 编程的核心范式是 strip-mining 循环:
1 | // 用 RVV intrinsic 实现向量加法(float32) |
相比 AVX 代码,RVV 代码天然可移植:同一份代码在 128-bit 和 512-bit 硬件上均可正确运行,硬件越宽,每次循环处理的元素越多,性能自然更高。
与 x86 SIMD 的对比
| 维度 | x86 AVX/AVX-512 | RVV |
|---|---|---|
| 寄存器宽度 | 固定(128/256/512 bit) | 硬件决定(VLA) |
| 代码可移植性 | ISA 版本绑定 | 一套代码跑所有实现 |
| 掩码机制 | AVX-512 opmask(1-bit/lane) | 向量掩码寄存器(v0) |
| 归约指令 | 需要 hadd 等组合 | 原生 vfredusum 等 |
| gather/scatter | 有 | 有(indexed load/store) |
| 当前生态 | 极其成熟 | 快速成长中 |
ARM NEON 与 MLX
官方参考:ARM NEON Intrinsics Reference(ACLE) | Apple MLX 源码(GitHub) | ARM Architecture Reference Manual(AArch64)
MLX 是 Apple 在 2023 年末开源的机器学习框架,专为 Apple Silicon(M 系列芯片)设计。它本身不是 SIMD ISA,也不能简单等同于 SIMD 教程;对做 CPU SIMD 的读者来说,它更适合作为 Apple 平台上统一内存、CPU/GPU 协同和部分 CPU kernel 设计的工程参考。
虽然 MLX 本身是高层框架,但其 mlx-core 中的部分 CPU kernel 和配套数据处理逻辑仍能帮助理解 Apple 平台上的向量化实践。
核心设计:
- 统一内存(Unified Memory):CPU 与 GPU 共享同一块物理内存,避免了 PCIe 传输,MLX 利用此特性实现零拷贝的 CPU↔GPU 数据共享
- Lazy Evaluation:计算图延迟求值,支持融合 kernel(operator fusion)
- ARM NEON:当前公开的 Apple Silicon CPU SIMD 主要是 128-bit NEON;截至目前公开 ISA 中并没有面向开发者开放的 SVE/SVE2 路径
下面给一个与这类 bf16 kernel 位操作思路相近的 NEON 示意片段:
1 | // ARM NEON:演示如何提取 BF16 的高 16 bit。 |
基本使用
编译选项
不同 SIMD 集需要对应的编译器标志:
1 | # x86:启用 AVX2 + FMA |
头文件
1 | /* x86 全家桶(自动包含所有 SSE/AVX/AVX-512 头) */ |
数据类型命名规律
x86 intrinsic 的类型名有严格规律:__m<宽度><类型后缀>
| 类型 | 含义 |
|---|---|
__m128 |
128-bit,4× float |
__m256d |
256-bit,4× double |
__m512i |
512-bit,整数(宽度由指令决定) |
__m256i |
256-bit,整数 |
函数名规律:_mm<宽度>_<操作>_<后缀>
1 | _mm256_add_ps → 256-bit,加法,packed single(float) |
在 load/store 命名里,u 后缀通常表示非对齐(unaligned)访问;无 u 则要求按寄存器宽度对齐(16/32/64 字节)。
内存对齐
1 | /* C11 对齐分配 */ |
对齐访问(_mm256_load_ps)比非对齐(_mm256_loadu_ps)在某些微架构上快约 5–15%,但现代 Skylake+ 对非对齐惩罚很小,不对齐时直接用 u 版本即可。
CPUID 运行时检测
在发布二进制前需运行时检测,避免在不支持的硬件上崩溃。注意:仅检查 CPUID feature bit 还不够,还必须确认操作系统已启用相应的 XSAVE/XGETBV 状态保存:
1 |
|
案例1:memcpy 与 memset 实现
标准库的 memcpy/memset 已经高度优化,这里的目的是展示 SIMD 编程的完整流程与性能分析思路。
思路分析
对于连续大块内存操作,性能瓶颈在内存带宽,而非计算单元。SIMD 的价值在于:
- 每次加载/存储 32/64 字节,减少循环迭代次数,降低前端压力
- 使用非时序写(Non-Temporal Store,NT Store)减少缓存污染与无效的 Read-For-Ownership(RFO)流量
- 配合
PREFETCHT0预取,隐藏内存延迟
NT Store 是关键:普通写分配 store 会触发 Read-For-Ownership(RFO),先把目标 cache line 拉进缓存再修改;而 vmovntdq 这类 non-temporal store 倾向于借助 write-combining 缓冲减少写分配和缓存污染,更适合“写完短期内不再读”的大块流式写入。
临界大小(Threshold)强依赖微架构、缓存层级和后续访问模式。一般来说,当数据会很快被再次读取时普通 store 往往更合适;只有在真正的流式大块写场景下,NT store 才更可能占优。
AVX2 memset 实现
1 |
|
AVX2 memcpy 实现
1 | /* |
性能分析
| 实现 | 256 MB 写带宽(Xeon Gold 6326) |
|---|---|
glibc memset(glibc 2.35) |
~42 GB/s |
上述 avx2_memset |
~44 GB/s |
avx512_memset(ZMM + NT) |
~48 GB/s |
| 理论 DDR4-3200 双通道峰值 | 51.2 GB/s |
测试方法:
perf stat可以帮助观察整体缓存行为,但 NT store 是否真的有收益,不宜只盯着某个 LLC miss rate。更可靠的是结合总带宽、uncore 内存控制器计数器,以及是否减少了写分配流量一起判断。
案例2:SIMD 实现矩阵转置
矩阵转置($A^T$)是典型的访存模式不规则场景:读是行序(连续),写是列序(跨步),导致写端 cache miss 严重。SIMD 可以在寄存器内完成局部转置,大幅减少跨步访存。
原理:4×4 float 转置
$4 \times 4$ 的 float 转置可以理解为“先做两两 unpack,再做跨对组合”,全在寄存器内完成:
1 | 输入(行主序,每行 4 个 float): |
8×8 float 转置(AVX2)
AVX2 中用 256-bit YMM 寄存器可一次处理 8×8 float 块:
1 | /* |
完整矩阵转置(out-of-place,带 cache blocking)
1 | /* |
性能对比
对 4096×4096 float 矩阵(64 MB)转置,测试平台:Intel Core i7-12700K:
| 实现 | 时间(ms) | 有效带宽(GB/s) |
|---|---|---|
| 朴素 scalar 转置 | 38.2 | 3.4 |
| scalar + cache blocking(64×64) | 9.1 | 14.1 |
| AVX2 8×8 blocking(本文实现) | 3.7 | 34.6 |
MKL mkl_somatcopy |
3.2 | 40.0 |
关键洞察:朴素转置的性能差完全由 cache miss 主导;cache blocking 后性能提升 4×;加上 AVX2 后再提升 2.5×,主要收益来自减少循环次数和向量化读写。
对比:RVV 矩阵转置思路
RVV 没有与 x86 这套固定宽度 unpacklo/unpackhi 完全同构的套路,转置更常通过 indexed/segment load-store、vrgather 或分块算法 实现,具体写法取决于硬件的 gather/scatter 带宽和寄存器长度:
1 | // RVV:逐列写(scatter store),等价于转置 |
总结
本文从硬件原理出发,梳理了 CPU SIMD 的核心脉络:
- SIMD 的本质:数据级并行,提高寄存器和执行单元利用率,核心收益是减少循环次数、降低前端压力、提升内存带宽利用率。
- x86 体系:SSE → AVX → AVX2 → AVX-512 → AMX,逐步扩宽寄存器、完善数据类型覆盖、引入掩码机制和矩阵加速。
- RVV:VLA 设计使代码天然可移植,
vsetvli+ strip-mining 是核心编程范式。 - ARM/Apple 平台:当前公开的 Apple Silicon SIMD 基础主要是 NEON;MLX 更适合作为 Apple 平台 CPU/GPU 协同与部分 CPU kernel 的工程参考,而不是把它当成一门新的 SIMD ISA。
- 工程实践:对齐分配、NT Store、cache blocking 是 SIMD 性能落地的三个关键工程技巧。
掌握 SIMD 编程后,建议进一步学习:
- Intel Intrinsics Guide:https://www.intel.com/content/www/us/en/docs/intrinsics-guide/
- RISC-V V Spec 1.0:https://github.com/riscv/riscv-v-spec
- Agner Fog 优化手册:https://www.agner.org/optimize/(微架构延迟/吞吐数据)
- LLVM Auto-Vectorization 文档:了解编译器向量化能做什么、做不了什么