SIMD 是什么
SSE/AVX/NEON 一条指令算多个数——CPU 上的向量化与 NumPy 的底层。
GPU 用成千上万线程并行;CPU 也有单指令多数据(SIMD):一条指令同时对 4/8/16/32 个 float 做加法。NumPy 快、PyTorch CPU 后端部分算子快,背后常常是 AVX-512 或 ARM NEON,而不是 Python 魔法。
直觉:一次端四杯 vs 四趟各端一杯
标量:c[i] = a[i] + b[i] 循环 1024 次,每次 1 个 float。SIMD:一次 __m256 寄存器装 8 个 float,一条 vaddps 加 8 对——理论 8× 吞吐(若内存跟得上)。
定义
| ISA | 平台 | 典型宽度 | 寄存器示例 |
|---|---|---|---|
| SSE | x86 | 128 bit = 4×float32 | XMM |
| AVX2 | x86 | 256 bit = 8×float32 | YMM |
| AVX-512 | x86 | 512 bit = 16×float32 | ZMM |
| NEON | ARM | 128 bit | V 寄存器 |
向量化(auto-vectorization):编译器把循环改 SIMD;intrinsics:程序员手写 _mm256_add_ps。
PyTorch torch.add 在 CPU 上 dispatch 到 vectorized kernel(ATen)。
例子一:理论加速比
float32 向量加,长度 :
- 标量: 次 add + 循环开销
- AVX2: 次向量 add
峰值 8×;实际 4–6× 常见(load/store、尾部处理、带宽)。
例子二: reduction 为何难向量化
# 依赖上一轮结果 — 串行依赖
s = 0.0
for x in data:
s += x求和有 loop-carried dependency;编译器用 partial sum + horizontal add 或 Kahan。BLAS sdot 高度优化即此类。
对比:无依赖逐元素:
# c[i] = a[i] * b[i] + 1 — 完美 SIMD与 GPU SIMT 对比
| SIMD(CPU) | SIMT(GPU) | |
|---|---|---|
| 线程模型 | 少量宽向量 lane | 大量轻量线程 |
| 分支 | 全 lane 执行相同 op | warp diverge |
| 规模 | 8–16 宽 | 32×数千 |
CPU 上小矩阵仍用 SIMD;大矩阵 offload GPU。
AI 中的 SIMD
- ONNX Runtime / oneDNN:CPU inference 用 AVX512 VNNI 做 INT8 dot product。
- Apple M 系列 AMX:矩阵扩展,接近小型 NPU。
- NumPy:
np.add(a,b)底层 ufunc 循环 SIMD。
Whisper small CPU 实时因子 ~0.5–2×(视核数);无 SIMD 优化可能 10×+ 慢。
编译器自动向量化条件
循环需:计数已知、无函数调用、无指针别名(restrict)、步长 1。#pragma omp simd 或 GCC __attribute__((optimize("tree-vectorize")))) 提示编译器。
PyTorch ATen 里 Vectorized<T> 抽象 SSE/AVX/NEON;float 向量加 dispatch 到 binary_kernel 的 SIMD 路径。你写 a + b 不必写 intrinsics,但 Python for 循环 到不了这层。
AMX 与 NPU 方向
Intel AMX(Sapphire Rapids+):tile 寄存器做 16×16 int8 矩阵块累加,服务 INT8 inference。Apple Neural Engine、高通 Hexagon 类似——专用矩阵单元 是 SIMD 的进化形态,功耗更低。
RISC-V Vector(RVV):可变长向量,长度运行时定,未来 edge AI 可能普及。
Eigen / MKL 与 BLAS 层级
numpy.dot、torch.mm 底层 OpenBLAS / MKL / cuBLAS。MKL 在 Intel CPU 上常快 2–5× OpenBLAS;AMD CPU 可用 AOCL。你写 Python,实际 hot path 是 Fortran 77 风格 GEMM 调 SIMD + 多线程——几十年 BLAS 生态 供养 AI CPU fallback。
Batch size 1 GEMV vs GEMM:矩阵-向量乘 GPU 利用率低;batched GEMM 才是 peak——推理框架 batching 的服务器端理由。
场景实战:numpy 与手写 Python 循环
import numpy as np, time
n = 5_000_000
a = np.random.rand(n).astype(np.float32)
b = np.random.rand(n).astype(np.float32)
t0 = time.perf_counter(); c = a + b; print("numpy ms", (time.perf_counter()-t0)*1000)
# 勿对 500 万元素真跑纯 Python list500 万元素 float32 加法,NumPy ~几 ms(AVX 宽向量 + 可能多线程);纯 Python for ~秒级。差距 100×+ 来自 SIMD + C 循环,不是 Python 语法 magic。PyTorch CPU tensor 同理 dispatch ATen vectorized kernel。
常见误区
- 「开 -O3 一定向量化」——指针别名、复杂索引、函数调用阻碍;看
-Rpass=loop-vectorize或numpy.show_config()。 - 「AVX-512 永远更快」——部分 CPU 降频(frequency scaling);短向量 workload 不一定赚。
- 「Python 多线程能 SIMD 并行」——GIL 限制 CPU bound Python;应用 SIMD 在 C 扩展里单线程宽向量,或多进程。
动手试一试
查看 NumPy/BLAS 链接与 CPU 特性:
import numpy as np
np.show_config() # 看 OpenBLAS/MKL、AVX 等
a = np.random.rand(1_000_000).astype(np.float32)
b = np.random.rand(1_000_000).astype(np.float32)
# 对比 float32 vs Python list 循环(勿对大 list 真跑)
c = a + b # SIMD 路径Linux 上:lscpu | grep -i avx
延伸思考
Apple Accelerate、Intel MKL-DNN、ARM Compute Library 各把 SIMD 藏在 BLAS/Conv API 后;PyTorch Inductor 生成 Triton 内核时自动选 tile size 匹配 SIMD 宽 × register 数。手写 kernel 的工程师读 SASS/PTX 见 fma.rn 一次算两个 float——SIMD 是显式的。Edge INT8 VNNI 一条指令 4× int8 乘加,MobileNet 在 CPU 上实时,靠 bit 窄 + SIMD 宽 而非 GPU。
温故:CPU 仍是 AI 栈的一翼
Tokenizer、HTTP、调度、数据库、feature store 跑 CPU;SIMD 让 CPU 预处理不拖 GPU 后腿。边缘设备无 GPU 时 INT8 + AVX/VNNI 是唯一出路。别只盯 CUDA——全 pipeline 的 bit 吞吐 才决定用户 latency。
补充:向量长度与 AVX-512
Ice Lake / Sapphire Rapids 服务器 CPU 有 AVX-512;消费级 Intel 12–14 代部分型号 禁用 AVX-512 以省 die。PyTorch wheel 发布时带 多种 CPU dispatch——同一 torch.add 在运行时选 AVX2 或 AVX512 路径。装 MKL 版 PyTorch 前,查 lscpu 确认 flags,避免 买了 AVX-512 机器却跑 AVX2 路径 浪费 ~30% 向量吞吐。
补充:OpenMP 与 SIMD
OMP_NUM_THREADS=1 时 NumPy 仍可能 多线程 BLAS(MKL 默认)。MKL_NUM_THREADS=1 + 单线程 Python 才能 isolate SIMD 增益 benchmark。** oversubscribe** 线程(8 核开 32 BLAS 线程)常 变慢——与 GPU occupancy 调优 analogous。
读完带走
CPU 向量宽 256–512 bit;一次 8–16 float 并行。NumPy/PyTorch CPU 路径 默认吃满 SIMD;Python for 不吃。预处理 pipeline 是 免费优化区。
小结
SIMD = 一条指令处理一个向量寄存器里的多个标量。CPU 靠它提升吞吐;与 GPU 大规模并行互补。理解 SIMD,就理解为何「写 Python 循环慢、调 NumPy 快」。DataLoader 预处理 尽量用 NumPy/Polars 向量算子,把 Python 循环留给业务逻辑,是免费 perf win。Tokenization 若纯 Python BPE,常是 CPU 瓶颈之一——Rust/HF tokenizers 用 SIMD + 并行。prefetch 下一 batch 与 GPU 重叠可 hide 延迟。
动手作业:打开 nvidia-smi 或活动监视器,对照本章公式估算当前进程的 权重与 KV 占用,误差在一个数量级内即合格。
AVX-512 服务器 CPU 向量化宽度翻倍;笔记本可能无此 ISA。
Polars/DuckDB 列式计算同样靠 SIMD;ETL 与训练前清洗可受益。
OpenMP parallel for 与 SIMD 是正交两维;两者同时开才吃满 CPU。
写 C++ 扩展 时 #pragma omp simd 与 -march=native 可让自定义算子接近 NumPy 速度;纯 Python 循环无此捷径。
RISC-V Vector 可变长向量是 edge AI 潜在标准;与固定宽 AVX 哲学不同。
numpy.show_runtime() 可看 BLAS 线程与 SIMD 库链接情况(若版本支持)。
Apple Accelerate vDSP 封装 SIMD;Swift/Objective-C ML 预处理常走此路径而非裸 intrinsics。
Wasm SIMD128 在浏览器跑小模型时可用;大 LLM 仍靠服务器 GPU。
Eigen::Map 把已有 buffer 当矩阵,零拷贝对接 C++ 推理引擎时常用。
向量内积 dot 在 BLAS 里高度优化;Attention 点积本质是 batched dot。
下一章 向量点积 会形式化 Attention 里的 Q·K 运算。
Horner 法 多项式求值可向量化;标量循环则慢。
第一章衔接
本章用 bit 语言 描述 AI 底座;第二章 数学 用 向量与矩阵 重述同一物理过程。两章对照读:bytes 是存储,FLOPs 是计算,带宽是瓶颈——三个量构成工程三角。SIMD 与 GPU 并行互补,构成现代 AI 算力底座。
下一篇:矩阵为什么连续存储——行优先与 stride 如何服务 SIMD/GPU。