PyTorch - contiguous() 有什么作用?

PyTorch - What does contiguous() do?

我在 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_length 10 时,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()是必要的。

来自pytorch documentation

contiguous() → Tensor
Returns a contiguous tensor containing the same data as self tensor. If self tensor is contiguous, this function returns the self tensor.

这里的contiguous不仅意味着在内存中是连续的,而且在内存中的顺序与索引顺序相同:例如进行转置不会改变内存中的数据,它只是改变了从索引映射到内存指针,如果您随后应用 contiguous() 它将更改内存中的数据,以便从索引到内存位置的映射是规范的。

正如在前面的回答中,contigous() 分配 连续内存块,当我们将张量传递给 c 或 c++ 后端代码时,它会很有帮助 其中张量 作为指针传递

PyTorch 中有一些张量操作不会改变张量的内容,但会改变数据的组织方式。这些操作包括:

narrow(), view(), expand() and 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

这就是 contiguous 概念的用武之地。在上面的示例中,x 是连续的,但 y 不是,因为它的内存布局是不同于从头开始制作的相同形状的张量。请注意,“连续” 这个词有点误导,因为张量的内容并不是散布在不连贯的内存块周围。这里字节还是分配在一块内存中,只是元素的顺序不同!

当您调用 contiguous() 时,它实际上会复制一张张量,使其元素在内存中的顺序与使用相同数据从头开始创建时的顺序相同。

通常你不需要担心这个。你通常可以安全地假设一切都会正常工作,等到你得到一个 RuntimeError: input is not contiguous ,其中 PyTorch 期望一个连续的张量添加对 contiguous().

的调用

tensor.contiguous()会创建张量的一个副本,副本中的元素会以连续的方式存储在内存中。 当我们首先 transpose() 张量然后重塑(查看)它时,通常需要 contiguous() 函数。首先,让我们创建一个连续的张量:

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

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

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

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()和tensor切片,expand()会让生成的tensor不连续。有趣的是,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.]])

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

Contiguous is the term used to indicate that the memory layout of a tensor does not align with its advertised meta-data or shape information.

在我看来,“连续”一词是一个 confusing/misleading 术语,因为在正常情况下,它表示内存未分散在断开连接的块中(即它的 "contiguous/connected/continuous")。

出于某种原因(最有可能是 gpu 等方面的效率),某些操作可能需要这种连续的 属性。

请注意,.view 是另一个可能导致此问题的操作。看看我通过简单地调用 contiguous 修复的以下代码(而不是导致它的典型转置问题,这是一个当 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


Sources/Resource:

采纳的答案太棒了,我试着去欺骗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")

我检查并得到这个结果 table:

您可以查看下面的检查代码,但我们举一个张量 不连续 的例子。我们不能简单地对该张量调用 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)

A tensor whose values are laid out in the storage starting from the rightmost dimension onward (that is, moving along rows for a 2D tensor) is defined as contiguous. Contiguous tensors are convenient because we can visit them efficiently in order without jumping around in the storage (improving data locality improves performance because of the way memory access works on modern CPUs). This advantage of course depends on the way algorithms visit.

Some tensor operations in PyTorch only work on contiguous tensors, such as view, [...]. In that case, PyTorch will throw an informative exception and require us to call contiguous explicitly. It’s worth noting that calling contiguous will do nothing (and will not hurt performance) if the tensor is already contiguous.

请注意,这比计算机科学中“连续”一词的一般用法(即连续和有序)更具体。

例如给定一个张量:

[[1, 2]
 [3, 4]]
Storage in memory PyTorch contiguous? Generally "contiguous" in memory-space?
1 2 3 4 0 0 0
1 3 2 4 0 0 0
1 0 2 0 3 0 4

一个一维数组 [0, 1, 2, 3, 4] 是连续的,如果它的项目在内存中是相邻的,如下所示:

如果存储它的内存区域如下所示,则它不是连续的:

对于二维或更多维数组,项目也必须彼此相邻,但顺序遵循不同的约定。 让我们考虑下面的二维数组:

>>> t = torch.tensor([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]])

内存分配是 C 连续的 如果行像这样彼此相邻存储:

这是 Pytorch 认为连续的。

>>> t.is_contiguous()
True

与数组关联的 stride 属性给出了为获取每个维度中的下一个元素而跳过的字节数

>>> t.stride()
(4, 1)

我们需要跳过 4 个字节才能转到下一行,但只需要一个字节就可以转到同一行中的下一个元素。

如其他答案所述,一些 Pytorch 操作不会更改内存分配,只会更改元数据。

比如转置法。 让我们转置张量:

内存分配没有改变:

但大步前进了:

>>> t.T.stride()
(1, 4)

我们需要跳过 1 个字节以转到下一行,并需要跳过 4 个字节以转到同一行中的下一个元素。张量不再是 C 连续的(实际上是 Fortran 连续的:每一列都存储在彼此旁边)

>>> t.T.is_contiguous()
False

contiguous() 将重新安排内存分配,使张量是 C 连续的:

>>> t.T.contiguous().stride()
(3, 1)