做AI芯片的软硬件设计,这六个字背后牵扯的东西远比你想象的多。我最早接触这个方向时,有个很直观的感受:芯片流片回来,如果没有一套能跑的软件栈,它就是一块发热的石头。硬件能算,软件调不通,性能连三成都发挥不出来。很多团队花大力气把MAC阵列堆得很猛,最后却发现瓶颈在数据搬运上;也有团队算法模型都调好了,换到自研芯片上精度直接崩掉,抠了很久才发现是数据格式的约定出了偏差。
这片文章更适合正在做AI加速器、嵌入式AI、或者准备从算法工程师转向芯片相关岗位的朋友看。我会从架构选型、算子映射、编译链、仿真验证这些环节完整梳理一遍,重点讲清楚软硬件是怎么接口的、怎么协同的,以及我们在实际项目中踩过哪些坑。
1. 整体设计与思路拆解
1.1 AI芯片不是“硬件先行”,而是“软硬件一起长”
以前做传统SoC,一般流程是先把硬件架构定下来,寄存器手册写清楚,然后软件团队照着适配。少则几个月多则半年就能衔接。但AI芯片完全走不了这条路。
神经网络结构迭代太快,硬件不可能等模型固定再设计。你今天针对某个CNN结构做了硬编码优化,明天出一个新的注意力模块又废了。反过来也一样,软件栈不能等芯片流片回来再写,因为编译器、量化工具、算子库的调试周期不比硬件短,等芯片回来再开始,产品窗口早过了。
所以实际做法是软硬件协同设计(hardware/software co-design)。在定义指令集和算力架构的同时,软件团队就要在模拟器或者建模环境里同步开发编译器后端、运行时和算子库。硬件和软件共用一份“接口契约”,比如指令编码、数据格式、内存布局、中断与状态寄存器定义,然后各自开发,定期对齐。
我打个比方。家有装修,你得先定插座位置、水管走向,再让木工泥工进场。如果先按“感觉”把墙砌好再改水电,代价就是砸墙。AI芯片的软硬件接口就是那些埋在墙里的管线,前期不规划好,后面每一版驱动和编译pass都可能要返工。
1.2 不要把第一颗芯片做成“万能加速器”
新手团队最容易犯的错,是一开始就想支持所有模型。Transformer要支持、CNN要支持、LSTM要支持、目前的量化要做到INT4还要兼容未来的INT2。需求摊得越平,架构就越复杂,最后做出来的东西又贵又慢,谁都不满意。
好的做法是先做需求收敛。问三个问题:
- 模型部署在哪类场景?摄像头端侧、边缘盒子、还是数据中心?
- 目标时延和吞吐量是多少?单帧图像要几十毫秒还是几百毫秒?
- 功耗墙在哪?电池供电和插电设备的架构取舍完全不同。
比如边缘摄像头场景,模型可能是YOLO类检测网络,输入640x640,单帧推理时延要求小于50ms,整板功耗小于5W。这个约束下的MAC规模、SRAM容量、内存带宽都能算出来。比如支持云端训练又是另一套思路,需要高带宽HBM、FP32/BF16高精度、大规模并行单元,功耗和成本都不是优先项。
确定场景之后,才真正进入架构选型。
1.3 NPU不是唯一解,但它是AI芯片设计的核心战场
AI芯片硬件路线不外乎几类:通用GPU、FPGA、固定功能NPU、可重构架构。
通用GPU适合云端,编程模型成熟,但功耗和成本极高。FPGA适合快速验证和小批量原型,但量产成本和能效都不占优势。可重构计算听起来很美好,实际工具链复杂度极高,团队没有五年积累做不稳。固定功能NPU是边缘端最务实的选择:算力效率高、确定性时延、容易满足功耗约束。
但NPU的代价是灵活性差。一旦模型结构超出硬件支持的算子模式,性能就会断崖式下跌。
这时候就要在硬件里加入“逃逸通道”。比如NPU处理不了的算子,可以通过软件切分成更小粒度来执行,或者回退到内置CPU上跑。这也是为什么现代AI芯片基本都是异构架构:CPU + NPU + 加速器外设(编解码等)。CPU负责控制流和杂项算子,NPU负责重计算负载。
设计重心一定在NPU上,因为NPU的每一点变化,都要编译器、运行时和驱动跟着变。可以把NPU比喻成一只“只能做填空题的速算天才”,CPU就是旁边的“教务主任”,负责出题、批改和安排监考。天才再厉害,也得有人给它排课和分卷子。
1.4 算力便宜,带宽昂贵
做AI芯片一条核心原则:数据搬运是成本大头,算法是次要的。一个MAC单元做一次乘加,大概是几个皮焦耳;但到外部DDR去读一个数,能耗要高出几十倍。所以软硬件设计的核心不是“怎么算得快”,而是“怎么让数据在片上被尽可能多地复用”。
比如卷积运算,同一个输入像素会被多个输出通道用到,同一份权重会被同一张特征图的多个位置重复使用。硬件设计里专门做数据复用,比如脉动阵列(systolic array)或者广播式数据流,让数据沿着某个方向横向流动,不需要从存储反复取。软件侧对应的就是数据布局转换和循环分块,把数据切成适合硬件复用的维度再发送。
这条原则贯穿了AI芯片设计从架构推导到算子映射再到编译器优化的每一个环节。
2. 核心细节解析与实操要点
2.1 硬件侧三大块:算力阵列、存储层次、片上互联
算力阵列的核心是MAC单元。一个MAC每周期完成一次乘加运算,以INT8为例,N个MAC阵列在频率F下能提供的算力大约是2×N×F的OP/s。注意这里的2是因为一个乘加算两次操作(乘法和加法)。通常厂商标注的TOPS指标,就是按这个公式来的。
阵列形态上,经典选择是脉动阵列(systolic array)。每个PE只和相邻PE通信,数据像水一样在阵列里流过,功耗低、布线规整、扩展性好。但脉动阵列对数据布局非常挑剔。软件必须把数据按特定方向排列好,否则利用率上不去。另一种是SIMD式阵列,每个MAC都能从某个片上buffer读数,灵活性高一些,但布线压力大,频率不会太高。没有绝对优劣,关键看软件能不能处理好。
存储层次上,外部LPDDR/DDR带宽有限,片上SRAM又快又贵。设计上通常分两层:全局共享SRAM和局部buffer。全局SRAM用于暂存中间特征图和模型权重,局部buffer则是当前运算周期内直接供MAC阵列读取的小块内存。经典的双缓冲(double buffering)就是一组buffer在算,另一组buffer在由DMA搬运,交替切换,把搬运和计算重叠起来。
片上互联的选择主要有AXI总线结构或者更自由的Network-on-Chip(NoC)。小规模设计用AXI足够,复杂多核场景就得考虑NoC避免总线竞争。
2.2 软件侧四件套:编译器、算子库、Runtime、工具链
软件栈是整个AI芯片的灵魂。拆开来看,四层各司其职:
编译器负责把PyTorch/TensorFlow/ONNX模型翻译成加速器指令。这层要做图优化(算子融合、常量折叠、死节点消除)、数据布局转换(NHWC、NCHW、NCHW16等)、以及面向特定硬件的循环分块(tiling)。算子融合很关键。比如Conv+BN+ReLU三段融合成一个算子,省掉中间张量的写回和读取,能省下大量DDR带宽。
算子库是为常用算子提供手工优化版本。很多情况下编译器自动生成的代码仍然不如人工调优的版本,所以算子库充当“高产模块”的角色。工程上可以维护一套极简算子库,覆盖卷积、矩阵乘、激活、池化、归一化、软硬件接口的基础算子。
Runtime负责模型加载、内存分配、任务调度、驱动接口。比如把模型权重加载到SRAM的分配策略,是在Runtime层决定的。Runtime还要把编译产物排成硬件能顺序执行的作业,处理等待或事件。硬件完成计算后,中断或者轮询状态寄存器来感知完成事件。
工具链包含编译驱动、模拟器、性能剖析器和调试工具。模拟器尤其重要,因为硬件流片之前,软件研发全靠它。性能剖析器帮你看某个算子到底卡在计算瓶颈还是带宽瓶颈,这是调优的主要线索。
2.3 算子映射:软硬件交联的微观世界
拿3x3卷积举例,操作主要分三段:输入数据搬运、权重广播、乘累加。硬件上收到的指令通常是:从外部DDR读一块特征图到SRAM,再从SRAM读到一个局部buffer;权重同理;然后让MAC阵列以脉动方式做乘累加。
决定映射效率的核心在于数据调度策略。一个是输入特征图的滑窗复用。卷积核在输入平面上滑动时,相邻位置有大量重叠。如果硬件支持横跨滑窗方向的数据复用,搬运量就减半以上。另一个是权重复用:同一个权重会被同一输出通道的所有位置使用,权重应该尽量留在片上。再一个是输出累积器:每个Psum累加循环内只留在MAC内部,不写回存储,直道所有输入通道都累加完毕。
下面给一个很粗糙的伪代码,展示编译器后端如何把一层Conv调度到硬件上:
// 假设已经完成tiling,feature在SRAM,weights在SRAM // 外层循环:输入通道块 for tile_in_ch in input_channels: // 针对每个输出的行块 for tile_out_h in output_height: // 加载输入块: load in_feat from SRAM to buf_in dma_load(buf_in, in_feat[..., tile_in_ch]) // 加载权重块: load weights from SRAM to buf_w dma_load(buf_w, weights[..., tile_in_ch]) // 脉动计算: PE array executes compute mac_array_exec(psum, buf_in, buf_w) // 内层循环结束,把psum写回DRAM/SRAM dma_store(out_feat, psum)这个调度背后最重要的事情是循环顺序的选择。如果通道维度在外层,权重要被多次搬入;如果输出块太小收益率低。编译器要综合SRAM容量、总线和阵列延时,选择最优的tile大小和循环顺序。这往往靠启发式搜索或者cost model。
2.4 精度与数据格式是软硬件之间最重要的“契约”
AI芯片算力指标通常按INT8标注,但实际硬件往往同时支持INT8、FP16、BF16甚至更低位的INT4。选择精度直接影响MAC阵列的实现复杂度、SRAM占用和总线带宽。软件团队如果默认模型运行时用FP16,硬件侧只有INT8高效支持,就可能白白损失一半以上的算力。所以精度格式在所有软硬件接口中需要明确约定。
INT8流程中量化算法是谁的活?软件团队的。他们需要提供校准工具,收集激活值统计信息(min/max、直方图等),选择per-tensor或per-channel的scale和zero_point。硬件再根据这些参数在计算时进行定点/定点累加,或者在写出前做反量化。
浮点格式也有讲究。BF16和FP16都占16bit,但数值范围和精度侧重不同。BF16的指数范围与FP32一致,大数不易溢出,对于训练场景鲁棒性好;FP16精度高但范围窄,容易出现溢出问题。不同公司和产品有不同的选择,但选型定了就是契约,硬件聚变单元、软件算子库都要围绕它实现。
3. 实操过程与核心环节实现
3.1 从需求到算力宏架构:先做一遍数学推导再说
假设我们要做一颗边缘端AI芯片。目标:跑一个7M参数量的检测网络,INT8精度下,单帧延迟小于50ms,功耗控制在5W内。设计初始,先看算力。
该网络的总计算量大约在20GOPs左右,这是可以通过网络结构一步步算出来的,主要是各卷积层的FLOPs累加。目标时延50ms,所以峰值算力需求大约:
算力需求 = 20e9 / 0.05 = 400 GOPS但实际利用率不可能到100%。由于数据搬运、算子切分、控制开销等因素,NPU利用率一般在50%到80%。保守取50%,则峰值算力要:
峰值算力 = 400 / 0.5 = 800 GOPS然后反推MAC阵列。工作频率取1GHz,那么满足800GOPS(即400GMAC/s)的MAC单元数量是:
N_MAC = 峰值的MAC/s / 频率 = 400e9 / 1e9 = 400 MAC取整数倍阵列,比如设计为16x32共512个MAC,那么在1GHz下理论算力约为1TOPS,留了余量。功耗估算按每MAC每次操作约0.3到1皮焦耳计算,INT8为例,512个MAC在1GHz下满载功耗大约1W上下,加上存储和互联,整颗芯片控制在5W内是可行的。
接下来算带宽。如果没有数据复用,每个MAC每周期要读取两个操作数,那么所需带宽会非常吓人:
512 MAC × 1GHz × 2(两个8bit操作数)× 8bit ≈ 8 TB/s任何一颗低成本边缘芯片都不可能直接提供8TB/s的DDR带宽。合理方案是片上SRAM大一点,比如2MB,再利用数据复用把对外部DDR带宽的要求降到实际有效值。拿7M参数的模型来说,如果全部权重都只加载一次,需要的搬运量是7MB×2,分时加载到片上。这个量在256bit位宽的DDR4-2400下,约需几十微秒,是可接受的。
这串推导做不做得出来,直接决定了这个项目能不能进入具体设计。
3.2 最小指令集怎么定:其实软件只需要看懂几条指令
很多团队在设计指令集时喜欢往复杂里做,甚至想做到VLIW那样一条指令多个执行槽位。我的建议是第一版尽量精简,先定义几条基础能通路的指令,跑通一条完整的卷积再说。
指令集最小集合可以这样:
- LOAD_WEIGHT:把权重从DDR搬运到SRAM weight buffer,并等待完成。
- LOAD_INPUT:把输入特征图从DDR或SRAM搬到input buffer。
- COMPUTE_CONV:启动一次卷积计算,包括累积器清零、阵列使能、输出结果写回psum buffer。
- STORE_OUTPUT:把psum buffer的数据经过量化或反量化写回DDR。
这四条指令配合状态机的周期流转已经足够让硬件先动起来。每条指令的编码格式说清楚是很重要的事情。例如:
31 24 23 16 15 0 [opcode] [type] [src/dst addr]对COMPUTE_CONV还需要附加kernel大小、通道数、stride、激活模式这些参数。参数可以放在一条副操作数指令后,也可以预先写到配置寄存器里,状态机在启动计算前读取寄存器组。
软件栈里编译器的后端要做的事情,就是把上层IR里的Conv节点,翻译成这些指令组成一个序列。碰到硬件不支持的算子,就要做“降级”:比如无硬件支持的某种激活函数,编译器会把它拆成CPU执行片段,因此Runtime需要支持CPU和NPU的管线交换。
3.3 用一个端到端demo打通全栈
这一步是从零到一的关键里程碑。选一条极小的网络,比如说一层Conv+BN+ReLU,先跑通验证流程。
第一步,Python侧用PyTorch定义这个网络,导出ONNX模型文件。最好固定输入尺寸,避免动态shape带来的编译复杂性。
第二步,自定义编译器前端读取ONNX,做图优化。将BN吸收进Conv,将ReLU设置为Conv的激活属性,这样得到一张融合后的图。
第三步,编译器后端进行tiling和数据布局转换,再翻译成第二章的指令序列。同时生成二进制权重文件,注意权重在存储上的顺序和硬件读取权重buffer的地址映射保持一致。
第四步,Runtime在模拟器上加载编译产物和权重,执行推理,并打印输出。
第五步,与PyTorch的float32参考输出以及ONNX Runtime的INT8输出进行比对,计算误差。此时目标不是精度完全一致,而是让误差落在可解释范围内,再用误差分析来定位问题。
这个demo全部打通,意味着软硬件的基础框架已经立住了。后续不管是硬件加模块还是软件加优化pass,都能通过回归测试来保护成果。
3.4 硬件仿真与软件回归协同验证的组织方式
芯片没有流片之前,软硬件验证都是靠仿真和FPGA原型。
仿真环境通常用SystemVerilog搭建testbench,再加上内存模型和参考模型。软件团队在模拟器上跑出一个算子的中间结果,转成hex或bin文件,灌进testbench作为激励。testbench里待测设计的输出再和软件参考结果比对。如果有差异,就用波形和软件日志逐级对比,定位是硬件bug还是编译生成bug。
这种验证法有三个注意点:
第一,激励一定要做版本快照。对应软件栈的每次回归,都应该记录当时的编译产物和二进制权重,否则bug复现不了。
第二,对比不只比最终输出,最好在硬件的每个关键观察点都预留trace接口。比如mac阵列输出之前,是否有断点能不能dump中间结果。这对定位精度问题是救命稻草。
第三,FPGA原型的频率往往低于目标芯片,但可以跑真实应用。全栈软件的稳定性测试(长时间跑、反复加载模型、内存泄漏检测)都可以在FPGA上做。
回归测试的组织是整个项目里面最不起眼但最重要的环节。软件每次合入一个新的优化pass,硬件每次修改RTL,都跑同一套测试用例。这套用例要覆盖每个算子类别、特殊shape、精度模式组合、以及典型的端到端模型。没有回归机制,两边的进度会像两辆对向行驶的车,到交汇点才发现问题,然后就撞了。
4. 常见问题与排查技巧实录
4.1 硬件算力明明够,端到端延迟却很高
这是最典型的性能问题。芯片标称1TOPS,但跑一个20GOPs的网络时实际要两三百毫秒而不是理论上的几十毫秒。
大概率的原因,第一是数据搬运没和计算重叠。DMA搬运特征图和权重时,MAC阵列在等数据;计算时DMA又闲着。解决方向是让软件按tiling切成小块,一块在算、另一块在搬,采用多级流水。
第二个常见原因是SRAM分配策略不对。大块特征图被放在外部DDR,每次访问都要走到片外,访问延迟高且带宽受限。应该在Runtime层用“memory planner”对被反复使用的中间张量做生命周期分析,尽可能放到SRAM上。
第三个原因是算子维度切分太小,MAC阵列利用率上不来。可以用profiler统计每个算子的实际利用率和空闲周期比例,看到底是哪一种。
排查技巧是先在模拟器上启用cycle-level统计,找出每个算子的计算周期数和搬运周期数。如果搬运占比明显过高,优先从数据复用和双缓冲下手。
4.2 量化后精度掉点
INT8量化后的精度损失是模型侧还是硬件侧?优先逐层对比浮点中间结果和定点中间结果。
做法通常是二分定位:先让硬件只执行某个特定层,用浮点模拟器输出对比,找到第一个精度突变的层。再看这层是scale算错、截断模式不对、还是溢出。
常见诱因有:calibration的样本数太少,激活分布没估准;per-tensor量化在通道间差异大的层导致误差放大,改成per-channel;某个子层(比如softmax或LayerNorm)在硬件上用了一个近似实现,没有在软件栈里同步。最后一种特别隐蔽,因为编译器文档里可能只写了“支持该算子”,没有写近似精度。
解决思路是让编译器对算子做分类:精确实现、近似实现、CPU回退。近似实现要控制在误差容限内,并且在模型发布报告里明示。
4.3 编译器生成的指令和硬件对不上
这类问题通常发生在硬件指令集版本更新之后。硬件加了某个新特性,在寄存器映射上做了一点调整,但编译器后端或者验证脚本没有同步更新。于是一边信誓旦旦认为地址是对的,另一边改了设计没通知。
更好的工程机制:指令集和寄存器定义的“唯一事实源”应该放在一个共享的YAML/JSON文件里。由这个文件自动生成三份文件:编译器头文件(指令编码)、硬件RTL的宏定义、仿真testbench的解析器。这样任何人改指令集,都必须改同一份文件,然后三端重新生成。版本控制还能追踪谁在什么时候改了什么。
4.4 常见问题速查表
| 现象 | 最可能原因 | 排查建议 |
|---|---|---|
| 算力利用率低于30% | 数据搬运和计算未重叠 | 分析cycle分布,开启双缓冲,调大tile尺寸 |
| 推理精度异常,尤其在深层突崩 | 某层量化scale不准或溢出 | 逐层对比浮点/定点输出,二分法定位突变层 |
| 硬件计算结果和软件模拟器不一致 | 指令编码版本脱节 | 检查共享指令定义文件生成是否最新,拉取版本记录 |
| DMA搬运时间占比居高不下 | 权重/特征图存储在DDR且无复用 | 优化tile大小,增加SRAM命中,算子层做卷积优化 |
| 模型切换后性能差异巨大 | 编译器pass未适配该结构,走CPU回退 | 用profiler查看算子落地类型,对高频新算子做算子库优化 |
| 多模型并发时内存分配冲突 | Runtime内存分配器未做池化 | 引入内存池和生命周期分析,避免重复malloc/free |
我一直保留一个习惯,不管多忙,每周都会把软硬件的契约文档和最新的回归结果过一遍。这是项目里最容易被忽略的管理动作,但正是这个动作帮我避开过很多次被一夜之间推翻设计的危机。AI芯片软硬件设计,说穿了不是某一方单方面做得多强,而是两边配合的精度,几乎决定了产品的真实性能和交付周期。希望这篇笔记能对正在做AI加速器或准备进场的朋友有一些实际参考价值。