这是 Ash Vardanian 关于利用 SIMD 进行高性能计算的博客文章,主要内容如下:
- 现代 CPU 的超能力:现代 CPU 具有通过单指令多数据(SIMD)并行处理实现的超标量操作,单个核心可并行执行多达 4、8、16 甚至 32 个操作,虽类似迷你 GPU 能同时进行大量计算,但由于编写并行操作复杂,其潜力未被充分挖掘,代码仍一次只执行一个操作。
- 开发者面临的问题:高性能计算实际是对架构细节的逆向工程;自动向量化不可靠,编译器无法可靠处理;SIMD 指令复杂,不同 CPU 上相同指令集的性能也不可预测;调试复杂,无查看寄存器的简单方法;特定于 CPU 的代码棘手,包括编译时或动态调度;不同 CPU 和指令集上浮点运算的精度不一致。
- 余弦相似度介绍:余弦相似度用于衡量两个向量的接近程度,通过测量向量间的夹角来计算。在机器学习中广泛使用,如农场机器人、新的基于 ML 的气候模型方法和检索增强生成(RAG)管道等。其简单的 Python 代码可通过 NumPy 实现性能提升,后文将用 C 语言利用常见 SIMD 指令实现并讨论 SIMD 编程的复杂性及解决方案。
- 混合精度:文中使用机器学习中最常见的
bfloat16
类型,它比float32
计算速度快,但精度较低,可在同一计算中混合不同数值类型,如用bfloat16
进行向量运算,乘入float32
累加器,最后提升到float64
进行归一化。但使用bfloat16
也有问题,如某些编译器可能不支持相关类型,编译器难以向量化低精度代码,双精度除法结合精确的std::sqrt
实现会引入很多代码分支和延迟等。 不同 CPU 的基线实现:
- Intel Haswell:支持 AVX2 指令集,可一次进行 8 个
float32
操作。通过_mm_loadu_si128
加载可能未对齐的内存地址,_simsimd_bf16x8_to_f32x8_haswell
将bfloat16
转换为float32
,_mm256_fmadd_ps
进行融合乘加操作积累点积,还需进行水平累加和归一化处理,处理输入长度非 8 的倍数时需手动添加部分加载逻辑。 - AMD Genoa:支持 AVX-512 指令集,可一次进行 32 个
bfloat16
操作。引入了掩码(masking)机制,可部分执行指令,通过_mm512_maskz_loadu_epi16
进行部分加载,使用_mm512_dpbf16_ps
进行特殊的点积操作,最后进行水平累加和归一化处理。 - Arm NEON:Arm 设备的 SIMD 指令集,类似 SSE,有 128 位寄存器。文中展示了多种计算余弦相似度的方法,如使用
vbfmmlaq_f32
和vbfmmlaq_f32
intrinsics 计算点积,以及使用BFMMLA
指令,但最终该方法比朴素方法慢 25%,说明 Arm 指令集不一定比 x86 更 RISC。还提到 Arm 的rsqrt
指令文档有限,需进行反向工程来估计其精度。 - Arm SVE:支持可变宽度的实现定义寄存器,可 128 到 2048 位宽。通过“progress masks”和“while less than” intrinsics 处理循环的最后迭代,以处理未填满整个向量的元素。
- Intel Haswell:支持 AVX2 指令集,可一次进行 8 个
- 性能提升总结:利用硬件特性进行优化,在 Intel 硬件上性能从 10 MB/s 提升到 60.3 GB/s,在 Arm 硬件上从 4 MB/s 提升到 29.7 GB/s,强调了专用硬件加速库对简单计算算法的重要性。
- 库的打包和分发:CPU 特定代码很重要,但正确分发较复杂。可在编译时针对特定硬件平台编译,或在运行时动态选择最佳后端。x86 可通过 CPUID 指令查询功能寄存器,Arm 则在 Linux 上查询 CPU 寄存器,在 Apple 设备上查询操作系统,且应缓存可用的 CPU 功能列表以提高性能。
- 结论:SIMD 充满挑战,但正确利用可解锁惊人性能,比朴素串行代码高一个数量级。文章还提到还有很多未涉及的方面,如分析 x86 不同指令集家族的端口利用率等,第二部分将讨论 Mojo 如何帮助解决这些问题。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。