1. 从一个朴素问题开始:CPU 为什么需要 AVX/AMX?
我们平时写程序时,脑子里常见的模型是:
for (int i = 0; i < n; ++i) { c[i] = a[i] + b[i];}这段代码看起来像是 CPU 一次处理一个 a[i] + b[i]。如果真是这样,CPU 每执行一次循环,只得到一个结果。
但现代 CPU 并不满足于”一次只算一个数”。很多任务天然有大量重复计算,例如:
- 图片处理:一批像素做同样的加减乘除。
- 音视频编解码:一批采样点做滤波、变换。
- 科学计算:一批浮点数做向量和矩阵运算。
- 机器学习:矩阵乘法、卷积、attention 里有大量乘加。
于是 CPU 里出现了面向数据并行的指令和执行单元。
SIMD 是 Single Instruction, Multiple Data,意思是:一条指令,同时处理多个数据。
AVX 和 AMX 都属于提高数据并行吞吐量的硬件扩展,只是抽象层次不同:
- AVX:Advanced Vector Extensions,是典型的 SIMD,主要面向一维向量计算。
- AMX:Advanced Matrix Extensions,主要面向二维矩阵块计算,尤其服务于 AI 里的矩阵乘法。
它们不是新的编程语言,也不是某个库,而是 CPU 指令集架构的一部分。软件要用它们,必须满足三件事:
- CPU 硬件真的支持这些指令。
- 操作系统愿意保存和恢复这些额外的寄存器状态。
- 编译器、运行库或程序员生成了对应指令。
这三层关系可以画成下面这样:
2. 指令集、寄存器和执行单元
要理解 AVX/AMX,先把几个概念分开。
指令集(ISA) 是 CPU 对软件承诺的”可用操作清单”。比如 add 是整数加法,vmulps 是 AVX 浮点向量乘法,tdpbf16ps 是 AMX BF16 tile 矩阵乘加。程序最终都会变成这些机器指令。
寄存器 是 CPU 内部最快的一小块存储。普通整数代码用通用寄存器,AVX 用 XMM/YMM/ZMM 这类向量寄存器,AMX 用 tile 寄存器。
执行单元 是真正干活的电路。指令被解码后,CPU 会把它送到合适的执行单元,例如整数 ALU、浮点单元、向量 FMA 单元,或者 AMX 的 TMUL 矩阵乘法单元。
所以,“支持 AVX”并不只是多了几个指令名字。它通常意味着 CPU 还提供了更宽的向量寄存器、更复杂的执行单元,以及相应的上下文保存机制。
3. AVX 是什么?
AVX 是 x86 平台上的向量指令扩展。它的核心思想是:把多个相同类型的数据打包进一个向量寄存器,然后用一条指令统一处理。
举例说,如果我们有 8 个 float:
a = [a0, a1, a2, a3, a4, a5, a6, a7]b = [b0, b1, b2, b3, b4, b5, b6, b7]普通标量代码会做 8 次加法。AVX 可以把这 8 个 float 放进一个 256-bit 的 YMM 寄存器,用一条向量加法得到 8 个结果。
常见 AVX 家族大致可以这样理解:
| 扩展 | 典型寄存器宽度 | 粗略理解 |
|---|---|---|
| SSE | 128-bit XMM | 更早期的 SIMD |
| AVX | 256-bit YMM | 主要增强浮点向量计算,引入 VEX 编码和三操作数形式 |
| AVX2 | 256-bit YMM | 把 256-bit SIMD 扩展到更多整数操作 |
| AVX-512 | 512-bit ZMM | 更宽向量、更多寄存器、mask 操作,包含很多子扩展 |
为了让这些名字更具体一点,可以看几条常见指令。下面的表不追求完整,只是帮助建立感觉:
| 指令 | 常见来源 | 大致含义 | 常见 intrinsic |
|---|---|---|---|
VADDPS | AVX | packed single-precision float 加法,也就是多个 float 并行相加 | _mm256_add_ps |
VMULPS | AVX | packed single-precision float 乘法 | _mm256_mul_ps |
VFMADD132PS / VFMADD213PS / VFMADD231PS | FMA | fused multiply-add,做 a * b + c,中间不单独舍入 | _mm256_fmadd_ps |
VPADDD | AVX2 | packed 32-bit integer 加法 | _mm256_add_epi32 |
VGATHERDPS | AVX2 | 根据一组索引从不连续地址收集 float | _mm256_i32gather_ps |
VPDPBUSD | AVX-512 VNNI | uint8 * int8 点积并累加到 int32,常见于 INT8 推理 | _mm512_dpbusd_epi32 |
从命名上也能看出一些规律:PS 常表示 packed single-precision float,PD 表示 packed double,D 常和 32-bit doubleword 整数有关。前缀 V 通常说明这是 VEX/EVEX 编码下的向量指令。
需要注意:AVX 不是一个单一能力开关。现实中经常会看到 AVX、AVX2、FMA、AVX512F、AVX512VNNI 等多个 feature flag。软件通常要检查自己需要的那一组,而不是只问”有没有 AVX”。
4. AVX 和软件的关系
软件使用 AVX 通常有三条路。
4.1 让编译器自动向量化
最轻量的方式是写普通循环,让编译器尝试自动把它变成向量指令。
void add_arrays(const float* a, const float* b, float* c, int n) { for (int i = 0; i < n; ++i) { c[i] = a[i] + b[i]; }}编译时打开优化和目标指令集,例如:
g++ -O3 -mavx2 add.cpp -o add如果循环足够规整,编译器可能会生成 AVX/AVX2 指令。
这种方式优点是代码干净,缺点是你不一定能完全控制编译器生成什么。循环里如果有复杂分支、别名问题、内存访问不连续,自动向量化就可能失败。
4.2 使用 intrinsics
intrinsics 是 C/C++ 里的一组函数形式 API,用来比较直接地表达底层 SIMD 指令。它不像手写汇编那么底层,但也比普通循环更接近硬件。
下面是一个 AVX 版本的 8 个 float 相加:
#include <immintrin.h>
void add8_avx(const float* a, const float* b, float* c) { __m256 va = _mm256_loadu_ps(a); __m256 vb = _mm256_loadu_ps(b); __m256 vc = _mm256_add_ps(va, vb); _mm256_storeu_ps(c, vc);}这里:
__m256表示 256-bit 向量,可以放 8 个float。_mm256_loadu_ps从内存加载 8 个float。_mm256_add_ps做 8 路浮点加法。_mm256_storeu_ps把结果写回内存。
intrinsics 的好处是可控,坏处是代码可读性下降,并且你要自己处理尾部数据、对齐、不同 CPU feature 的分发等问题。
4.3 使用已经优化过的库
多数时候,尤其是 Python 用户,并不会直接写 AVX 指令,而是通过库间接使用。
例如:
import numpy as np
a = np.random.rand(1_000_000).astype(np.float32)b = np.random.rand(1_000_000).astype(np.float32)c = a + b这段 Python 看起来很普通,但真正执行加法的通常是 NumPy 底层的 C/C++/Fortran 代码。对于矩阵乘法、卷积、深度学习算子,底层可能调用 OpenBLAS、MKL、oneDNN,或者 PyTorch/TensorFlow 自带的 CPU 后端。这些库会根据 CPU 能力选择 SSE、AVX2、AVX-512、AMX 等不同实现。
所以 Python 使用 AVX/AMX 的常见方式不是”在 Python 里写指令”,而是:
- 使用 NumPy/PyTorch/TensorFlow/scikit-learn 这类底层已优化的库。
- 安装适合当前平台的二进制包。
- 确认库是否启用了对应后端,例如 BLAS、oneDNN、MKL。
5. AVX 的限制:不是越宽越无脑快
AVX 很有用,但它不是魔法。
第一,SIMD 喜欢规整的数据。连续数组、相同操作、少分支,是 SIMD 最舒服的场景。如果数据结构是链表、树、哈希表,或者每个元素走不同分支,AVX 很难发挥。
第二,性能可能被内存带宽限制。如果计算很简单,比如只是 a[i] + b[i],CPU 可能不是卡在加法,而是卡在从内存读写数据。
第三,AVX-512 这类宽向量在某些处理器上可能影响频率。具体影响和微架构、负载、散热、功耗策略有关,不能简单理解成”512-bit 一定比 256-bit 快两倍”。
第四,代码需要处理兼容性。如果你用 -mavx2 编译整个程序,生成的二进制在不支持 AVX2 的 CPU 上可能直接非法指令崩溃。工程上通常会做 runtime dispatch:启动时检测 CPU feature,然后选择对应实现。
6. AMX 是什么?
AMX 是 Intel 面向矩阵计算引入的扩展。和 AVX 主要处理一维向量不同,AMX 引入了二维的 tile 寄存器。
根据 Intel 对 AMX 的介绍,AMX 的核心由两部分组成:
- Tiles:每个核心上的二维 tile 寄存器。Intel 文档中描述为 8 个 tile,每个 tile 最大 1 KiB。
- TMUL:Tile Matrix Multiplication,负责对 tile 做矩阵乘法相关计算的硬件单元。
直观上可以把 AMX 想成:CPU 核心里多了一组”小矩阵暂存区”,以及专门吃这些小矩阵的乘加单元。
AMX 的目标非常明确:加速深度学习训练和推理中常见的矩阵乘法。它支持的具体数据类型随扩展而定,例如 AMX-BF16、AMX-INT8、AMX-FP16 等。
AMX 的指令比 AVX 更有”操作 tile”的味道。常见指令可以粗略分成三类:
| 指令 | 所属能力 | 大致含义 | 常见 intrinsic |
|---|---|---|---|
LDTILECFG | AMX-TILE | 从内存加载 tile 配置,告诉 CPU 每个 tile 有多少行、每行多少字节 | _tile_loadconfig |
TILELOADD | AMX-TILE | 从内存加载一个二维矩阵块到 tile 寄存器 | _tile_loadd |
TILESTORED | AMX-TILE | 把 tile 寄存器里的矩阵块存回内存 | _tile_stored |
TILEZERO | AMX-TILE | 把某个 tile 清零,常用于初始化累加器 | _tile_zero |
TILERELEASE | AMX-TILE | 释放 tile 配置和 tile 状态 | _tile_release |
TDPBF16PS | AMX-BF16 | BF16 矩阵块点积,累加到 FP32 tile | _tile_dpbf16ps |
TDPBSSD / TDPBUSD | AMX-INT8 | INT8 矩阵块点积,累加到 INT32 tile;不同后缀表示 signed/unsigned 组合 | _tile_dpbssd / _tile_dpbusd |
TDPFP16PS | AMX-FP16 | FP16 矩阵块点积,累加到 FP32 tile | _tile_dpfp16ps |
如果说 AVX 的典型指令像是”对 8 个或 16 个数一起做加法/乘法”,AMX 的典型指令更像是”把 A 的一个小矩阵块和 B 的一个小矩阵块相乘,并累加到 C 的一个小矩阵块”。这也是为什么 AMX 更适合 GEMM、卷积、Transformer 里那些最终会落到矩阵乘法的计算。
这和 AVX/AVX-512 的关系不是”谁替代谁”,而是抽象层次不同:
| 维度 | AVX / AVX-512 | AMX |
|---|---|---|
| 数据形态 | 一维向量 | 二维 tile |
| 常见任务 | 向量加减乘除、FMA、数据重排 | 矩阵乘法、AI 推理/训练 |
| 编程复杂度 | 中等,intrinsics 已经较常见 | 更高,需要 tile 配置和 OS 支持 |
| Python 用户常见入口 | NumPy、BLAS、PyTorch 底层库 | oneDNN、PyTorch/TensorFlow 的 CPU 后端 |
7. AMX 如何使用?
AMX 比 AVX 更不像”随手写两行就能用”。它至少涉及三层准备。
7.1 硬件和系统支持
首先 CPU 必须支持 AMX 相关 feature。常见检查方式:
lscpu | grep -i amx在 Linux 上,也可以看:
grep -o 'amx[^ ]*' /proc/cpuinfo | sort -u其次,操作系统需要支持 AMX 的扩展状态管理。AMX tile 数据比传统寄存器状态大得多,线程切换时需要 OS 知道如何保存和恢复这些状态。在 Linux 上,AMX 用户态程序通常还需要通过 arch_prctl 请求使用 tile data 扩展状态。否则即使 CPU 支持,程序也可能不能直接使用 tile 指令。
7.2 C/C++ intrinsics
AMX 也有 intrinsics,但写法比 AVX 更复杂,因为你需要先配置 tile 的形状。
下面是高度简化的结构示意,不建议把它当成可直接复制运行的完整 GEMM:
#include <immintrin.h>#include <stdint.h>
struct tile_config { uint8_t palette_id; uint8_t start_row; uint8_t reserved[14]; uint16_t colsb[16]; uint8_t rows[16];};
void configure_tiles() { tile_config cfg = {}; cfg.palette_id = 1;
// tmm0/tmm1/tmm2 的行数和每行字节数。 // 真实代码要根据矩阵分块、数据类型和指令要求仔细设置。 cfg.rows[0] = 16; cfg.colsb[0] = 64; cfg.rows[1] = 16; cfg.colsb[1] = 64; cfg.rows[2] = 16; cfg.colsb[2] = 64;
_tile_loadconfig(&cfg);}AMX 的典型流程是:
- 配置 tile 形状。
- 从内存把矩阵块加载到 tile。
- 使用 tile dot-product 指令做矩阵乘加。
- 把结果 tile 存回内存。
- 释放或重置 tile 状态。
实际工程里,手写 AMX kernel 的门槛很高,因为你还要处理矩阵分块、cache locality、数据布局、线程并行、尾块、不同数据类型和不同 CPU feature。除非是在写底层算子库,否则更常见的选择是使用 oneDNN、MKL、PyTorch 等已经实现好 AMX kernel 的库。
7.3 Python 中使用 AMX
Python 代码本身不会直接变成 AMX 指令。它通常通过深度学习框架或数值库间接使用。
例如,PyTorch 在 CPU 上跑 BF16 推理时,如果底层 oneDNN 和当前 CPU 支持 AMX-BF16,就可能走 AMX 优化路径。用户侧写的是:
import torch
model = model.eval().to("cpu")inputs = inputs.to("cpu")
with torch.no_grad(), torch.autocast(device_type="cpu", dtype=torch.bfloat16): outputs = model(inputs)这段代码是否真的用到 AMX,取决于 CPU、操作系统、PyTorch 构建方式、oneDNN 后端、模型算子形状和数据类型。判断方法通常不是看 Python 源码,而是看运行环境、框架日志、性能计数器或 profiler。
8. 如何判断自己的机器支持什么?
在 Linux 上可以先看 CPU flags:
lscpu或者:
grep -m1 flags /proc/cpuinfo你可能会看到类似:
avx avx2 fma avx512f avx512_vnni amx_bf16 amx_tile amx_int8对 C/C++ 程序来说,更可靠的做法是在运行时检测 feature。GCC/Clang 提供了类似接口:
#include <iostream>
int main() {#if defined(__x86_64__) || defined(__i386__) __builtin_cpu_init();
std::cout << "avx2: " << __builtin_cpu_supports("avx2") << "\n"; std::cout << "avx512f: " << __builtin_cpu_supports("avx512f") << "\n";#endif}工程化一点的程序会准备多个实现:
- 标量 fallback:任何机器都能跑。
- SSE/AVX2 实现:覆盖较多桌面和服务器 CPU。
- AVX-512 实现:用于支持更宽向量的 CPU。
- AMX 实现:用于支持 AMX 且任务适合矩阵 tile 的服务器 CPU。
然后在启动时选择最快的可用实现。
9. 一个简单的学习路线
如果只是想理解 AVX/AMX,不建议一上来就手写 AMX GEMM。更合理的顺序是:
- 先理解标量、SIMD、向量寄存器和 cache。
- 用 C++ 写一个普通数组加法,让编译器自动向量化,配合
-O3 -mavx2观察汇编。 - 用 AVX intrinsics 写
_mm256_add_ps这种小例子。 - 学习矩阵乘法的分块:为什么要 block,为什么内存布局重要。
- 再看 AMX tile 配置、tile load/store、tile dot-product。
- 最后阅读 oneDNN、BLAS 或深度学习框架里的 CPU kernel 思路。
对 Python 用户来说,实用路线则是:
- 确认 NumPy/PyTorch/TensorFlow 的构建和后端。
- 用合适的数据类型,例如 BF16/INT8。
- 关注模型形状和 batch size,让算子足够大,值得调用底层优化 kernel。
- 用 profiler 验证,而不是凭感觉判断。
10. 总结
AVX 和 AMX 都是 CPU 为了提高吞吐量而提供的指令集扩展,但它们服务的计算形态不同。
AVX 的核心是 SIMD:一条指令处理多个一维数据。它适合数组、向量、图像、信号处理和很多数值计算。C++ 可以通过自动向量化或 intrinsics 使用;Python 通常通过 NumPy、BLAS、PyTorch 等库间接使用。
AMX 的核心是 tile:把矩阵块放进二维 tile 寄存器,再用 TMUL 这类硬件单元做矩阵乘加。它更偏向 AI 训练和推理里的矩阵乘法,直接手写门槛较高,通常通过 oneDNN、MKL、PyTorch/TensorFlow CPU 后端间接使用。
最重要的一点是:硬件能力、操作系统支持、编译器/库支持必须同时成立。只看 CPU 名字或只看 Python 代码,都无法保证 AVX/AMX 真的被用上。
参考资料
- Intel: Instruction Set Architecture
- Intel: Intel Intrinsics Guide
- Intel: Intel AVX-512 Overview
- Intel: What Is Intel Advanced Matrix Extensions?
- Intel: AMX intrinsics code sample