PyTorch-contiguous()


90

我正在通过github (link)上的LSTM语言模型示例进行研究。对我来说,它的一般功能非常​​清楚。但是我仍在努力理解调用的contiguous()作用,这在代码中多次发生。

例如,在代码的第74/75行中,创建了LSTM的输入和目标序列。数据(存储在中ids)为二维,其中第一维为批处理大小。

for i in range(0, ids.size(1) - seq_length, seq_length):
    # Get batch inputs and targets
    inputs = Variable(ids[:, i:i+seq_length])
    targets = Variable(ids[:, (i+1):(i+1)+seq_length].contiguous())

举一个简单的例子,当使用批处理大小1和seq_length10时inputstargets如下所示:

inputs Variable containing:
0     1     2     3     4     5     6     7     8     9
[torch.LongTensor of size 1x10]

targets Variable containing:
1     2     3     4     5     6     7     8     9    10
[torch.LongTensor of size 1x10]

因此,总的来说,我的问题是,contiguous()我需要什么,为什么需要它?

此外,我不明白为什么要针对目标序列而不是针对输入序列调用此方法,因为两个变量都包含相同的数据。

怎么可能targets不连续并且inputs仍然连续?

编辑: 我试图省略调用contiguous(),但这在计算损失时会导致错误消息。

RuntimeError: invalid argument 1: input is not contiguous at .../src/torch/lib/TH/generic/THTensor.c:231

因此,显然contiguous()必须在此示例中进行调用。

(为了保持可读性,我避免在此处发布完整的代码,可以使用上面的GitHub链接找到它。)

提前致谢!


一个更具描述性的标题将很有用。我建议您改善标题或至少写tldr; to the point summary一个简洁的要点摘要。
查理·帕克


Answers:


186

在PyTorch中,对Tensor进行的操作很少真正改变张量的内容,而仅改变如何将张量的索引转换为字节位置。这些操作包括:

narrow()view()expand()transpose()

例如:当您调用时transpose(),PyTorch不会生成具有新布局的新张量,它只是修改Tensor对象中的元信息,因此偏移和步幅是用于新形状的。转置张量和原始张量确实共享内存!

x = torch.randn(3,2)
y = torch.transpose(x, 0, 1)
x[0, 0] = 42
print(y[0,0])
# prints 42

这就是引入连续的概念的地方。上面x是连续的,但这y并不是因为其内存布局不同于从头开始的相同形状的张量。请注意,“连续”一词有点误导,因为它不是张量的内容散布在未连接的内存块周围。这里字节仍然分配在一个内存块中,但是元素的顺序不同!

当您调用时contiguous(),它实际上会复制张量,因此元素的顺序将与从头开始创建相同形状的张量相同。

通常,您无需为此担心。如果PyTorch期望连续张量,但如果不是,那么您将得到RuntimeError: input is not contiguous,然后只需向调用即可contiguous()


我只是再次遇到了这个。您的解释很好!我只是想知道:如果内存中的块没有广泛散布,那么“不同于从头开始制作的相同形状的张量”的内存布局又有什么问题呢?为什么某些操作仅是连续的要求?
MBT

4
我无法确切地回答这个问题,但我的猜测是,某些PyTorch代码使用C ++中实现的操作的高性能矢量化实现,并且该代码无法使用Tensor的元信息中指定的任意偏移/步幅。不过,这只是一个猜测。
Shital Shah

1
为什么被叫者不能简单地contiguous()自己打电话呢?
information_interchange

很有可能,因为您不希望以连续的方式进行操作,因此控制自己的工作总是很高兴的。
shivam13juna

2
另一个流行的张量运算是permute,它也可能返回非“连续”的张量。
奥列格

31

从[pytorch文档] [1]:

contiguous()→张量

Returns a contiguous tensor containing the same data as self 

张量。如果自张量是连续的,则此函数返回自张量。

contiguous这里,这不仅意味着在内存中是连续的,而且在内存中的顺序也与索引顺序相同:例如,进行转置不会更改内存中的数据,它只是将映射从索引更改为内存指针,如果您应用contiguous()它会更改内存中的数据,以便从索引到内存位置的映射是规范的映射。[1]:http//pytorch.org/docs/master/tensors.html


1
谢谢您的回答!您能告诉我为什么/何时需要连续数据吗?仅仅是性能还是其他原因?PyTorch是否需要连续数据才能进行某些操作?为什么目标需要是连续的而不是输入?
MBT

这只是为了性能。我不知道为什么代码针对目标而不针对输入。
patapouf_ai

2
因此,显然pytorch要求损失中的目标在内存中是连续的,但神经网络的输入不需要满足此要求。
patapouf_ai

2
太好了谢谢!我认为这对我来说很有意义,我注意到contiguous()也被应用到前向函数中的输出数据(当然以前是输入),因此在计算损失时输出和目标都是连续的。非常感谢!
MBT

1
@patapouf_ai否。您对此的解释不正确。正如正确答案中指出的那样,它根本不是关于连续的内存块。
Akaisteph7

14

tensor.contiguous()将创建张量的副本,并且副本中的元素将以连续方式存储在内存中。当我们首先对张量进行transpose()然后对其进行整形(查看)时,通常需要contiguous()函数。首先,让我们创建一个连续的张量:

aaa = torch.Tensor( [[1,2,3],[4,5,6]] )
print(aaa.stride())
print(aaa.is_contiguous())
#(3,1)
#True

stride()返回(3,1)的意思是:当沿着第一维移动每一步(逐行)时,我们需要在内存中移动3步。沿第二维(逐列)移动时,我们需要在内存中移动1步。这表明张量中的元素是连续存储的。

现在我们尝试将come函数应用于张量:

bbb = aaa.transpose(0,1)
print(bbb.stride())
print(bbb.is_contiguous())

#(1, 3)
#False


ccc = aaa.narrow(1,1,2)   ## equivalent to matrix slicing aaa[:,1:3]
print(ccc.stride())
print(ccc.is_contiguous())

#(3, 1)
#False


ddd = aaa.repeat(2,1)   # The first dimension repeat once, the second dimension repeat twice
print(ddd.stride())
print(ddd.is_contiguous())

#(3, 1)
#True


## expand is different from repeat.
## if a tensor has a shape [d1,d2,1], it can only be expanded using "expand(d1,d2,d3)", which
## means the singleton dimension is repeated d3 times
eee = aaa.unsqueeze(2).expand(2,3,3)
print(eee.stride())
print(eee.is_contiguous())

#(3, 1, 0)
#False


fff = aaa.unsqueeze(2).repeat(1,1,8).view(2,-1,2)
print(fff.stride())
print(fff.is_contiguous())

#(24, 2, 1)
#True

好的,我们可以发现transpose(),narrow()和张量切片以及expand()将使生成的张量不连续。有趣的是,repeat()和view()不会使它不连续。所以现在的问题是:如果我使用不连续张量会怎样?

答案是view()函数不能应用于不连续的张量。这可能是因为view()要求将张量连续存储,以便可以在内存中进行快速整形。例如:

bbb.view(-1,3)

我们将得到错误:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-63-eec5319b0ac5> in <module>()
----> 1 bbb.view(-1,3)

RuntimeError: invalid argument 2: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Call .contiguous() before .view(). at /pytorch/aten/src/TH/generic/THTensor.cpp:203

要解决这个问题,只需将contiguous()添加到不连续的张量中,以创建连续的副本,然后应用view()

bbb.contiguous().view(-1,3)
#tensor([[1., 4., 2.],
        [5., 3., 6.]])

10

就像在前面的答案中contigous()分配连续的内存块一样,当我们将张量传递给c或c ++后端代码(其中张量作为指针传递)时,这将很有帮助


3

公认的答案是如此之大,我试图欺骗transpose()功能效果。我创建了两个可以检查samestorage()和的函数contiguous

def samestorage(x,y):
    if x.storage().data_ptr()==y.storage().data_ptr():
        print("same storage")
    else:
        print("different storage")
def contiguous(y):
    if True==y.is_contiguous():
        print("contiguous")
    else:
        print("non contiguous")

我检查并以表格的形式得到了以下结果:

职能

您可以在下面查看检查器代码,但是当张量不连续时,让我们举一个示例。我们不能简单地调用view()该张量,我们需要reshape()它,也可以调用.contiguous().view()

x = torch.randn(3,2)
y = x.transpose(0, 1)
y.view(6) # RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
  
x = torch.randn(3,2)
y = x.transpose(0, 1)
y.reshape(6)

x = torch.randn(3,2)
y = x.transpose(0, 1)
y.contiguous().view(6)

还要注意,有些方法最终会创建连续非连续的张量。有些方法可以在相同的存储上运行,有些方法flip()会在返回之前创建一个新的存储(请阅读:克隆张量)。

检查代码:

import torch
x = torch.randn(3,2)
y = x.transpose(0, 1) # flips two axes
print("\ntranspose")
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nnarrow")
x = torch.randn(3,2)
y = x.narrow(0, 1, 2) #dim, start, len  
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\npermute")
x = torch.randn(3,2)
y = x.permute(1, 0) # sets the axis order
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nview")
x = torch.randn(3,2)
y=x.view(2,3)
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nreshape")
x = torch.randn(3,2)
y = x.reshape(6,1)
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nflip")
x = torch.randn(3,2)
y = x.flip(0)
print(x)
print(y)
contiguous(y)
samestorage(x,y)

print("\nexpand")
x = torch.randn(3,2)
y = x.expand(2,-1,-1)
print(x)
print(y)
contiguous(y)
samestorage(x,y) 

0

据我了解,这是一个更概括的答案:

连续是一个术语,用于指示张量的内存布局与其广告的元数据或形状信息不对齐。

在我看来,“连续”一词是一个令人困惑/误导性的术语,因为在正常情况下,这意味着内存不以不连续的块形式散布(即其“连续/连接/连续”)。

由于某些原因(某些情况下最有可能提高gpu效率),某些操作可能需要此连续属性。

请注意这.view是另一个操作,可能会导致此问题。看一下我通过简单地调用连续代码(而不是导致它的典型转置问题,这里是一个示例,当RNN对它的输入不满意时会导致的示例)来修复的代码:

        # normal lstm([loss, grad_prep, train_err]) = lstm(xn)
        n_learner_params = xn_lstm.size(1)
        (lstmh, lstmc) = hs[0] # previous hx from first (standard) lstm i.e. lstm_hx = (lstmh, lstmc) = hs[0]
        if lstmh.size(1) != xn_lstm.size(1): # only true when prev lstm_hx is equal to decoder/controllers hx
            # make sure that h, c from decoder/controller has the right size to go into the meta-optimizer
            expand_size = torch.Size([1,n_learner_params,self.lstm.hidden_size])
            lstmh, lstmc = lstmh.squeeze(0).expand(expand_size).contiguous(), lstmc.squeeze(0).expand(expand_size).contiguous()
        lstm_out, (lstmh, lstmc) = self.lstm(input=xn_lstm, hx=(lstmh, lstmc))

我过去得到的错误:

RuntimeError: rnn: hx is not contiguous


来源/资源:

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.