我这一年里反复被同一个问题击中:同样的深度学习模型,在 GPU 上跑得飞快,换到端侧 NPU 上延迟翻了几倍,而这两块硬件明明算力差距没那么大。刚开始我以为是框架问题,后来深入到指令集、访存、算子和数据排布,才意识到自己一直活在某层"符号系统"里——算法是一堆漂亮的数学符号,硬件是一个充满时序、带宽、热功耗的物理世界,夹在两者之间的是编译器、算子库、微架构和半导体工艺的层层翻译。这个翻译过程做得干净,性能就是跃迁;做得粗糙,再好的算法也像给超跑装了限速器。
我想了很久怎么描述这种感受,后来找到一句话:从符号到物理,是 AI 算法与计算硬件在同一场底层协同跃迁中的双向奔赴。这篇文章就想把我在这条链路里的观察、踩坑和一些实操经验整理出来,既写给算法工程师,也写给做硬件和做编译器的人——大家平时各忙各的,但最精彩的性能故事,恰恰发生在两边的交界处。
1. 符号世界与物理现实之间的"翻译税":算法复杂度不等于实际速度
1.1 复杂度分析漏掉的东西:常数项、访存和并行度
读大学的时候,算法课讲排序,分析完冒泡排序是 O(n²)、快排是 O(n log n),所有人就默认快排更好。毕业做了几年 AI 算法,我发现自己被这种"符号化思维"骗过好多次。一个 O(n) 的算子,可能因为它的内存访问模式极为散乱,在 CPU 上被缓存未命中拖成乌龟;一个 O(n²) 的简单双循环,反而因为纯顺序访问和硬件预取完美契合,跑出远超理论预期的效果。
这种 "符号复杂度" 与 "物理表现" 的偏差,我后来称之为"翻译税"。算法表达的是计算逻辑的整洁符号系统,但硬件执行的是物理过程:数据要从 DRAM 搬到缓存,寄存器要按周期吞吐,流水线要应对分支跳转带来的冲刷,缓存要配合空间局部性和时间局部性。时间复杂度只刻画了计算量的增长趋势,却完全不感知这些物理机制。
一个很典型的例子就是排序算法在真实硬件上的表现阈值。对小数组,插入排序经常赢过快排,因为它的内存访问完全顺序化,分支预测命中率高;CPU 的乱序执行和缓存预取让它如鱼得水。等到数组大到缓存装不下,快排的分治访问模式才会体现优势。符号上更"快"的算法,在物理世界里不一定更快,这就是翻译税的正反两面。
对于 AI 深度学习算法来说,翻译税更明显。一个卷积算子,理论上 FLOPs 可以算得清清楚楚,但硬件执行时,最贵的往往不是乘法器干活的那几个周期,而是数据在多层存储之间搬来搬去的等待。我遇到过一款自研 NPU,峰值算力标得不错,一跑大通道数的卷积就露馅——因为片上缓存太小,每次都要去外部 DDR 反复搬运中间结果,算力利用率直接掉到两成以下。后来优化数据排布策略,把输入切块做得更贴合缓存容量,利用率才拉到六成。这整个过程中,算法的数学形式一点没变,变的只是"数据在物理存储里怎么摆"。
1.2 从指令集到微架构:软件感受不到的硬件"性格"
算法工程师眼中的"同一个算子",在不同硬件上对应的是完全不同的指令序列和电路行为。
拿矩阵乘法来说,在 x86 CPU 上,编译器会把它自动向量化,用 AVX 指令集的 FMA 指令,一个时钟周期完成多次乘加;在 GPU 上,它可能被映射成大量线程的 SIMT 执行,每个线程算输出矩阵的一个 tile;而在 NPU 上,它可能根本没被翻译成"指令",而是被配置成脉动阵列里一层层流过的数据。同一个数学符号,三种物理实现,中间隔着指令集架构、微架构设计和编译器后端的层层关卡。
指令集是软硬件之间一份相对稳定的"契约",但契约之下的微架构实现各有性格。同样是支持 SIMD 指令的 CPU,不同厂商对乱序窗口大小、访存队列深度、缓存延迟的取舍完全不同,导致同一段向量化代码在不同计算硬件上的表现可能差出两三倍。我做过一个算子专项优化,在 Intel 平台上用数据对齐和显式预取把延迟砍掉 40%,同样的代码丢到某 ARM 平台上几乎没变化——对方编译器里那些指令的调度策略差异太大,我的优化假设在它的物理结构上根本不成立。
这种时候我就深刻体会到:算法符号世界里的"优化",如果不对准硬件的物理结构,就是自嗨。后来我养成了一个习惯,每接触一个新的计算平台,先看它的微架构白皮书,搞清楚缓存层级、带宽数字、指令吞吐,再回去看自己的代码和算子实现。符号到物理,中间这层"架构语义"才是翻译的关键语法。
2. 让一段矩阵乘法真正"烧"起来:从数学公式到片上物理过程
2.1 卷积、全连接的本质是乘加运算的物理搬运
深度学习中绝大多数计算,拆到底都是乘加。一个全连接层,就是矩阵乘法;一个卷积层,本质是更复杂的乘加叠加。做算法的人通常关注 FLOPs——每秒浮点运算次数,但实际跑起来,决定延迟的往往是带宽、并行度和数据复用。
这里的核心概念是计算强度(arithmetic intensity),也就是单位字节数据搬运对应多少次浮点运算。一个成功的算子实现,要做的就是无限逼近目标硬件的 roofline 模型所允许的性能上限。Roofline 这个模型我在做算子优化时才真正理解它的价值:把算法本身的计算强度画出来,再把硬件能提供的算力带宽比画出来,两条线的交点就是"再多优化也不会超过这个数"的物理天花板。
举个例子,一个 1024×1024 的矩阵乘,输出矩阵里每个元素要做 1024 次乘加,总计算量约 20 亿次浮点运算。这个数在符号层面很简单。但物理层面呢?如果初版实现把三个矩阵全部丢在 DRAM 里,每个输出元素都要三次读入、一次写回,那么访存量暴涨到几十 GB。就算 DRAM 带宽再高,也会被访存拖死。优化的思路通常是用分块(tiling):把矩阵切成适合 L2 缓存的小块,让数据在片上缓存里反复复用,把对 DRAM 的访问压到最低。这一步不改任何数学逻辑,只改数据的物理流动,性能就能翻好几倍。
2.2 访存墙和片上存储:AI 计算的真正瓶颈
所有 AI 芯片设计者都在面对两面墙:访存墙和功耗墙。所谓访存墙,就是算力增长的斜率远高于外部存储带宽增长的斜率。芯片内部乘法器堆得再多,数据喂不进来,算力也只能闲置。功耗墙则更直接:高性能计算意味着高功耗,而功耗最终以热量形式散发,物理上散不走就限制频率和电压。
所以我做算子性能分析的时候,第一件事永远是看 profiling(性能分析)结果里的访存指标:cache miss rate 高不高、HBM/AXI 带宽占用到没到顶。很多时候,算法本身没有任何问题,纯粹是数据排布或者算子分块策略没适配硬件的存储层级,导致计算单元在空等数据。这就像战场上的炮兵,炮弹(数据)在后方仓库(DRAM),前方炮管(计算单元)再快,传送带(总线)不够粗就白搭。
硬件工程师在 RTL 层面解决的问题,其实就是怎么让这条"传送带"更宽、更短、更智能。一位做 FPGA 加速的朋友跟我聊过,他用 Verilog 写卷积加速器时,最费心的是设计片上 SRAM 的双缓冲:当前一块数据在计算阵列里"烧"的时候,下一块数据同步从 DRAM 预取到片上,计算和搬运重叠起来,流水线才转得动。这个"双缓冲"的思路,反过来也影响了我做算法部署时的选择——想办法让数据生产者与数据消费者重叠,而不是一板一眼地同步等待。符号世界的"并行",在物理世界就是流水线重叠和资源复用。
3. 算法为物理"让路"的三个典型动作:量化、剪枝、稀疏化落地指南
3.1 量化:用离散的位宽装连续的精度
我之前跑模型优化项目,最喜欢也最怕的就是量化。喜欢是因为收益立竿见影,怕是因为精度掉起来毫无规律。
量化的思路很简单也很物理:把 FP32 的 32 位浮点表示,压成 INT8、INT4,甚至更低。位宽降了,乘法器面积变小,功耗降低,访存带宽占用减半,计算单元吞吐翻倍。这一点可以从芯片物理层面理解:一个 32 位浮点乘法器和 8 位整数乘法器,在版图上面积差将近一个量级,算同样的乘加,功耗和延迟自然差出一截。所以用 INT8 推理,延迟减半不是算法变聪明了,是物理世界变得更"省力"了。
但量化在算法层面对的难题是:浮点数的连续区间,要映射到一个有限离散符号集里。这需要校准——统计每一层激活值的分布,选择合理的缩放因子(scale)和零点(zero point)。我踩过的最典型的坑,是激活值里存在少量极大异常值,导致量化区间被拉得很宽,绝大多数数据的有效精度反而被压缩,模型精度暴跌。后来我学会了用 KL 散度或者百分比截断法做校准:先跑一批校准数据,统计激活值的直方图,寻找一个能让量化前后信息损失最小的阈值,把异常值直接裁掉。这个思路属于典型的"算法向物理让步":精度不是越高越好,而是分布合理才最好。
另一个实操心得是训练时量化(QAT)和训练后量化(PTQ)的选择。PTQ 快,但对权重分布敏感,尤其是小模型,经常在校准后精度动辄掉几个点;QAT 能把量化误差作为伪噪声带进训练过程,模型自己学会抵抗,精度恢复效果好得多。过程慢一些,但务求稳定。做部署的同事应该都有印象:量化方案选错,模型上线后出现偶发的个别样本输出异常,排查起来最痛苦。
3.2 剪枝与结构化约束:想让硬件真正加速,就得先"遵其规矩"
很多人以为把模型里多余的权重置零就是剪枝,其实那只是"符号层面的剪枝"。真正要让硬件提速,必须考虑物理层面的数据结构。
非结构化剪枝自由度最高——权重矩阵里任意位置都可以置零,精度保持得好。但这种稀疏模式落到硬件上,计算公式变成:要么用一个稀疏矩阵的索引结构去访存权重,结果非连续访问;要么干脆跳过零值,但每一个跳过的分支判断都要在计算流水线里加判断,带来额外开销。我在 GPU 上试过非结构化稀疏,精度确实保住了,但推理速度不升反降,就是因为稀疏索引的访存模式完全打破了原来的缓存友好顺序。
硬件需要的,是结构化剪枝。比如 Channel Pruning(通道剪枝)或者 Block Pruning(块剪枝),把整块通道或规则块一次性剪掉。这样稀疏后的矩阵仍然是规整的形状,访存连续,计算单元无需处理不规则跳转。代价是精度损失略大,需要用重训练来恢复。
这里有个算法搜索的空间:剪哪些通道、每层剪多少、哪些层适合剪厚一点,本质是一个组合优化问题。这类搜索适合用启发式算法去做。我在组件选择时常用贪心算法做基线:每层按权重范数排序,优先剪掉范数小的通道,简单有效。想再精细一些,可以用模拟退火或者粒子群算法在层间剪枝比例的空间里搜索——把精度作为目标函数,把模型大小/延迟作为约束,这类"符号层面的搜索算法"在解决问题时,输出结果却要落在"物理世界的真实延迟"上。这又是符号与物理的一次握手。
3.3 稀疏化的硬件支持现状:为什么大家都说稀疏,却很少有人真正用起来
稀疏计算被说了很多年,但我在实际工程里,真正把稀疏化的收益吃满的机会屈指可数。原因很现实:通用硬件对稀疏的支持一直很有限。
NVIDIA 从 Ampere 架构开始支持 2:4 结构化稀疏,也就是每四个权重里至少两个为零,硬件可以直接跳过一半的计算。这是个聪明的折中,因为 2:4 模式是规则的,硬件可以用固定逻辑加速,不需要复杂的索引查找。但要想达到这个模式,训练时就需要特殊的稀疏掩码约束,实际操作门槛并不低。
NPU 这边更麻烦。有的芯片厂商在硬件里做了稀疏感知的存储格式和计算单元,但配套编译器通常不太成熟;有的厂商直接不开放硬件稀疏接口,软件上硬塞稀疏权重反而更慢。所以我给团队的指导意见一直是:如果硬件链路(模型转换、算子库、编译器)明确支持结构化稀疏,就认真考虑;如果支持度含糊,宁可在量化和模型裁剪上多花功夫,也别把时间耗在花哨的稀疏实现上。
4. 硬件开始向算法"站队":Transformer与大模型时代的新计算范式
4.1 为什么 Transformer 逼着所有人重新思考硬件
我早年做 CNN 模型部署时,各类芯片的优化套路已经比较成熟:卷积网络算子结构相对规整,为它定制加速器的硬件方案很多。但 Transformer 出现之后,情况变了——自注意力机制的基础矩阵乘法、Softmax、LayerNorm 这类算子,对访存极度敏感。
以自注意力里的 QKV 计算和 Softmax 为例,计算 FLOPs 未必很高,但它们要读取一个序列长度的中间状态,访存开销大。序列一长,中间矩阵就大,带宽瓶颈立刻显现。因此出现了 FlashAttention 这类算子,它通过分块(tiling)把注意力计算过程重新组织,让中间结果尽量留在片上 SRAM,避免频繁与 HBM 交互。这不是改变模型的数学定义,而是在算法实现层面向物理硬件特性靠拢——把算子的"算术强度"提高了,访存压力骤降。
我曾经在一张大模型推理任务上,直接换上 FlashAttention 的实现,不做任何模型改动,decode 阶段的并发提升了一大截。那一刻我非常确信:在符号层面看起来完全等价的算子实现,因为物理访存模式不同,工程表现可以天差地别。Transformer 把整个生态都拉到了"访存至上"的语境里,这也解释了为什么新一代 AI 芯片把片上 SRAM 容量和大带宽当成关键卖点。
4.2 GPU、NPU、端侧芯片:不同物理结构,决定了算法能怎么跑
硬件世界远不是"算力越高就越强"这么简单。GPU 的物理结构是大规模 SIMT 线程并行,依赖缓存与大量线程切换来掩盖访存延迟,适合训练阶段的超大矩阵吞吐;NPU 则更多是专用数据流架构,数据在脉动阵列里流水式地流动,计算效率高、能效比好,适合推理阶段相对固定的模型结构;CPU 偏重控制流与通用性,在传统服务端做预处理、后处理和轻量模型推理时依旧不可替代。
这也是算法工程师在做部署选型时常陷入的困惑:为什么同样的主干网络,在一款算力只有 GPU 十分之一的 NPU 上,推理速度和 GPU 差距远没有算力差距那么大?原因就在于 NPU 的物理数据流与算子结构更契合,把算力用在了刀刃上。反过来,如果你把模型里的算子设计成某个 NPU 完全不适配的形式,比如大量动态 shape、复杂数据依赖的循环,那再高的硬件峰值也就是画饼。
大模型和 AI Agent 的兴起,又提出了一个 GPU 之外的问题:实时性和功耗。AI Agent 这类应用里,模型要高频地推理、决策,可能跑在云端,也可能跑在端侧边缘设备上,对单次推理延迟和能效比极度敏感。这倒逼硬件从"极致算力堆料"转向"算法-硬件联合设计"。比如有些端侧芯片直接为 Transformer 的算子定制计算单元,在保持较低功耗的同时完成实时推理任务,让 Agent 的响应延迟降到可用的水平。算法形态在变,硬件就在变,双方在底层互相塑造。
5. 一线协同开发手记:从 RTL 到算子库到应用的踩坑与心得
5.1 我经历的一次算子性能优化全过程
具体说说一个我印象深刻的 case。某个视觉模型里有一个 3×3 卷积算子,在端侧 NPU 上性能特别差,profile 显示 NPU 的利用率只有 15%。我第一反应是算子分块参数没调好,于是尝试各种 tiling 尺寸,没什么用。
后来把问题抛给芯片厂商的 FAE,他们让我们检查数据排布。我这才意识到:模型从 PyTorch 导出 ONNX 再转 NPU 模型时,默认的 tensor 布局可能被转换成了 NCHW,而这款 NPU 的原生数据排布是 NHWC。NHWC 下通道维在最后,卷积计算时同一个位置的输入通道数据在内存里是连续的,向量化加载和乘法累加都高效;NCHW 下则需要频繁做通道维的跨步访问,访存效率暴跌。然后我重新设计了数据排布转换,加了一个自定义算子把网络输入直接转成 NHWC,避免中间反复排列,整个瓶颈就消失了。
这件事教给我的不是 NHWC 比 NCHW 好,而是:每个硬件都有自己偏爱的"物理存储姿势",算法工程师不能只把数据当成符号张量来操作,还得知道它在 DRAM 里的字节流动路线。所有 AI 框架的 tensor 抽象、自动微分,本质都是符号系统的便利;而性能,永远只发生在物理层。
5.2 编译器、算子库与 RTL 语言(Verilog)的协作分工
搞深度学习系统的人都知道,算法要落到硬件,中间要过三层关卡:编译器把高级算子翻译成指令或配置流;算子库针对热点算子做手工调优;而 RTL(硬件描述语言,比如 Verilog)则把这些语义落实到真正的门电路之上。
这三层不是各自割裂的。我现在做系统设计,经常要同时跟三拨人打交道:写 Verilog 的同事告诉我某个计算单元一次能搬多少数据、多长时间能出一组结果;编译器团队告诉我,某个融合算子在指令调度时能不能覆盖访存的空闲周期;我自己则要从算法层判断,ROI 到底该花在写一个融合 kernel 上,还是换个算子结构更划算。
这里我要给做算法的新人一个建议:不要觉得 Verilog 或者汇编这些东西离你很远。你要做的不是会写 RTL,而是能读懂硬件行为模型,知道你在算法层写出的那个均匀规整的循环,在硬件流水线里是什么样。我见过太多算法工程师在框架层堆各种小技巧,反而忽略了最该看的"算子实现细节"。反过来,只懂硬件不懂算法也不行,因为你不知道哪些精度损失可以接受、哪些权重结构可以被压缩,就无法为硬件留出优化空间。
5.3 建立"符号-物理"协同思维的几个具体实践
文章最后,我把自己这几年用下来比较有效的几个实操习惯列出来,供大家参考:
- 拿到一个新的推理平台或者新硬件,先选一个典型算子做 roofline 分析。把算子的计算强度画出来,再对照硬件手册里的峰值算力和带宽,看瓶颈在 compute-bound 还是 memory-bound。别急着调参,先找准物理瓶颈。
- 做量化或者剪枝之前,一定先准备一个有代表性的校准集和评估集。我吃过亏:训练集上评估量化模型,精度看着还行,一到真实业务数据上就崩。数据分布不一样,符号层面的统计规律就失效了。
- 在用手工优化算子之前,先查一遍编译器自动优化后的性能基线。很多情况下,编译器已经做得很好了,手工优化半天收益不大;真正值得手工优化的,往往是编译器不认识、但硬件很适合的融合模式。
- 模型结构设计阶段就要考虑部署硬件的算子支持范围。比如某个非线性激活函数在目标 NPU 上实现代价大,那在算法设计时换成硬件友好的近似形式,比后期裁剪更省事。这是把"协同"前置到算法设计的第一步。
- 过滤器类专业部署项目多留出性能调优时间。我曾经在项目排期里只估了模型训练和转换的时间,结果卡在算子适配和性能调优上多花了两周。从符号到物理,真正决定上线节奏的,往往就是这段"翻译"过程啊。
踩过几次坑之后,我越来越觉得,"从符号到物理"不只是这篇文章的标题,更是做 AI 底层技术的人的一种必备视角。算法决定的,是事情在数学上能做;硬件决定的,是事情在物理上能不能跑得快、跑得省、跑得稳。两者之间那一整层编译器、算子库、微架构和 RTL 实现,才是把符号世界的优雅翻译成物理世界效率的关键。希望这篇文章能给正在这个交界处挣扎的同行一点参考,别让那些看不见的"翻译税"把你绕晕。