以 SincNet Conv1d 为例,串联模型对称性、混合精度、矩阵映射与软件流水,解释如何把模型语义转化为硬件性能收益。
SincNet Conv1d 优化:从模型对称性到矩阵单元利用率
面向读者: 理解 Conv/GEMM,希望知道“除了调整 kernel 参数,还能从哪里找到优化机会”的算子工程师。本文从固定 shape 出发,沿模型契约、精度预算、矩阵映射与片上流水,解释怎样将模型语义转化为硬件收益,并进一步追踪编译器未能利用的并行机会。
核心问题与结论: 一个能够正确执行的通用 Conv1d,为什么会在特定模型上浪费大量计算资源?这个项目先修复 Cin=1 与通用路径的映射错配,再利用滤波器奇偶对称性折叠计算,用三角 HP6 减少高精度矩阵乘项数,并重组数据布局与流水。最终方案在目标 shape 上相对原 HP9 路径获得约 300× 的 kernel 加速。对照手工与自动排布,还定位到隐藏地址状态缺少精确内存操作数、导致预处理组被过宽依赖阻塞的问题。
范围与依据: 本文复盘同一款加速器上、固定 SincNet 第一层工作负载的优化,输入与权重为 FP32,目标是推理专用路径。性能数字来自项目记录,硬件发射限制来自后续实卡 profile 的观察,编译器部分依据预处理阶段的 MIR 与调度依赖诊断;本文整理时没有重新运行实卡实验。芯片单元使用功能名称,不提供公司源码、内部硬件型号、实现及测试版本标识。约 300× 是指定基线和 shape 下的结果。
最初框架提供的是固定 shape 和随机权重。我先沿调用链确认 Conv1d 进入了通用 Conv2d 高精度实现,再分析 shape 为什么没有被有效利用。随后追踪模型来源,确认该层的滤波器结构,并与框架侧约定特化条件。这个过程依次回答三个问题:通用实现在哪浪费,模型允许少算什么,剩下的计算怎样在硬件上连续执行。
1. 从 shape 找到通用路径的浪费
1.1 先把 Conv1d 展开成矩阵问题
目标输入采用 NCL 布局,即 batch、channel、length:
| 对象 | 形状 |
|---|---|
| 输入 | [21, 1, 160000] |
| 权重 | [80, 1, 251] |
| bias | [80] |
| 输出 | [21, 80, 15975] |
卷积参数为 stride=10, padding=0, dilation=1, groups=1。输出长度为:
为避免 batch 大小和矩阵名称混淆,以下用
对
1.2 为什么 Cin=1 会成为问题
原路径沿通用 Conv2d 的通道维度组织 128-wide 计算。在当前 Cin=1 的工作负载中,每个核位置只有一条有效 lane,其余位置被 mask,随后再遍历 251 个核位置。
因此第一步是改变 K 维组织:把通道与核位置合起来,形成长度为 251 的有效向量,让连续核位置填入原本空闲的计算位置。这一阶段不依赖权重对称,随机权重也能验证它。
我首先尝试让 DMA 直接生成 im2col,但该路径的最小 offset 粒度是 32 个元素,不能表达窗口内部逐元素移动。于是把连续搬运与细粒度重排分给不同单元:
1 | 连续原始输入 |
这一步的实卡周期从 338,021,562 降到 28,062,366,约 12.045×。它验证了映射方向,但还没有解决高精度分量的重复处理、矩阵列利用率和流水阶段之间的等待。
2. 从模型契约获得进一步特化
2.1 对称性来自模型,不来自 shape
固定 shape 无法证明权重对称。追踪模型后,这个工作负载对应 ParamSincFB(80,251):前 40 个输出滤波器为偶对称,后 40 个为奇对称。公开实现先拼接偶对称滤波器,再拼接奇对称滤波器;后者的中心权重为零。这是具体滤波器构造的性质,不能推广到任意同 shape 的 Conv1d。ParamSincFB 源码
框架侧显式保证这个契约,kernel 按两组通道处理,不在热路径扫描或猜测权重。非对称权重继续使用通用实现;新增专用入口不等于已经实现了自动检测或自动 fallback。
2.2 偶对称相加,奇对称相减
偶对称滤波器满足
因此定义:
奇对称滤波器满足
奇对称中心项不参与乘积。为了统一表示,可以令
两组结果分别为:
这里的
1 | 同一个原始滑窗 |
镜像相加或相减在 FP32 中完成,再进行后续分量分解。这样可以避免先量化两份输入再折叠带来的额外低精度误差;浮点结果与原始逐项卷积是否满足门限,仍需验证。
3. 精度预算怎样决定矩阵乘次数
矩阵单元擅长 BF16 乘法,但接口输入和精度要求是 FP32。项目采用多分量表示:将每个 FP32 数分解为主值、第一层残差和第二层残差对应的三个 BF16 分量,再用矩阵乘组合它们。此类做法利用低精度乘法与较高精度累加构造更高精度结果,相关原理可参见 Henry 等人的论文。
令两侧的分量分别为
| 乘积项 | |||
|---|---|---|---|
三角 HP6 保留
相比 HP9,少算的是两个三阶项和一个四阶项。偶、奇两组分别应用这一分解与乘积选择。
项目的比较门限为 rtol=1e-5, atol=1e-5。六项方案的依据是保留全部到二阶的乘积,使分解与截断误差在良态输入下进入约
4. 让计算结构匹配硬件
4.1 先说明几个硬件角色
下文只保留影响决策的功能与约束,名称经过通用化处理:
| 部件 | 承担的工作 | 调度时关心什么 |
|---|---|---|
| DMA | 外存与片上存储之间搬运连续数据 | 粒度、启动开销及能否与计算重叠 |
| 片上重排单元 | 生成滑窗、镜像配对与矩阵转置 | 吞吐、输入就绪与发射限制 |
| 向量计算单元 | FP32 折叠、分量生成、打包及结果累加 | 短依赖与寄存器占用 |
| 矩阵输入暂存队列、右操作数寄存器 | 准备并保存矩阵单元即将使用的数据 | 哪份数据何时可覆盖、能被复用多久 |
| MXU(Matrix Multiply Unit) | 矩阵乘与相应累加 | 阵列利用率、操作间发射间隔 |
功能独立不自动意味着任意指令都能并行发射。后面的数据流先按依赖组织,实际能够重叠多少,再由 profile 判断。
4.2 为什么选择转置 GEMM
最初的逻辑矩阵是
可以选择以下三种组织方式;比较时均允许使用前面的奇偶折叠与三角 HP6:
| 方案 | 改善 | 仍需付出的代价 |
|---|---|---|
| 按输出块即时生成输入分量,计算 |
避免完整 im2col 落到外存 | 分量准备和切换容易重复,矩阵列未满 |
| 在片上保留输入分量,仍计算 |
延长分量生命周期,增加复用 | 输出通道维度仍然较窄 |
| 按奇偶组计算 |
把长输出位置维放到矩阵列方向 | 需要相应转置、两组折叠与片上调度 |
最终采用第三种。每个 batch 内,每组输出的逻辑形状为 [40,15975]。输出位置沿 128-wide 列方向分块,40 个通道在当前 8-row 发射粒度下正好是五组。这里描述的是矩阵计算的硬件宽度与发射粒度,不是最终采用的软件 tile 尺寸。
两组分别写回前、后 40 个通道,组合后的 [80,15975] 恰好匹配该 batch 的连续 NCL 输出布局,不需要再把整个输出转置回去。bias 在对应通道的结果上添加。
4.3 把重复准备变成片上复用
权重的三组分量预先生成,并在后续输出位置计算中复用。输入按局部窗口处理:
1 | 连续 raw input → 片上缓冲 → 滑窗及镜像配对 |
图中两路表达数据关系,不要求实现始终同时保留两套完整中间结果。相应 stage 只需持有当前计算和后续准备所需的数据。
在同一奇偶组中,右侧激活分量可以依次供多个权重分量和输出通道块使用。例如
4.4 为什么后来反而缩小 M tile
这里的 M tile 指一次准备的输出位置范围。增大它可以摊薄搬运与控制开销,但也会增加原始窗口、折叠结果和分量缓冲的占用,使能够同时在途的预处理 stage 变少。
HP6 的多个分量乘积,以及同一输入分量跨通道块的使用,增强了已搬入数据的复用。因此,缩小 M tile 后不必立刻退化成“每搬一点数据只做很少计算”。我后续减小 M tile,将省下的片上空间用于更多预处理 stage,让搬运、重排、量化与矩阵计算拥有更多交叠机会。
1 | HP6 分量与通道复用 |
这是一项容量与并行度的取舍。tile 太小也会增加边界、搬运启动和控制开销,可能重复读取相邻滑窗重叠部分。最终看完整 kernel 的周期和 stage 等待,而不能只看单个 tile 的计算密度。这里保留设计原因,不填写尚未确认的前后 tile 尺寸或 stage 数量。
4.5 让 producer 和 consumer 按各自资源推进
producer 负责连续输入的搬运、滑窗重排、折叠和分量准备;consumer 负责矩阵输入装填、六项计算及结果回收。两者通过有明确所有权的片上缓冲交接:producer 写完才能交给 consumer,consumer 最后一次使用完成后才能复用该缓冲。
准备后续输入时,应尽量保留当前矩阵输入的有效生命周期;当前计算执行时,可以推进依赖已经满足的后续准备。多 stage 使这些工作能够交叠,但不消除数据就绪和覆盖顺序要求。
最终方案的组织重点,是配平重排、矩阵输入供给、量化和 MXU 消费的吞吐,并让控制与结果回收融入稳态。增加预处理 stage 提供更多独立工作,能否真正缩短等待,还取决于实际发射限制。
5. 性能结果与理想化结构收益
5.1 先固定比较对象
以下均针对前述固定 shape,与原通用 HP9 路径比较:
| 阶段 | 完整 kernel cycles | 相对原 HP9 |
|---|---|---|
| 通用 HP9 | 338,021,562 | 1× |
| 固定 shape 的紧凑 K 维路径 | 28,062,366 | 12.045× |
| 奇偶折叠、三角 HP6 与最终数据流 | —(仅保留约数加速比) | 约 300× |
最终记录保留的是约 300× 的汇总值。不能用它反算一个看似精确的 cycles 数,再当作实测记录。上述结果描述 kernel 收益,不是完整模型或服务的端到端加速。
5.2 614.4× 是怎样算出来的
若只看原路径中的结构浪费,可以按四个因素做一个理想化估算:
| 因素 | 计算方式 | 理想化收益 |
|---|---|---|
| 通道维只有 1 条有效 lane | 用核位置填充原来闲置的 128-wide 计算位置 | 128× |
| 镜像折叠 | 偶对称 251→126,奇对称 251→125;近似取减半 | 2× |
| 高精度乘积项 | HP9 九项改为三角 HP6 六项 | 9/6=1.5× |
| 矩阵列利用率 | 从约 80/128 提高到接近满列 | 128/80=1.6× |
于是得到:
这里将各因素视为可组合,并假定节省的工作能够转换为有效吞吐。它可作为理想化的结构收益上限参考,不能当作严格的完整 kernel 时间下界,也不是把几个独立实测加速比相乘。两组折叠的准备成本、对齐与尾块、量化、搬运、输出回收和真实发射约束均未被这个乘积完整计入。
因此,约 300× 与 614.4× 的比例也不能解释成硬件利用率。
5.3 为什么实际没有达到这个估算
后续实卡 profile 发现,一代硬件存在额外的 hold-issue 问题:转置单元与 MXU 虽然在这些操作之间没有算法数据依赖,实际发射仍出现了限制,削弱了预期的重叠。这是项目中识别出的主要问题。
这里能够确认的是“存在额外的跨单元发射限制”,尚未细分为单向或双向阻塞,也没有把全部额外周期归因到某一个方向。它与算法上的等待不同:即使下一 stage 的数据已经就绪,硬件仍可能暂时不允许相关指令发射。
此外,尾块、量化、DMA 启动、控制和结果回收也有成本。缩小 M tile 与增加 stage 提供了隐藏等待的机会,但不能突破硬件发射约束;约 300× 是这些因素共同作用后的完整 kernel 结果。
6. 从手工流水追到编译器的依赖建模
手工排布能够交叠多组预处理后,另一个问题是:为什么 LLVM 没有自动排出相同的并行?这条调查关注的是输入分量生成与打包阶段。诊断记录中的“四路”指多个已有预处理组之间的交叠,不是四个硬件核心,也不是最终软件 tile 的尺寸。
6.1 先看节点能否进入候选队列
最初容易怀疑寄存器压力或展开策略:编译器是不是认为两路比四路更划算?但检查源码后发现,这段预处理通过模板在编译期展开,相关指令已经存在;观察到的两个数据流也是源码显式提供的,并非某个循环优化 pass 选择了 factor=2。
因此下一步应检查 MachineScheduler 的依赖图。只有当节点的依赖允许它进入 READY 候选集合,才有必要讨论它是否被收益或寄存器压力启发式放弃。
诊断中,后续组的输入 load 被地址状态设置指令的 Ord Barrier 边挡住,尚未进入可选择范围。寄存器压力确实偏高,但它不是这些节点无法提前 READY 的第一原因。
6.2 NoAlias 已成立,为什么仍不能重排
折叠输入与打包输出已通过别名分析证明是不同存储对象,普通访存之间的假 MayAliasMem 边已经消除。剩下的限制来自一种设置索引访存地址生成状态的指令,下文用功能名 set_addr_state 表示它。
这条指令设置隐藏状态,本身不写折叠输入或打包输出。但后端将它标记为 mayStore=1,最终 MachineInstr 又缺少说明访问对象的 MachineMemOperand(MMO)。在该后端的调度逻辑中,它因此被视为需要全局排序的未知内存操作:
1 | 设置地址生成状态 |
关键是,全局 barrier 的处理发生在相关 AA 查询之前,甚至把已归为 NoAlias 的访存也连接进来。因此继续添加 restrict 并不能消除这条约束:缺失的是隐藏状态的精确建模,而不是两个普通指针之间的独立性证明。这里的 barrier 是编译器依赖图中的排序约束,不意味着程序主动发出了一条全局硬件 fence。
6.3 收窄错误依赖,同时保留真实约束
地址状态不能简单忽略。后续 indexed load/store 要读取它;同一状态被覆盖前,还必须保证旧消费者不会误用新地址。普通访存与索引访存也仍然共享加载/存储执行资源。
诊断提出用 PseudoSourceValue(PSV) 表示地址生成器的隐藏资源,并通过 MMO 将其读写传递到机器指令。功能模型如下,名称仅用于解释:
1 | set_addr_state(n) : 写地址状态 n |
这样可以保留同一状态的设置、使用和覆盖顺序,同时让无关的普通访存与向量计算获得重排空间。
| 约束 | 如何处理 |
|---|---|
| 地址状态设置 → 使用它的 indexed 操作 | 保留真实数据依赖 |
| 旧 indexed 操作 → 同一状态的下一次覆盖 | 保守保留,除非明确建模了状态重命名 |
| 普通与 indexed 访存的执行资源竞争 | 保留在资源模型中 |
| 对实际重叠存储的读写依赖 | 保留真实 alias 约束 |
| 设置地址状态 → 所有无关内存操作的全局排序 | 用精确资源依赖收窄 |
共享一个执行通道,限制的是同周期能发射哪些工作,并不自动要求所有相关指令严格按源码顺序执行。直接清除 mayStore 或按 opcode 删除全部 barrier,会同时抹掉需要保留的约束。
6.4 验证顺序:依赖、调度、正确性、性能
验证这种修复,需要先检查机器指令是否携带地址状态与真实内存的 MMO,再确认调度图中无关的全局边消失、必要的设置/使用/覆盖边仍存在。随后检查同一时刻可见的预处理组、寄存器压力及 spill/reload,最后验证地址访问正确性与完整 kernel 周期。
诊断还发现浮点延迟模型可能偏保守,但这属于另一项校准,应与依赖修复分开做单变量对照。一次修改多个条件再只看总周期,无法判断收益来自哪里。
这份诊断记录止于根因定位与修复建议,不单独提供修复后的收益证明。它建立的可迁移方法是:先区分“还不允许调度”和“已经允许但没有被选中”,再定位限制来自真实数据、硬件资源,还是过宽的编译器模型。
前面的跨单元 hold-issue 是实卡发射限制;本节的全局 barrier 是编译器建模问题。解除后者不会自动消除前者,也不能据此承诺固定倍数的加速。
7. 正确性与扩展边界
7.1 用未折叠卷积检验特化
正确性 reference 应使用完整 251 项的 FP32 Conv1d,包含 bias,并按目标 NCL 输出比较。只用一个折叠实现验证另一个折叠实现,可能让相同的索引或符号错误相互掩盖。
项目记录描述的检查包括镜像 pair、中心项、batch 与块边界、并行计算路径、高精度分量及错误参数,并通过框架的高精度门禁。本文不将整理过程中的公式核对冒充新的实卡验收。
逐元素检查使用 rtol=1e-5, atol=1e-5。若把 CPU 结果作为参考项,对应条件为:
allclose 的相对误差项依赖第二个参数,测试时应固定参数顺序。接近零的输出主要由绝对误差门限约束。PyTorch allclose 文档
特化入口只处理已保证奇偶契约的推理输入。布局、梯度或权重结构不满足条件时,不应静默套用该公式;通用实现的可用性与框架是否自动选择它,是两个不同问题。
7.2 可以复用什么,仍需重新验证什么
对固定的输入通道数、核形状和输出通道数,输出位置增多主要意味着重复更多局部块。只在片上生成当前和相邻 stage 所需的 im2col,可避免完整中间矩阵随输出长度增长。
设单次配置下的活跃 stage 数固定,稳态可以表示为:
活跃片上工作集不必随
这种 producer/consumer 划分也提供了扩展方向:ConvXd 的滑窗坐标生成可放在 producer 中,高精度方案保留哪些分量乘积可交给 consumer 的调度表描述。更换 HP 项数后,消费者吞吐和结果累加压力会变化,必须重新配平。本文不据此宣称任意 ConvXd 已得到相同性能或完成全部正确性验证。
8. 可迁移的方法与未解问题
这个项目的排查顺序是:先用 shape 和调用链定位实际浪费,再向模型寻找显式契约,用精度预算筛选计算方案,再让数据布局、资源复用和在途 stage 共同服务于完整 kernel 的吞吐;当自动排布没有利用这些机会时,继续追踪机器指令上的依赖建模。
如果一项优化只减少了数学运算,却增加了无法隐藏的准备或发射等待,它不一定更快。反过来,缩小 tile、增加局部中间结果,也可能因为提供了更多重叠机会而改善整体性能。性能模型用来解释和预测,profile 用来检查硬件是否兑现这些机会。
关于 MMO、调度 DAG 与寄存器压力的通用背景,可继续阅读 LLVM 跨层优化与诊断复盘。
附录:奇偶折叠与三角 HP6 的误差分析
A. 分开实数等价与浮点误差
对任意一个通道,令
这是实数域中的恒等式。浮点实现改变了加法和乘法的组织顺序。以下采用 round-to-nearest 模型,假设相关运算没有溢出、下溢或 flush-to-zero 导致的模型失效;特殊值不包含在相对误差推导中。
FP32 unit roundoff 为
$$
\widetilde f_k=\mathrm{fl}{32}(a_k+s b_k)
=(a_k+s b_k)(1+\delta_k),\qquad |\delta_k|\le u{32}.
$$
因此仅由 fold 带来的输出扰动满足:
偶对称中心值直接保留,没有 pair fold;奇对称中心权重为零。该界比较的是对同一份存储输入进行实数计算的结果,不包含之前输入量化的误差。
B. 三个 BF16 分量怎样生成
令
在上述正常范围内,量化值与被量化值足够接近;这些残差减法可在 FP32 中精确表示。于是:
三分量重构残差进入
C. 六项截断的界
设
从完整九项中省略的部分是:
再用
故六项在实数累加下满足:
对一条折叠后的点积,将各项绝对误差相加:
该估算保留了分量幅值中的
D. 为什么不能只看这个系数
设未加 bias 的精确折叠点积为
乘积同号时
此外,实际 MXU 点积、多个分量乘积的合并和 bias 加法都有舍入。若某段实现符合长度为
乘以相应被累加项绝对值之和做保守估算。但实际归约树、分块和融合乘加方式必须按实现判断,不能把这一模型直接当作芯片的实测误差。
最终需要同时考虑:
与 FP32 reference 比较时,还要考虑 reference 自身的舍入。因此,“约
E. 为什么选三角六项
| 候选乘积集合 | 首先省略的项 | 对选择的影响 |
|---|---|---|
| 左上 2×2 的四项 | 省略了二阶项,误差预算更紧 | |
| 左上 3×2 的矩形六项 | 虽然同为六项,仍省略二阶项 | |
| 保留全部到二阶的乘积,是本文选择 | ||
| 全部九项 | 不截断分量乘积 | 仍有分解残差与累加误差,矩阵计算更多 |
四项或矩形六项并非在每个输入上都会失败,但不能仅凭项数较少就假定满足门限。三角六项是本文分量展开中保留全部到二阶乘积所需的六项,不是对所有可能高精度算法的全局最少乘法证明。
参考与复现材料
- ParamSincFB 固定源码:核对奇偶滤波器的生成顺序、镜像符号与中心值,仅用于公开模型契约说明。
- Leveraging the bfloat16 Artificial Intelligence Datatype For Higher-Precision Computations:多分量 BF16 构造高精度计算的背景;本文附录单独列出所用误差假设。
- PyTorch allclose 文档:误差门限与参数顺序。
- 项目源码、内部 profile 附件与公开复现包:—。不公开: 本文涉及公司内部实现,保留工作负载、方案、结果与限制,不提供内部代码及版本标识。