【问题标题】:Numpy element-wise dot product without loop and memory error没有循环和内存错误的 Numpy 逐元素点积
【发布时间】:2018-01-03 16:49:23
【问题描述】:

我正在用 numpy 处理一个简单的问题。我有两个矩阵列表——比如A,B——编码为形状分别为(n,p,q)(n,q,r)的3D数组。

我想计算它们的逐元素点积,即 3D 数组 C 使得 C[i,j,l] = sum A[i,j,:] B[i,:,l]。这在数学上非常简单,但我必须遵循以下规则:

1) 我只能使用 numpy 函数(dottensordoteinsum 等):没有循环和 cie。这是因为我希望它可以在我的 gpu(使用 cupy)上工作,并且循环在它上面很糟糕。我希望在当前设备上进行所有操作。

2) 由于我的数据可能非常大,通常 AB 已经占用了几十 Mb 的内存,我不想构建任何形状比 (n,p,q),(n,q,r),(n,p,r) 更大的项目(没有中间 4D数组必须存储)。

例如,我找到的解决方案 there ,即使用:

C = np.sum(np.transpose(A,(0,2,1)).reshape(n,p,q,1)*B.reshape(n,q,1,r),-3)

在数学上是正确的,但意味着中间创建了一个 (n,p,q,r) 数组,这对我的目的来说太大了。

我也遇到过类似的问题

C = np.einsum('ipq,iqr->ipr',A,B)

我不知道底层的操作和构造是什么,但它总是会导致内存错误。

另一方面,有点天真,比如:

C = np.array([A[i].dot(B[i]) for i in range(n)])

在内存方面似乎还可以,但在我的 gpu 上效率不高:列表似乎是在 CPU 上构建的,并且将其重新分配给 gpu 很慢(如果有一种对 cupy 友好的方式来编写它,它将是一个不错的解决方案!)

感谢您的帮助!

【问题讨论】:

  • n,p,q,r 在您的实际使用案例中的典型值是多少?
  • 希望它可以与n =~ 5000 一起使用,并说p=q=r = 50(但越多越好)。例如,形状为(5000,50,50)cp.array() 在我的gpu 上占用112 Mb(超过2 Gb),这没关系,但如果我必须存储(5000,50,50,50) 数组...
  • 我认为np.einsum('ipq,iqr->ipr',A,B) 最适合这些形状。它会导致内存错误吗?如果是这样,为什么不切块呢?
  • 实际上 gpu 上的einsum 确实很容易产生内存错误......即使使用n = 1000p=q=r=50 它也不起作用(而至少我提到的其他解决方案可以处理这么少的数据)。你把东西切成块是什么意思?不管怎样,我很确定它可以在不存储更大数组的情况下完成...我的意思是,np.dot 不会创建(p,q,r) 数组,那么我为什么要为元素操作这样做呢?...
  • 还有一件事是在初始化输出数组后使用传统循环C,然后使用np.dot 方法。

标签: python arrays numpy gpgpu


【解决方案1】:

你想要numpy.matmul (cupy version here)。 matmul 是一个“广播”矩阵乘法。

我认为人们已经知道 numpy.dot 语义很不稳定,并且需要广播矩阵乘法,但是在 python 获得 @ 运算符之前,引入更改的动力并不大。我看不到dot 有什么用处,但我怀疑更好的语义和使用A @ B 的便利性意味着dot 将随着人们发现新功能和运算符而失宠。

【讨论】:

  • 为什么投反对票?这正是 OP 所要求的:)
  • 不知道。也许他们认为它不能翻译成cupy?
  • 嘿!这正是我一直在寻找的。在初步测试中,它没有引发任何内存错误,而且似乎很容易用于我的目的。我不知道为什么我自己没有得到这个解决方案(以及为什么我读到的其他链接提到了扭曲的解决方案)。非常感谢你 ! (对不起,我不知道谁对你投了反对票,看起来很奇怪)。
【解决方案2】:

您试图避免的迭代方法可能不是那么糟糕。例如,考虑以下时间:

In [51]: A = np.ones((100,10,10))
In [52]: timeit np.array([A[i].dot(A[i]) for i in range(A.shape[0])])
439 µs ± 1.35 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [53]: timeit np.einsum('ipq,iqr->ipr',A,A)
428 µs ± 170 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [54]: timeit A@A
426 µs ± 54.6 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

在这种情况下,所有三个都需要大约相同的时间。

但是我把后面的维度加倍,迭代的方式其实更快:

In [55]: A = np.ones((100,20,20))
In [56]: timeit np.array([A[i].dot(A[i]) for i in range(A.shape[0])])
702 µs ± 1.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [57]: timeit np.einsum('ipq,iqr->ipr',A,A)
1.89 ms ± 1.63 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [58]: timeit A@A
1.89 ms ± 490 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

当我将 20 更改为 30 和 40 时,同样的模式成立。我有点惊讶 matmul 时间与 einsum 如此接近。

我想我可以尝试将这些推到内存限制。我没有花哨的后端来测试这方面。

一旦考虑到内存管理问题,对一个大问题进行少量迭代并不是那么可怕。在 numpy 中,您想要避免的事情是对一个简单任务进行多次迭代。

【讨论】:

  • 我认为问题在于 OP 正在尝试使用 cupy 并且在这种情况下,每次迭代不仅是内存副本,而且是 gpu-memory -> 更昂贵的主内存副本,尤其是相对于您希望从 gpu 操作中看到的加速。
  • 是的,这正是重点 :) 我想避免 gpu-->cpu 旅行 :)
  • 供您参考:我在 GPU (Tesla P100) 上尝试了循环实现与 matmul 相比,对于 n = 5000、p、q、r =,matmul 的运行速度似乎快了大约 1000 倍80,100,120 ,所以是的,那里的循环成本很高!奇怪的是,当在 cpu(使用 numpy)上运行时,matmul 似乎比循环慢一些......(比如 4 秒对 1 秒)。
猜你喜欢
  • 1970-01-01
  • 2022-06-28
  • 1970-01-01
  • 2017-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-23
相关资源
最近更新 更多