Python torch/np einsum在内部是如何工作的

Python torch/np einsum在内部是如何工作的,python,numpy,pytorch,numpy-einsum,Python,Numpy,Pytorch,Numpy Einsum,这是一个关于GPU中torch.einsum的内部工作的查询。我知道如何使用einsum。它是执行所有可能的矩阵乘法,然后只选择相关的矩阵乘法,还是只执行所需的计算 例如,考虑两个张量 < > b>代码>,形状(n,p)< /代码>,我希望找到每个对应张量 Ni 的点积,形状(1,p)< /代码>。 使用einsum,代码为: torch.einsum('ij,ij->i',a,b) 在不使用einsum的情况下,获得输出的另一种方法是: torch.diag(a @ b.t())

这是一个关于GPU中
torch.einsum
的内部工作的查询。我知道如何使用
einsum
。它是执行所有可能的矩阵乘法,然后只选择相关的矩阵乘法,还是只执行所需的计算

例如,考虑两个张量<代码> < <代码> > <代码> b>代码>,形状<代码>(n,p)< /代码>,我希望找到每个对应张量<代码> Ni <代码>的点积,形状<代码>(1,p)< /代码>。 使用einsum,代码为:

torch.einsum('ij,ij->i',a,b)
在不使用einsum的情况下,获得输出的另一种方法是:

torch.diag(a @ b.t())
现在,第二个代码应该比第一个代码执行更多的计算(例如,如果
N
=
2000
,则它执行的计算量是第一个代码的
2000
倍)。然而,当我尝试对这两个操作计时时,它们完成所需的时间大致相同,这就引出了一个问题。
einsum
是否执行所有组合(如第二个代码),并选择相关值

要测试的示例代码:

import time
import torch
for i in range(100):
  a = torch.rand(50000, 256).cuda()
  b = torch.rand(50000, 256).cuda()

  t1 = time.time()
  val = torch.diag(a @ b.t())
  t2 = time.time()
  val2 = torch.einsum('ij,ij->i',a,b)
  t3 = time.time()
  print(t2-t1,t3-t2, torch.allclose(val,val2))

这可能与GPU可以并行计算
a@b.t()
有关。这意味着GPU实际上不必等待每一行-列乘法计算完成,然后再计算下一次乘法。
如果你检查CPU,你会发现
torch.diag(a@b.t())
要比
torch.einsum('ij,ij->i',a,b)
慢得多,对于大型
a
b
我不能代表
torch
,但几年前我曾详细地与
np.einsum
合作过。然后,它基于索引字符串构造了一个自定义迭代器,只进行必要的计算。从那时起,它就以各种方式进行了修改,并在可能的情况下将问题转化为
@
,从而利用BLAS(etc)库调用

In [147]: a = np.arange(12).reshape(3,4)
In [148]: b = a

In [149]: np.einsum('ij,ij->i', a,b)
Out[149]: array([ 14, 126, 366])
我不能确定在这种情况下使用什么方法。通过“j”求和,也可以通过以下方式完成:

In [150]: (a*b).sum(axis=1)
Out[150]: array([ 14, 126, 366])
正如您所注意到的,最简单的
创建了一个更大的数组,我们可以从中提取对角线:

In [151]: (a@b.T).shape
Out[151]: (3, 3)
但这不是使用
@
的正确方法
@
通过提供高效的“批量”处理,扩展了
np.dot
。因此,
i
维度是第一批,而
j
维度是
dot
维度

In [152]: a[:,None,:]@b[:,:,None]
Out[152]: 
array([[[ 14]],

       [[126]],

       [[366]]])
In [156]: (a[:,None,:]@b[:,:,None])[:,0,0]
Out[156]: array([ 14, 126, 366])
换言之,它是使用(3,1,4)和(3,4,1)来产生(3,1,1),在共享尺寸4维上做乘积之和

一些采样时间:

In [162]: timeit np.einsum('ij,ij->i', a,b)
7.07 µs ± 89.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [163]: timeit (a*b).sum(axis=1)
9.89 µs ± 122 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [164]: timeit np.diag(a@b.T)
10.6 µs ± 31.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [165]: timeit (a[:,None,:]@b[:,:,None])[:,0,0]
5.18 µs ± 197 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

因此,只要有可能,最好使用
einsum
提高计算(和时间)效率,我可以相信einsum只执行必要的计算?(就我而言,我通常需要处理4D阵列并执行各种此类操作)一般来说,
torch.einsum
在内存和时间上不一定是最有效的(请参阅)。有一些像opt_einsum(请参阅)这样的项目可以为您提供更高效的实现。我会查看它们。