1. 案例1

案例1:AI芯片基础与算子映射 学习卡片

任务说明

本案例面向AI芯片入门者,目标是通过一个可运行的最小示例,理解AI芯片“计算单元—存储—控制”的基本架构,并掌握将卷积(Conv2D)与全连接(FC)算子映射到硬件执行的基本流程。你将得到一个简化的硬件抽象模型(模拟AI芯片的MAC阵列与累加器),并完成算子到硬件的映射与调度,最终验证映射结果与参考实现的数值一致性。

AI芯片基础架构抽象

为便于在代码中呈现,我们将AI芯片抽象为三个核心部件:

  • MAC阵列:由 \( R \) 行 \( C \) 列乘累加单元组成,每周期可完成 \( R \times C \) 次乘加运算;
  • 片上存储(SRAM/寄存器堆):存放输入特征图、权重与输出累加中间值;
  • 控制单元:负责指令译码、数据搬运调度与循环控制。

算子映射的本质是将算子计算图中的循环嵌套(如卷积的 \( H \times W \times C_{in} \times C_{out} \) 循环)转换为MAC阵列可执行的指令序列,并决定数据在存储层级间的搬运时机。本案例聚焦于“循环分块(tiling)”与“数据复用”两个核心映射决策。

自洽实现:算子到硬件的映射模拟器

以下代码实现了一个最小但完整的映射模拟器。它接收一个卷积层或全连接层的参数,将其内部循环按MAC阵列尺寸分块,生成调度指令序列,并模拟执行得到输出。核心逻辑包括:

  1. 将卷积展开为隐式矩阵乘(im2col思路,但为教学演示直接按滑动窗口取数);
  2. 按MAC阵列行/列维度对输出通道与输入通道分块;
  3. 生成每个分块的MAC指令(含输入/权重地址与累加控制);
  4. 模拟执行并返回输出张量。
import numpy as np

class AIAccelerator:
    """简化AI芯片模拟器:MAC阵列 + 累加器 + 控制单元"""
    def __init__(self, mac_rows=4, mac_cols=4):
        self.R = mac_rows   # MAC阵列行数(输出通道分块)
        self.C = mac_cols   # MAC阵列列数(输入通道分块)
        self.accum = None   # 累存器

    def conv2d_mapping(self, x, weight, bias=None, stride=1, pad=0):
        """
        将Conv2D映射到MAC阵列执行
        x: [N, Cin, Hin, Win], weight: [Cout, Cin, KH, KW]
        返回: [N, Cout, Hout, Wout]
        """
        N, Cin, Hin, Win = x.shape
        Cout, _, KH, KW = weight.shape
        Hout = (Hin + 2*pad - KH) // stride + 1
        Wout = (Win + 2*pad - KW) // stride + 1

        # pad输入
        xp = np.pad(x, ((0,0),(0,0),(pad,pad),(pad,pad)), mode='constant')
        out = np.zeros((N, Cout, Hout, Wout))

        # 控制单元:按输出通道分块(行方向),输入通道分块(列方向)
        for co_start in range(0, Cout, self.R):
            co_end = min(co_start + self.R, Cout)
            for ci_start in range(0, Cin, self.C):
                ci_end = min(ci_start + self.C, Cin)
                # 生成并执行MAC指令块
                for n in range(N):
                    for ho in range(Hout):
                        for wo in range(Wout):
                            # 累加器清零(新输出像素)
                            acc = np.zeros(co_end - co_start)
                            for ci in range(ci_start, ci_end):
                                for kh in range(KH):
                                    for kw in range(KW):
                                        # 取输入像素(广播到当前输出通道块)
                                        in_val = xp[n, ci, ho*stride+kh, wo*stride+kw]
                                        # MAC阵列:每个输出通道一个乘累加单元
                                        for i, co in enumerate(range(co_start, co_end)):
                                            acc[i] += in_val * weight[co, ci, kh, kw]
                            # 写回输出
                            for i, co in enumerate(range(co_start, co_end)):
                                out[n, co, ho, wo] += acc[i]

        if bias is not None:
            out += bias.reshape(1, -1, 1, 1)
        return out

    def fc_mapping(self, x, weight, bias=None):
        """
        全连接映射:将FC视为1x1卷积或直接矩阵乘
        x: [N, Cin], weight: [Cout, Cin]
        返回: [N, Cout]
        """
        N, Cin = x.shape
        Cout = weight.shape[0]
        out = np.zeros((N, Cout))
        # 按输出通道分块
        for co_start in range(0, Cout, self.R):
            co_end = min(co_start + self.R, Cout)
            for n in range(N):
                acc = np.zeros(co_end - co_start)
                for ci in range(Cin):
                    in_val = x[n, ci]
                    for i, co in enumerate(range(co_start, co_end)):
                        acc[i] += in_val * weight[co, ci]
                out[n, co_start:co_end] = acc
        if bias is not None:
            out += bias.reshape(1, -1)
        return out

# ---------- 验证用例 ----------
if __name__ == "__main__":
    np.random.seed(0)
    acc = AIAccelerator(mac_rows=4, mac_cols=4)

    # 卷积映射验证
    x = np.random.randn(1, 3, 8, 8).astype(np.float32)
    w = np.random.randn(6, 3, 3, 3).astype(np.float32)
    b = np.random.randn(6).astype(np.float32)
    out_hw = acc.conv2d_mapping(x, w, b, stride=1, pad=1)

    # 参考实现(直接卷积)
    def conv_ref(x, w, b, stride=1, pad=1):
        N, Cin, Hin, Win = x.shape
        Cout, _, KH, KW = w.shape
        Hout = (Hin + 2*pad - KH)//stride + 1
        Wout = (Win + 2*pad - KW)//stride + 1
        xp = np.pad(x, ((0,0),(0,0),(pad,pad),(pad,pad)))
        out = np.zeros((N, Cout, Hout, Wout))
        for n in range(N):
            for co in range(Cout):
                for ho in range(Hout):
                    for wo in range(Wout):
                        s = 0
                        for ci in range(Cin):
                            for kh in range(KH):
                                for kw in range(KW):
                                    s += xp[n,ci,ho*stride+kh,wo*stride+kw]*w[co,ci,kh,kw]
                        out[n,co,ho,wo] = s + b[co]
        return out

    ref = conv_ref(x, w, b)
    diff_conv = np.max(np.abs(out_hw - ref))
    print(f"卷积映射最大误差: {diff_conv:.2e}")

    # 全连接映射验证
    x_fc = np.random.randn(2, 10).astype(np.float32)
    w_fc = np.random.randn(5, 10).astype(np.float32)
    b_fc = np.random.randn(5).astype(np.float32)
    out_fc = acc.fc_mapping(x_fc, w_fc, b_fc)
    ref_fc = x_fc @ w_fc.T + b_fc
    diff_fc = np.max(np.abs(out_fc - ref_fc))
    print(f"全连接映射最大误差: {diff_fc:.2e}")

落地与验证

运行方式:将上述代码保存为 mapping_sim.py,执行 python mapping_sim.py。需要NumPy环境(pip install numpy)。

验证判定标准:程序输出两行误差值。若卷积与全连接的最大误差均小于 1e-5,则说明算子到MAC阵列的映射逻辑正确,数值与参考实现一致。该误差仅来源于浮点累加顺序差异,并非实现错误。

AI芯片算子映射流程(Conv2D示例) 输入特征图 [N, Cin, Hin, Win] 例: [1, 3, 8, 8] 权重 [Cout, Cin, KH, KW] 例: [6, 3, 3, 3] 控制单元 循环分块 (Tiling) 输出通道块: Cout/4 输入通道块: Cin/4 生成MAC指令序列 调度数据搬运 累加器清零/写回 MAC阵列 (4×4) 每周期16次乘加 输出特征图 [N, Cout, Hout, Wout] 验证:与参考实现对比 最大误差 < 1e-5 即通过 数据 权重 指令 分块策略: Cout分块(行) × Cin分块(列)

映射要点说明

上述代码中,conv2d_mapping 展示了卷积算子映射的核心决策:外层循环按输出通道(MAC阵列行数)和输入通道(MAC阵列列数)分块,内层循环完成每个输出像素的滑窗乘累加。这种分块方式使得同一输入像素可被多个输出通道复用(输入复用),同一权重也可在多个空间位置复用(权重复用),是AI芯片设计中减少数据搬运的关键手段。fc_mapping 则展示了全连接层可视为卷积的特例(1×1卷积或直接矩阵乘),映射逻辑更简单,仅需按输出通道分块即可。

通过调整 mac_rows 与 mac_cols 参数,可以观察不同MAC阵列尺寸对分块粒度的影响——阵列越大,单次可并行处理的通道数越多,但片上存储压力也越大,这正是实际AI芯片设计中的面积-带宽折中。