资讯详情

资讯详情

深度学习加速器中的Buffer Hierarchy:数据编排与驻留策略全解读

最近在系统翻译《Data Orchestration in Deep Learning Accelerators》的时候第三章 Buffer Hierarchies 是我花时间最多的一章。原因很简单这一章牵扯的知识点太密片上存储层次怎么搭、数据在时间维和空间维如何复用、每层缓冲区之间靠什么机制同步、不同数据流策略各自适合哪种计算模式几乎每一段都要反复查资料佐证才能把译文写到“硬件组同事能直接拿去评估设计”的程度。这篇就当作第三章节选的详细解读把我在翻译中反复琢磨的技术点、参数取舍和译法处理一并分享出来。适合正在做 AI 芯片、NPU 数据通路设计的工程师也适合刚接触计算机体系结构或深度学习加速器方向的研究生——想搞清楚“算力之外的隐藏瓶颈”这一章是绕不开的必修课。1. 为什么 Buffer Hierarchy 值得单独成章1.1 先厘清 Data Orchestration 到底指什么很多人第一次看到 “Data Orchestration” 这个词容易把它理解成“数据调度”或者“数据编排”其实都不太完整。在加速器设计语境里它回答的是四个问题数据什么时候搬到哪一层、以什么粒度搬、搬给谁、搬完后以什么形式复用。整个问题的跨度从主机内存一直延伸到乘累加单元旁边的寄存器堆。第三章的焦点集中在片上缓冲层次也就是从 DRAM 之外进入芯片之后数据如何在全局缓冲、局部缓冲、寄存器堆之间流动。这部分之所以难翻译是因为原文常常把搬运、复用、映射、同步四件事混在同一段里讨论如果不把层次关系先拎清楚很容易读着读着就迷路。我倾向于把 “Data Orchestration” 译成“数据编排”而不是“数据调度”。调度听起来像是一张顺序表编排更像是在空间和时间两个维度上同时铺排数据的位置和到达时刻更贴合这一章的气质。1.2 缓冲层次决定了加速器的“真实性能上限”芯片厂商在发布会上都喜欢标算力几百 TOPS 听起来很猛。但真正跑模型的时候峰值算力往往是“理论存在”真实吞吐率几乎总是被数据供给卡住。这一点在第三章开头就点明了计算单元只有在拿到输入数据的那一刻才产生价值数据到不了乘累加单元再高的 MAC 数也是空转。从这个角度看Buffer Hierarchy 其实是加速器性能的第一道闸门。设计一个 128x128 的脉动阵列不难难的是让这 16384 个乘累加单元每个周期都有数据可算。每个 PE 每周期要消耗一份输入特征图、一份权重、还可能读回一份部分和如果缓冲区带宽只够喂三分之一 PE实际利用率就只有三分之一。所以业内评估加速器时通常会同时看三份指标峰值算力、片上总带宽、缓冲总容量。只有三者匹配整个芯片的“有效算力”才接近标称值。第三章给我的最大启发就是Buffer Hierarchy 不是计算的附属品它就是加速器的主干。2. 缓冲层次结构与设计动机2.1 能耗账读一次 DRAM 比一次乘加贵两个数量级先看一组在加速器设计圈流传很广的参考数据以 8bit 定点运算为基准不同存储位置的访问能耗差异惊人。访问目标相对能耗倍数容量/带宽特征寄存器堆RF1x容量极小带宽极高通常在 PE 内部片上 SRAM局部缓冲/全局缓冲6x ~ 10x容量中等带宽由端口数和 bank 数决定片外 DRAM200x 左右容量大带宽受引脚数和封装限制这组数字的意义很直接如果某个数据在计算过程中可以被复用 10 次把它留在 SRAM 里反复读能耗大概只有从 DRAM 读一次的十分之一左右。而如果复用 200 次以上留在寄存器堆里几乎就是“免费”的。这也是为什么所有主流加速器都把“把数据尽量留在片上、留在低层缓冲”当作设计铁律。第三章花了很大篇幅讨论的 dataflow本质上就是围绕这份能耗账单做取舍。2.2 三个层次与各自的任务分工片上缓冲一般被划分为三个层次。第一层是寄存器堆位于每个 PE 内部容量最小、带宽最猛用来存放当前周期正在参与计算的操作数或部分和。第二层是局部缓冲通常是 PE 阵列共享的 SRAM负责暂存一个 tile 的输入特征图、权重或输出供多个 PE 访问。第三层是全局缓冲容量最大一般用来承接 DMA 从 DRAM 搬运回来的数据块再按 tile 分发给局部缓冲。这三层各有各的职责不能用同一个设计思路去套。寄存器堆追求的是“单周期内同时喂饱多个操作数”所以端口数比容量重要局部缓冲追求的是“尽量覆盖复用窗口”容量和带宽要平衡全局缓冲追求的是“把 DRAM 访问次数压到最低”所以容量优先。我在翻译中最常需要向中文读者补充的一点是这三层之间不是简单的一一对应关系而是一张多级网络。一个 PE 可能同时从局部缓冲读输入、从自己的寄存器堆读权重全局缓冲也可能直接旁路局部缓冲给某些 PE 送数据。层级之间的数据通路数量往往比层级本身的容量更能决定跑不跑得满。2.3 容量堆上去了带宽不一定跟得上很多团队做第一版加速器时习惯性地把 SRAM 容量做大觉得容量大了就能省更多 DRAM 访问。但实际流片后发现性能提升远没有想象中明显——问题往往出在带宽而不是容量。容量是水塘带宽是水管。水塘再大管子细单位时间能送出去的水就那么多。SRAM 如果只有一个读写端口所有 PE 都得排队等数据再大的容量也只是摆设。第三章里提到的 bank 划分、端口扩展、多播广播设计本质上都是在解决“数据从缓冲区到 PE 的传送速度”问题。所以评估一个缓冲层次设计时不能只看容量多少 MB还要看总带宽能达到多少字节每周期。我习惯用一个简单公式估算卷积层单周期需供数据量 输入通道数 × 每通道字节数 × 空间复用度这个值必须小于缓冲层次实际可提供的带宽否则 PE 阵列必然饥饿。3. 数据复用与映射策略拆解3.1 时间复用与空间复用先分清两条基本路径数据复用是 Buffer Hierarchy 的灵魂而所有复用手段归根到底都可以归入两类。时间复用指的是同一个数据在同一个计算单元上被多次使用只不过分布在不同的时钟周期。典型场景是卷积的累加循环同一个输入像素会被多个输出通道的卷积核反复用到只要循环不结束这个像素就一直在缓冲区里待着反复被读取。空间复用指的是同一个数据在同一时刻被多个计算单元同时使用。典型场景是输入特征图被广播给整排 PE每个 PE 用不同的权重和它做乘加。空间复用能把数据从缓冲区到 PE 的数据通路一次派出让整个阵列同时工作。两者的成本逻辑不同。时间复用省的是缓冲区带宽因为只服务一个 PE但可能带来重复的读取功耗空间复用省的是数据搬移次数但要确保所有 PE 都能在一个周期内拿到同一份数据这对片上网络的“扇出”能力提出了要求。第三章里的各种 dataflow 策略本质都是在两种复用之间找平衡点。3.2 权重驻留权重尽量留在 PE 内权重驻留Weight Stationary有些资料译作“权值固定”是最经典的策略脉动阵列风格的加速器基本都是这个思路。它的做法很简单把某个 filter 的权重一次性加载到 PE 的寄存器堆里然后反复使用这份权重去和不断流入的输入特征图做乘加。这种策略最明显的好处是权重流向极小。只要 filter 尺寸固定权重在整个卷积过程中基本只从外部读一次其余时间全部在 PE 内部“原地踏步”。这对带宽紧张的环境非常友好。但它的代价是输入特征图必须“流动起来”不断从全局缓冲或相邻 PE 推送到阵列中。如果输入特征图的复用度不够高或者每次只滑动一个像素就全量换入带宽压力就会转移到输入侧。另外输出部分和也要寻找合适的位置暂存处理不好就会在 PE 之间产生大量中间数据传输。3.3 输入驻留与输出驻留反着来同样成立输入驻留Input Stationary的逻辑是反过来的输入特征图尽量留在 PE 里权重不断轮换。这种策略适合输入特征图复用度极高的层比如大尺寸 feature map 的卷积一个数据块可以被多个输出通道共享。缺点是权重缓冲要做得宽否则权重轮换会成为瓶颈。输出驻留Output Stationary则把注意力放在部分和上。每个 PE 负责一个固定位置的输出像素把该位置上所有输入通道的累加都做完再把最终结果写回缓冲。这样做的好处是输出回写的流量小因为部分和被锁在 PE 内部不需要频繁往 SRAM 里写缺点是输入特征图和权重都需要广播式供给对数据通路的宽度要求高。实际设计里很少只用一种策略。像 Eyeriss 这类公开学术设计会在不同层之间切换数据流同一层内也存在混合映射。第三章用大量篇幅解释这些策略的判断标准读下来就会发现选哪种数据流不是看“哪个更高端”而是看“模型里哪一侧的数据流量最贵”。3.4 策略对比一张表看懂取舍数据流策略驻留对象主要优势主要瓶颈典型场景权重驻留权重权重搬运量小输入特征图需快速流动脉动阵列风格、Filter 较多的层输入驻留输入特征图输入复用充分权重轮换带宽大大 Feature Map、通道复用高的层输出驻留输出部分和输出回写流量小输入与权重都需广播输出通道多、累加次数多的层行驻留卷积窗内行数据对卷积窗复用友好调度逻辑复杂面向 CNN 的可重构阵列行驻留Row Stationary这个概念比较特殊它在卷积窗口内部做文章把卷积窗里同一行的数据锁在 PE 里随着窗口滑动只更新边界列最大限度利用行内复用。这种策略对现代 CNN 尤其合适因为 3x3、5x5 这类小卷积核的行间、行内复用度都很高。我在翻译这一部分时刻意保留了一些英文原词比如 Stationary 统一处理为“驻留”没有用“固定”。一是“固定”容易让人误以为是常量化计算二是“驻留”更准确地描述了数据“停留在某层缓冲区中持续服务计算”的状态。后面再看到“权重驻留”时心里就明白是权重一直待在 PE 内部而不是权重值不可变。3.5 真实芯片很少只用单一策略把上一节的四种策略并列出来容易让人觉得一个加速器只能选一种。但真实产品里基本全是混合体有的层用权重驻留跑卷积有的层用输出驻留跑全连接甚至会根据输入 tensor 的 shape在运行时动态切换 dataflow。切换数据流需要程序可控的缓冲区重配置能力。第三章强调了“programmable data orchestration”的意义——不是硬件想怎么流就怎么流而是由编译器根据 layer 的参数提前决定每一层数据从哪搬、搬到哪、在哪里驻留、如何广播。这也是为什么加速器编译器远比普通 CPU 编译器复杂的原因之一它不仅要生成指令还要规划数据在存储层次中的完整生命周期。4. 缓冲层次设计的关键实操细节4.1 双缓冲把搬运时延藏到计算后面片上缓冲区域如果只有一个副本DMA 往缓冲区写入下一批数据的时候PE 阵列只能干等。解决办法就是双缓冲Double Buffering也就是把同一段逻辑容量拆成 ping 和 pong 两块DMA 往 ping 里写数据的同时PE 从 pong 里取数计算下一轮再互换身份。# 双缓冲调度示意伪代码 for tile_id in range(num_tiles): if tile_id % 2 0: dma_load(tile_id, buf_ping) # 后台搬运到 ping compute_from(buf_pong) # 同时计算 pong else: dma_load(tile_id, buf_pong) # 后台搬运到 pong compute_from(buf_ping) # 同时计算 ping这段伪代码看着简单实际落地时有两个坑。第一个坑是容量成本翻倍同样容量的数据双缓冲会占掉两份地址空间在 SRAM 面积预算里要提前留出来。第二个坑是同步粒度问题如果计算一批数据的速度比 DMA 搬运下一批的速度快计算单元照样要等搬运完成所以设计 DMA 的 burst 长度和搬运优先级时必须以“搬运时间 计算时间”为目标。有一个实用经验tile 切得越大双缓冲的隐藏效果越好因为搬运时间在总时间里占比会变小但 tile 太大又会撑爆 SRAM 容量。我一般会先按片上容量的 60% 估算单份 tile 大小剩下 40% 留给另一份缓冲再回头倒推 DMA 带宽够不够这样迭代两三轮基本能收敛到合理配置。4.2 环形缓冲处理滑动窗口的好帮手卷积的访问模式是一个窗口接着一个窗口地滑动如果用普通缓冲存整个输入 feature map会发现每个新的窗口其实只有一列数据更新其他行列都在重复读取。重复读取就是浪费带宽。环形缓冲Ring Buffer专门处理这种滑动窗口问题。它把数据组织成环形结构新数据覆盖最旧的数据每次窗口滑动只需要搬入新的边界列其余数据继续留在原位置被复用。这种设计能显著降低输入特征图的重复搬运量。我在实际项目中接触过一个 3x3 卷积的加速器把输入缓冲改成环形结构后输入侧带宽需求下降了接近一半。代价是地址计算变复杂了因为 PE 访问的地址需要按环形逻辑取模不能直接用线性地址。设计时最好把窗口大小和环形深度都做成 2 的幂取模运算就能退化成与门省掉不少硬件开销。4.3 多 Bank 划分与冲突规避如果所有 PE 都往同一个 SRAM bank 里读数据这一个 bank 的端口数量迟早成为瓶颈。常见的做法是把逻辑缓冲切成多个 bank每个 bank 有独立端口支持多线程并行访问。比如一个 64KB 的缓冲可以切成 4 个 16KB 的 bank按地址交叉编址interleaving使得连续数据分散在不同 bank 里。这样 PE 阵列需要并行读 4 个不同地址时可以同时命中 4 个 bank带宽瞬间提升 4 倍。但 bank 多了也有副作用最典型的是 bank 冲突bank conflict如果多个 PE 恰好要访问同一个 bank 里的不同地址它们又会退回排队状态。第三章没有深入讲如何做地址映射但我在实际测试中确认了一个规律把卷积的通道维尽量映射到不同的 bank冲突概率最低。因为同一通道的数据往往在同一个窗口内被多个 PE 复用冲突最多跨通道的数据访问错开冲突自然减少。4.4 同步机制PE 之间怎么保持步调一致缓冲层次里还藏着一个很多人容易忽略的问题同步。寄存器堆和 SRAM 这类 Scratchpad 缓冲本身没有硬件一致性协议不像 CPU 的 Cache 会自动维护多核数据同步加速器里的数据搬运顺序必须由显式同步指令来保证。这里的同步分两种粒度。一种是 PE 与 DMA 之间的同步必须等数据真正搬进缓冲后PE 才能开始读否则可能读到上一轮残留的旧数据。另一种是 PE 与 PE 之间的同步在脉动阵列里数据是沿着邻近 PE 一个周期一个周期传的节奏天然一致几乎不需要显式握手但在非脉动结构中不同 PE 可能完成同一阶段的时刻不同就得加 barrier 等待。项目里做数据通路验证时最常出的 bug 就是同步设置过松或过紧。过松会读到脏数据过紧会白白损失性能。我目前比较认可的做法是先把计算阶段切细在每一段搬运完成处都加同步点功能验证通过后再逐步删掉多余同步点用性能回归测试确认哪些可以去掉。这个流程看起来笨但比试图一步到位精确配置同步点要稳得多。5. 常见优化手段与整体取舍5.1 Scratchpad 和 Cache加速器为什么几乎都选前者加速器领域基本形成了共识宁可自己管理片上存储也不愿意用 CPU 那套 Cache 体系。这不是因为 Cache 技术不成熟而是使用场景的本质不同。Cache 面向的是“不可预测的访存模式”靠硬件自动捕捉时间局部性和空间局部性Scratchpad 则面向“完全可知的数据生命周期”由编译器显式安排每份数据的搬入、驻留和搬出。对比项Scratchpad片上 SRAM 缓冲Cache数据管理方式软件/编译器显式控制硬件自动管理一致性处理无需 Coherence 协议多核场景需要复杂协议面积与功耗更高因为不含 Tag 等元数据有一定额外开销可预测性高访问延时确定低有 Miss 风险适配场景规则访存、卷积、矩阵乘不规则访存、动态行为Cache 的最大问题是不可预测。一次 Miss 可能导致整个 PE 阵列停顿好几拍而加速器最怕的就是停顿因为停顿意味着占用了几百上千个计算单元却毫无产出。Scratchpad 虽然把调度负担转给了编译器但换来的是“每一拍都在计划之中”的确定性这对追求稳定吞吐率的深度学习加速来说远更重要。5.2 Tile 切分给缓冲区容量做一道经济账任何缓冲层次都无法一次性装下整张 feature map 和全部权重所以要把大张量切成 tile分块反复搬入片上。tile 切得越大DRAM 访问次数越少越大越贵因为要占更多 SRAM。这个平衡就是第三章一直在强调的容量-带宽-能耗三角关系。给一个简化示例假设一个卷积层输出是 64x64x64输出通道 64每通道 8bit。输出部分和如果驻留在全局缓冲里需要 64x64x64x1B 256KB如果切到什么瓷砖尺寸可以斜坡计算。实际中没人把整层数据全留在片上一般是沿高度维和通道维同时切切成小立方体。切完后每个 tile 到底多大完全看全局缓冲容量和 DMA 带宽能接受多大的搬运粒度。我通常的做法是把“DRAM 访问量最小化”设为第一目标在满足容量约束的前提下把 tile 的尺寸尽量推大然后再检查权重驻留还是输入驻留更适合当前 tile 的形状。如果 tile 的高度方向太大会导致重复加载的权重过多这样就得不偿失。5.3 能耗优先时的决策顺序在实际优化加速器时有一个决策顺序值得参考。第一优先级是增加数据复用不论时间还是空间复用意味着更少的搬移次数。第二优先级是把访问尽量往低层缓冲压寄存器堆能解决的事绝不放 SRAMSRAM 能解决的事绝不放 DRAM。第三优先级才对银行化、地址映射、burst 长度这些细节做打磨。有一类额外优化不属于 Buffer Hierarchy但常常被混在一起讨论比如零值跳过和量化压缩。这类方案确实能减少数据体积但它们是“改变数据形态”的策略缓冲结构本身不一定感知到这些变化。第三章把重点放在数据形态不变前提下的搬运与复用读的时候最好先把这两种优化分开理解再考虑如何叠加。6. 翻译与工程实践结合的几点体会翻译过程中最难的其实不是技术理解而是术语统一。整章翻下来我在术语表里积累了这样几条经验hierarchy 统一译“层次结构”保留原文 H 的层级感buffer 译“缓冲”不译“缓冲区”每次都要加“区”字行文太啰嗦stationary 统一为“驻留”突出“停留原地持续服务”的含义orchestration 用“编排”既有空间铺排也有时间安排比“调度”准确。除了翻译本身这一章对实际工作的引导作用也超出我的预期。以前评估一个加速器方案我习惯先看它的 MAC 阵列多大、主频多高很少把缓冲层次的带宽和容量放在同样重要的位置。翻译完这一章后再看方案我会先画一张类似下面的表格每个存储层的容量、端口数、每周期带宽、访问能耗然后逐层问“这层数据供给速度够不够喂下一层计算”。这张表的用法很简单从 DRAM 出发逐层往下折算有效带宽只要有一层换算下来的字节每周期超过上层能力那里就是性能瓶颈。用这个办法我帮团队在流片前发现过一次局部缓冲 bank 数不够的问题如果等到芯片回来再查改版成本会高得多。第三章值得反复读因为每一次读都会带着新的设计问题找到对应段落。对做 AI 芯片的人来说它的价值不亚于一本手册对刚入门的研究生它是把“加速器”从黑盒变成结构化系统的起点。我在翻译时反复提醒自己的一句话也送给读这篇文章的人算力决定加速器的上限数据编排决定它到底能跑多接近这个上限。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →