我想计算每层LeNet-5(paper)需要多少次触发器。有些论文总共为其他架构提供了FLOP(1,2,3)但是,这些论文没有详细说明如何计算FLOP的数量,我不知道非线性激活函数需要多少FLOP。例如,计算tanh(x)
需要多少FLOP?
我想这将是实现,也可能是硬件特定的。但是,我主要感兴趣的是获得一个数量级。我们在谈论10个FLOP吗? 100 FLOP? 1000 FLOP?因此,选择您想要的任何架构/实现来获得答案。 (虽然我很欣赏接近“常见”设置的答案,例如Intel i5 / nvidia GPU / Tensorflow)
答案 0 :(得分:6)
注意:这个答案不是特定于python的,但我不认为tanh之类的东西在语言上是根本不同的。
Tanh通常通过定义上限和下限来实现,分别返回1和-1。中间部分用不同的函数近似如下:
Interval 0 x_small x_medium x_large
tanh(x) | x | polynomial approx. | 1-(2/(1+exp(2x))) | 1
存在精确到多个精确浮点的多项式,并且还存在双精度。 该算法称为Cody-Waite算法。
引用this description(您可以在那里找到有关数学的更多信息,例如如何确定x_medium), Cody和Waite的理性形式需要四次乘法,三次加法,一次单精度除法,七次乘法,六次加法,一次双精度除法。
对于负x,您可以计算| x |并翻转标志。 因此,您需要比较x所在的区间,并评估相应的近似值。 总共:
现在,这是1993年的一份报告,但我认为这里没有太大的改变。
答案 1 :(得分:5)
如果我们查看tanh(x)
的glibc实现,我们会看到:
x
值更高22.0和双精度,tanh(x)
可以安全地假设为1.0,因此几乎没有成本。x
,(让我们说x<2^(-55)
)另一种便宜的近似值是可能的:tanh(x)=x(1+x)
,因此只需要两个浮点运算。tanh(x)=(1-exp(-2x))/(1+exp(-2x))
。但是,一个必须准确,因为1-exp(t)
由于重要性丢失而对小t值非常有问题,因此使用expm(x)=exp(x)-1
并计算tanh(x)=-expm1(-2x)/(expm1(-2x)+2)
。所以基本上,最坏的情况大约是expm1
所需操作次数的2倍,这是一个非常复杂的功能。最好的方法可能只是测量计算tanh(x)
所需的时间与两个双精度乘法所需的时间相比较。
我在英特尔处理器上的(草率)实验产生了以下结果,这给出了一个粗略的想法:
因此,对于非常小且数字> 22,几乎没有成本,对于高达0.1
的数字,我们支付6 FLOPS,然后成本上升到每tanh
个约20 FLOPS - caclulation。 / p>
关键点:计算tanh(x)
的成本取决于参数x
,最大成本介于10到100个FLOP之间。
有一条称为F2XM1
的英特尔指令可为2^x-1
计算-1.0<x<1.0
,可用于计算tanh
,至少在某个范围内。但是,如果要相信agner's tables,则此操作的成本约为60 FLOP。
另一个问题是矢量化 - 据我所知,正常的glibc实现没有矢量化。因此,如果您的程序使用矢量化并且必须使用非实现的tanh
实现,那么它将进一步减慢程序的速度。为此,intel编译器具有mkl-library,其中vectorizes tanh
。
正如您在表格中所看到的,每次操作的最大成本约为10个时钟(浮动操作的成本约为1个时钟)。
我想你有可能通过使用-ffast-math
编译器选项赢得一些FLOP,这会导致程序更快但更不精确(这是Cuda或c / c ++的一个选项,不知道这是否可以为python / numpy做。)
生成图形数据的c ++代码(用g ++ -std = c ++ 11 -O2编译)。它的目的不是提供确切的数字,而是关于成本的第一印象:
#include <chrono>
#include <iostream>
#include <vector>
#include <math.h>
int main(){
const std::vector<double> starts={1e-30, 1e-18, 1e-16, 1e-10, 1e-5, 1e-2, 1e-1, 0.5, 0.7, 0.9, 1.0, 2.0, 10, 20, 23, 100,1e3, 1e4};
const double FACTOR=1.0+1e-11;
const size_t ITER=100000000;
//warm-up:
double res=1.0;
for(size_t i=0;i<4*ITER;i++){
res*=FACTOR;
}
//overhead:
auto begin = std::chrono::high_resolution_clock::now();
for(size_t i=0;i<ITER;i++){
res*=FACTOR;
}
auto end = std::chrono::high_resolution_clock::now();
auto overhead=std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin).count();
//std::cout<<"overhead: "<<overhead<<"\n";
//experiments:
for(auto start : starts){
begin=std::chrono::high_resolution_clock::now();
for(size_t i=0;i<ITER;i++){
res*=tanh(start);
start*=FACTOR;
}
auto end = std::chrono::high_resolution_clock::now();
auto time_needed=std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin).count();
std::cout<<start<<" "<<time_needed/overhead<<"\n";
}
//overhead check:
begin = std::chrono::high_resolution_clock::now();
for(size_t i=0;i<ITER;i++){
res*=FACTOR;
}
end = std::chrono::high_resolution_clock::now();
auto overhead_new=std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin).count();
std::cerr<<"overhead check: "<<overhead/overhead_new<<"\n";
std::cerr<<res;//don't optimize anything out...
}
答案 2 :(得分:1)
该问题表明它是在机器学习的上下文中提出的,因此重点是单精度计算,特别是使用 IEEE-754 binary32
格式。 Asker 表示 NVIDIA GPU 是一个令人感兴趣的平台。我将专注于使用 CUDA 来使用这些 GPU,因为我不熟悉 CUDA 的 Python 绑定。
在谈到 FLOPS 时,除了简单的加法和乘法之外,还有各种关于如何计算 FLOPS 的思想流派。例如,GPU 计算软件中的除法和平方根。识别浮点指令并计算它们没有那么模糊,这就是我在这里要做的。请注意,并非所有浮点指令都会以相同的吞吐量执行,这也取决于 GPU 架构。有关指令吞吐量的一些相关信息可以在 CUDA 编程指南中找到。
从图灵架构(计算能力 7.5)开始,GPU 包含一条指令 MUFU.TANH
以计算约 16 位精度的单精度双曲正切。多功能单元 (MUFU) 支持的单精度函数通常通过存储在 ROM 中的表中的二次插值计算。据我所知,MUFU.TANH
是在虚拟汇编语言 PTX 级别公开的,但不是(从 CUDA 11.2 开始)作为内部设备函数公开。
但鉴于功能是在 PTX 级别公开的,我们可以使用一行内联汇编轻松创建自己的内在函数:
// Compute hyperbolic tangent for >= sm75. maxulperr = 133.95290, maxrelerr = 1.1126e-5
__forceinline__ __device__ float __tanhf (float a)
{
asm ("tanh.approx.f32 %0,%1; \n\t" : "=f"(a) : "f"(a));
return a;
}
在计算能力 < 7.5 的旧 GPU 架构上,我们可以通过代数变换和使用机器指令 MUFU.EX2
和 MUFU.RCP
来实现具有非常匹配特征的内在函数,它们计算以 2 为底的指数和倒数,分别。对于幅度较小的参数,我们可以使用 tanh(x) = x 并通过实验确定两个近似值之间的良好切换点。
// like copysignf(); when first argument is known to be positive
__forceinline__ __device__ float copysignf_pos (float a, float b)
{
return __int_as_float (__float_as_int (a) | (__float_as_int (b) & 0x80000000));
}
// Compute hyperbolic tangent for < sm_75. maxulperr = 108.82848, maxrelerr = 9.3450e-6
__forceinline__ __device__ float __tanhf (float a)
{
const float L2E = 1.442695041f;
float e, r, s, t, d;
s = fabsf (a);
t = -L2E * 2.0f * s;
asm ("ex2.approx.ftz.f32 %0,%1;\n\t" : "=f"(e) : "f"(t));
d = e + 1.0f;
asm ("rcp.approx.ftz.f32 %0,%1;\n\t" : "=f"(r) : "f"(d));
r = fmaf (e, -r, r);
if (s < 4.997253418e-3f) r = a;
if (!isnan (a)) r = copysignf_pos (r, a);
return r;
}
使用 CUDA 11.2 为 sm_70
目标编译此代码,然后使用 cuobjdump --dump-sass
反汇编二进制文件显示八条浮点指令。我们还可以看到生成的机器代码 (SASS) 是无分支的。
如果我们想要一个具有完全单精度精度的双曲正切,我们可以对小幅度的参数使用极小极大多项式近似,同时对参数使用代数变换和机器指令 MUFU.EX2
和 MUFU.RCP
量级更大。超过一定幅度的论证,结果将为±1。
// Compute hyperbolic tangent. maxulperr = 1.81484, maxrelerr = 1.9547e-7
__forceinline__ __device__ float my_tanhf (float a)
{
const float L2E = 1.442695041f;
float p, s, t, r;
t = fabsf (a);
if (t >= 307.0f/512.0f) { // 0.599609375
r = L2E * 2.0f * t;
asm ("ex2.approx.ftz.f32 %0,%1;\n\t" : "=f"(r) : "f"(r));
r = 1.0f + r;
asm ("rcp.approx.ftz.f32 %0,%1;\n\t" : "=f"(r) : "f"(r));
r = fmaf (r, -2.0f, 1.0f);
if (t >= 9.03125f) r = 1.0f;
r = copysignf_pos (r, a);
} else {
s = a * a;
p = 1.57394409e-2f; // 0x1.01e000p-6
p = fmaf (p, s, -5.23025580e-2f); // -0x1.ac766ap-5
p = fmaf (p, s, 1.33152470e-1f); // 0x1.10b23ep-3
p = fmaf (p, s, -3.33327681e-1f); // -0x1.5553dap-2
p = fmaf (p, s, 0.0f);
r = fmaf (p, a, a);
}
return r;
}
此代码包含一个依赖于数据的分支,查看 CUDA 11.2 为 sm75
目标生成的机器代码表明该分支被保留。这意味着一般来说,在所有活动线程中,一些将遵循分支的一侧,而其余的将遵循分支的另一侧,需要后续同步。因此,为了对所需的计算工作量有一个现实的想法,我们需要结合两个执行路径的浮点指令计数。这是 13 条浮点指令。
以上代码注释中的错误界限是通过对所有可能的单精度参数进行详尽测试而建立的。