1. 从项目名说起:为什么 AI 芯片的设计绕不开“软硬件一起做”
这两年只要聊到 AI 算力,基本绕不开芯片。但很多人一听到“AI 芯片软硬件设计”这个标题,脑子里第一反应可能是“不就是设计一颗芯片吗?”,实际上远没这么简单。真正把一颗 AI 芯片从概念推到量产,软件和硬件的耦合程度高到超乎想象——这不是简单的“硬件造出来、软件去适配”,而是从架构定义的第一天开始,两边的人就得坐在一起,把算法、编译器、驱动、硬件流水线全部对齐。
我参与过几款边缘侧 AI 芯片的完整设计流程,坦白讲,最难的从来不是某个加速器模块本身,而是“这颗芯片到底要跑什么样的模型、以什么样的精度跑、软件栈能不能把硬件吃透”。换句话说,硬件只是骨架,真正让芯片有灵魂的是软件工具链。这个项目标题里的“软硬件设计”五个字,本质上点出了一种协同设计的思路:HW/SW Co-Design。
展开之前先说明一下,这篇文章覆盖的主要内容包括:
- AI 芯片设计的前期需求拆解:搞清楚要服务的模型类型、算子构成、精度需求、功耗约束;
- 硬件架构的关键模块:MAC 阵列、片上存储、数据通路、低功耗策略,这些模块为什么长这样;
- 软件栈的核心构成:编译器、算子库、运行时 runtime、量化工具、仿真器;
- 软硬件协同的具体工作流:从模型到仿真的完整设计链条怎么打通;
- 真实工程中容易踩的坑:性能仿真误差、精度回调、总线瓶颈、验证遗漏等。
适合的读者:芯片设计工程师、AI 算法工程师、做边缘计算平台的同学,以及想入门 AI 芯片方向的在校研究生。有算法背景但不懂硬件的人也能读,我会尽量把复杂概念讲得像搭积木一样清楚。
2. 软硬件协同设计:核心思路与设计空间探索
2.1 为什么纯硬件思维做不出好 AI 芯片
先讲一个我自己观察到的现象:很多团队做 AI 芯片,一上来就喜欢堆算力,把 MAC(乘累加)阵列做得巨大无比,INT8 可以跑到几十 TOPS,数字上很漂亮。但等芯片回片以后,跑一个 MobileNet 或 YOLO 系列模型,帧率死活达不到预期。问题出在哪?访存瓶颈。
AI 推理有个很核心的特点:算力密度高,但数据复用性差异大。卷积操作虽然可以在行、列方向上做输入或权重复用,但每一次从 DRAM 搬运大量数据都要消耗能量和时间。业界有个经常引用的数据:一次 DRAM 访问的能量开销,可能是 MAC 运算的几个数量级。如果片上 SRAM 容量不足、存调策略不合理,哪怕算力再强,也会被“喂不饱数据”卡住。
所以真正的设计起点,不是“我要做一颗 100 TOPS 的芯片”,而是先问:这个芯片的典型工作负载是什么?面向边缘侧还是数据中心?要跑多少路视频流?模型更新的频率是什么样子?语言模型、卷积模型还是 Transformer 结构占主导?这些问题决定了 MAC 阵列的规模、片上存储的层次、甚至外部存储接口的带宽。
顺带提一句,现在很多设计团队开始在主控 CPU 之外加入专用的 AI 加速子系统,比如常见的CPU + NPU或CPU + DSP + NPU的异构架构。NPU 承担重计算任务,CPU 负责调度和通用逻辑控制,两边通过共享内存或高速总线通信。这既保证了灵活性,又能在能效比上拉开差距。
2.2 架构定义不是拍脑袋:设计空间探索怎么做
软硬件协同设计的第一步,是把算法模型的算子级特征梳理出来。常规做法是这样的:
跑一组代表性的模型,打开 profiler(比如 PyTorch 的性能分析工具),统计各算子的耗时占比、访存量、内存占用、并行度。得到一张算子分布表,比如:
- 卷积/全连接类算子可能占 70% 左右的计算量;
- 激活函数、池化、Normalization 等元素级算子,计算量不大但数量多;
- Transformer 结构中还会有大量的矩阵乘、Softmax、LayerNorm、多头注意力里的 reshape / transpose 变换。
得到这些数据后,团队会做一轮架构探索,通常用一个高层的性能模型(performance model)来做。它的核心逻辑是:把硬件参数(如 MAC 数量、频率、SRAM 容量、DMA 带宽)和算法特征(算子类型、张量形状、重复调用次数)建模,估计每一层的耗时和能效。通过改参数、跑反馈、再调整的方式,架构师才能确定具体实现规模。
这个性能模型有点像“搭积木之前先画堆叠图”——不用等 RTL 写出来,就知道哪些配置可能合理。常见架构探索维度包括:
- MAC 阵列的规模和排布方式,通常有
1D、2D脉动阵列、N*N二维栅格; - 数据精度组合:
FP32、FP16、BF16、INT8、混合精度; - 片上存储容量分配:是偏向输入激活缓存,还是偏向权重复用;
- 数据流方式:
权值固定(Weight Stationary)、输入固定(Input Stationary)、输出固定(Output Stationary)。
这些名词看起来有点多,实际理解也不复杂。权值固定类似“把一摞菜谱放在案板上反复翻看”,输入固定则是“一块肉反复换不同工具处理”,它们影响的是在计算过程中把数据放在哪里、什么数据被复用,从而决定访存开销。选哪种数据流,跟模型算子的 shape 有很大关系。
2.3 一个可行的“软硬件接口”分层
软硬件设计的另一个核心工作是定义“软硬件切分点”。切分点太高,硬件能做加速的范围小,软件负担重;切分点太低,硬件灵活性差,模型迭代受限。目前业界主流做法,是把指令分成三级:
- 任务级指令:面向整个网络层的最小调度粒度,软件把某一层的执行参数装进寄存器;
- 微码/微指令:控制 MAC 阵列内部的数据搬移和运算节奏;
- DMA 配置:管理数据在 DRAM 和 SRAM 之间的搬运。
从软件侧看,编译器负责把模型转换成一个“指令流”,驱动与 runtime 负责管理硬件资源、提交任务、同步中断和轮询状态。这种分级方式的好处是:底层保持高效调度,上层仍保留灵活性——增加新算子时,不需要改动底层数据通路,只需要在编译器、运行时和算子库层面添加支持。
提示:软硬件接口一旦冻结,后续改动成本极高。架构阶段多花一两个月去打磨指令集和内存模型,比后期 RTL freeze 后打补丁要划算得多。
3. 硬件核心模块拆解:从算子到底层电路,中间隔了多少层
3.1 算力核心:MAC 阵列是怎么组织的
先说最核心的 MAC(乘加)运算阵列。一个 MAC 单元完成一次乘法和一次加法,对应卷积神经网络的乘累加操作。INT8 精度下,一个 MAC 单元不算复杂,但阵列一扩大,数据排布、连接方式、计算调度的复杂度会成倍增长。
具体到实际硬件,阵列的组织通常围绕脉动阵列(Systolic Array)或二维卷积核对齐架构。脉动阵列的特点是数据在阵列里“像水流一样”逐级流动,每个 PE 只和相邻 PE 通信,局部性极好,但也对数据进入阵列的时序有严格要求。以 TPU 为代表的脉动阵列,权重预载到 PE 内部寄存器,输入激活一个 cycle 一个 cycle 地从左侧流入,然后部分和向下累加,最后在底部输出。这种结构让“数据的移动消耗”降到最低,代价是数据排布需要编译器精心处理。
相比之下,更灵活的二维 MAC 阵列会把多个输入数据广播到多行 PE,每个 PE 的权重存储在自己局部的 SRAM 里。这样一来,不同的 kernel 大小、stride 都可以直接映射,但控制逻辑更复杂。
设计 MAC 阵列的关键指标之一是算子映射效率。假设芯片理论算力是20 TOPS,跑 3x3 卷积时如果能跑到 80% 利用率,就是 16 TOPS 的实际算力;但如果某个算子因为 padding 或 stride 导致很多 PE 空转,算力可能会掉到 50% 以下。所以主频和 MAC 数量固然重要,调度器的好坏才是决定真实性能的分水岭。
3.2 数据搬运:DMA 与片上存储层次的设计原则
AI 芯片的片上存储,一般会按层次组织,类似电脑里L1 Cache → L2 Cache → 主存的概念,但更强调“可预测”——AI 任务不能动不动 cache miss,因为一次 miss 就会导致计算阵列暂停。常见做法:
- 紧贴 MAC 阵列的是 PE 内部的寄存器文件,用于保存权重和部分和;
- 上一层是共享 SRAM / TCM(Tightly Coupled Memory),通常几百 KB 到几 MB;
- 再往上是 L2 或直接接 DRAM / LPDDR。
DMA 引擎负责在 DRAM 和 SRAM 之间做数据搬运,设计时主要考虑连续传输效率、二维/三维搬运模式(比如一次搬一个完整 feature map 的某个 tile)、以及多通道并行的 QoS 控制。好的 DMA 设计可以做到“计算和数据搬运完全重叠”,也就是后台搬运下一层数据的同时,当前层还在计算,让几乎 100% 的时间都在干活。
在跑大分辨率输入(比如 4K 视频帧检测)时,单层网络可能要处理非常大的 feature map,片上内存根本放不下,这时就得做 tiling:把一张大图切块,分批送入计算阵列。这个切块策略也是编译器要解决的问题,需要根据存储容量、算子类型、输出对齐要求选出一个较优的块尺寸。硬件侧如果能支持任意尺寸的 tile,编译器就灵活得多,但地址生成逻辑和维护复杂度都会上升。
3.3 低功耗设计:AI 芯片掉电速度比你想得快
边缘场景对功耗极度敏感。像智能摄像头、穿戴设备、机器人,动不动就是“5W、2W、甚至 mW 级”的限制。AI 芯片一旦长时间满负荷跑,热点和功耗墙立刻就会出现。所以在架构设计阶段,就要考虑动态电压频率调节(DVFS)、时钟门控(Clock Gating)、算力单元的电源域划分。
一个很实用的做法是:把不同深度学习任务的“算力需求档位”量化,然后设计多个计算集群或功能岛。比如一个芯片可以同时支持小算力的背景任务和大算力的主任务,小任务只用低功耗小核,大任务时才唤醒大核。另一个常规操作是精度模式的动态切换——处理简单场景时用 INT8,低光/复杂场景自动切成 FP16,能效比能差出一倍以上。
这类低功耗策略在实际落地时非常依赖软件配合。runtime 需要根据当前负载动态配置硬件状态,驱动要能支持运行时的算子切换。所以低功耗设计问题绝不是 IC 后端工程师单独收尾的事,而是软硬件协同层面必须提前约定的协议。
4. 软件栈设计与编译优化:真正决定“用不用得起来”的部分
4.1 AI 芯片的软件栈到底长什么样
在我接触过的不少项目中,芯片流片后最大的风险,其实是软件工具链的成熟度。一颗新的 AI 芯片,如果给算法工程师的只有一份非常简陋的算子库,他们根本没法把自己的模型迁移过来。软件栈一般可以分成五层:
- 上层:对接 PyTorch / ONNX / TensorFlow 等模型格式的解析协议;
- 中间层:图优化与算子融合,负责计算图重写、内存布局转换、算子拆并;
- 编译层:把优化后的图转为硬件后端指令/文件;
- 运行时层:任务提交、调度、内存管理、与驱动交互;
- 驱动层:直接管理硬件寄存器、中断、DMA、电源状态。
每一层之间都有严格的 API 约定。比如通常的做法是 ONNX 作为模型中间表示,后端编译器读入 ONNX 后做一遍graph optimization,包括算子融合,比如把Conv + BN + ReLU合并成一个融合算子;把多个小 GEMM 合并成大矩阵乘;把连续的 reshape、transpose 在数据排布层面吃掉,避免真实搬运。
融合之后才是“指令生成”阶段。编译器要把每个算子的逻辑展开成硬件的指令流,决定数据放在哪一级存储、什么时候发起 DMA、MAC 阵列如何被编程。这个环节做得好不好,直接影响算力利用率。
4.2 算子映射与调度:从一张计算图到可执行指令
算法工程师看到的是计算图,硬件工程师看到的是资源调度表。编译器在其中扮演“翻译官”。
以一次3x3 卷积为例,硬件执行时重点要解决三件事:
- Input tile如何从 DRAM 载入 SRAM,如何做 padding;
- Weight tile如何预装载到 PE 阵列;
- Partial sum如何累加、输出。
编译器会生成一个详细的loop nest,把卷积层的循环结构映射到三个维度:MMA 指令内部的循环、PE 阵列的空间维度、时间轴上的分块调度。这还不是最难的,更难的是处理像Transformer里的Softmax和LayerNorm这类算子——它们不依赖大算力,却对精确度和内存访问模式有特殊要求。很多芯片对这些算子没有专门的硬件加速单元,软件只能把它们拆成通用计算指令来执行,这时就要尽量让它们和其他算子流水重叠,避免拖慢关键路径。
实操中常用cost model来做调度决策。每个算子在硬件上的执行时间被建模成“计算时间、访存时间、指令发射时间”的函数,调度器通过贪心策略或动态规划,尽量使前一个算子的尾和后一个算子的头在时间轴上重叠起来,利用率自然就提上去了。
注意:在评估编译器优化效果时,不要只看单个算子的耗时,要看端到端(end-to-end)性能。单纯追求某个卷积算子跑得极快,却让后面的数据搬运堵死,整体性能并不会好。
4.3 量化工具链:模型压缩与精度校准的实战经验
现在绝大多数边缘 AI 芯片都支持 INT8 计算,个别支持 INT4。量化工具链的任务,是把浮点模型转换成定点模型,同时损失尽量少的精度。
常用的量化方案有:
- 训练后量化(PTQ):简单粗暴,不用重训练,适合快速部署;
- 量化感知训练(QAT):在训练过程里模拟量化误差,精度更好,但要改动训练流程。
PTQ 又分为两种,一种是不需要标定数据的盲量化,直接用 min/max 统计范围,另一种是带标定集的最小化 KL 散度或均方误差校准。工程上绝大多数情况都会用带几百张到几千张图片的标定集来做校准,目的是找到每个张量的最大绝对值范围,然后算出 scale 和 zero point。
量化最大的坑是不同 channel 之间数据范围差异极大。某些卷积层的权重分布很集中,但个别 channel 稀疏或数值很小,如果做 per-tensor 量化,很小的数值会被彻底“压没”,精度大幅恶化。所以现在的工具链基本都支持 per-channel 或 per-group 量化,让每个输出通道都拥有独立的 scale。
在我目前的工作流程里,一般会准备三套测试集来验证量化后的精度:一组纯干净数据(比如 ImageNet 验证集子集)、一组含噪声/对抗样本的数据、一组实际场景采集的脏数据。三组结果对比以后,才决定这个量化配置能上车。
5. 实测拆解:从模型到芯片仿真的完整设计链路
5.1 硬件开发流程中的 RTL 与性能验证
从架构定稿到可以写代码,硬件团队通常要经历:
SystemC或高级综合(HLS)层做快速性能仿真;- 写 RTL,用定向测试和随机测试在仿真器里验证功能;
- 搭建 FPGA 原型平台,跑真实模型,验证软件栈和硬件的配合;
- 正式流片前做门级仿真,检查时序约束和功耗。
这个流程中最耗时的,往往是“性能仿真”和“RTL 仿真”之间的迭代。芯片计算阵列的一个 cycle 行为,在系统级仿真里可能被简化,但 RTL 一旦做出来,整个信号细节都定了,许多之前被忽略的pipeline stall(流水线停顿)问题会浮现出来。比如 DMA 和计算单元争用同一块 SRAM 的端口,导致每搬运几十行数据就停顿几个 cycle,端到端性能直接掉 15%。这类问题,在上层性能模型里很难提前看清,大多数靠经验提前规避。
所以现在很多团队在架构探索阶段,会直接构建cycle approximate的仿真模型——不做完整 RTL 但又能较精确地估算流水线冲突。这类模型虽然开发起来也费人力,但相比“流片了才发现慢”,性价比高出天际。
5.2 一个“最小可行性”的软硬件协同开发实例
假设手头要设计一颗只跑 YOLO 系列目标检测模型的芯片,硬件团队和软件团队的工作可以编排成这样:
需求对齐期(约 2 周):确定支持的模型(YOLOv5s、YOLOv8n),确定输入分辨率(640x640 或 1280x1280),帧率目标(比如 30 FPS),功耗约束(边缘盒子场景 10W 以下),精度要求(INT8 量化后 mAP 掉点不超过 2%)。
架构探索期(约 1 个月):性能模型里试各种 MAC 阵列尺寸和频率组合。假设使用4K MAC、主频1 GHz,理论算力就是4K * 2 * 1G = 8 TOPS。然后用性能模型跑 YOLO 的计算图,发现实际利用率只能到 55% 左右,也就是大约 4.4 TOPS,帧率大约是 25 FPS。离 30 FPS 差一点,于是把 MAC 加到5.6K,或者把部分大层切成 INT8 和 INT4 混合精度,最终达标。
软件工具链开发期(与 RTL 并行):编译器先基于指令集模拟器开发,确保能生成指令并跑通算法。这一步非常关键——等 RTL 仿真环境好了再开发编译器,整个项目至少晚三个月。
硬件验证与软件联调期:FPGA 原型上加载编译器和 runtime,跑实际 YOLO 模型。大概率第一次跑通时会有精度问题或性能偏差,但因为有仿真层和工具链层可调试,定位起来相对快。
从这份清单可以看到,软硬件协同不是一个线性过程,而是并行的、多角色深度咬合的流程。架构师的沟通能力甚至比某项专门技术能力更重要。
5.3 指令集模拟器的搭建迭代方法
指令集模拟器(ISS,Instruction Set Simulator)是埋在软件工具链里最不起眼但最关键的一块。它负责在拿到 RTL 之前,先让编译器团队有地方跑指令。模拟器的抽象层次可以设计成三种:
- 仅功能:不关心 cycle,只验证指令语义是否正确;
- 周期估算:每个指令给个粗略 cycle 数,快速跑性能趋势;
- 周期精确:完整模拟流水线和存储冲突,结果可以当作 RTL 参考。
工程上比较合理的是同时保留前两个,第三个视时间为可选项。因为周期精确模拟的运行速度可能比真实硬件慢几个数量级,YOLO 这种模型跑一遍要很久,不适合日常迭代。日常用功能模拟验证正确性,性能都用性能模型解决。
我在工具链实现里很看重一件事——日志的可追溯性。给每条指令分配一个instruction trace ID,硬件仿真和 ISS 的 log 能做逐条对比。这样一旦联调时出现指令对不上,立刻可以通过 diff 脚本定位到是哪一层编译引入的偏差,而不是拿 OLED 显示的几个数字在那边干瞪眼。
6. 软硬件联调的常见问题和排查技巧
6.1 精度不对:到底是算法问题还是硬件问题
AI 芯片回片或上 FPGA 平台后,最容易出现的问题就是“输出结果和预期不一致”。我总结的经验是,先按下面顺序排查:
- 跑一个最简单的算子,比如单一卷积层,输入固定随机数,和 PyTorch / CPU 参考结果比对。这一步判断硬件计算本身有没有 bug;
- 跑完整模型,在几个中间层插入 dump 点,拿到每一层的数值输出,逐层对比,定位是哪一层开始“歪”的;
- 检查量化配置:是不是某个 scale 溢出,某个 per-channel 配置没生效;
- 检查算子融合后的算法等价性:比如
BN 层融合进卷积的公式变换是否准确,浮点到定点的近似是否引入误差。
绝大多数精度问题都是量化配置错误或编译器模式匹配错误,真正纯硬件 bug 导致的反而比较少。
6.2 性能比预期慢:先分清是“算力没吃满”还是“访存卡顿”
我遇到很多性能达不到预期的情况,往往只有一个大写的原因——计算和搬运在互相等待。判别方法也容易:
在硬件上统计计数器寄存器的值,例如:
busy_cycle:MAC 阵列处于计算状态的循环数;stall_cycle:MAC 阵列等待数据的循环数;dma_idle_cycle:DMA 无任务可做的循环数。
如果busy_cycle / total_cycle已经接近 80%,那问题很可能出在算子映射效率上,比如某些层的维度没法填充整个 MAC 阵列,需要对编译器里的 shape 拆分规则做调整。如果只有 50%,但stall_cycle很高,那就要看是 SRAM 容量不足导致 tile 切得太碎,还是总线带宽不够导致 DMA 传不过来。
访存类瓶颈在工程上有个快速见效的解法:把连续层的 feature map 存在片上,不要频繁写回 DRAM。很多编译器默认每算完一层就写回,其实可以支持“多级保留”,让数据直接在片上从一个算子的输出成为下一个算子的输入。
6.3 部署现场偶发卡死:稳定性问题排查思路
芯片在实验室跑得好好的,到了客户现场偶发卡死,这类问题最磨人。常见原因有几类:时钟或复位的问题、内存访问越界、驱动中断处理漏掉某种边界条件、电源供电在大负载切换时出现压降。
经验做法是:
- 在 runtime 里加入看门狗机制,超时自动复位;
- 把每次任务的配置寄存器(或配置的 hash)记录下来,复现时能精确回放当时场景;
- 优先排查“长时间运行后才会浮现”的问题,多半是内存碎片化或静态 RAM 内容残留。
注意:如果能回放问题,就先把功耗切换和 DVFS 状态变化的变量全部关掉再做 A/B 对比。很多偶发卡顿都跟电压/频率切换时的时序有关系。
6.4 实战速查表:软硬件联调排障清单
| 现象 | 优先排查方向 | 常用手段 |
|---|---|---|
| 单个算子输出不符 | 硬件逻辑、量化配置 | 固定随机数比对,逐层 dump |
| 整体精度掉点多 | 量化策略、算子融合等价性 | 校准集扫描、per-channel 检查 |
| 性能差且利用率低 | 算子映射、指令流水 | 计算/暂停周期统计,调整调度 |
| 性能差但利用率还行 | 访存带宽、tile 切分 | 检查 DMA 配置、增大片上复用 |
| 偶发死机或卡死 | 复位/时钟/电源/DVFS | 看门狗、配置回放、关闭动态调频 |
这类表格放团队里当文档参考,比口头传承要稳得多。我有一个习惯:每次排查到问题,都会把根因和修复方式补到一张持续更新的“坑位文档”里。这既方便团队协作,也是后续做下一代芯片架构决策时最宝贵的素材。
7. 个人实操体会:如果重新做一遍项目,我会怎么调整
软硬件协同设计听起来很“正统”,但真正的行业经验往往是在混乱中积累出来的。我最大的体会是:别把软硬件协同当成一个阶段,而应该把它当成一种日常工作方式。
架构阶段就让编译器团队参与,哪怕只是提前看看哪些算子不好映射;算法团队在做模型选型时,也应该拿到硬件性能模型的反馈——有些看起来网络结构很精妙的模型,硬件加速器根本跑不出效率,倒不如选一个结构规整但精度稍逊的模型,实际效果反而好。
还有一个容易忽视的问题是文档和接口版本的控制。指令集、寄存器映射、编译器的 IR 格式,任何一处变更都要同步到所有团队,否则就会出现“编译器已经生成新指令,但驱动还按旧寄存器表配置硬件”的低级问题。建议从项目第一天就维护一份软硬件接口规格书,把它当合同一样管理,变更必须走评审流程。
另外,性能仿真的精度不用追求“绝对准确”,但一定要能捕捉“趋势”。如果在架构探索阶段连“加大 MAC 数量是否带来收益”都判断不了,那后续所有决策都是空中楼阁。一个好用的性能模型不一定很复杂,但要足够反映真实的调度行为和访存冲突。
这个项目做到后期,我越来越意识到:技术能力往往是团队里最容易补齐的部分,最难的是架构决策时,各团队能不能放弃局部最优、去争取全局最优。硬件团队不坚持多堆一堆算力,软件团队不执着于让某个算子的效率刷到极致,算法团队愿意为硬件姿势调整模型结构,这种平衡才是 AI 芯片软硬件设计真正值得花时间琢磨的地方。