关于主题np.einsum
,我已经在以下位置阅读了很多讨论:
为了更多地了解为什么np.eimsum
比平时的np.sum
,np.product
等(甚至anaconda中的最新numpy版本)要快,我在使用{ {1}},查看优化过程进行了哪些优化。
这样做时,我发现了一个有趣的现象。考虑这个最小的例子:
np.einsum_path
输出全部相同:
import numpy as np
for i in 'int8 int16 int32 int64 uint8 uint16 uint32 uint64 float32 float64'.split():
print(np.einsum_path('i->', np.empty(2**30, i))[1])
优化FLOP 在哪里增加(这意味着需要更多的计算量?),而理论上的加速则小于1(意味着速度越慢)。但是,如果我们实际上计时时间:
Complete contraction: i->
Naive scaling: 1
Optimized scaling: 1
Naive FLOP count: 1.074e+09
Optimized FLOP count: 2.147e+09
Theoretical speedup: 0.500
Largest intermediate: 1.000e+00 elements
--------------------------------------------------------------------------
scaling current remaining
--------------------------------------------------------------------------
1 i-> ->
如果我们查看对应于“原始”时间除以优化时间的第二列,它们都接近1,这意味着优化并没有使其变慢:
for i in 'int8 int16 int32 int64 uint8 uint16 uint32 uint64 float32 float64'.split():
a = np.empty(2**27, i)
raw = %timeit -qon9 a.sum()
noOp = %timeit -qon9 np.einsum('i->', a, optimize=False)
op = %timeit -qon9 np.einsum('i->', a, optimize='greedy')
print(i, raw.average/op.average, noOp.average/op.average, sep='\t')
我想知道int8 4.485133392283354 1.0205873691331475
int16 3.7817373109729213 0.9528030137222752
int32 1.3760725925789292 1.0741615462167338
int64 1.0793509548186524 1.0076602576129605
uint8 4.509893894635594 0.997277624256872
uint16 3.964949791428885 0.9914991211913878
uint32 1.3054813163356085 1.009475242303559
uint64 1.0747670688044795 1.0082522386805526
float32 2.4105510701565636 0.9998241152368149
float64 2.1957241421227556 0.9836838487664662
的伤口会说需要更多的FLOP而速度更慢吗?我相信计时是直接根据FLOP的数量计算的,因此这两个基准基本上是指同一件事。
顺便说一句,我要附上一个示例,展示np.einsum_path
的“通常”行为如何说服上面的结果是异常的:
np.einsum_path
输出:
a = np.empty((64, 64))
print(np.einsum_path('ij,jk,kl->il', a, a, a)[1])
noOp = %timeit -qon99 np.einsum('ij,jk,kl->il', a, a, a, optimize=False)
op = %timeit -qon99 np.einsum('ij,jk,kl->il', a, a, a, optimize='greedy')
print('Actual speedup:', noOp.average / op.average)
答案 0 :(得分:0)
我刚刚研究了np.einsum_path
的源代码。根据此处的评论(即here):
# Compute naive cost
# This isn't quite right, need to look into exactly how einsum does this
以及计算最佳费用的方式(即here,而不是发布时间过长)。似乎两种费用的计算方式不一致,而第一个费用据记录是“不太正确”。
然后我打印了“本机”(即未优化)的einsum_path:
import numpy as np
print(np.einsum_path('i->', np.empty(2**30, 'b'), optimize=False)[1])
令人惊讶的是“不同于本地”:
Complete contraction: i->
Naive scaling: 1
Optimized scaling: 1
Naive FLOP count: 1.074e+09
Optimized FLOP count: 2.147e+09
Theoretical speedup: 0.500
Largest intermediate: 1.000e+00 elements
--------------------------------------------------------------------------
scaling current remaining
--------------------------------------------------------------------------
1 i-> ->
因此,所报告的速度下降的原因仅仅是触发器计数错误。 np.einsum
实际上没有做任何 path 优化(尽管出于某种原因它仍然比本地np.sum
更快)。