【问题标题】:PyTorch - What does contiguous() do?PyTorch - contiguous() 做什么?
【发布时间】:2018-08-01 14:50:39
【问题描述】:

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

【问题讨论】:

标签: python memory pytorch contiguous


【解决方案1】:

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

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

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

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

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

【讨论】:

  • 我无法明确回答这个问题,但我的猜测是某些 PyTorch 代码使用 C++ 中实现的操作的高性能矢量化实现,并且此代码不能使用 Tensor 元信息中指定的任意偏移量/步幅.不过这只是猜测。
  • 为什么被调用者不能自己调用​​contiguous()
  • 另一个流行的张量运算是permute,它也可能返回非“连续”张量。
  • 什么情况下我们确实需要致电contiguous
  • @CharlieParker 我也想知道需要contiguous 的情况。我将此作为问题发布here
【解决方案2】:

来自pytorch documentation

contiguous() → Tensor
返回包含与 self 相同数据的连续张量 张量。如果 self 张量是连续的,则此函数返回 self 张量。

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

【讨论】:

  • 感谢您的回答!你能告诉我为什么/何时我需要数据是连续的吗?只是性能,还是其他原因? PyTorch 是否需要某些操作的连续数据?为什么目标需要连续而输入不需要?
  • 所以显然pytorch要求损失中的目标在内存中是连续的,但是神经网络的输入不需要满足这个要求。
  • 非常感谢!我认为这对我来说很有意义,我注意到 contiguous() 也应用于前向函数中的输出数据(当然以前是输入),因此在计算损失时输出和目标都是连续的。非常感谢!
  • 数据何时不在连续块中?为什么它的存储方式很重要?
  • @CharlieParker 如果你不使用它并且你需要它,torch 会给你一个你需要的错误。一些张量运算需要连续的张量,但不是全部。
【解决方案3】:

tensor.contiguous() 将创建张量的副本,副本中的元素将以连续的方式存储在内存中。 当我们首先转置()张量然后重塑(查看)它时,通常需要 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.]])

【讨论】:

  • 如果 contiguous 创建一个副本,是否会阻止 autodiff 通过原始张量计算梯度?
  • 这应该是公认的答案。它给出了为什么需要连续的原因。
【解决方案4】:

正如上一个答案 contigous() 分配 连续内存块,当我们 将张量传递给张量所在的 c 或 c++ 后端代码时会很有帮助作为指针传递

【讨论】:

    【解决方案5】:

    接受的答案太棒了,我试图欺骗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)
    

    另外需要注意的是,有一些方法最终会创建 contiguousnon contiguous 张量。有一些方法可以对相同的存储进行操作,还有一些方法如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)
    

    【讨论】:

    • 从 torch.narrow 返回并不总是连续的。这取决于维度In [145]: x = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) In [146]: torch.narrow(x, 1, 1, 2).is_contiguous() Out[146]: False
    • @Saibō,随时更新此提示。
    【解决方案6】:

    如果一维数组 [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)
    

    【讨论】:

      【解决方案7】:

      一个张量,其值从最右边的维度开始排列在存储中(即,对于二维张量,沿行移动)被定义为contiguous连续张量很方便,因为我们可以有效地按顺序访问它们,而无需在存储中四处乱跳(由于内存访问在现代 CPU 上的工作方式,提高数据局部性可以提高性能)。这种优势当然取决于算法访问的方式。

      PyTorch 中的一些张量操作仅适用于连续张量,例如view,[...]。在这种情况下,PyTorch 将抛出一个信息异常并要求我们显式调用 contiguous。值得注意的是,如果张量已经是连续的,则调用 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

      【讨论】:

        【解决方案8】:

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

        连续是用于表示张量的内存布局与其宣传的元数据或形状信息不一致的术语。

        在我看来,连续这个词是一个令人困惑/误导性的术语,因为在正常情况下,它意味着当内存没有散布在断开的块中时(即它的“连续/连接/连续”)。

        某些操作可能出于某种原因需要此连续属性(很可能是 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
        
        

        来源/资源:

        【讨论】:

          猜你喜欢
          • 2020-05-30
          • 2018-12-28
          • 1970-01-01
          • 2019-12-05
          • 2017-12-21
          • 2020-10-14
          • 2021-07-31
          • 2020-04-03
          • 2016-10-02
          相关资源
          最近更新 更多