Files
flykhan 2536c937e3 feat: 完整中文翻译 maths-cs-ai-compendium(数学·计算机科学·AI 知识大全)
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。

第01章 向量 | 第02章 矩阵 | 第03章 微积分
第04章 统计学 | 第05章 概率论 | 第06章 机器学习
第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音
第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络
第13章 计算与操作系统 | 第14章 数据结构与算法
第15章 生产级软件工程 | 第16章 SIMD与GPU编程
第17章 AI推理 | 第18章 ML系统设计
第19章 应用人工智能 | 第20章 前沿人工智能

翻译说明:
- 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留
- mkdocs.yml 配置中文导航 + language: zh
- README.md 已翻译为中文(兼 docs/index.md)
- docs/ 目录包含指向各章文件的 symlink
- 约 29,000 行中文内容,排除 .cache/ 构建缓存
2026-05-03 10:23:20 +08:00

451 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# x86与AVX
*x86处理器来自Intel和AMD,主导着大多数ML训练所在的数据中心服务器。本文涵盖x86 SIMD的演进、AVX/AVX2内联函数编程、AVX-512、用于矩阵操作的Intel AMX、内存对齐、性能陷阱以及性能分析——在全球最常见的服务器CPU上榨取最大性能的工具。*
- 如果你的训练在云虚拟机(AWS、GCP、Azure)上运行,它几乎肯定运行在x86上。即使是GPU密集训练也有CPU瓶颈:数据加载、预处理、梯度聚合和检查点保存都在CPU上运行。使用x86 SIMD优化这些环节可以有意义地减少端到端训练时间。
## x86 SIMD演进
- x86 SIMD经历了越来越宽的向量寄存器:
| 代次 | 年份 | 寄存器宽度 | 寄存器数量 | 关键特性 |
|------|------|---------|----------|----------|
| MMX | 1997 | 64位 | 8mm0-7 | 仅整数,与FPU共享 |
| SSE | 1999 | 128位 | 8xmm0-7 | 4个浮点数,专用寄存器 |
| SSE2 | 2001 | 128位 | 8/16 | 2个双精度浮点数,整数操作 |
| AVX | 2011 | 256位 | 16ymm0-15 | 8个浮点数,三操作数指令 |
| AVX2 | 2013 | 256位 | 16 | 整数256位,FMA,收集 |
| AVX-512 | 2017 | 512位 | 32zmm0-31 | 16个浮点数,掩码寄存器,分散 |
| AMX | 2023 | 瓦片寄存器 | 8个瓦片 | 矩阵乘法(BF16INT8 |
- 每一代都将向量化代码的吞吐量翻倍。用SSE内联函数编写的代码可以在2001年以来制造的每一个x86 CPU上运行。AVX2需要2013年以后的CPU。AVX-512需要Intel Xeon和一些消费级芯片。AMX是最新的(Sapphire Rapids及以后)。
- **向后兼容性**x86 SSE寄存器(xmm)是AVX寄存器(ymm)的低128位,后者是AVX-512寄存器(zmm)的低256位。旧的SSE代码无需修改即可在新的CPU上运行。
## AVX2编程
- AVX2操作256位寄存器(YMM),同时处理8个浮点数或4个双精度浮点数。它是可移植高性能代码的甜点区域:在几乎所有现代x86 CPU(2013+)上可用。
### 内联函数命名约定
- 所有x86内联函数遵循模式:`_mm[宽度]_[操作]_[类型]`
- `_mm` = MMX/SSE128位),`_mm256` = AVX256位),`_mm512` = AVX-512512位)
- 操作:`add``mul``fmadd``load``store``set`
- 类型:`ps` = 打包单精度(float32),`pd` = 打包双精度(float64),`epi32` = 打包int32`si256` = 256位整数
```cpp
#include <immintrin.h> // 所有x86 SIMD内联函数
// 数据类型
__m256 a; // 256位寄存器,保存8个float32
__m256d b; // 256位寄存器,保存4个float64
__m256i c; // 256位寄存器,保存整数(8x32、16x16或32x8
```
### 加载和存储数据
```cpp
// 从内存加载8个浮点数
__m256 v = _mm256_loadu_ps(ptr); // 非对齐加载(适用于任何地址)
__m256 v = _mm256_load_ps(ptr); // 对齐加载(ptr必须32字节对齐,更快)
// 存储8个浮点数到内存
_mm256_storeu_ps(out_ptr, v); // 非对齐存储
_mm256_store_ps(out_ptr, v); // 对齐存储
// 将单个值广播到所有8个通道
__m256 ones = _mm256_set1_ps(1.0f); // [1, 1, 1, 1, 1, 1, 1, 1]
// 设置各个值(很少需要)
__m256 v = _mm256_set_ps(7,6,5,4,3,2,1,0); // 注意:逆序!
// 零寄存器
__m256 z = _mm256_setzero_ps();
```
### 算术运算
```cpp
__m256 c = _mm256_add_ps(a, b); // c[i] = a[i] + b[i]
__m256 d = _mm256_mul_ps(a, b); // d[i] = a[i] * b[i]
__m256 e = _mm256_sub_ps(a, b); // e[i] = a[i] - b[i]
__m256 f = _mm256_div_ps(a, b); // f[i] = a[i] / b[i](比乘法慢)
// 融合乘加:r = a * b + c(一条指令,一次舍入)
__m256 r = _mm256_fmadd_ps(a, b, c); // ML最重要的指令
// 最小值和最大值
__m256 mn = _mm256_min_ps(a, b); // min(a[i], b[i]) — 用于裁剪
__m256 mx = _mm256_max_ps(a, b); // max(a[i], b[i]) — 用于ReLU
```
### 实践示例:AVX2点积
```cpp
#include <immintrin.h>
float dot_avx2(const float* a, const float* b, int n) {
__m256 sum = _mm256_setzero_ps(); // 8个累加器初始化为0
int i = 0;
for (; i + 8 <= n; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
sum = _mm256_fmadd_ps(va, vb, sum); // sum += va * vb
}
// 水平归约:将sum的8个元素相加
// 步骤1:将上128位加到下128位
__m128 hi = _mm256_extractf128_ps(sum, 1);
__m128 lo = _mm256_castps256_ps128(sum);
__m128 sum128 = _mm_add_ps(hi, lo); // 4个部分和
// 步骤2:在128位寄存器内水平相加
sum128 = _mm_hadd_ps(sum128, sum128); // [a+b, c+d, a+b, c+d]
sum128 = _mm_hadd_ps(sum128, sum128); // [a+b+c+d, ...]
float result = _mm_cvtss_f32(sum128); // 提取标量
// 标量清理
for (; i < n; i++) {
result += a[i] * b[i];
}
return result;
}
```
- **为什么水平归约如此丑陋**:SIMD是为垂直操作设计的(通道0与通道0,通道1与通道1)。水平操作(跨通道求和)与硬件对抗。这就是点积在末尾有尴尬归约代码的原因。向量化循环是简洁的;归约是样板代码。
- **性能**:与NEON版本(文件02)相比,AVX2每次迭代处理8个浮点数,而NEON处理4个。对于长向量,这比NEON快2倍(忽略内存带宽限制)。
### 实践示例:AVX2 Softmax(简化版)
- Softmax需要:找到最大值,减去它,求指数,求和,除法。以下是最值查找步骤:
```cpp
float vector_max_avx2(const float* data, int n) {
__m256 max_vec = _mm256_set1_ps(-INFINITY);
int i = 0;
for (; i + 8 <= n; i += 8) {
__m256 v = _mm256_loadu_ps(data + i);
max_vec = _mm256_max_ps(max_vec, v);
}
// 将8个最大值归约为1个
__m128 hi = _mm256_extractf128_ps(max_vec, 1);
__m128 lo = _mm256_castps256_ps128(max_vec);
__m128 max128 = _mm_max_ps(hi, lo);
// 通过混洗和取最大值找到单一最大值
max128 = _mm_max_ps(max128, _mm_shuffle_ps(max128, max128, 0b01001110));
max128 = _mm_max_ps(max128, _mm_shuffle_ps(max128, max128, 0b10110001));
float result = _mm_cvtss_f32(max128);
for (; i < n; i++) {
result = result > data[i] ? result : data[i];
}
return result;
}
```
- `_mm_shuffle_ps` 指令在寄存器内重排元素。二进制常量 `0b01001110` 控制哪些元素去哪里。这称为**置换**,它直接连接到置换矩阵(第2章):打乱SIMD通道是做硬件级别的乘以置换矩阵。
## AVX-512
- AVX-512再次加倍宽度:512位寄存器(ZMM),同时处理16个浮点数。
```cpp
__m512 a = _mm512_loadu_ps(ptr); // 加载16个浮点数
__m512 c = _mm512_fmadd_ps(a, b, c); // 16个FMA同时进行
float sum = _mm512_reduce_add_ps(a); // 内置水平求和(无需手动归约!)
// 掩码操作:操作通道子集
__mmask16 mask = _mm512_cmpgt_ps_mask(a, zero); // 哪些通道 > 0
__m512 relu = _mm512_maskz_mov_ps(mask, a); // 负通道置零 = ReLU
```
- **掩码寄存器**`__mmask16`)是AVX-512最强大的功能。每个位控制一个通道是否参与操作。这取代了标量清理循环:最后一次迭代使用掩码,只有有效通道是激活的,处理任何向量长度而无需单独标量循环。
- **AVX-512频率降频**:在许多Intel CPU上,使用AVX-512指令会导致CPU暂时降低时钟频率(以保持在热限制内)。这意味着对于短时爆发,AVX-512并不总是比AVX2快——频率惩罚可能抵消更宽向量的优势。对于持续工作负载(如矩阵乘法),AVX-512胜出。对于混合代码(部分SIMD、部分标量),频率转换可能造成损失。
## Intel AMX:矩阵乘法硬件
- **AMX**(高级矩阵扩展)增加了专用矩阵乘法单元。AMX操作的不是SIMD向量,而是**瓦片**:2D数据块(最多16行 × 每行64字节)。
```cpp
#include <immintrin.h>
// AMX瓦片乘法:C += A * BBF16格式)
// A为16x32 BF16B为32x16 BF16C为16x16 FP32
_tile_loadd(0, a_ptr, stride_a); // 从A加载瓦片0
_tile_loadd(1, b_ptr, stride_b); // 从B加载瓦片1
_tile_dpbf16ps(2, 0, 1); // 瓦片2 += 瓦片0 * 瓦片1BF16矩阵乘法,FP32累加)
_tile_stored(2, c_ptr, stride_c); // 存储瓦片2到C
```
- AMX在一条指令中执行完整的16×32 × 32×16矩阵乘法。这是数百次FMA操作同时进行,专门为Transformer推理中主导的小矩阵乘法设计(注意力得分计算、MLP层)。
- AMX支持BF16bfloat16)和INT8,匹配ML推理中使用的精度。结合用于其他操作的AVX-512,配备AMX的CPUIntel Sapphire Rapids、Emerald Rapids)可以在Transformer推理中与入门级GPU竞争。
## 内存对齐
- **对齐内存访问**是指数据地址是向量寄存器宽度的倍数(SSE为16字节、AVX为32字节、AVX-512为64字节)。对齐访问在某些CPU上更快,并且是 `_mm256_load_ps`(相对于 `_mm256_loadu_ps`)的要求。
```cpp
// 分配对齐内存
float* data = (float*)aligned_alloc(32, n * sizeof(float)); // AVX的32字节对齐
// C++对齐分配
#include <new>
float* data = new (std::align_val_t(32)) float[n];
// 或者使用编译器属性
alignas(32) float data[1024];
```
- **实际上**:在现代CPU(Haswell及以后)上,当数据不跨越缓存行边界时,非对齐加载(`loadu`)几乎与对齐加载一样快。非对齐访问的性能惩罚已基本消失,但缓存行分割(数据跨越两个64字节缓存行)仍可能使特定加载变慢约2倍。对齐分配完全避免了这种情况。
## 性能陷阱
- **AVX-SSE转换惩罚**:在较旧的Intel CPUSkylake之前)上,在AVX256位)和SSE(128位)指令之间切换会造成惩罚(约70周期)。这就是为什么你应该在从使用AVX的函数返回之前使用 `_mm256_zeroupper()`(或 `vzeroupper` 指令)清除YMM寄存器的上128位。现代CPU(Skylake+)没有此惩罚。
- **寄存器压力**:AVX2有16个YMM寄存器。如果你的核函数使用太多变量,编译器会将寄存器溢出到栈(内存),从而破坏性能。保持内循环简单,活变量少。
- **数据依赖**`sum = _mm256_fmadd_ps(a, b, sum)``sum` 有依赖:每次迭代必须等待前一个FMA完成(约4-5个周期的延迟)。解决方案:使用多个独立累加器并在结束时归约:
```cpp
// 单累加器:受FMA延迟限制(4-5个周期)
__m256 sum = _mm256_setzero_ps();
for (...) {
sum = _mm256_fmadd_ps(a, b, sum); // 每个依赖前一个
}
// 四个累加器:4倍吞吐量(隐藏延迟)
__m256 sum0 = _mm256_setzero_ps();
__m256 sum1 = _mm256_setzero_ps();
__m256 sum2 = _mm256_setzero_ps();
__m256 sum3 = _mm256_setzero_ps();
for (...) {
sum0 = _mm256_fmadd_ps(a0, b0, sum0); // 独立
sum1 = _mm256_fmadd_ps(a1, b1, sum1); // 独立
sum2 = _mm256_fmadd_ps(a2, b2, sum2); // 独立
sum3 = _mm256_fmadd_ps(a3, b3, sum3); // 独立
}
sum0 = _mm256_add_ps(sum0, sum1);
sum2 = _mm256_add_ps(sum2, sum3);
sum0 = _mm256_add_ps(sum0, sum2);
```
- 这是**循环展开**以隐藏延迟。CPU可以背靠背发出FMAs,因为它们写入不同的寄存器。这是数值代码中最有影响力的微优化之一。
## 性能分析
- **性能计数器**提供硬件级测量:
```bash
# Linux perf(需要内核支持)
perf stat ./my_program # 基本计数器:周期、指令、IPC
perf stat -e cache-misses,cache-references ./my_program # 缓存行为
perf record -g ./my_program && perf report # 调用图分析
# Intel VTune(详细的x86性能分析)
vtune -collect hotspots -- ./my_program
vtune -collect memory-access -- ./my_program # 内存带宽分析
```
- **需要关注什么**
- **IPC**(每周期指令数):CPU被使用的效率。IPC > 2 良好。IPC < 1 表明内存停顿或分支预测错误。
- **缓存缺失率**:高L1/L2缺失率表明数据局部性差。需重构数据访问模式。
- **分支预测错误率**:> 5% 表明分支不可预测。如可能,转换为无分支代码(SIMD比较+混合)。
- **实际FLOPS vs 屋顶线**:将你的实测FLOPS与屋顶线模型(文件01)比较。如果你低于屋顶线,还有改进空间。
## 编程任务(在x86——Intel/AMD上用g++或clang++编译)
1. 编写标量点积和AVX2点积。对两者进行基准测试并测量8路SIMD带来的加速比。
```cpp
// task1_avx_dot.cpp
// 编译:g++ -O3 -mavx2 -mfma -o task1 task1_avx_dot.cpp
#include <iostream>
#include <chrono>
#include <vector>
#include <immintrin.h>
float dot_scalar(const float* a, const float* b, int n) {
float sum = 0.0f;
for (int i = 0; i < n; i++) sum += a[i] * b[i];
return sum;
}
float dot_avx2(const float* a, const float* b, int n) {
__m256 sum = _mm256_setzero_ps();
int i = 0;
for (; i + 8 <= n; i += 8) {
__m256 va = _mm256_loadu_ps(a + i);
__m256 vb = _mm256_loadu_ps(b + i);
sum = _mm256_fmadd_ps(va, vb, sum);
}
// 归约:上128加到下128,然后水平相加
__m128 hi = _mm256_extractf128_ps(sum, 1);
__m128 lo = _mm256_castps256_ps128(sum);
__m128 r = _mm_add_ps(hi, lo);
r = _mm_hadd_ps(r, r);
r = _mm_hadd_ps(r, r);
float result = _mm_cvtss_f32(r);
for (; i < n; i++) result += a[i] * b[i];
return result;
}
int main() {
const int N = 10'000'000;
std::vector<float> a(N, 1.0f), b(N, 2.0f);
volatile float s1 = dot_scalar(a.data(), b.data(), N);
volatile float s2 = dot_avx2(a.data(), b.data(), N);
auto bench = [&](auto fn, const char* name) {
auto start = std::chrono::high_resolution_clock::now();
volatile float s;
for (int t = 0; t < 100; t++) s = fn(a.data(), b.data(), N);
auto end = std::chrono::high_resolution_clock::now();
double ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
std::cout << name << ": " << ms << " ms(结果: " << s << "\n";
return ms;
};
double t1 = bench(dot_scalar, "标量");
double t2 = bench(dot_avx2, "AVX2 ");
std::cout << "加速比: " << t1 / t2 << "x\n";
return 0;
}
```
2. 使用 `_mm256_max_ps` 实现AVX2 ReLU并与标量循环比较。然后尝试使用多累加器(循环展开)以隐藏FMA延迟。
```cpp
// task2_avx_relu.cpp
// 编译:g++ -O3 -mavx2 -o task2 task2_avx_relu.cpp
#include <iostream>
#include <chrono>
#include <vector>
#include <immintrin.h>
void relu_scalar(const float* in, float* out, int n) {
for (int i = 0; i < n; i++) {
out[i] = in[i] > 0.0f ? in[i] : 0.0f;
}
}
void relu_avx2(const float* in, float* out, int n) {
__m256 zero = _mm256_setzero_ps();
int i = 0;
for (; i + 8 <= n; i += 8) {
__m256 x = _mm256_loadu_ps(in + i);
_mm256_storeu_ps(out + i, _mm256_max_ps(x, zero));
}
for (; i < n; i++) out[i] = in[i] > 0.0f ? in[i] : 0.0f;
}
// 展开:每次迭代处理32个浮点数(4 x 8)
void relu_avx2_unrolled(const float* in, float* out, int n) {
__m256 zero = _mm256_setzero_ps();
int i = 0;
for (; i + 32 <= n; i += 32) {
__m256 x0 = _mm256_loadu_ps(in + i);
__m256 x1 = _mm256_loadu_ps(in + i + 8);
__m256 x2 = _mm256_loadu_ps(in + i + 16);
__m256 x3 = _mm256_loadu_ps(in + i + 24);
_mm256_storeu_ps(out + i, _mm256_max_ps(x0, zero));
_mm256_storeu_ps(out + i + 8, _mm256_max_ps(x1, zero));
_mm256_storeu_ps(out + i + 16, _mm256_max_ps(x2, zero));
_mm256_storeu_ps(out + i + 24, _mm256_max_ps(x3, zero));
}
for (; i + 8 <= n; i += 8) {
_mm256_storeu_ps(out + i, _mm256_max_ps(_mm256_loadu_ps(in + i), zero));
}
for (; i < n; i++) out[i] = in[i] > 0.0f ? in[i] : 0.0f;
}
int main() {
const int N = 16'000'000;
std::vector<float> in(N), out(N);
for (int i = 0; i < N; i++) in[i] = (float)(i % 200) - 100.0f;
auto bench = [&](auto fn, const char* name) {
fn(in.data(), out.data(), N); // 预热
auto start = std::chrono::high_resolution_clock::now();
for (int t = 0; t < 100; t++) fn(in.data(), out.data(), N);
auto end = std::chrono::high_resolution_clock::now();
double ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
double bw = 2.0 * N * sizeof(float) / ms / 1e6; // 读取+写入
std::cout << name << ": " << ms << " ms" << bw << " GB/s\n";
};
bench(relu_scalar, "标量 ");
bench(relu_avx2, "AVX2 ");
bench(relu_avx2_unrolled, "AVX2展开 ");
return 0;
}
```
3. 测量内存对齐的效果。比较在大数组上的对齐加载与非对齐加载。
```cpp
// task3_alignment.cpp
// 编译:g++ -O3 -mavx2 -o task3 task3_alignment.cpp
#include <iostream>
#include <chrono>
#include <cstdlib>
#include <immintrin.h>
int main() {
const int N = 16'000'000;
// 对齐分配(AVX2为32字节)
float* aligned = (float*)aligned_alloc(32, N * sizeof(float));
// 非对齐:从对齐边界偏移4字节(1个浮点数)
float* raw = (float*)malloc((N + 1) * sizeof(float));
float* unaligned = raw + 1; // 保证未对齐
for (int i = 0; i < N; i++) {
aligned[i] = 1.0f;
unaligned[i] = 1.0f;
}
auto bench = [&](float* ptr, bool use_aligned, const char* name) {
__m256 sum = _mm256_setzero_ps();
// 预热
for (int i = 0; i + 8 <= N; i += 8) {
__m256 v = use_aligned ? _mm256_load_ps(ptr + i) : _mm256_loadu_ps(ptr + i);
sum = _mm256_add_ps(sum, v);
}
auto start = std::chrono::high_resolution_clock::now();
for (int t = 0; t < 100; t++) {
sum = _mm256_setzero_ps();
for (int i = 0; i + 8 <= N; i += 8) {
__m256 v = use_aligned ? _mm256_load_ps(ptr + i) : _mm256_loadu_ps(ptr + i);
sum = _mm256_add_ps(sum, v);
}
}
auto end = std::chrono::high_resolution_clock::now();
double ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
double bw = (double)N * sizeof(float) / ms / 1e6;
std::cout << name << ": " << ms << " ms" << bw << " GB/s\n";
};
bench(aligned, true, "对齐加载 ");
bench(unaligned, false, "非对齐加载");
free(aligned);
free(raw);
return 0;
}
```