PyTorch - contigu ()


90

Je passais par cet exemple de modèle de langage LSTM sur github (lien) . Ce qu'il fait en général est assez clair pour moi. Mais j'ai encore du mal à comprendre ce que fait l'appel contiguous(), ce qui se produit plusieurs fois dans le code.

Par exemple, à la ligne 74/75 du code d'entrée et des séquences cibles du LSTM sont créées. Les données (stockées dans ids) sont bidimensionnelles, la première dimension étant la taille du lot.

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

Donc, à titre d'exemple simple, lorsque vous utilisez la taille de lot 1 et seq_length10 inputset targetsressemble à ceci:

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]

Donc, en général, ma question est la suivante: qu'est-ce que c'est contiguous()et pourquoi en ai-je besoin?

De plus, je ne comprends pas pourquoi la méthode est appelée pour la séquence cible et non pour la séquence d'entrée, car les deux variables sont composées des mêmes données.

Comment pourrait targetsêtre non contigu et inputstoujours contigu?

EDIT: J'ai essayé de ne pas appeler contiguous(), mais cela conduit à un message d'erreur lors du calcul de la perte.

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

Il est donc évidemment contiguous()nécessaire d' appeler cet exemple.

(Pour garder cela lisible, j'ai évité de publier le code complet ici, il peut être trouvé en utilisant le lien GitHub ci-dessus.)

Merci d'avance!


un titre plus descriptif serait utile. Je vous suggère d'améliorer le titre ou au moins d'écrire un tldr; to the point summaryrésumé concis au point.
Charlie Parker


Réponses:


186

Il y a peu d'opérations sur Tensor dans PyTorch qui ne changent pas vraiment le contenu du tenseur, mais seulement comment convertir les indices en tenseur en octet. Ces opérations comprennent:

narrow(), view(), expand()Ettranspose()

Par exemple: lorsque vous appelez transpose(), PyTorch ne génère pas de nouveau tenseur avec une nouvelle disposition, il modifie simplement les méta-informations dans l'objet Tensor afin que le décalage et la foulée soient pour une nouvelle forme. Le tenseur transposé et le tenseur d'origine partagent en effet la mémoire!

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

C'est là que le concept de contigu entre en jeu. Ci x- dessus est contigu mais ce yn'est pas parce que sa disposition de mémoire est différente d'un tenseur de même forme fait à partir de zéro. Notez que le mot «contigu» est un peu trompeur car ce n'est pas que le contenu du tenseur est réparti autour de blocs de mémoire déconnectés. Ici, les octets sont toujours alloués dans un bloc de mémoire mais l'ordre des éléments est différent!

Lorsque vous appelez contiguous(), il fait en fait une copie de tensor afin que l'ordre des éléments soit le même que si un tenseur de même forme était créé à partir de zéro.

Normalement, vous n'avez pas à vous en soucier. Si PyTorch attend un tenseur contigu mais si ce n'est pas le cas, vous obtiendrez RuntimeError: input is not contiguouset vous ajouterez simplement un appel à contiguous().


Je viens de tomber dessus à nouveau. Votre explication est très bonne! Je me demande simplement: si les blocs en mémoire ne sont pas largement répandus, quel est le problème avec une disposition de mémoire qui est "différente d'un tenseur de même forme fait à partir de zéro" ? Pourquoi la contiguïté n'est-elle requise que pour certaines opérations?
MBT

4
Je ne peux pas répondre définitivement à cela, mais je suppose qu'une partie du code PyTorch utilise une implémentation vectorisée haute performance des opérations implémentées en C ++ et ce code ne peut pas utiliser les décalages / foulées arbitraires spécifiés dans les méta-informations de Tensor. Ce n'est qu'une idée cependant.
Shital Shah

1
Pourquoi l'appelé ne pourrait-il pas simplement appeler contiguous()par lui-même?
information_interchange

très probablement, parce que vous ne le voulez pas de manière contiguë, et il est toujours agréable d'avoir le contrôle sur ce que vous faites.
shivam13juna

2
Une autre opération de tenseur populaire est permute, qui peut également renvoyer un tenseur non "contigu".
Oleg

31

De la [documentation pytorch] [1]:

contigu () → Tenseur

Returns a contiguous tensor containing the same data as self 

tenseur. Si le tenseur de soi est contigu, cette fonction renvoie le tenseur de soi.

contiguousici signifie non seulement contigu en mémoire, mais aussi dans le même ordre en mémoire que l'ordre des index: par exemple, faire une transposition ne change pas les données en mémoire, cela change simplement la carte des index aux pointeurs de mémoire, si vous alors l'appliquer contiguous()changera les données en mémoire de sorte que la carte des index à l'emplacement mémoire soit celle canonique. [1]: http://pytorch.org/docs/master/tensors.html


1
Merci pour votre réponse! Pouvez-vous me dire pourquoi / quand j'ai besoin que les données soient contiguës? S'agit-il uniquement de performances ou d'une autre raison? PyTorch nécessite-t-il des données contiguës pour certaines opérations? Pourquoi les cibles doivent-elles être contiguës et non les intrants?
MBT

C'est juste pour la performance. Je ne sais pas pourquoi les codes le font pour les cibles mais pas pour les entrées.
patapouf_ai

2
Donc, apparemment, pytorch exige que les cibles de la perte soient contingentes en mémoire, mais les entrées de neuralnet n'ont pas besoin de satisfaire cette exigence.
patapouf_ai

2
Super merci! Je pense que cela a du sens pour moi, j'ai remarqué que contigu () est également appliqué aux données de sortie (qui étaient bien sûr auparavant l'entrée) dans la fonction avant, donc les sorties et les cibles sont contiguës lors du calcul de la perte. Merci beaucoup!
MBT

1
@patapouf_ai Non. Votre explication est incorrecte. Comme indiqué dans la bonne réponse, il ne s'agit pas du tout de blocs de mémoire contigus.
Akaisteph7

14

tensor.contiguous () créera une copie du tenseur, et l'élément de la copie sera stocké dans la mémoire de manière contiguë. La fonction contiguous () est généralement requise lorsque nous transposons d'abord () un tenseur, puis le remodelons (visualisons). Commençons par créer un tenseur contigu:

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

Le retour stride () (3,1) signifie que: lors du déplacement le long de la première dimension à chaque pas (ligne par ligne), nous devons nous déplacer de 3 pas dans la mémoire. Lors du déplacement le long de la deuxième dimension (colonne par colonne), il faut se déplacer d'un pas dans la mémoire. Cela indique que les éléments du tenseur sont stockés de manière contiguë.

Maintenant, nous essayons d'appliquer les fonctions come au tenseur:

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, nous pouvons trouver que transpose (), narrow () et tenseur slicing, et expand () rendront le tenseur généré non contigu. Il est intéressant de noter que repeat () et view () ne le rend pas non-contigu. Alors maintenant, la question est: que se passe-t-il si j'utilise un tenseur non contigu?

La réponse est que la fonction view () ne peut pas être appliquée à un tenseur non contigu. Ceci est probablement dû au fait que view () nécessite que le tenseur soit stocké de manière contiguë afin de pouvoir effectuer un remodelage rapide en mémoire. par exemple:

bbb.view(-1,3)

nous obtiendrons l'erreur:

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

Pour résoudre cela, ajoutez simplement contiguous () à un tenseur non contigu, pour créer une copie contiguë puis appliquez view ()

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

10

Comme dans la réponse précédente contigous () alloue des blocs de mémoire contigus , cela sera utile lorsque nous passons le tenseur au code backend c ou c ++ où les tenseurs sont passés en tant que pointeurs


3

Les réponses acceptées étaient si bonnes, et j'ai essayé de duper transpose()l'effet de fonction. J'ai créé les deux fonctions permettant de vérifier le samestorage()et le 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")

J'ai vérifié et j'ai obtenu ce résultat sous forme de tableau:

les fonctions

Vous pouvez consulter le code du vérificateur ci-dessous, mais donnons un exemple lorsque le tenseur n'est pas contigu . Nous ne pouvons pas simplement faire appel view()à ce tenseur, nous en aurions besoin reshape()ou nous pourrions aussi appeler .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)

De plus, il existe des méthodes qui créent des tenseurs contigus et non contigus à la fin. Il existe des méthodes qui peuvent fonctionner sur un même stockage , et certaines méthodes flip()qui créeront un nouveau stockage (lire: cloner le tenseur) avant le retour.

Le code du vérificateur:

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

D'après ce que je comprends, une réponse plus résumée:

Contigu est le terme utilisé pour indiquer que la disposition de la mémoire d'un tenseur ne s'aligne pas avec ses métadonnées ou informations de forme annoncées.

À mon avis, le mot contigu est un terme déroutant / trompeur car dans des contextes normaux, il signifie lorsque la mémoire n'est pas répartie dans des blocs déconnectés (c'est-à-dire son "contigu / connecté / continu").

Certaines opérations peuvent avoir besoin de cette propriété contiguë pour une raison quelconque (efficacité très probable dans gpu, etc.).

Notez que .viewc'est une autre opération qui peut provoquer ce problème. Regardez le code suivant que j'ai corrigé en appelant simplement contiguous (au lieu du problème de transposition typique qui le cause, voici un exemple qui est causé lorsqu'un RNN n'est pas satisfait de son entrée):

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

Erreur que j'avais l'habitude d'obtenir:

RuntimeError: rnn: hx is not contiguous


Sources / Ressource:

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.