news 2026/10/13 11:51:11

NPU体系结构详解:从MAC阵列到带宽墙的AI芯片优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NPU体系结构详解:从MAC阵列到带宽墙的AI芯片优化

1. 为什么专门把NPU从“计算机体系结构”里拎出来

体系结构课上讲CPU,绕不开流水线、分支预测、乱序执行、Cache层级;讲GPU,绕不开SIMT、线程块、warp调度。但一旦讲到NPU,你会发现话题突然拐向了另一个方向:二维乘加阵列、片上SRAM、DMA搬运、数据复用。这不是教材故意漏掉了什么,而是NPU本来就是另一套设计哲学:它把“控制”这件事尽量弱化,把“数据搬来搬去”这件事推向极端。

NPU不是CPU,也不是GPU。CPU的特点是“什么都能算,但单个线程的效率天花板明显”;GPU的特点是“大量线程并行跑,能覆盖图像、通用计算,但没有专门为某一种算子做死电路”。NPU走的是另一条路:先把最常见的算子(卷积、矩阵乘、激活、归一化)给固化成硬件数据通路,然后围绕这些算子的访存模式来倒推内存系统和调度方式。它的直接目标不是让任意程序跑得快,而是让某几类程序跑得极快、单位功耗算得极多。

所以这篇文章才值得单开一篇。它不是讲“神经网络怎么训练”,而是站在计算机体系结构的角度,把NPU当作一个具体算例来解剖:它由哪些模块组成,为什么长成这个形状,公式层面的性能极限在哪里,以及作为一个工程师该怎么让它真正跑满。

适合读这篇的人有三类。第一类是正在补体系结构课的在校生,想搞清楚“NPU和CPU在体系结构上到底差在哪”;第二类是刚转去做AI芯片或者AI编译器的新人,需要一个不绕弯子的入门框架;第三类是搞模型部署的工程师,虽然不设计芯片,但非常需要理解“为什么我的模型在NPU上总跑不出理论算力”。

2. NPU体系结构解剖:它到底由什么组成

2.1 核心部件:MAC阵列、向量单元、SRAM、DMA

一个典型的NPU,内部其实只有四大块:计算阵列、向量/标量单元、片上存储、DMA搬运引擎。这个结构和CPU“取指-译码-执行-访存”的经典五级流水完全不同,甚至可以说它根本没有传统意义上的通用指令流水线。

计算阵列是整颗芯片的心脏。它由大量乘加单元(MAC,Multiply-Accumulate)排成二维网格构成。为什么是二维,而不是一堆独立的乘法器挂在一个总线上?因为神经网络里的卷积和矩阵乘,本质上是“高维规约计算”:把一大块数据反复搬进ALU,不如让数据在阵列里按固定方向流动。二维阵列的好处是,同一份数据可以同时喂给多个乘法器,寄存器之间直接转发,不需要每次都从内存取数。

乘加单元本身不复杂,一个乘法器加一个加法器就够了。但阵列规模一上去,功耗和面积就非常敏感。于是各种实现细节就出来了:低精度乘数器可以用更少的逻辑门,INT8乘法在实际电路里比FP32乘法省好几倍面积;累加器则需要保留更高位宽,因为INT8乘完累加很容易溢出,常见做法是累加器用INT32甚至FP32。这些细节看似琐碎,却直接决定了NPU的“真实算力”而不是“纸面算力”。

向量单元负责处理那些“不是矩阵乘”的活。比如ReLU激活、通道间Pooling、LayerNorm、残差相加、逐元素乘法。为什么要单独立一块?因为把这些操作也用MAC阵列去跑,利用率极低——MAC阵列张开一次要做几千个乘加,为几个元素激活去铺开它实在太浪费。所以NPU一般会有一个宽度适中的向量处理单元,按几百个元素一批去跑逐元素运算。很多NPU性能上不去,问题不在MAC阵列,而是向量单元不够宽,或者向量单元和MAC阵列之间没有独立的Buffer通道,导致激活计算和矩阵乘排队打架。

片上SRAM是NPU的生命线。它通常被切成几个物理分区:激活Buffer、权重Buffer、输出Buffer、共享Scratchpad。注意,这里没有Cache的概念。Cache是硬件自动管理、按地址命中的;NPU里的Buffer是“软件显式管理”的,也就是程序员或编译器要明确告诉硬件“这块数据放哪、什么时候放进来、什么时候用完可以覆盖”。这听着很原始,但恰恰是为了可控性:DRAM访问的延迟和带宽都是可预测的,但Cache命中不确定性会带来计算单元的随机停顿,这对一个以“确定性吞吐”为目标的引擎是不可接受的。

DMA引擎负责片外DRAM和片上SRAM之间的数据搬运。它支持二维甚至三维的地址描述,可以直接搬一个Tile块,不需要CPU逐行拷贝。DMA做完一次搬运后会触发一个中断或状态位,计算单元再开始消费数据。这套机制保证了“搬运”和“计算”可以重叠:上一块数据在算的时候,下一块数据已经在路上。如果你见过“双缓冲”三个字,指的就是这件事。

2.2 “指令”是什么?一个被简化到极致的控制通路

很多人第一反应是问:NPU跑的是什么程序?答案是:一种数据流描述,或者说是一串“Tile指令”。

NPU的控制器不像CPU那样每个周期去取一条指令、译码一次。它的做法更接近“任务级流水”:编译器先分析整个模型,把计算切成一个个Tile,生成一张任务图;运行时再按任务图向NPU的队列里下发描述符。每个描述符里面大概写着:把哪块输入搬到哪个Buffer,把哪块权重搬到哪个Buffer,让MAC阵列做一次矩阵乘,结果写到哪个地址,然后对结果做一次ReLU,最后发一个同步信号给下一个任务。

这种指令不包含“分支跳转”。NPU里几乎没有条件判断,就算有,也是“算完这个Tile后是否跳到下一个更小的循环”,而不是复杂的控制流。为什么可以这样?因为神经网络的拓扑在推理阶段其实是静态的:模型有几层、每层输入输出多大,在编译期就确定了。NPU要做的只是按固定顺序把一堆数据流任务执行完。

那如果模型是动态的呢?比如序列长度变化、有循环网络、有基于数据的条件分支?这种场景下NPU一般有两种处理方式:要么把动态部分卸载回CPU/GPU,要么把动态模型拆成多个静态子图,在NPU上分次加载执行。这也是为什么NPU很少单独存在,它总是和CPU绑在一起,形成一个SoC:CPU负责控制逻辑和动态分支,NPU负责纯计算。

3. 算力不等于性能:NPU的带宽墙和访存公式

3.1 一个必须写进脑子里的概念:计算强度

先看一个最简单的例子。假设一个NPU标称算力是100 TOPS(INT8),也就是每秒钟能做100万亿次整数运算。听起来很吓人。但问题是:这100万亿次运算所需要的数据,是从哪里来?

如果每一次乘加都需要从外部DRAM读两个操作数,那访存量就是200TB/s。就算今天的HBM技术也做不到这种带宽,成本早就把芯片本身压垮了。所以NPU设计的第一原则就是:让数据在片上被反复用,而不是每次都用完就丢。

量化这个原则的指标叫“计算强度”(Arithmetic Intensity),定义是:每从片外读一个字节,能在片上完成多少次运算。单位是FLOP/Byte或者MAC/Byte。这个概念在体系结构里其实不新,老早的Roofline模型就用它来判断一段代码是计算密集型还是访存密集型,但在NPU这里它变成了芯片设计指南。

如果计算强度太低,NPU满速运转时就需要海量外部带宽,最后瓶颈一定在内存,标称算力根本发挥不出来。这叫“带宽墙”。相反,如果计算强度足够高,数据从片外读一次,在片内被重复利用几百次,那外部带宽要求就会急剧下降,芯片才有机会逼近理论算力。

3.2 一眼算出你的NPU能不能跑满

把上面的逻辑转成公式。假设:

  • 目标算力是 P(MAC/s)
  • 外部存储带宽是 B(Byte/s)
  • 你的算法平均计算强度是 I(MAC/Byte)

那么要跑满算力,必须满足 P / I ≤ B,也就是带宽需求 B_need = P / I。

反过来,给你一块芯片,你也能反推它能跑满多高的有效算力:P_eff = min(P, B × I)。

举个例子。某个NPU标称INT8算力50 TOPS,即50×10^12 MAC/s,片外带宽50GB/s。如果算法的计算强度只有100 MAC/Byte,那么带宽能支撑的算力就是50GB/s × 100 = 5 TOPS。这意味着标称50 TOPS的芯片,在这个算法上最多跑出5 TOPS,利用率只有10%。这不是芯片不行,是算法访存特性不行。

那怎么提高计算强度?靠数据复用。拿卷积来说,一个3×3卷积,同一个输入特征图的某个像素,会参与所有输出通道的计算。在当前输出通道这层里,它被读进片上之后,可以被复用很多次:不同卷积核位置、不同输出通道都能共用。所以卷积天然比“两个大矩阵直接做逐元素乘加”更容易跑满带宽。矩阵乘法的计算强度也和分块尺寸强相关:K维度越长,每个权重元素被复用的次数越多。这也是为什么很多NPU编译器都在疯狂优化Tile尺寸,靠Tile选得巧,能把片外访问量降低一个数量级。

这个公式还能解释另一个现象:为什么现在大家都在做INT8、INT4、FP16的低精度推理。因为计算强度不变的情况下,低精度直接把每字节能承载的运算次数翻倍。同样是1字节,FP32只能存一个数,INT8能存一个,FP16能存两个,INT4能存四个。哪怕MAC阵列的物理结构完全不变,“有效计算强度”也提高了,带宽墙自然被推远了一些。所以低精度不只是为了省存储,更是为了突破带宽瓶颈。

3.3 片上容量是藏在背后的限制条件

不过,光有带宽公式还不够。计算强度能提高,前提是“把一块数据留在片上反复用”。但如果这块数据太大,片上SRAM根本放不下,那你就只能分块处理,或者频繁把中间结果写回DRAM。所以第二个限制条件是片上SRAM容量。

一个实际检查方法:如果你要复用的数据块是10MB,而某NPU的片上SRAM只有2MB,那无论如何编译器都只能切成5块,每块数据对应的复用范围也跟着缩小,计算强度会被大幅削弱。因此,NPU设计的核心矛盾从来不只是“算得多快”,而是“在有限的片上存储里,怎么把数据流动的设计做到最优”。

这带来一个有趣的设计结论:有些NPU宁愿把MAC阵列做得小一点,也要把SRAM做大。因为算力可以通过提高频率或加阵列宽度硬堆,但访存效率一旦被存储容量卡死,堆更多算力也只是浪费面积。你先保住“有效利用带宽”,再谈峰值算力,这才是NPU架构的正确姿势。

4. 让NPU跑起来:Tile、DMA和编译器的协同

4.1 一切计算的单位都是Tile

给NPU编程,不考虑整张计算图,考虑的是“Tile”。一个Tile就是一块刚好能塞进片上SRAM的数据片段,比如输入特征图的某个空间区域和对应的部分通道。矩阵乘法也是一个道理:把一个大矩阵拆成若干小矩阵块,每次计算一个块。

为什么要分块?因为片上SRAM有限,而模型往往巨大。编译器的工作就是从外层循环开始,把整个模型的计算切成合适的Tile序列,让每块数据在片上的生命周期尽量长。这里的生命周期不是指时间长短,而是指它被读入后,在写回或丢弃之前,参与了多少次运算。

我常看到有人用下面这段伪代码来理解NPU的调度逻辑,它虽然简化,但基本抓住了精髓:

// 一个矩阵乘 C = A × B,分块到 SRAM for (int m0 = 0; m0 < M; m0 += TILE_M) for (int n0 = 0; n0 < N; n0 += TILE_N) for (int k0 = 0; k0 < K; k0 += TILE_K) { // 异步搬 A 和 B 的块到 SRAM dma_load(A_buf, A[m0][k0], TILE_M, TILE_K); dma_load(B_buf, B[k0][n0], TILE_K, TILE_N); // 等 DMA 完成后开始计算 wait_dma_done(); mac_compute(C_buf, A_buf, B_buf, TILE_M, TILE_K, TILE_N); // 把 C 块积累到片外或保留在片上继续累加 dma_store(C_buf, C[m0][n0], TILE_M, TILE_N); }

这段代码里的关键点有三个:一是DMA是异步的,计算还没结束就能预取下一块;二是计算单元和DMA可以并行工作,这是NPU高利用率的来源;三是对K维度的循环往往不把中间结果写回DRAM,而是直接留在C Buffer里累加,这能节省大量带宽。

4.2 卷积在NPU上是怎么落地的

卷积说起来复杂,落到NPU上通常会转换成两种形态。第一种是显式Im2col,把卷积展开成一个大矩阵乘,优点是统一了计算内核,缺点是会额外产生数据拷贝,消耗带宽。第二种是“隐式Im2col”,也就是不让数据真的变成矩阵,而是在DMA搬运和地址计算时按卷积窗口的坐标去取数,计算内核仍然是矩阵乘,但避免了物理拷贝。

隐式Im2col是现在主流编译器喜欢的方式。它会为每个输入像素计算好“它应该出现在哪次乘加里”,然后用地址偏移去描述。这个描述看起来复杂,实际上就是一组仿射变换,编译器可以静态算出来。这也是为什么NPU编译器里有大量循环嵌套生成的代码,其实都是在做坐标映射。

卷积里另一个烦人的东西是Padding。如果输入特征图边缘要补零,编译器有两种选择:一种是在DMA搬运时直接把零补齐,搬进来就是完整块;另一种是在SRAM里临时清零。两者都有代价,最怕的是每个Tile边缘都有Padding,导致大量重复搬运。处理这类问题没什么魔法,就是控制好Tile边界,让Padding区域尽量落在同一批搬运范围内。

4.3 编译器本质上在做数据流图重构

从上层看,神经网络模型是一个计算图:节点是算子,边是Tensor。但NPU不认计算图,它只认“数据搬进Buffer,算一下,搬到下一块”。所以编译器干的活是把计算图重写成“图调度”:每个算子必须被切分成多个Tile,每个Tile挂上DMA任务和计算任务,任务之间再按依赖关系排好顺序。

这个过程中最容易被忽略的是同步机制。两个任务之间如果共享同一个Buffer,就必须插入依赖和屏障,否则后一个任务会把前一个任务还没用完的数据覆盖。很多NPU暴露给软件的同步原语是一组事件ID或者Barrier,编译器负责为每个任务分配事件号,并在正确位置WAIT。判断一个编译器成熟不成熟,就看它生成的同步是不是足够稀疏:同步太密会拖慢流水线,太多空等会让DMA和MAC互相卡死;同步太稀又是错的,数据不一致。

另外,编译器还要决定哪些算子可以“融进”前一个算子的计算任务里。比如ReLU、偏置加法、残差相加,这些逐元素操作如果独立成一个任务,就必须把中间结果写回DRAM再读一遍,浪费两个搬移周期。理想做法是把它们直接挂在MAC阵列的输出流水线上,计算完矩阵乘直接做激活,再决定存不存。这种融合优化对有效带宽的影响极大,是NPU编译器里收益最明显的一项工作。

5. 实战排查:为什么NPU跑不满

5.1 常见问题的速查表

在实际项目里摸爬滚打,你会发现NPU跑不出预期性能,问题通常不是芯片坏了,而是下面几类情况。整理成一张表,遇到问题可以逐条对照。

现象大概率原因排查思路
算力利用率不到50%MAC阵列经常空等数据检查DMA预取深度,看看有没有双缓冲;计算等待搬运的周期占比
整模型延迟正常,但单层时快时慢片上Buffer容量不足导致Tile切分过细打印每个算子的Tile尺寸,看是否有大量“不足一个Tile”的尾部块
有效带宽远低于硬件标称带宽访问地址跨度过大,DRAM Bank冲突调整数据布局,把按照NCHW排列的特征图换成编译器期望的NHWC排列
向量单元占用过高,MAC阵列空闲激活、LayerNorm等逐元素算子比重大检查算子融合,优先把激活融进卷积结算流程
同一模型跑INT8比FP16快不了多少模型里混有大量非INT8友好算子对算子做量化回退分析,找出量化敏感层并优化或混合精度
延迟波动明显片外内存访问和其他子系统抢占带宽给NPU访问通道分配更高优先级,或者使用独立内存通道

这里要特别提一个高频问题:只看“理论瓶颈”不看“实际访存模式”。很多人拿带宽公式算完觉得瓶颈不在内存,结果一测还是慢。原因是公式里的计算强度是理论上的“理想复用次数”,但编译器切分Tile时可能把复用切丢了。比如一个很大的卷积核,如果Tile的通道方向取得太小,权重就只能被读N次而不是被复用足够多次。遇到这种情况,别急着改芯片配置,先看编译器日志里每个Tile的“读入字节数/有效运算次数”,通常一眼就能看出问题。

5.2 一条我自己沿用很久的排查步骤

先说观点:性能问题优先怀疑访存,而不是算力。因为芯片厂商给算力数字时都很诚实,但“把数据送进计算单元”这件事,它们通常只字不提。

我的排查顺序是这样的:

第一步,拿到一个算子,先手工估算计算强度,再对照芯片带宽,算出理论最优延迟,这是天花板。

第二步,用芯片厂商工具或仿真器拿到每层的实际运行时间,和理论最优延迟对比。差3倍以内,说明调度还不错;差10倍以上,不用看别处,访存规划一定有问题。

第三步,看每一层的DMA搬运量和搬运次数。如果DMA搬了理论最少数据量的3倍以上,逐元素中间结果的回写或Padding重复搬运大概率是元凶。这时候去查算子融合和Tile边界,改动收益最快。

第四步,如果访存没问题,看MAC阵列利用率。阵列利用率低,往往是任务粒度过小。每个任务的启动开销远大于实际计算时间,导致计算单元频繁启停。这种情况的解法是把连续多个小算子合并成一个任务,或者增加串行执行的批次数。

这套流程看着简单,但几乎每次都能在最合理的层面发现问题。很多团队一上来就怀疑芯片设计有问题,其实大部分时候是被编译器生成的搬运代码坑了。

6. NPU本质是“把数据流动写死”的计算机

体系结构课上讲的通用处理器,核心思想是“指令流驱动数据流”:程序在哪、数据在哪,一切由指令控制。NPU把这个思路颠倒过来,它更像“数据流驱动计算”:数据按预定的路径流动,计算单元在正确的时间窗口被触发。所以判断一个硬件是不是真NPU,不看你标了多少TOPS,而是看它是不是围绕“数据复用”来设计存储和调度。

设计NPU和设计CPU有个完全不同的地方:CPU追求对任意代码的通用性,所以它的流水线深度、分支预测器、Cache替换策略都要处理“最坏情况”;NPU追求的是对一小类计算模式的极致效率,所以它愿意把大量芯片面积花在SRAM和定向数据通路上。一个通用计算芯片不会为“连续几千次乘加”专门铺一大片阵列,NPU会,因为这就是它的全部价值。

这也决定了NPU的编程方式和CPU完全不同。程序员写CPU代码时,关心指令、分支、内存地址;写NPU代码时,关心Tile大小、Buffer分配、DMA双缓冲、同步屏障。前者是“控制流”的思维,后者是“数据流”的思维。两种思维能自由切换的人,才是体系结构里最难培养的能力。

至少对我个人而言,做完一个NPU相关的项目后最大的体会是:不要迷信峰值算力,不要迷信TOPS,更不要一上来就买大板子。先把“数据从片外到片内要走多久、能在片上复用几次、最终有效算力是多少”这三个问题算清楚,比什么优化技巧都管用。很多所谓的性能问题,根本不是优化技巧能解决的,是设计阶段就走错了方向。如果你刚开始接触NPU,也建议先拿一组真实模型做一遍理论带宽估算,再去看任何厂商的SDK文档,那样你会少走很多弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 11:50:55

Smartbi报表设置全指南:从数据源到权限发布的完整配置链路

简介&#xff1a;这份Smartbi报表设置文档面向使用Smartbi进行报表开发的数据分析与报表人员&#xff0c;聚焦从模板引用到预览发布的完整设置流程&#xff0c;适合需要快速上手或查漏补缺的初中级使用者。资源包内含1个docx文档&#xff0c;压缩包约1.05MB&#xff0c;以图文步…

作者头像 李华
网站建设 2026/10/13 11:50:17

MinIO生产级对象存储实践:S3兼容与私有化部署核心指南

前阵子有个做SaaS的朋友跟我聊起他们内部的文件存储方案&#xff0c;说现在所有业务都跑在云上&#xff0c;但就是有个存储细节让他非常纠结&#xff1a;图片、附件、导出报表这一类的数据越来越多&#xff0c;按量付费的存储成本一路飙升&#xff0c;而且上云之后跨区域数据合…

作者头像 李华
网站建设 2026/10/13 11:49:29

基于小波变换与卡伦堡变换的输电线路行波测距Simulink仿真

故障测距这个方向&#xff0c;我前前后后做了近两个月的仿真&#xff0c;最深的感触是&#xff1a;理论公式看起来不难&#xff0c;一上Simulink全是细节。这次要分享的项目是基于小波分解与重构、卡伦堡变换的故障线路识别和行波测距研究&#xff0c;整套内容包括Simulink仿真…

作者头像 李华
网站建设 2026/10/13 11:49:11

Python字典及字典基本操作方法详解

前言 字典&#xff08;dict&#xff09;提供了一批内置方法&#xff0c;名字都短&#xff0c;但行为差别不小&#xff1a;get 和 setdefault 都能「给个默认值」&#xff0c;可一个只读不写、一个会写回去&#xff1b;pop 和 popitem 都删东西&#xff0c;但一个删你指定的键、…

作者头像 李华
网站建设 2026/10/13 11:47:52

人工智能日报:五条工程信号与长上下文评测、端侧成本实操

做AI日报这段时间&#xff0c;我发现一件很有意思的事&#xff1a;越到2026年&#xff0c;行业里每天能拿出来说的“大新闻”反而越难写了。不是说AI没进展&#xff0c;而是大多数进展的形状太相似——模型更大、上下文更长、评测分数更高&#xff0c;但让一线工程师真正手痒的…

作者头像 李华
网站建设 2026/10/13 11:46:47

三合一效率神器:剪贴板历史+全局搜索+快速笔记实测

最近我在某个代码托管平台上的工具合集里翻到一个效率神器&#xff0c;用下来真的只想说一句话&#xff1a;太流批了&#xff0c;神仙工具&#xff0c;值得收藏。具体名字我就不念了&#xff0c;公开页面生成一长串英文字母&#xff0c;我叫它“小拾”。它的核心工作是把三件每…

作者头像 李华