4. 数据流与访存模式:卷积层数据复用模式、矩阵乘法的数据流优化、Transformer注意力机制的访存特征
各位同学,今天我们聊一个硬核话题——数据流与访存模式。说白了,就是数据在芯片里怎么跑、怎么存、怎么搬。我做了十几年芯片设计,发现很多团队算法调得飞起,一上芯片就卡在带宽上。嗯,这里面的门道,我今天给你们掰开揉碎了讲。
4.1 卷积层数据复用模式
卷积层是CNN的算力大户,也是访存大户。你想想看,一个3x3的卷积核,在特征图上滑动,每次只算9个乘法,但数据要反复从DDR里搬进搬出。这效率能高吗?
我个人习惯把卷积的访存模式分成三类:
- 输入特征图复用:同一个输入像素,会被多个卷积核用到。比如一个3x3的窗口,9个像素会被所有输出通道共享。
- 权重复用:同一个卷积核,会在整个特征图上滑动。权重只加载一次,但要用很多次。
- 输出累加复用:多个输入通道的卷积结果,要累加到同一个输出像素上。
我在项目中遇到过一个问题:某团队做边缘AI芯片,卷积加速器跑不快。一查,发现每次卷积核滑动时,都要从外部存储器重新读输入数据。这就是典型的「没利用好数据复用」。
核心原则:卷积加速器的设计目标,就是最大化数据在片上缓冲区的复用次数,减少对外部存储器的访问。
具体怎么做?我建议用循环分块(Loop Tiling)技术。把大的特征图切成小块,让小块数据在片上SRAM里完成所有计算,再写回外部存储器。
// 伪代码:卷积循环分块
for (oh = 0; oh < OH; oh += TH) // 输出高度分块
for (ow = 0; ow < OW; ow += TW) // 输出宽度分块
for (oc = 0; oc < OC; oc += TC) // 输出通道分块
// 加载输入块、权重块到片上SRAM
load_input_block(oh, ow, TH, TW);
load_weight_block(oc, TC);
// 在片上完成所有计算
for (kh = 0; kh < KH; kh++)
for (kw = 0; kw < KW; kw++)
for (ic = 0; ic < IC; ic++)
output[oh][ow][oc] +=
input[oh+kh][ow+kw][ic] * weight[kh][kw][ic][oc];
// 写回结果
store_output_block(oh, ow, oc, TH, TW, TC);
你看,这样每次加载的数据,在片上被反复用了很多次。我曾经在一个项目里,用这个技巧把DDR带宽占用从80%降到了30%。
避坑指南:分块大小不是越大越好。块太大,片上SRAM放不下;块太小,复用次数不够。我一般用「存储墙分析」来算最优块大小——先看SRAM容量,再看计算密度,最后定分块参数。
4.2 矩阵乘法的数据流优化
矩阵乘法是深度学习的基础操作。Transformer、全连接层、甚至卷积都能拆成矩阵乘法。但矩阵乘法的访存模式,和卷积不太一样。
矩阵乘法有三种经典的数据流:
| 数据流类型 | 描述 | 适用场景 |
|---|---|---|
| 内积数据流 | 一次计算一个输出元素,累加所有输入 | 小矩阵、低带宽场景 |
| 外积数据流 | 一次加载一列输入和一行权重,生成部分和 | 大矩阵、高带宽场景 |
| 分块数据流 | 把矩阵切成小块,在片上完成计算 | 通用场景,平衡带宽和计算 |
我个人最常用的是分块数据流。为什么呢?因为它能同时利用时间和空间局部性。你想想看,把矩阵A和B都切成小块,每次加载一个小块到片上,然后做矩阵乘法。这样每个数据在片上被用了很多次。
关键指标:计算与访存比(OPs/Byte)。这个比值越高,说明数据复用越好,带宽利用率越高。
举个例子,一个MxK的矩阵乘以KxN的矩阵,如果分块大小是BxK和KxB,那么计算量是2*B*B*K,访存量是2*B*K(输入)+ B*B(输出)。计算访存比大约是B。分块越大,比值越高。
我曾经在一个项目里,把矩阵乘法的分块从16x16改成64x64,计算访存比从16提升到了64。结果呢?同样的带宽下,吞吐量翻了近4倍。当然,前提是片上SRAM够大。
注意:分块数据流有个陷阱——部分和的管理。如果分块太小,部分和太多,写回外部存储器的开销会很大。我建议用「输出驻留」策略:让部分和一直留在片上,直到累加完成再写回。
4.3 Transformer注意力机制的访存特征
Transformer的注意力机制,访存模式跟卷积、矩阵乘法都不一样。它有个特点:计算量不大,但访存量巨大。
为什么?你看注意力公式:
Attention(Q, K, V) = softmax(Q * K^T / sqrt(d)) * V
这里有个关键操作:Q * K^T 生成一个NxN的注意力矩阵(N是序列长度)。这个矩阵的大小是N^2。当N=1024时,注意力矩阵就有1M个元素。如果N=8192,那就是64M个元素。
我在项目中遇到过一个问题:某团队做Transformer加速器,发现推理速度上不去。一分析,发现大部分时间都花在读写注意力矩阵上。这就是典型的「计算稀疏、访存密集」场景。
Transformer的访存特征可以总结为三点:
- 注意力矩阵的读写:NxN的矩阵,需要先写后读。这对片上缓冲区的容量要求很高。
- KV Cache的访问:在自回归生成时,需要缓存所有历史K和V。序列越长,缓存越大。
- softmax的归约操作:需要对每一行做求和和除法,这涉及大量的数据搬移。
核心挑战:注意力机制的访存带宽需求,随着序列长度平方增长。这是Transformer推理的主要瓶颈。
怎么优化?我建议从三个方向入手:
- Flash Attention:把注意力计算分块,让注意力矩阵的中间结果留在片上,不写回外部存储器。这能大幅减少访存量。
- KV Cache量化:把K和V从FP16量化到INT8甚至INT4。虽然精度有损失,但访存量减半甚至减到四分之一。
- 稀疏注意力:只计算部分位置的注意力权重,比如局部窗口或全局稀疏模式。这能直接减少N^2的访存开销。
我记得有一次,一个客户要做长文本生成(序列长度32K)。如果用标准注意力,光注意力矩阵就要1GB的带宽。后来我们用了Flash Attention + KV Cache量化,带宽需求降到了原来的十分之一。嗯,这才是能落地的方案。
避坑指南:Flash Attention虽然好,但实现起来有坑。比如分块大小怎么选?太小了计算效率低,太大了片上放不下。我一般用「计算访存比分析」来定——先算每块的计算量,再算访存量,找到最优分块。
4.4 三种访存模式的对比
好了,我们把三种模式放在一起看看:
| 模式 | 数据复用方式 | 访存瓶颈 | 优化方向 |
|---|---|---|---|
| 卷积层 | 输入/权重/输出复用 | 特征图反复加载 | 循环分块、数据重用 |
| 矩阵乘法 | 分块复用 | 部分和写回 | 输出驻留、计算访存比 |
| 注意力机制 | 注意力矩阵临时复用 | N^2矩阵读写 | Flash Attention、量化、稀疏 |
你发现没有?这三种模式的核心矛盾是一样的:计算越来越快,但访存越来越慢。说白了,就是「存储墙」问题。我做了这么多年芯片,最大的体会就是:算法设计要考虑访存,架构设计要匹配访存,系统设计要优化访存。
最后送大家一句话:带宽是芯片设计的硬约束,数据复用是突破约束的软技巧。希望今天的分享对你们有帮助。