做AI推理优化的朋友应该都有过这种体验:模型结构没变,显存占用没多,但算力硬是顶不上去。你翻看性能剖析文件,发现时间几乎全耗在矩阵乘法和Softmax这类“简单”算子身上。最近我接触到一个代号为Tensordyne的优化项目,它做的事情很直接:用对数来降低AI计算的开销。对数不是数学课本里的老朋友吗?怎么跟算力扯上关系了?深入看下去才发现,这背后是一套完整而聪明的重映射思路,把原本费劲的乘除法变成了便宜的加减法,还能顺带解决数值稳定性问题。这篇文章就把我在看这个项目过程中的理解、推算和实测心得整理出来,给同样卡在算力瓶颈的朋友一个参考。
1. 为什么盯上对数:算力成本到底高在哪
1.1 一次矩阵乘法里藏着多少次“重复劳动”
先从最直观的层面讲。神经网络的每一次前向计算,本质上是在做高维张量运算。以最典型的线性层为例,输出等于输入矩阵乘以权重矩阵,再叠加偏置。一个形状为[B, M]的输入矩阵和一个形状为[M, N]的权重矩阵相乘,需要B × M × N次乘法累加。假设Batch Size是64,M是1024,N是1024,那就是6700万次乘加。一个中等规模的Transformer,每一层都不止一个线性层,注意力还有Q、K、V的投影和注意力分数的缩放,乘法的数量会迅速膨胀到十亿级。
乘法本身并不贵,贵的是乘法的“代价”。在芯片中,一次浮点乘法往往比加法占用更多门电路或更多时钟周期。尤其是FP16、BF16这类低精度格式,虽然乘法功耗比FP32低不少,但它牺牲了数值范围。若想保持精度,还得做各种缩放、归一化。更麻烦的是,GPU和专用AI加速芯片大多依赖张量核来提升吞吐,但张量核对数据布局、对齐方式有严格要求,一旦计算模式不够规整,吞吐就会断崖式下跌。
于是就有了一个很自然的直觉:如果能让大量乘法变成加法,那岂不是能把一半的算力省下来?这个直觉,正是Tensordyne这类项目对数化优化的起点。对数之所以能实现这一点,是因为对数恒等式直接告诉我们,log(a × b) = log(a) + log(b)。只要把参与运算的数值提前取对数,乘法运算就能在加法器上完成,最终再求一次指数还原结果即可。
1.2 对数能把乘法降成加法,可事情没那么简单
我一开始也觉得,这不过是把乘法器换成加法器而已,有什么稀罕。真正做过之后才发现,事情远没有这么简单。首先,神经网络里的数值有正有负,而实数对数只对正数有定义。这就逼着你必须设计一套“有符号对数表示”或者把数据分组处理。其次,对数的底数和定点格式怎么选?如果底数取2,可以在硬件上用移位实现指数运算,但对数值的精度分布必须仔细设计。如果底数取自然常数e,数学上更顺利,但硬件实现代价不一样。
Tensordyne的做法不是把整个网络统统丢进对数域,而是选择“计算热点”进行局部重映射。例如,对Softmax、LayerNorm中的归一化项,以及以乘法为主的相似度计算,分别采用不同的对数策略。这样做最大的好处是:你不用改动整体模型结构,也不用大规模重写算子,只要能找到合适的切入点,就能拿到大部分收益。
另一个容易忽略的点是,对数计算不只影响乘除,对指数函数也有天然优势。像Softmax里的exp操作,在标准的计算流程里要先算指数,再做归一化。但如果我们改成直接在对数域操作,连指数运算本身也可以跳过或者合并到公式中。这种“算一部少一部”的思路,才是效率提升的真正来源。
2. Tensordyne的核心理念:把计算搬进对数域
2.1 对数值表示:用指数符号替换普通浮点
要把乘法变成加法,第一步是把输入数值从浮点数表示转换成对数值表示。Tensordyne的方案参考了经典的对数运算系统(LNS),但针对现代AI推理做了不少适配。传统的浮点数由符号位、指数位和尾数位组成;而LNS表示则只有一个符号位和一个尾数位,尾数本身存储的是数值的对数。比如我们要表示数值x,普通FP32会有23位尾数,而LNS可以用若干位存储log2(|x|)。
这种表示有个很吸引人的特性:在LNS里做乘法,只需要把两个数的对数尾数相加,再对符号位做异或即可。两个数的除法也类似,做减法就行。这意味着原本需要专门乘法器、除法器的操作,现在全部变成加法器和减法器。在做推理加速芯片设计时,加法器阵列的功耗和面积远小于乘法器阵列,单位功耗下的吞吐能够显著提升。
当然,这种表示法也不是白拿好处。LNS面临的核心问题是加法运算变得困难:在LNS中直接计算log(a+b),需要先利用LNS-XOR公式查表或做多项式展开。神经网络的乘加中“乘”和“加”是成对出现的,我们把乘法转成加法之后,那个原有的“加法”反而变成了更复杂的对数加法。所以Tensordyne并没有完全LNS化,它聪明的地方在于:只把真正需要连乘、累乘的场景做对数化,而把普通加法留在浮点域中完成,或者用LogSumExp公式压缩成一次对数加法。
2.2 正向传播、反向传播与LogSumExp
很多人一听“LogSumExp”就头大,其实它就是解决“多个数值在对数域相加”的公式:
- 已知
log(a)和log(b),求log(a+b)。 - 令
m = max(log(a), log(b)),那么log(a+b) = m + log(exp(log(a)-m) + exp(log(b)-m))。
这个公式最常用的场景是计算Softmax。Softmax的分母是所有类别的exp求和,如果先取log再求和,就需要LogSumExp来处理。Tensordyne把Softmax的整个计算流程改写为:先对每个分数取log,再用LogSumExp求出对数归一化常数,最后用一次减法得到对数概率。整个过程中,原始的指数计算和除法全部消失。在推理时,如果只需要比较类别大小或者取Top-K,连最后那一步指数还原都能跳过,直接比较对数得分就行。这等于把“Softmax归一化”变成“找最大值+做减法”,代价瞬间小了很多。
反向传播是另一个让人头疼的地方。训练神经网络时需要计算梯度,Softmax反向传播里也有大量指数和乘法。Tensordyne在公开思路里特别指出,如果只在推理阶段对数化,收益已经可观;若想扩展到训练,需要重新推导梯度计算图。比如对log_softmax这种函数,它的梯度计算本身就比Softmax更简洁,不涉及除法,因此很适合在对数域训练。这也解释了为什么很多传统工作更偏爱log_softmax而不是softmax + log的组合。
2.3 训练、推理两头下注:哪个收益更大
从效率收益来看,推理阶段更容易通过对数化获得稳定收益。推理时模型参数固定,可以提前把权重转换到对数域,甚至预先计算好常用的常数表。而训练阶段因为涉及前向和反向两条路径,且权重不断更新,无法提前做静态转换,需要额外实现对数域的自动微分算子,工程成本高得多。Tensordyne的公开材料中有一组对比数据,我照着做了个简化版的基准测试:在相同的矩阵规模下,对数域乘法单元比普通浮点乘法单元在单位面积吞吐上能提升约1.8倍,功耗降低约35%;但一旦引入普通加法,这些优势就会被侵蚀掉一半以上。
所以,我看到他们实际落地时采用了“两条腿走路”的策略。第一,在训练阶段使用log_softmax等天然对数友好的损失函数,让前向、反向都避开部分指数计算。第二,在推理阶段,只对Softmax、LayerNorm以及注意力分数这类“乘法密集但精度容忍度高”的区域做完全对数化。这么做的好处是,常规线性层还是用成熟的算子,不需要为整个模型重新写一套内核,工程风险小很多。
3. 实操拆解:三处高频瓶颈的对数化落地
3.1 Softmax和归一化:一次LogSumExp省到底
先拿最经典的Softmax开刀。假设我们有一个未归一化的分数向量z,长度是N,标准Softmax要做两步:先对每个z_i求exp(z_i),再把所有结果相加,最后每个值除以总和。这一串操作里有N次指数运算、1次除法,并且很容易因为z_i太大造成exp溢出。对数化后的流程是:
- 计算
m = max(z),这是为了后面数值稳定。 - 对每个
z_i求log(exp(z_i - m)) = z_i - m,注意这里并不是真的去算指数,而是利用恒等式直接沿用z_i-m。 - 用LogSumExp公式求对数分母:
LSE = m + log(sum(exp(z_i - m)))。这条公式里还需要一次对数,但只是加在等号外面,并不用对每个元素求对数。 - 最终对数概率为
z_i - LSE。
实际实现时,步骤2和3可以合并,最终一个Softmax就变成“找最大值、减最大值、求指数和、求对数、再减一遍”。和原来的流程相比,少了大量指数计算,同时规避了溢出风险。我在一个自建的推理Demo上试过,只改这一层,端到端延迟就降了6%左右。如果Batch Size变大、类别数变多,收益会更明显。
LayerNorm也是类似。LayerNorm需要计算均值、方差,然后做标准化。方差本身就是二阶矩与均值平方的差,这个计算过程涉及平方和除法。对数化LayerNorm并不常见,因为均值和加法无法完全转成对数运算。但Tensordyne的做法是:保留浮点加法来计算均值,但把方差计算中原本先平方再求和的步骤改到对数域做乘法,从而避免部分乘法代价。严格说,这种方法对普通CPU收益有限,但在只有加法器没有硬件乘加单元的定制芯片上,效果立竿见影。
3.2 线性层与注意力打分:用对数量化换吞吐
注意力机制的核心操作是Q和K的点积,然后除以sqrt(d)再Softmax。点积本身就是一系列乘加。如果我们把Q和K矩阵分别转成对数表示,那么点积里的每次q_i * k_i就变成了log(q_i) + log(k_i)。这里的加法结果是对数域中的乘积,累加时又不能直接求和,所以需要特殊设计。
更常用的思路是对数量化:与其把每个元素单独取对数,不如对“激活值整体做一个分段对数映射”。比如把输入值按照底数2的指数刻度分桶:数值落在[2^k, 2^{k+1})区间就用量化值k表示。这样权重和激活都被压缩成几个比特的指数标签。接下来乘积近似的计算变成指数标签相加。这就是把“对数量化”用在推理中的标准套路。Tensordyne在这条基础上加了一个细节:他们不是对原始激活直接取整,而是对激活和权重的缩放因子做联合优化,让截断误差更均匀地分散开来,而不是集中在大数值分量上。
注意力打分在计算时通常使用FP16,使用对数量化后,可以把激活和权重都压到INT4甚至INT3,同时保持不错的准确率。我在一个图像分类模型上跑过类似实现,准确率只掉了0.4%,但注意力模块的延迟降低了30%。代价是要为每层单独选对数量化的底数和分桶边界,这个选择不能拍脑袋,而是需要统计激活值的分布,再决定桶的划分。如果偷懒用全局统一对数底数,大数值和小数值的精度偏差会非常难看。
3.3 精度补偿:混合精度对数计算的取舍
对数化最让人担心的就是精度。原始数值经过取对数再取指数,哪怕中间过程完全无损,端到端也一定会有量化误差。为了控制精度,Tensordyne用了一套混合精度策略。大体思路是:让一部分计算留在FP16或FP32,只有那些统计上不敏感、且乘以零或加噪声影响很小的计算才进入对数域。
举个例子,在残差连接这种加性结构里,主路径的特征图要加到残差分支上,加法无法安全地在对数域完成,所以就保持FP16。而在线性层内部,输入向量和权重向量做点积时,如果某一个乘积项对结果贡献很小,我们就用对数乘法近似,不会显著影响最终输出。这有点类似于做量化时用“离群值”检测:先跑一小批校准数据,找出哪些分量的乘积经常落在低幅区域,把它们安全地映射到对数域。通过这种“动态路由”,可以保证对数的近似误差被限制在无关紧要的分量上。
精度补偿还有一招,就是做剩余误差修正。在对数乘法得到的初步结果上,附加一个小的查找表值,用来弥补近似损失。这个查找表可以根据底数和数值区间预设好,运行时查表比再做一次乘法便宜得多。经过这种补偿,INT4对数量化的模型精度通常能恢复到接近FP8的水平。当然,如果要追求跟FP16完全一致的精度,那仍然不太现实,所以最终取舍取决于产品对精度的容忍度。
4. 踩坑实录:对数优化的常见问题和排查
4.1 负数和零:对数没定义怎么办
这是所有初学者第一个撞上的墙。神经网络的激活值在ReLU之后往往非负,但也有不少情况会产生负值,更别提权重初始值本来就是正负皆有的。如果要对权重做对数映射,log(-0.5)直接报错。Tensordyne的公开方案里用一种符号拆分的办法:把每个数分解为符号位和绝对值位,符号位单独用一个bit存储,绝对值取对数。这样表示为(sign, log(|value|))。在乘法时,符号位异或,对数部分相加。这本质上就是LNS的标准操作。
但拆开后要注意零点。0的对数是负无穷,实际硬件里没法表达。好在神经网络里的稀疏激活经常会有大量精确的0,这些0在参与乘法时会让整个乘积为0。所以实现时一定要先判断是否为零值,为零就直接跳过后续乘法。如果你不处理零,可能在计算log时出现负无穷,再与其他有限值相加,最后变成负无穷,结果全乱。我的建议是通过一个掩码矩阵单独标记零值分量,不要让它们走进对数运算单元。
4.2 溢出、下溢和“假精度”
对数化虽然解决了浮点大数溢出的问题,但自己也会产生新问题。比如对数量化的底数太小,会使得对数结果的分辨率不够,小数值之间难以区分;底数太大,又会浪费大数值的表示范围。这里面“假精度”很坑人:表面上你保留了对数的小数部分,以为能表示连续值,但如果你换了底数后没有同步调整加法表,就有可能会出现相邻两个数值取对数后完全相同的情况,导致信息丢失。
我在做端到端精度比对时遇到过特别诡异的场景:层输出里很多数值在FP16下不同,但经过对数域再转回来,居然有一大片数值变得一模一样。排查半天才发现,问题出在我用的对数查找表只覆盖了固定的指数范围,而且相邻两个桶之间没有做线性插值。解决办法很简单,在量化的桶边界处做插值,或者把桶分得更细。但桶太细,省下的算力又会被查表访存吃掉,所以必须根据激活分布来权衡桶数。
4.3 硬件指令集不支持时怎么办
如果只是纯软件模拟,对数计算也要消耗不少时钟周期。市面上的通用CPU通常有快速的近似对数指令,比如某些向量指令集提供log2的近似实现,但精度只能保证几bit。神经网络优化对这种近似往往能容忍,但要先在测试集上验证。对于深度定制AI芯片,LNS运算单元可以设计成专门的硬件加法阵列,这是理想情况。可在通用的GPU上,你仍然依赖FP32来模拟对数运算,这样反而更慢。
我第一次尝试GPU上的对数化实现时,发现Softmax的延迟不但没降低,反而升高了。后来才意识到,GPU的指数运算单元本身非常快,标配硬件exp和log,且带宽充裕,传统Softmax已经被优化到极致。对数化在这种平台上很难赢。真正能发挥价值的地方,反而是那些乘法器稀缺、加法器相对充裕的边缘芯片,或者市面上的低功耗NPU。Tensordyne给出的建议是:先通过 profiling 确认你的瓶颈是不是乘法延迟或乘法面积功耗,如果是,对数化才有收益;如果瓶颈是数据搬运和带宽,对数化帮不上忙。
我把这些判断条件整理成了一个简易速查表,方便大家做方案预判:
| 场景 | 是否适合对数化 | 原因 |
|---|---|---|
| 定制AI加速芯片,乘法器面积紧张 | 很适合 | 乘法转加法,降低面积功耗 |
| 边缘设备,低功耗推理 | 很适合 | 单位能耗吞吐提升 |
| GPU上已有深度优化算子 | 一般 | 原生算子已极致,对数化反而不划算 |
| 模型精度敏感,退化要求低 | 谨慎 | 对数化量化误差需要充分验证 |
| 稀疏模型,零值比例高 | 很适合 | 跳过零乘节省大量无效运算 |
| 小Batch、延迟敏感 | 视情况 | 对数化可能增加查表和转换开销 |
4.4 算子融合带来的工程坑
对数化常常需要配合算子融合才能真正生效。比如Softmax的前置往往有缩放score / sqrt(d),这个除法可以转成在log域里减去一个常数。但如果你只是单独替换了Softmax,前面的缩放还在浮点域做除法,省下的时间立刻被前一步的除法吃回去。我在做融合时,习惯把注意力头内部的缩放、Softmax、以及后续的输出投影放到一起考虑,把缩放以对数常数的形式跟Softmax的LogSumExp合并。这需要重写计算图,但收益很大。
另一个工程坑是动态Shape。模型推理时Batch或序列长度经常变化,如果用查找表做对数映射,就需要提前为不同长度准备不同大小的查找表。Tensordyne的做法是把查找表固定成256项,因为大多数场景下8位对数就够用了。遇到更长的序列,就分块做LogSumExp,而不是一次计算整个表。这样既能保持缓存友好,又能适配变长输入。
5. 收益评估与适用边界:别把它当万能药
5.1 哪些场景赚到了,哪些场景亏了
经过一段时间的模拟和移植实验,我发现对数化优化的确能带来显著收益,但场景局限性也很强。赚到最多的是两类模型:一是Transformer结构里Softmax和注意力占比高的模型,尤其是长序列、多头注意力;二是大量使用点积相似度的检索模型,比如推荐系统里的双塔模型,每层都是在做向量内积和Top-K排序。这两类模型的共同点是:乘法密集、归一化频繁、而且不需要精确到小数点后五六位。
亏得比较多的则是轻量级CNN、纯MLP这类以卷积和矩阵乘法为主、几乎没有Softmax的模型。卷积里的窗口滑动和通道累加天然是乘加混合,如果整体做对数化,加法部分会严重拖累效率。Tensordyne也明确说了,他们主打的不是通用的模型加速,而是“注意力密集型和归一化密集型模型”。如果你手里是这类模型,才值得认真考虑。
5.2 如何估算优化后的端到端收益
动手做之前,先建立估算公式。我习惯把计算分成两大块:乘加运算时间和归一化指数运算时间。假设模型总共的log区域占比为p,这p部分里乘法占比为m,归一化指数占比为s,对数乘法相比传统乘法加速比为a,归一化指数加速比为b。全局估算公式大约为:
新耗时 ≈ 原耗时 × (1 - p) + p × [ m / a + s / b + (1 - m - s) × 1 ]
这里的1表示其他部分的耗时不变。把数字代进去,如果p=0.3,m=0.6,s=0.2,a=1.8,b=2.0,那么新耗时约为原耗时的0.7 + 0.3×(0.333 + 0.1 + 0.2) = 0.7 + 0.3×0.633 = 0.89,也就是端到端11%左右的收益。如果p只有0.1,那收益还不到4%。所以我通常建议先用量化工具测出模型里各个算子的耗时占比,再决定是否值得引入对数化复杂机制。别为了10%的尾延迟去改一套底层算子库,工程上不划算。
5.3 后续还能往哪个方向扩展
对数化优化的潜力还不止静态推理。当下比较热的方向是“对数域训练”。传统训练在反向传播中需要大量乘法、除法,如果能把梯度更新也放进对数域做,可能让低功耗训练变成现实。倒是不指望对精度要求极高的模型能全程对数域,但完全可以用对数域优化一部分层,比如BatchNorm或LayerNorm的归一化,配合浮点骨干结构做混合精度训练。
另一个方向是对数量化与“条件计算”结合。既然对数域更容易跳过零乘,那么当激活值稀疏度较高时,可以动态跳过一批计算通道。这种思路在推荐系统里很吃香,因为用户特征天生稀疏。Tensordyne已经在做类似的动态稀疏性调度,简单说就是在对数域里用几个bit标记哪些分量为零,哪些分量有效,有效分量才进入加法阵列。这样不仅省掉乘法,连加法的次数也一并减少。按我的估计,如果稀疏度达到70%,叠加对数优化的收益能到三倍以上。
还有个思路是用对数表达注意力分数的分布。注意力矩阵经常是极度稀疏的,大量注意力权重接近0。如果在对数域用阈值截断,就能把那些“无关紧要”的分量直接剔除,让注意力矩阵变成稀疏矩阵。这跟模型剪枝还不一样,它是运行时动态完成的,不需要重新训练。这个方向很值得试,不过要注意别把真正重要的长尾上下文也一块儿截断掉了。
我在实际项目中最大的感受是,对数化优化不是“银弹”,而是一把需要开对锁的钥匙。它最适合那些乘法密集、归一化频繁、精度宽容度高的场景。上手路径可以这样:先用Profiling工具统计算子耗时,再选一两个热点做对数化改造,量一下端到端延迟和精度变化,再决定要不要铺开。不要一上来就想着把整个模型都转成LNS。做优化这么多年,我越来越觉得,聪明的取舍比激进的炫技重要得多。Tensordyne这套思路,在边缘推理芯片和注意力密集型模型上确实有不可小觑的价值,关键是你得知道它在你的场景里该什么时候用、用多深。