
1. AI芯片软硬件协同设计的核心逻辑1.1 为什么单靠硬件堆料已经走不通了做AI芯片这行十来年我最大的感受就是算力数字的军备竞赛早就不是胜负手了。前几年大家比谁家的TOPS高现在你去看真正落地跑Transformer推理的板子标称算力和实际吞吐之间经常差着三五倍。问题出在哪出在软硬件之间的那道墙。Transformer架构跟早期的CNN有个本质区别CNN的卷积核在空间维度上做局部滑窗数据复用率高权值可以常驻在片上缓存里反复用而Transformer的核心是自注意力机制它要做的是Q、K、V三个矩阵的乘法序列长度直接决定了中间激活值的大小。你序列长度拉到2048甚至4096中间那个注意力矩阵就是N×N的规模对片上存储和带宽的压力是指数级上升的。所以做AI芯片的软硬件设计第一件事不是画架构图而是把模型的计算图吃透。你得知道哪些算子是计算密集型的哪些是访存密集型的哪些可以融合哪些必须拆开。这个判断直接决定了你后面脉动阵列怎么摆、缓存怎么分级、数据流怎么调度。1.2 脉动阵列为什么成了矩阵乘法的标配脉动阵列这个概念不新鲜上世纪80年代就有了但真正在AI芯片里大放异彩是Google TPU带起来的。它的核心思想很朴素让数据像心跳一样有节奏地流过计算单元阵列每个PE只做乘加算完就把部分和往下一个PE推。为什么这个结构适合Transformer里的矩阵乘法因为矩阵乘法的本质是规约运算——C[i][j] Σ A[i][k] × B[k][j]。脉动阵列把K维度的累加拆到时间轴上每个周期完成一次乘加并传递部分和这样就不需要每个周期都去访问外部存储读累加结果。对于Transformer里动辄几百维的矩阵乘这个设计能把访存带宽需求压下来一个数量级。但脉动阵列不是万能的。它的效率高度依赖于矩阵形状和阵列尺寸的匹配度。你搞一个128×128的阵列结果模型里全是64×64的小矩阵乘那利用率直接掉到25%。所以实际设计里要么做多尺寸可重构的阵列要么在编译器层面把多个小矩阵拼成大矩阵来喂满阵列。这就是软硬件协同的典型场景——硬件给你一个能力边界软件在这个边界内做最优调度。1.3 软硬件协同到底协同什么很多人把软硬件协同理解成“硬件做完了软件适配一下”这是大错特错。真正的协同是从模型定义阶段就开始的联合优化。具体来说协同发生在三个层面算子层面硬件支持哪些算子原语软件就要把模型映射到这些原语上。比如硬件如果只支持INT8的矩阵乘那软件就得做量化感知训练或者训练后量化把FP32的权重和激活值压到INT8同时保证精度损失可控。数据流层面硬件的缓存层级和带宽决定了数据怎么分块、怎么复用。软件编译器要根据硬件的SRAM大小和DMA引擎的搬运能力把计算图切分成合适的tile让数据在片上多停留、少搬运。调度层面硬件的并行度和流水线深度决定了指令怎么排。软件要做的就是把矩阵乘、LayerNorm、Softmax这些操作编排成一条高效的指令流让计算单元尽量不空转。这三个层面里数据流层面的协同是最容易被忽视也最影响实际性能的。我见过太多项目硬件理论算力很高但因为数据搬运没设计好实际跑起来DMA成了瓶颈计算单元一半时间在等数据。2. Transformer在AI芯片上的映射与优化2.1 自注意力机制的计算特征拆解要把Transformer映射到AI芯片上先得把自注意力的计算过程拆开看。以标准的Scaled Dot-Product Attention为例Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V这里面包含几个关键步骤QK^T矩阵乘形状是[N, d_k] × [d_k, N] → [N, N]计算量是N²×d_k缩放和Softmax对[N, N]的矩阵做行方向的归一化注意力加权形状是[N, N] × [N, d_v] → [N, d_v]计算量是N²×d_v当序列长度N512、d_k64时QK^T的计算量是512×512×64≈1670万次乘加。而一个典型的FFN层计算量是N×d_model×d_ff×2假设d_model512、d_ff2048那就是512×512×2048×2≈10.7亿次乘加。FFN才是计算量大头注意力矩阵乘的计算量相对小但访存压力大。这个特征决定了硬件设计上的取舍矩阵乘单元要能高效处理FFN里的大矩阵同时缓存系统要能扛住注意力矩阵的中间结果。很多芯片设计时把注意力矩阵整个放在片上SRAM里N512时一个FP16的[N, N]矩阵就是512KB加上Q、K、V的缓存片上存储没有个几MB根本不够用。2.2 矩阵乘在脉动阵列上的分块策略脉动阵列处理矩阵乘核心问题是怎么分块。假设你有一个128×128的脉动阵列要计算一个[512, 512] × [512, 2048]的矩阵乘你不能一次性把整个矩阵塞进去必须分块。常见的分块策略有三种输出 stationary每个PE固定负责输出矩阵的一个元素输入矩阵的行和列轮流流过。这种策略适合输出矩阵较小的情况因为每个PE要缓存部分和。权重 stationary权重矩阵固定在PE里激活值流过。这种策略适合权重可以复用的场景比如batch size较大的推理。行 stationary输入矩阵的行固定在PE里权重矩阵的列流过。这种策略在Transformer的注意力计算里比较常见因为Q矩阵的行可以复用。实际设计里往往是混合策略。比如Google TPU用的是权重stationary的变体把权重预加载到PE阵列里然后激活值从左侧流入、部分和从下方流出。而一些做边缘推理的芯片因为batch size通常为1权重stationary的优势不明显反而会用输出stationary来减少部分和的搬运。分块大小的选择也有讲究。块太小数据复用率低DMA搬运次数多块太大片上缓存放不下得频繁换入换出。我一般会按这个公式估算块大小 sqrt(片上SRAM容量 / (3 × 数据类型字节数))比如片上SRAM有2MB数据类型是FP162字节那块大小大概是sqrt(2MB / 6)≈590取个整就是512。这个估算假设Q、K、V三个矩阵都要缓存实际还要根据具体的数据流调整。2.3 从计算图到指令流的编译过程模型映射的最后一步是编译。这一步是把高层框架PyTorch、TensorFlow定义的模型翻译成芯片能执行的指令流。这个过程通常分几个阶段图优化阶段做算子融合、常量折叠、死代码消除。比如把LayerNorm和后面的矩阵乘融合成一个算子减少中间结果的写回。这个阶段跟硬件无关但融合策略会影响后续的调度。算子映射阶段把优化后的计算图里的每个算子映射到硬件支持的原语上。比如一个矩阵乘算子映射成“加载权重→加载激活→执行脉动阵列→写回结果”这样一组指令。这个阶段需要硬件的指令集支持通常由芯片厂商提供。调度与内存分配阶段决定指令的执行顺序和中间结果的存放位置。这个阶段最复杂要同时考虑计算单元的利用率、DMA的带宽、片上缓存的容量。一个好的调度器能把端到端延迟降低30%以上。代码生成阶段把调度结果翻译成具体的机器指令包括DMA描述符、PE阵列配置、同步信号等。整个编译流程里调度与内存分配是软硬件协同最密集的地方。硬件提供的能力阵列尺寸、缓存大小、DMA通道数是约束条件软件要在这个约束下找到最优解。这也是为什么很多AI芯片公司把编译器团队和硬件团队放在一起办公——编译器的人要知道硬件的微架构细节硬件的人要理解编译器的调度需求。3. 实操从零搭建一个Transformer推理的软硬件仿真环境3.1 环境准备与工具选型要验证一个AI芯片的软硬件设计是否合理最直接的办法是搭一个仿真环境把Transformer模型跑起来看性能瓶颈在哪。这里我分享一套我自己常用的仿真方案基于Python和SystemC的混合仿真。工具选型上我推荐这几个Python NumPy做算法层面的建模和验证快速验证矩阵乘的分块策略和数值精度。SystemC TLM-2.0做硬件架构的事务级建模模拟DMA、缓存、计算单元的行为。Verilator如果需要更精确的周期级仿真可以用Verilator把RTL代码编译成C模型跟SystemC联合仿真。PyTorch作为参考模型验证仿真结果的数值正确性。这套组合的好处是开发效率高。事务级建模不需要写RTL用C就能描述硬件行为仿真速度比RTL快几个数量级。对于架构探索阶段的软硬件协同设计这个速度足够支撑快速迭代。环境搭建的具体步骤# 创建Python虚拟环境 python -m venv ai_chip_sim source ai_chip_sim/bin/activate # 安装Python依赖 pip install numpy torch matplotlib # 安装SystemC以Ubuntu为例 sudo apt-get install libsystemc-dev # 安装Verilator sudo apt-get install verilator3.2 脉动阵列的行为级建模脉动阵列的行为级模型核心是模拟PE阵列的乘加和传递行为。下面是一个简化版的Python模型模拟128×128的权重stationary脉动阵列import numpy as np class SystolicArray: def __init__(self, rows128, cols128): self.rows rows self.cols cols self.weights np.zeros((rows, cols), dtypenp.float16) self.accumulators np.zeros((rows, cols), dtypenp.float32) def load_weights(self, weight_matrix): 加载权重矩阵到PE阵列 assert weight_matrix.shape (self.rows, self.cols) self.weights weight_matrix.astype(np.float16) def compute(self, activation_vector): 执行一次矩阵乘activation_vector × weights assert activation_vector.shape[0] self.rows # 模拟脉动阵列的乘加行为 # 每个PE做一次乘加部分和沿列方向传递 result np.zeros(self.cols, dtypenp.float32) for i in range(self.rows): for j in range(self.cols): result[j] activation_vector[i] * self.weights[i, j] return result def reset_accumulators(self): self.accumulators np.zeros((self.rows, self.cols), dtypenp.float32)这个模型虽然简单但能帮你快速验证分块策略。比如你可以把一个大矩阵乘拆成多个128×128的块依次喂给这个阵列看总的周期数和数据搬运量。3.3 Transformer推理的端到端仿真有了脉动阵列的模型接下来把Transformer的推理过程串起来。下面是一个简化版的单头注意力推理仿真def scaled_dot_product_attention(Q, K, V, systolic_array): 在脉动阵列上模拟自注意力计算 seq_len, d_k Q.shape # 第一步QK^T # 分块计算每次处理128行 scores np.zeros((seq_len, seq_len), dtypenp.float32) for i in range(0, seq_len, 128): for j in range(0, seq_len, 128): q_block Q[i:i128, :] k_block K[j:j128, :] # 把K的转置加载到脉动阵列 systolic_array.load_weights(k_block.T) for row in range(q_block.shape[0]): scores[irow, j:j128] systolic_array.compute(q_block[row]) # 第二步缩放和Softmax scores scores / np.sqrt(d_k) attention_weights softmax(scores, axis-1) # 第三步注意力加权 output np.zeros((seq_len, d_k), dtypenp.float32) for i in range(0, seq_len, 128): for j in range(0, seq_len, 128): attn_block attention_weights[i:i128, j:j128] v_block V[j:j128, :] systolic_array.load_weights(v_block) for row in range(attn_block.shape[0]): output[irow] systolic_array.compute(attn_block[row]) return output def softmax(x, axis-1): x_max np.max(x, axisaxis, keepdimsTrue) exp_x np.exp(x - x_max) return exp_x / np.sum(exp_x, axisaxis, keepdimsTrue)这个仿真跑起来之后你可以统计每个阶段的周期数和数据搬运量。我实测下来QK^T和注意力加权的数据搬运量占了总搬运量的70%以上而计算量只占30%左右。这个结果直接指导了硬件设计要么增大片上缓存要么优化DMA的搬运效率。3.4 性能瓶颈的定位与优化仿真跑通之后下一步是定位瓶颈。我一般会从三个维度看计算利用率实际执行的乘加次数除以理论峰值乘加次数。如果这个值低于60%说明计算单元在等数据瓶颈在访存。DMA带宽利用率实际搬运的数据量除以理论最大搬运量。如果这个值接近100%说明DMA是瓶颈需要增加DMA通道或者优化数据复用。片上缓存命中率从片上缓存读取的数据量除以总读取量。如果这个值低于80%说明缓存太小或者分块策略不合理。针对Transformer推理我总结了几条优化经验Q、K、V的缓存要分开管理。Q在计算QK^T时被反复读取K和V在后续步骤被读取它们的生命周期不同混在一起管理会导致缓存颠簸。Softmax的结果可以原地覆盖QK^T的结果。Softmax是逐行归一化输入和输出的形状一样不需要额外的缓存空间。FFN层的权重可以预加载到脉动阵列里。FFN的权重在推理过程中不变预加载后可以省去每次计算的权重搬运时间。4. 常见问题与排查技巧实录4.1 数值精度问题为什么仿真结果和PyTorch对不上这是最常见的问题。仿真跑出来的结果跟PyTorch参考模型一比误差大得离谱。原因通常有几个累加精度不够。脉动阵列里的部分和如果只用FP16累加几百次乘加之后误差会累积到不可接受。解决办法是部分和用FP32累加最后再转回FP16。这个改动在硬件上就是给每个PE的累加器多分配一倍位宽面积代价不大但精度提升明显。Softmax的数值稳定性。直接对FP16的分数做exp很容易溢出。标准做法是先减去最大值再exp但硬件实现时减最大值这个操作需要额外的比较和减法电路。如果硬件不支持可以在软件层面做分数缩放把分数压到一个安全的范围内。量化误差。如果硬件只支持INT8那权重量化带来的误差可能比浮点累加误差还大。这时候需要做量化感知训练在训练阶段就模拟量化误差让模型自己去适应。排查这类问题的技巧逐层对比。不要一上来就比最终输出先把每一层的输出跟PyTorch对齐找到误差开始变大的那一层然后针对性地分析。4.2 性能不达预期计算单元为什么在空转仿真跑出来的周期数比理论值高出一大截计算单元利用率只有30%多。这种情况我遇到过好几次原因基本逃不出这几个DMA搬运和计算没有重叠。很多仿真模型是串行的先搬数据再计算再搬结果。实际硬件里DMA和计算应该是流水线并行的。解决办法是在仿真里引入双缓冲机制一块缓存在被计算单元读取时另一块缓存同时在接收DMA搬运的数据。分块大小跟阵列尺寸不匹配。比如阵列是128×128但分块是100×100那每次计算有28行28列是浪费的。解决办法是分块大小取阵列尺寸的整数倍或者做阵列的可重构设计。同步开销太大。每次矩阵乘结束后都要等所有PE完成才能开始下一次这个同步等待时间在仿真里可能被放大了。实际硬件里可以用异步流水线让不同的PE组独立工作减少全局同步。下面这个表格是我总结的常见性能问题速查表现象可能原因排查方法解决思路计算利用率低于50%DMA瓶颈统计DMA搬运周期占比增加DMA通道、优化数据复用计算利用率低于50%缓存命中率低统计片上缓存读写次数增大缓存、调整分块策略端到端延迟高同步开销大统计同步等待周期异步流水线、减少全局同步数值误差大累加精度不够逐层对比参考模型部分和用FP32累加数值误差大量化误差对比量化前后输出量化感知训练、混合精度4.3 缓存不够用片上存储怎么省着花Transformer推理对片上存储的需求很大尤其是注意力矩阵。N1024时一个FP16的注意力矩阵就是2MB加上Q、K、V的缓存没有个8MB根本不够。但片上SRAM面积贵不可能无限加。这时候就得想办法省。注意力矩阵分块计算。不要一次性算完整个N×N的注意力矩阵而是分成多个块算完一块就做Softmax然后跟V相乘结果累加到输出上。这样只需要缓存一个块大小的注意力矩阵存储需求从O(N²)降到O(N×block_size)。K和V的缓存复用。在自回归生成比如GPT推理里K和V是逐token生成的已经生成的K和V可以缓存起来下一个token只需要计算新的Q跟缓存的K做注意力。这就是KV Cache机制能把生成阶段的存储需求从O(N²)降到O(N)。权重的压缩存储。FFN层的权重矩阵很大但很多权重接近零。可以用稀疏化或者低秩分解来压缩权重减少存储需求。不过这个需要模型训练阶段就做约束不是推理阶段能临时抱佛脚的。4.4 实操心得几个让我少走弯路的习惯做了这么多年AI芯片的软硬件设计有几个习惯我觉得特别值钱先建数值模型再建性能模型。不要一上来就写SystemC先用NumPy把算法跑通确认数值正确性。数值模型跑通了再往上加周期和带宽的约束逐步逼近真实硬件行为。这样出问题的时候容易定位——是算法错了还是硬件模型错了。仿真日志要记全。每次仿真的配置、输入、输出、周期数、带宽占用全部记下来。我习惯用CSV格式存方便后面做对比分析。很多时候优化效果不明显翻之前的日志才发现是某个参数没调对。瓶颈定位要量化。不要说“感觉DMA是瓶颈”要统计出DMA搬运周期占总周期的百分比。数据说话才能说服团队里的硬件工程师去改设计。保持跟PyTorch的交叉验证。每次改完硬件模型或者调度策略都要跟PyTorch的输出对一遍。精度掉了一点可能不影响最终结果但掉多了肯定有问题。我一般设一个阈值比如相对误差超过1%就报警。最后再分享一个小技巧用Transformer预测正弦数据来验证仿真环境。这个任务足够简单输入输出都是已知的但包含了完整的注意力计算和FFN计算。如果仿真环境能正确预测正弦数据说明数值精度和计算流程都没问题。然后再上真实的语言模型逐步增加复杂度。这个渐进式的验证方法帮我省了很多调试时间。