PyTorch - contiguo ()


94

Stavo esaminando questo esempio di un modello di linguaggio LSTM su GitHub (collegamento) . Quello che fa in generale è abbastanza chiaro per me. Ma sto ancora lottando per capire cosa contiguous()fa la chiamata , che si verifica più volte nel codice.

Ad esempio, nella riga 74/75 del codice di input e vengono create le sequenze di destinazione dell'LSTM. I dati (memorizzati ids) sono bidimensionali dove la prima dimensione è la dimensione del batch.

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

Quindi, come semplice esempio, quando si utilizzano batch di dimensioni 1 e seq_length10 inputse si targetspresenta così:

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]

Quindi, in generale, la mia domanda è: cosa serve contiguous()e perché ne ho bisogno?

Inoltre non capisco perché il metodo viene chiamato per la sequenza di destinazione e ma non per la sequenza di input poiché entrambe le variabili sono composte dagli stessi dati.

Come potrebbe targetsessere non contiguo ed inputsessere ancora contiguo?

EDIT: ho provato a tralasciare la chiamata contiguous(), ma questo porta a un messaggio di errore durante il calcolo della perdita.

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

Quindi ovviamente contiguous()è necessario chiamare questo esempio.

(Per mantenerlo leggibile ho evitato di pubblicare il codice completo qui, può essere trovato usando il collegamento GitHub sopra.)

Grazie in anticipo!


sarebbe utile un titolo più descrittivo. Ti suggerisco di migliorare il titolo o almeno di scrivere un tldr; to the point summarysommario conciso con il punto.
Charlie Parker


Risposte:


193

Ci sono poche operazioni su Tensor in PyTorch che non cambiano realmente il contenuto del tensore, ma solo come convertire gli indici in tensor in byte location. Queste operazioni includono:

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

Ad esempio: quando chiami transpose(), PyTorch non genera un nuovo tensore con un nuovo layout, modifica solo le meta informazioni nell'oggetto Tensore in modo che offset e andatura siano per una nuova forma. Il tensore trasposto e il tensore originale condividono davvero la memoria!

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

È qui che entra in gioco il concetto di contiguo . Sopra xè contiguo, ma ynon perché il suo layout di memoria sia diverso da un tensore della stessa forma creato da zero. Si noti che la parola "contiguo" è un po 'fuorviante perché non è che il contenuto del tensore sia distribuito attorno a blocchi di memoria scollegati. Qui i byte sono ancora allocati in un blocco di memoria ma l'ordine degli elementi è diverso!

Quando chiami contiguous(), in realtà crea una copia del tensore in modo che l'ordine degli elementi sarebbe lo stesso di un tensore della stessa forma creato da zero.

Normalmente non devi preoccuparti di questo. Se PyTorch si aspetta un tensore contiguo, ma se non lo è, otterrai RuntimeError: input is not contiguouse quindi aggiungi una chiamata a contiguous().


Mi sono appena imbattuto di nuovo in questo. La tua spiegazione è molto buona! Mi chiedo solo: se i blocchi in memoria non sono molto diffusi, qual è il problema con un layout di memoria che è "diverso da un tensore della stessa forma creato da zero" ? Perché essere contigui è solo un requisito per alcune operazioni?
MBT

4
Non posso rispondere in modo definitivo, ma la mia ipotesi è che parte del codice PyTorch utilizzi un'implementazione vettorializzata ad alte prestazioni delle operazioni implementate in C ++ e questo codice non può utilizzare offset / passi arbitrari specificati nelle meta informazioni di Tensor. Questa è solo un'ipotesi però.
Shital Shah

1
Perché il chiamato non poteva semplicemente chiamare contiguous()da solo?
information_interchange

molto probabilmente, perché non lo vuoi in modo contiguo, ed è sempre bello avere il controllo su ciò che fai.
shivam13juna

2
Un'altra operazione popolare del tensore è permute, che può anche restituire un tensore non "contiguo".
Oleg

32

Dalla [documentazione pytorch] [1]:

contiguo () → Tensore

Returns a contiguous tensor containing the same data as self 

tensore. Se l'autotensore è contiguo, questa funzione restituisce l'autotensore.

Dove contiguousqui significa non solo contiguo in memoria, ma anche nello stesso ordine in memoria dell'ordine degli indici: per esempio fare una trasposizione non cambia i dati in memoria, cambia semplicemente la mappa da indici a puntatori di memoria, se poi applicarlo contiguous()cambierà i dati in memoria in modo che la mappa dagli indici alla locazione di memoria sia quella canonica. [1]: http://pytorch.org/docs/master/tensors.html


1
La ringrazio per la risposta! Puoi dirmi perché / quando ho bisogno che i dati siano contigui? È solo performance o qualche altro motivo? PyTorch richiede dati contigui per alcune operazioni? Perché gli obiettivi devono essere contigui e gli input no?
MBT

È solo per le prestazioni. Non so perché i codici lo faccia per gli obiettivi ma non per gli input.
patapouf_ai

2
Quindi apparentemente pytorch richiede che gli obiettivi nella perdita siano contingenti in memoria, ma gli input di neuralnet non hanno bisogno di soddisfare questo requisito.
patapouf_ai

2
Ottimo grazie! Penso che questo abbia senso per me, ho notato che contiguous () viene applicato anche ai dati di output (che ovviamente era in precedenza l'input) nella funzione forward, quindi sia gli output che i target sono contigui quando si calcola la perdita. Molte grazie!
MBT

1
@patapouf_ai No. La tua spiegazione non è corretta. Come sottolineato nella risposta corretta, non si tratta affatto di blocchi di memoria contigui.
Akaisteph7

14

tensor.contiguous () creerà una copia del tensore e l'elemento nella copia verrà archiviato nella memoria in modo contiguo. La funzione contiguous () è solitamente richiesta quando prima trasponiamo () un tensore e poi lo rimodelliamo (visualizziamo). Per prima cosa, creiamo un tensore contiguo:

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

Il ritorno stride () (3,1) significa che: quando ci si sposta lungo la prima dimensione di ogni passo (riga per riga), abbiamo bisogno di spostare 3 passi nella memoria. Quando ci spostiamo lungo la seconda dimensione (colonna per colonna), dobbiamo muoverci di 1 passo nella memoria. Ciò indica che gli elementi nel tensore sono memorizzati in modo contiguo.

Ora proviamo ad applicare le funzioni come al tensore:

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, possiamo scoprire che transpose (), narrow () e tensor slicing ed expand () renderanno il tensore generato non contiguo. È interessante notare che repeat () e view () non lo rendono non contiguo. Quindi ora la domanda è: cosa succede se utilizzo un tensore non contiguo?

La risposta è che la funzione view () non può essere applicata a un tensore non contiguo. Ciò è probabilmente dovuto al fatto che view () richiede che il tensore sia memorizzato in modo contiguo in modo che possa rimodellare velocemente in memoria. per esempio:

bbb.view(-1,3)

otterremo l'errore:

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

Per risolvere questo problema, aggiungi semplicemente contiguous () a un tensore non contiguo, per creare una copia contigua e quindi applica view ()

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

10

Come nella risposta precedente, contigous () alloca blocchi di memoria contigui , sarà utile quando passiamo il tensore al codice backend c o c ++ dove i tensori vengono passati come puntatori


3

Le risposte accettate sono state fantastiche e ho provato a ingannare l' transpose()effetto della funzione. Ho creato le due funzioni che possono controllare il samestorage()e il 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")

Ho controllato e ho ottenuto questo risultato come tabella:

funzioni

Puoi rivedere il codice del checker in basso, ma diamo un esempio quando il tensore non è contiguo . Non possiamo semplicemente invocare view()quel tensore, ne avremmo bisogno reshape()o potremmo anche chiamare .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)

Inoltre, ci sono metodi che creano tensori contigui e non contigui alla fine. Esistono metodi che possono operare su una stessa memoria e alcuni metodi flip()creeranno una nuova memoria (leggi: clona il tensore) prima del ritorno.

Il codice del checker:

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

Da quello che ho capito questa una risposta più sintetica:

Contiguo è il termine usato per indicare che il layout di memoria di un tensore non è allineato con i suoi metadati pubblicizzati o con le informazioni sulla forma.

Secondo me la parola contiguo è un termine confuso / fuorviante poiché in contesti normali significa quando la memoria non è diffusa in blocchi scollegati (cioè il suo "contiguo / connesso / continuo").

Alcune operazioni potrebbero richiedere questa proprietà contigua per qualche motivo (molto probabilmente l'efficienza in gpu, ecc.).

Nota che .viewè un'altra operazione che potrebbe causare questo problema. Guarda il seguente codice che ho risolto semplicemente chiamando contiguo (invece del tipico problema di trasposizione che lo causa, ecco un esempio che è causa quando un RNN non è soddisfatto del suo input):

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

Errore che ho usato per ottenere:

RuntimeError: rnn: hx is not contiguous


Fonti / Risorsa:

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.