用Python加速一个变量相关矩阵的逆运算

mwkjh3gx  于 2023-01-10  发布在  Python
关注(0)|答案(1)|浏览(199)

我有一个迭代问题,在每次迭代中,我需要执行一系列矩阵运算,我希望加快这些运算的速度。除了一个变量外,大多数计算都涉及常量变量。我的问题是:有没有可能建立一个这个操作的“ backbone ”,以加快第一次迭代之后的计算速度?(有点类似于CVXPY中优化问题的参数热启动).我需要处理的代码目前在一个类的专用函数中,它是这样的:

# some constants definitions (in a class)
m = 5501
Y = np.random.rand(10, m)
U = np.random.rand(75, m)
sig_on = 0.15
sig_off = 0.25
a = 15
L = 25

def obtain_A_B(x):
   # for readability I omit internal variable declarations as a = self.a
   varying_part = (a * sig_on / np.linalg.norm(x)) ** 2 + L * sig_off
   F = varying_part * np.identity(m) + Y.T @ Y
   Fi = np.linalg.inv(F)
   UFiUTi = np.linalg.inv(U @ Fi @ U.T)
   A = (Fi - Fi @ U.T @ UFiUTi  @ U @ Fi) @ Y.T
   B = Fi @ U.T @ UFiUTi 
   return A, B

x = np.random.rand(10)
result = obtain_A_B(x)

我对加速A和B的计算很感兴趣,而'varying_part'返回一个标量,该标量在给定x的情况下每次迭代时都会发生变化。由于这里真正耗时的部分是计算逆Fi,因此加快计算速度将是一个很大的帮助。
我已经将求逆函数移到了专用变量中(如上所述),以减少需要计算的次数--这将执行时间从约41秒降低到约10秒--但如果我能使执行速度更快就更好了(这就是“操作 backbone ”想法的由来)。

sz81bmfz

sz81bmfz1#

基本优化

首先,在A中使用B的表达式,因此可以首先计算B并在A的表达式中重用B
此外,varying_part * np.identity(m) + Y.T @ Y可以更有效地计算。事实上,varying_part * np.identity(m)创建了2个很大的临时数组,填充速度很慢。这是因为现在的RAM与CPU的性能相比很慢(参见“内存墙”)。您可以只更新数组的对角线。
此外,计算B @ (U @ Fi)比计算(B @ U) @ Fi(等价于B @ U @ Fi)要快得多,而两者在分析上是等价的,因为矩阵乘法是 associative(尽管在数值上有点不同)。
下面是优化的实施:

def obtain_A_B_opt(x):
   varying_part = (a * sig_on / np.linalg.norm(x)) ** 2 + L * sig_off
   F = Y.T @ Y
   np.fill_diagonal(F, F.diagonal()+varying_part)
   Fi = np.linalg.inv(F)
   tmp = Fi @ U.T
   UFiUTi = np.linalg.inv(U @ tmp)
   B = tmp @ UFiUTi 
   A = (Fi - B @ (U @ Fi)) @ Y.T
   return A, B

这种优化使我的机器上的代码速度提高了55%,将近90%的时间花在了np.linalg.inv(F)函数调用上。
坏消息是,矩阵求逆很难在CPU上优化。在Numpy中用于执行此计算的默认OpenBLAS库是次优的,但它相对较好,因为它使用相对优化的本地C代码并行运行。此外,更改对角线会严重影响计算,所以我认为不可能编写逆的增量计算。实际上,将F[0,0]增加1使得在高斯-乔丹算法中强制重新计算所有线的权重t0(并且因此所有随后的权重计算)。
如果不降低浮点精度或算法的准确性(例如近似值),很难编写出更快的代码。如果您有服务器GPU,或者降低精度对您来说没什么问题,GPU会有很大帮助。

基于GPU的实现

我们可以使用GPU来提高目标平台和输入浮点类型的代码速度。离散GPU往往比主流CPU具有更大的计算能力。然而,对于主流PC GPU上的双精度来说,这并不正确。事实上,PC GPU针对单精度浮点数据进行了优化(通常用于游戏和3D应用程序)。为了实现快速的双精度计算,需要服务器端GPU。此外,需要注意的是,CPU和GPU之间的数据传输非常昂贵。但矩阵求逆是瓶颈,它在O(N**3)中运行,因此数据传输可以忽略不计。Cupy可以用来轻松编写GPU实现(适用于Nvidia GPU)。以下是生成的GPU代码:

import cupy as cp

def obtain_A_B_gpu_opt(x):
   varying_part = (a * sig_on / np.linalg.norm(x)) ** 2 + L * sig_off

   # Send data to the GPU
   device_Y = cp.array(Y)
   device_U = cp.array(U)

   # GPU-based computation
   F = device_Y.T @ device_Y
   cp.fill_diagonal(F, F.diagonal()+varying_part)
   Fi = cp.linalg.inv(F)
   tmp = Fi @ device_U.T
   UFiUTi = cp.linalg.inv(device_U @ tmp)
   B = tmp @ UFiUTi 
   A = (Fi - B @ (device_U @ Fi)) @ device_Y.T

   # Get data back from the GPU
   return A.get(), B.get()

注意,Cupy在第一次执行时可能会非常慢,但之后应该会快得多(这对于这里的迭代循环来说是很好的)。

实验结果

以下是在我的机器上使用i5- 9600 KF CPU和Nvidia 1660超级GPU(主流独立PC GPU)的结果:

double-precision:
 - obtain_A_B:          4.10 s
 - obtain_A_B_opt:      2.64 s
 - obtain_A_B_gpu_opt:  3.03 s

simple-precision:
 - obtain_A_B:          3.17 s
 - obtain_A_B_opt:      2.52 s
 - obtain_A_B_gpu_opt:  0.28 s  <----

我们可以看到,基于GPU的实现在处理简单精度浮点输入时明显更快。这是因为我的GPU为此进行了优化。在计算服务器上,obtain_A_B_gpu_opt应该比其他实现快得多。请注意,cp.linalg.inv在我的GPU上仍然占用了90%以上的时间。

相关问题