PyTorch - contíguo ()

94

Eu estava examinando este exemplo de um modelo de linguagem LSTM no github (link) . O que ele faz em geral é bastante claro para mim. Mas ainda estou lutando para entender o que a chamada contiguous()faz, o que ocorre várias vezes no código.

Por exemplo, na linha 74/75 da entrada de código e as sequências de destino do LSTM são criadas. Os dados (armazenados em ids) são bidimensionais, onde a primeira dimensão é o tamanho do lote.

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())

Portanto, como um exemplo simples, ao usar os tamanhos de lote 1 e seq_length10 inputse se targetsparecer com isto:

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]

Então, em geral, minha pergunta é: o que é contiguous()e por que preciso disso?

Além disso, não entendo por que o método é chamado para a sequência de destino e não a sequência de entrada, pois ambas as variáveis ​​são compostas dos mesmos dados.

Como pode targetsser não contíguo e inputsainda assim ser contíguo?

EDIT: Tentei deixar de ligar contiguous(), mas isso leva a uma mensagem de erro ao calcular a perda.

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

Então, obviamente, chamar contiguous()neste exemplo é necessário.

(Para manter isso legível, evitei postar o código completo aqui, ele pode ser encontrado usando o link do GitHub acima.)

Desde já, obrigado!

MBT
fonte
um título mais descritivo seria útil. Eu sugiro que você melhore o título ou pelo menos escreva um tldr; to the point summarycom um resumo conciso e direto ao ponto.
Charlie Parker de
postado cruzado
Charlie Parker

Respostas:

193

Existem poucas operações no Tensor em PyTorch que não mudam realmente o conteúdo do tensor, mas apenas como converter índices em tensor para localização de byte. Essas operações incluem:

narrow(), view(), expand()Etranspose()

Por exemplo: quando você chama transpose(), o PyTorch não gera um novo tensor com o novo layout, ele apenas modifica as metainformações no objeto Tensor para que o deslocamento e o passo sejam para a nova forma. O tensor transposto e o tensor original estão de fato compartilhando a memória!

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

É aqui que entra o conceito de contíguo . Acima xé contíguo, mas ynão porque seu layout de memória seja diferente de um tensor de mesmo formato feito do zero. Observe que a palavra "contíguo" é um pouco enganosa porque não é que o conteúdo do tensor esteja espalhado em torno de blocos desconectados de memória. Aqui, os bytes ainda são alocados em um bloco de memória, mas a ordem dos elementos é diferente!

Quando você chama contiguous(), ele realmente faz uma cópia do tensor, de forma que a ordem dos elementos seria a mesma como se o tensor da mesma forma fosse criado do zero.

Normalmente você não precisa se preocupar com isso. Se o PyTorch espera um tensor contíguo, mas se não, você obterá RuntimeError: input is not contiguouse apenas adicionará uma chamada a contiguous().

Shital Shah
fonte
Acabei de encontrar isso de novo. Sua explicação é muito boa! Eu só me pergunto: se os blocos na memória não estão amplamente espalhados, qual é o problema com um layout de memória que é "diferente de um tensor de mesmo formato feito do zero" ? Por que ser contíguo é apenas um requisito para algumas operações?
MBT
4
Eu não posso responder isso definitivamente, mas meu palpite é que parte do código PyTorch usa implementação vetorizada de alto desempenho das operações implementadas em C ++ e este código não pode usar deslocamento / avanços arbitrários especificados nas metainformações do Tensor. Isso é apenas um palpite.
Shital Shah
1
Por que o receptor não poderia simplesmente ligar contiguous()sozinho?
information_interchange
muito possivelmente, porque você não quer isso de uma forma contígua, e é sempre bom ter controle sobre o que você faz.
shivam13juna
2
Outra operação de tensor popular é permute, que também pode retornar tensores não "contíguos".
Oleg
32

Da [documentação do pytorch] [1]:

contíguo () → Tensor

Returns a contiguous tensor containing the same data as self 

tensor. Se autotensor for contíguo, esta função retornará o autotensor.

Onde contiguousaqui significa não apenas contíguo na memória, mas também na mesma ordem na memória que a ordem dos índices: por exemplo, fazer uma transposição não muda os dados na memória, simplesmente muda o mapa de índices para ponteiros de memória, se você então aplicá- contiguous()lo mudará os dados na memória de forma que o mapa dos índices para a localização da memória seja o canônico. [1]: http://pytorch.org/docs/master/tensors.html

patapouf_ai
fonte
1
Obrigado pela sua resposta! Você pode me dizer por que / quando preciso que os dados sejam contíguos? É apenas desempenho ou algum outro motivo? O PyTorch requer dados contíguos para algumas operações? Por que os alvos precisam ser contíguos e as entradas não?
MBT de
É apenas para desempenho. Não sei por que os códigos fazem isso para alvos, mas não para entradas.
patapouf_ai
2
Então, aparentemente, o pytorch requer que os alvos na perda sejam contíguos na memória, mas as entradas de neuralnet não precisam satisfazer esse requisito.
patapouf_ai
2
Muito obrigado! Acho que isso faz sentido para mim, percebi que contíguo () também é aplicado aos dados de saída (que era, naturalmente, anteriormente a entrada) na função de avanço, portanto, as saídas e os destinos são contíguos ao calcular a perda. Muito obrigado!
MBT
1
@patapouf_ai Não. Sua explicação está incorreta. Como apontado na resposta correta, não se trata de blocos contíguos de memória.
Akaisteph7 01 de
14

tensor.contiguous () criará uma cópia do tensor, e o elemento na cópia será armazenado na memória de forma contígua. A função contiguous () é normalmente necessária quando primeiro transpomos () um tensor e depois o remodelamos (visualizamos). Primeiro, vamos criar um tensor contíguo:

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

O stride () return (3,1) significa que: ao percorrer a primeira dimensão a cada passo (linha por linha), precisamos mover 3 passos na memória. Ao mover ao longo da segunda dimensão (coluna por coluna), precisamos mover 1 passo na memória. Isso indica que os elementos no tensor são armazenados de forma contígua.

Agora tentamos aplicar as funções de vir ao tensor:

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

Ok, podemos descobrir que transpose (), narrow () e fatiamento de tensor, e expand () farão com que o tensor gerado não seja contíguo. Curiosamente, repeat () e view () não o tornam descontínuo. Portanto, agora a pergunta é: o que acontece se eu usar um tensor descontíguo?

A resposta é que a função view () não pode ser aplicada a um tensor descontíguo. Provavelmente, isso ocorre porque view () requer que o tensor seja armazenado de forma contígua para que possa fazer uma remodelagem rápida na memória. por exemplo:

bbb.view(-1,3)

obteremos o erro:

---------------------------------------------------------------------------
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

Para resolver isso, basta adicionar contiguous () a um tensor descontíguo, para criar uma cópia contígua e, em seguida, aplicar view ()

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

Como na resposta anterior, contigous () aloca blocos de memória contíguos , será útil quando estivermos passando tensor para código de back-end c ou c ++ onde tensores são passados ​​como ponteiros

p. vignesh
fonte
3

As respostas aceitas foram tão boas, e tentei enganar o transpose()efeito da função. Criei as duas funções que podem verificar o samestorage()e o 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")

Eu verifiquei e obtive este resultado como uma tabela:

funções

Você pode revisar o código do verificador abaixo, mas vamos dar um exemplo quando o tensor não é contíguo . Não podemos simplesmente chamar view()esse tensor, precisaríamos reshape()disso ou também poderíamos chamar .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)

Além disso, há métodos que criam tensores contíguos e não contíguos no final. Existem métodos que podem operar em um mesmo armazenamento , e alguns métodos flip()que criarão um novo armazenamento (leia-se: clonar o tensor) antes do retorno.

O código do verificador:

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) 
Prosti
fonte
0

Pelo que entendi, esta é uma resposta mais resumida:

Contíguo é o termo usado para indicar que o layout da memória de um tensor não se alinha com seus metadados ou informações de forma anunciados.

Na minha opinião, a palavra contíguo é um termo confuso / enganoso, pois em contextos normais significa quando a memória não está espalhada em blocos desconectados (ou seja, seu "contíguo / conectado / contínuo").

Algumas operações podem precisar dessa propriedade contígua por algum motivo (mais provavelmente, eficiência em gpu, etc.).

Observe que .viewé outra operação que pode causar esse problema. Veja o seguinte código que corrigi simplesmente chamando contíguo (em vez do típico problema de transposição que o causa, aqui está um exemplo que causa quando um RNN não está satisfeito com sua entrada):

        # 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))

Erro que costumava obter:

RuntimeError: rnn: hx is not contiguous


Fontes / recursos:

Charlie Parker
fonte