Python numpy:高效、大网点产品

Python numpy:高效、大网点产品,python,performance,numpy,Python,Performance,Numpy,我正在尝试执行一个大型线性代数计算,以将一个通用协方差矩阵KK_l_obs(shape(NL,NL))转换为一个缩减空间Kmap_PC(shape(q,q,X,Y))中协方差矩阵的映射 关于如何为每个空间位置构建Kmap\u PC的信息保存在其他数组a、I0和k\u l\u th中。前两个形状为(X,Y),第三个形状为(nl,nl)。观察到的空间和缩小的空间之间的转换由EingInvertorsE(shape(q,nl))进行。请注意NLNL Kmap\u PC的空间元素计算如下: Kmap_P

我正在尝试执行一个大型线性代数计算,以将一个通用协方差矩阵
KK_l_obs
(shape
(NL,NL)
)转换为一个缩减空间
Kmap_PC
(shape
(q,q,X,Y)
)中协方差矩阵的映射

关于如何为每个空间位置构建
Kmap\u PC
的信息保存在其他数组
a
I0
k\u l\u th
中。前两个形状为
(X,Y)
,第三个形状为
(nl,nl)
。观察到的空间和缩小的空间之间的转换由EingInvertors
E
(shape
(q,nl)
)进行。请注意
NL
NL

Kmap\u PC
的空间元素计算如下:

Kmap_PC[..., X, Y] = E.dot(
    KK_l_obs[I0[X, Y]: I0[X, Y] + nl,
             I0[X, Y]: I0[X, Y] + nl] / a_map[X, Y] + \
    k_l_th).dot(E.T)
理论上,第一个点积中的位可以使用
np.einsum
直接计算,但会占用数百GB的内存。我现在正在做的是循环遍历
Kmap_PC
的空间索引,这相当慢。我还可以使用MPI来分发计算(这可能会带来3-4倍的加速,因为我有16个内核可用)

我想知道:

(a) 如果我能更有效地进行计算——也许可以明确地将其分解为空间元素组;及

(b) 如果我能改善这些计算的内存开销

代码片段

import numpy as np
np.random.seed(1)

X = 10
Y = 10
NL = 3000
nl = 1000
q = 7

a_map = 5. * np.random.rand(X, Y)
E = np.random.randn(q, nl)

# construct constant component
m1_ = .05 * np.random.rand(nl, nl)
k_l_th = m1_.dot(m1_)

# construct variable component
m2_ = np.random.rand(NL, NL)
KK_l_obs = m2_.dot(m2_.T)

# where to start in big cov
I0 = np.random.randint(0, NL - nl, (X, Y))

# the slow way
def looping():
    K_PC = np.empty((q, q, X, Y))
    inds = np.ndindex((X, Y))

    for si in inds:
        I0_ = I0[si[0], si[1]]
        K_PC[..., si[0], si[1]] = E.dot(
            KK_l_obs[I0_ : I0_ + nl, I0_ : I0_ + nl] / a_map[si[0], si[1]] + k_l_th).dot(E.T)

    return K_PC

def veccalc():
    nl_ = np.arange(nl)[..., None, None]
    I, J = np.meshgrid(nl_, nl_)

    K_s = KK_l_obs[I0[..., None, None] + J, I0[..., None, None] + I]
    K_s = K_s / a_map[..., None, None] + k_l_th[None, None, ...]
    print(K_s.nbytes)

    K_PC = E @ K_s @ E.T
    K_PC = np.moveaxis(K_PC, [0, 1], [-2, -1])

    return K_PC
调整#1 在NumPy中,一个非常简单的性能调整通常被忽略,那就是避免使用除法和乘法。在处理相同形状的阵列时,处理标量到标量或阵列到阵列的分割时,这一点不明显。但是NumPy的隐式广播对于允许在不同形状的数组之间或数组和标量之间广播的分区来说非常有趣。对于那个些情况,我们可以通过和倒数相乘得到明显的提升。因此,对于所述问题,我们将预先计算
a_map
的倒数,并将其用于乘法而不是除法

因此,在开始时,请执行以下操作:

r_a_map = 1.0/a_map
然后,在嵌套循环中,将其用作:

KK_l_obs[I0_ : I0_ + nl, I0_ : I0_ + nl] * r_a_map[si[0], si[1]]
调整#2 我们可以使用相乘的
相联的
性质:

A*(B + C) = A*B + A*C
因此,在所有迭代中求和但保持不变的
k_l_th
可以在循环之外进行,并在退出嵌套循环后求和。它的有效总和是:
E.dot(k\u l\u th).dot(E.T)
。因此,我们将此添加到
K_PC


最后确定和基准 使用tweak#1和tweak#2,我们最终会得到一个修改的方法,如下所示-

def original_mod_app():
    r_a_map = 1.0/a_map
    K_PC = np.empty((q, q, X, Y))
    inds = np.ndindex((X, Y))
    for si in inds:
        I0_ = I0[si[0], si[1]]
        K_PC[..., si[0], si[1]] = E.dot(
            KK_l_obs[I0_ : I0_ + nl, I0_ : I0_ + nl] * \
            r_a_map[si[0], si[1]]).dot(E.T)
    return K_PC + E.dot(k_l_th).dot(E.T)[:,:,None,None]
使用与问题中使用的相同示例设置进行运行时测试-

In [458]: %timeit original_app()
1 loops, best of 3: 1.4 s per loop

In [459]: %timeit original_mod_app()
1 loops, best of 3: 677 ms per loop

In [460]: np.allclose(original_app(), original_mod_app())
Out[460]: True

因此,我们在那里获得了
2x+
的加速。

在一台相对适中的机器(4G内存)上,整个10x1000x1000空间的matmul计算工作

def looping2(n=2):
    ktemp = np.empty((n,n,nl,nl))
    for i,j in np.ndindex(ktemp.shape[:2]):
        I0_ = I0[i, j]
        temp = KK_l_obs[I0_ : I0_ + nl, I0_ : I0_ + nl]
        temp = temp / a_map[i,j] + k_l_th
        ktemp[i,j,...] = temp
    K_PC = E @ ktemp @ E.T      
    return K_PC

K = loop()
k4 = looping2(n=X)
np.allclose(k4, K.transpose(2,3,0,1))  # true
我还没有尝试对
IO\uu
映射进行矢量化。我的重点是推广双点积

等效的
einsum
为:

K_PC = np.einsum('ij,...jk,lk->il...', E, ktemp, E) 
这会产生一个
ValueError:iterator对于n=7来说太大了

但最新版本

K_PC = np.einsum('ij,...jk,lk->il...', E, ktemp, E, optimize='optimal')
适用于全7x7x10输出

时间安排并不乐观。2.2秒用于原始
循环
,3.9秒用于大型matmul(或einsum)。(我使用
原装的
应用程序获得了相同的2倍加速比)

============

构建(10,1010001000)数组的时间(迭代):

用@(比施工时间长)将其减少到(10,10,7,7)的时间

相同两个操作的时间,但在循环中减少

In [33]: %%timeit 
    ...:     ktemp = np.empty((n,n,q,q))
    ...:     for i,j in np.ndindex(ktemp.shape[:2]):
    ...:         I0_ = I0[i, j]
    ...:         temp = KK_l_obs[I0_ : I0_ + nl, I0_ : I0_ + nl]
    ...:         ktemp[i,j,...] = E @ temp @ E.T

1 loop, best of 3: 858 ms per loop

在循环中执行点积可以减小保存到
ktemp
的子阵列的大小,从而弥补计算成本。大数组上的点操作本身比循环更昂贵。即使我们可以“矢量化”
KK_l_obs[I0:I0+nl,I0:I0+nl]
它也无法弥补处理如此大数组的成本。

主题行有误导性,听起来像是你从多个
队列创建了一个数组。这是一个很大的
dot
产品问题,
E.dot(a).dot(E.T)
。我想看看
einsum
表达式,以及一个小测试用例,我可以用简单的复制粘贴运行它。仅仅通过阅读您的描述就很难理解计算。只是添加了一个具有循环实现的示例,并且数据维度相对较小。现在使用基于
einsum
的示例,使用这些数字,您可以制作100个双点产品,涉及
(71000)@(10001000)@(1000,7)=>(7,7)
。如果我可以做
I0
映射(处理索引和内存大小),那么最大的问题就是
(71000)@(10,1010001000)@(1000,7)->(10,10,7,7)
我已经处理了
I0
映射。基本上,问题是当X、Y接近70左右时;随着
NL
NL
接近3000和4000(这更接近我真正的问题所在),中间矩阵
K_s
变得非常大。将
r_a_map
的乘法也拉到循环的末尾是否可能/有利?@DathosPachy我已经尝试过了,在我的末尾有一个完全矢量化的版本,但速度较慢,所以不上传:)接受这个答案,因为它提供了相当可观的性能改进。我还分析了我的代码片段,发现矢量化示例并没有加快速度……我还见过其他情况,在较小的点积上进行少量迭代比一次大计算要快。如果迭代计数相对于计算总数较小,则迭代开销较小。我怀疑内存管理问题会降低大型计算的速度。因此,对于您的循环,我们会这样做
In [32]: timeit E @ ktemp @ E.T
1 loop, best of 3: 1.17 s per loop
In [33]: %%timeit 
    ...:     ktemp = np.empty((n,n,q,q))
    ...:     for i,j in np.ndindex(ktemp.shape[:2]):
    ...:         I0_ = I0[i, j]
    ...:         temp = KK_l_obs[I0_ : I0_ + nl, I0_ : I0_ + nl]
    ...:         ktemp[i,j,...] = E @ temp @ E.T

1 loop, best of 3: 858 ms per loop