向量处理器

数据级并行与向量处理器架构的设计方法

Posted by CloudingYu on May 13, 2026

一、AI处理器设计导论:动机与方法论

1.1 为什么需要专用AI处理器

以设计一个专用的 AI CPU(AICPU)为引子,讨论专用处理器的设计动机。

核心场景:面对一个特定算法(如深度学习中的大量乘加运算 $y = Wx + b$、神经元计算),目前市面上的 GPU 都能跑这个算法,但存在两个瓶颈:

  • 功耗墙(Power Wall):GPU 功耗太大,不适合做小型边缘设备。
  • 价格贵:GPU 成本高,不适合大规模部署或消费级产品。

做任何项目、写任何论文,第一件事情是问”为什么要做这件事”(motivation)。这个问题必须让别人一听就觉得”有道理、很痛、很难解决”。以 DeepSeek 的 MoE(Mixture of Experts)为例——大模型全部参数激活根本跑不动,所以分而治之的思路一出来,大家就觉得有道理。

1.2 设计路径:RISC-V + 指令扩展 vs 协处理器

以 RISC-V 为基础,有两种设计思路:

  1. 不扩展指令集:就用现有的乘法、加法指令组合完成乘加运算,硬件不加任何新模块。
  2. 扩展指令集:新增一条专门的乘加指令(如 muladd),硬件上可以名正言顺地加一个单独的硬件模块来加速。

起一个好名字是项目成功的一半。叫 GPU 太土了——GPU 已经有了,别人会问”你到底是 AI 还是 CPU?”花里胡哨的名字先问清楚它到底做了什么。

1.3 软件-硬件的资源权衡(Trade-off)

这是工程中最核心的决策问题:

  • 扩展指令越多 → 编译器越难改 → 软件投资越大
  • 扩展指令越多 → 硬件改动越大 → 硬件投资越大
  • 你有一笔固定预算,投多少给硬件、投多少给软件?

计算机体系结构中,这个问题没有具体公式可套。因为到处都是”墙”——功耗墙、带宽墙(bandwidth wall)。很多时候卡的不是计算,而是带宽。设计了一条超强的指令,但带宽跟不上,等于白搭。


二、从指令级并行到数据级并行

2.1 指令级并行(ILP)的局限

  • ILP 已经”玩不出什么大花头”了:能折腾的方法已经被折腾得差不多了(流水线、forwarding、Tomasulo 算法、分支预测等)。
  • 太细节:ILP 看不了全局,不能做更广范围的根本性优化。
  • ILP 是在”指令安排”上做文章,而忽视了被处理的数据本身。

之前是把干活的人来回调配(ILP),现在应该好好看看要干的活是什么——数据有什么特征?这些数据能不能换一把新铲子(新架构)来挖?

2.2 数据级并行的发现

当仔细看数据时,发现一个惊人的事实:大量应用中(图像处理、深度学习、科学计算),数据之间没有依赖关系

例如深度学习中的神经元计算: \(y_i = A \cdot x_i + y_i\)

  • 不同 $i$ 之间的计算完全独立,没有 data dependence。
  • 图像处理中每幅图像用完就扔,对 cache 的命中率要求不高。
  • 大量数据运算彼此之间都是数据并行的。

以前为什么拿个小叉子在那叉?因为这个东西错综复杂,拿个大叉子一叉就乱糟糟的。现在发现它们都没关系——大叉子上,没问题!这就是数据级并行(DLP)为什么那么自然而然来的。

2.3 SISD 到 SIMD 的转变

  • SISD(Single Instruction Single Data):一条指令处理一个数据
  • SIMD(Single Instruction Multiple Data):一条指令处理多个数据

动机很简单:取一条指令就能处理多个数据(比如 8 个),大大降低了取指令的带宽压力,同时数据之间没有依赖,硬件实现也非常规整。

以前处理一个字节(8位),读进来 16 个字节的 cache line,只用一个字节还很得意地说 cache 命中了。别整了——把寄存器放大到 128 位,一次搞 16 个字节,一次全搞定,多痛快。


三、SIMD 的三种形态

3.1 向量处理器(Vector Architecture)

最早在超级计算机上出现(如 Cray 系列),核心特征是:

  • 向量寄存器:一组专门的宽寄存器,每条可以存放多个数据元素。
  • 向量指令:对向量寄存器整体操作,一条指令 = 数十个标量操作。
  • 深度流水线:向量操作可以全流水化执行。
  • 交叉存储(Memory interleaving):多个 memory bank 并行提供数据。

当年的超级计算机主要应用于科学计算,没有复杂逻辑(没有大量 if-else 跳转),都是大块数据集。向量处理器的设计其实不难——把数据拉过来,搞一排计算单元,哐哐哐就处理完了。

3.2 面向多媒体的 SIMD 指令集扩展

在通用 CPU 上增加 SIMD 指令(如 x86 的 MMX/SSE/AVX、ARM 的 NEON),使通用处理器也能利用数据级并行。这是对已有指令集的增量扩展。

3.3 GPU

GPU 是 SIMD 思想的大规模实现:成百上千个计算核心同时处理大量数据。


四、向量处理器架构详解

4.1 两种实现方式的对比

方式 描述 优点 缺点
Memory-Memory 所有指令的源操作数和结果都在 memory 中 指令少 访存次数太多
Vector Register 只有 load/store 才访问 memory,计算在向量寄存器间进行 减少访存,数据在寄存器间复用 需要专门的向量寄存器堆

最终 Vector Register(RR 型) 胜出:load 进来的数据在寄存器间反复计算,不用反复访问 memory,也不需要 cache——数据直接在寄存器里流转。

4.2 核心组件

  • 向量寄存器(Vector Register):RISC-V V 扩展中提供 V0-V31,每个寄存器宽度可配置(如 128/256/512 位)。
  • 标量寄存器(Scalar Register):如 x0-x31,用于存放单个标量(如 $A$ 在 $y = Ax + b$ 中)。
  • 向量功能单元(Vector Functional Unit):全流水化的运算单元,通常 4-8 个 lane。
  • 向量 Load/Store 单元:专门负责大带宽数据搬移。

4.3 向量长度可配置

通过 vsetvl 指令配置向量长度(VL),支持不同硬件规模:

  • 有钱(土豪):设置 VL = 8 或 VL = 16,一次处理更多元素
  • 没钱:设置 VL = 2 或 VL = 4,一次处理较少元素

同一条向量指令(如 vadd.vv)在不同 VL 设置下可以处理不同数量的元素,软件不用改,这就是向量架构的可扩展性


五、向量指令与优化技术

5.1 向量指令格式

RISC-V V 扩展中的典型指令:

  • vadd.vv v1, v2, v3 — 向量加法
  • vmul.vv v1, v2, v3 — 向量乘法
  • vle.v v1, (rs1) — 向量 load
  • vse.v v1, (rs1) — 向量 store
  • vfmadd.vv v1, v2, v3 — 向量融合乘加

本质上就是把标量指令的寄存器换成向量寄存器——指令格式紧凑,表达能力很强。

5.2 Lane 结构(车道)

多条并行执行通路,每条称为一个 Lane(车道):

  • 单 Lane(省钱版):只有一个计算单元,流水线加深,数据一个一个排队走。
  • 多 Lane(土豪版):4 个 Lane(四条车道),4 个数据同时计算,每个 Lane 直接连接 memory。

以前一条道大家排着队走,现在来四条车道一起走。只要设计好一溜的,然后 Ctrl+C、Ctrl+V 直接搞八个就出来了。

5.3 Chaining(链接)

Chaining 本质就是向量处理器中的 forwarding(前递/定向):标量流水线中,forwarding 让上一条指令的结果不等写回寄存器就直接传给下一条指令使用。向量处理器中同理——向量 load 的数据不等全部写完,第一个元素一出来就可以立即送给后续运算单元使用。

这对包含依赖的向量操作(如 $D = A \times (B + C)$ )至关重要。

5.4 Convoy 与 Chime

  • Convoy(护航指令组):一组可以并行执行的向量指令(无结构冲突)。本质就是向量版本的多发射(multiple issue)。
  • Chime:执行一个 Convoy 所需的时间。

5.5 Strip Mining(条带化处理)

当程序中的向量长度超过硬件支持的最大向量长度(MVL)时,需要分批次处理:

  1. 先做 $\lfloor N / \text{MVL} \rfloor$ 个完整的 Convoy
  2. 剩余 $N \bmod \text{MVL}$ 个元素单独处理

5.6 向量 Mask 寄存器

当向量中只有部分元素需要计算时(如第 0、7、13 个不计算),用 mask 寄存器标记:

  • mask 位 = 1:对应元素参与计算
  • mask 位 = 0:对应元素不参与计算

这样做比把有效元素”挤到一起”更高效——重新排列数据的开销往往大于直接屏蔽几个元素。

5.7 稀疏向量与 Gather/Scatter

  • 密集向量:所有元素都有效,顺序存放。
  • 稀疏向量:只有少数元素非零或有意义,分布不连续。

处理稀疏数据时,gather(集中)和 scatter(分散)指令按索引表来收集/分布数据元素:

1
2
3
// 例如: A[K[i]] 的计算
for (i = 0; i < n; i++)
    C[i] = A[K[i]] + B[i];

其中 $K[i]$ 不连续,gather 指令按索引 $K$ 从 memory 中收集 $A[K[i]]$ 到向量寄存器中。但如果数据是密集图像流,就别折腾压缩了——赶紧做好 IO、提高访存带宽才是正道。所有设计都要根据实际数据特征来。


六、向量处理器的内存系统

6.1 对 Cache 的需求降低

对于数据密集型操作(流处理、图像处理、神经网络推理),数据”用完就扔”,不需要大量缓存。这正是 GPU 上不太依赖大容量 cache 的原因。

6.2 HBM vs GDDR

内存类型 全称 特点 使用场景
HBM High Bandwidth Memory 超高带宽,3D 堆叠,通过硅中介层连接 高端 GPU、AI 加速器
GDDR Graphics Double Data Rate 在 DDR 基础上调高参数 中端 GPU、游戏显卡

HBM 产线优先供给 GPU/AI 芯片,价格更高。CPU 不用 HBM 不是因为不喜欢快,而是性价比不支持。

6.3 Memory Bank 与交错访问

向量处理器需要一次读 128 位甚至 512 位数据。如果所有数据都在同一个 memory bank 上,就要反复读同一个 bank——带宽被瓶颈。

Memory interleaving(交错访问):把数据分散到多个 bank 上,从每个 bank 同时读一部分,拼成完整的向量。不但要快,还要灵活——每个 bank 需要能独立访问,支持多个 load/store 同时进行。

6.4 跨步访问(Stride Access)

在很多算法(如矩阵乘法)中,数据不是连续存放的,而是以固定的步长(stride)间隔存放。例如图像处理中,每个像素单元可能是 9 个像素一组,每次跳 9 个像素。因此需要支持 stride load/store 指令。


七、性能分析与实践考虑

7.1 向量长度不是越大越好

实验测试从 VL = 10 到 VL = 200,观察到:

  • VL 从 0 到 64,每个元素的执行时间递减(均摊的启动开销降低)
  • VL = 65 时(刚好超过 64),时间突然跳升——因为需要两次向量操作(64 + 1),余数部分无法摊销启动开销
  • 再增大到 128/256,继续递减,但边际收益递减
  • 目前普遍选择 VL = 128,差不多就够用了

7.2 编译器和程序员的协作

编译器做向量化优化时,有时候缺乏足够的信息来判断数据是否可以并行。解决方案:程序员在代码中加 directive(#pragma),提示编译器这个循环可以向量化、这里没有数据依赖、这里适合怎样的并行方式。

这不属于 C 语言标准语法,但编译器和程序员之间事先约定好。编译器拿到更多信息,就能做出更好的优化。设计 CPU 不一定是做硬件,也可能写编译器——需要在代码里面为编译器留线索。


八、工程思维与实战智慧

8.1 峰值性能的数字陷阱

某系统宣称 512 个节点,每秒 128GB 的网络带宽。听起来太厉害了——现在家用千兆网络也才 1Gbps。但仔细算一下:16 个 CPU 在主板上的板卡连接,每个达到 1GB/s 不过分;16 个就是 16GB/s;如果每个 CPU 实际只需要 1/16 的主频就能达到,那 16 个凑一凑就是 100GB/s 以上。

128GB/s 听起来吓人,实际上是把所有”可能的”带宽加总在一起——但这只有在数据完美分布、零依赖、全部跑满的理想情况下才可能达到。买回去一定达不到峰值。

以后看到数据要上点心。尤其当你作为一个部门负责人要去买设备的时候——”原则上可以”就等于”到你手上肯定干不了”。永远记住:理想情况就是让你听听而已。

8.2 好名字与好工程的关系

Tensor 本质上就是多维数组。谷歌把它叫 Tensor(张量),听起来就高大上。很多新技术不过是把老概念重新包装一下、起个好名字。但名字确实重要——它能影响论文被接受、项目被认可的程度。花里胡哨的名字先问它做了什么,但自己的项目一定要起个好名字。

8.3 Top500 超算排名

中国曾用 Intel 芯片堆出排名第一的超级计算机集群,美国随后禁运。中国转而用自研芯片(华为等),后来干脆退出排名不参加了。