PyTorch - contiguo ()

90

Estaba revisando este ejemplo de un modelo de lenguaje LSTM en github (enlace) . Lo que hace en general me queda bastante claro. Pero todavía estoy luchando por entender qué contiguous()hace la llamada , lo que ocurre varias veces en el código.

Por ejemplo, en la línea 74/75 de la entrada de código y se crean las secuencias de destino del LSTM. Los datos (almacenados en ids) son bidimensionales, donde la primera dimensión es el tamaño del 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())

Entonces, como un ejemplo simple, cuando se usa el tamaño de lote 1 y seq_length10 inputsy se targetsve así:

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]

Entonces, en general, mi pregunta es, ¿qué es contiguous()y por qué lo necesito?

Además, no entiendo por qué se llama al método para la secuencia objetivo y no para la secuencia de entrada, ya que ambas variables están compuestas por los mismos datos.

¿Cómo podría targetsser no contiguo y inputsseguir siendo contiguo?

EDITAR: Traté de omitir las llamadas contiguous(), pero esto lleva a un mensaje de error al calcular la pérdida.

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

Entonces, obviamente, contiguous()es necesario llamar en este ejemplo.

(Para mantener esto legible, evité publicar el código completo aquí, se puede encontrar usando el enlace de GitHub anterior).

¡Gracias por adelantado!

MBT
fuente
sería útil un título más descriptivo. Le sugiero que mejore el título o al menos escriba un tldr; to the point summaryresumen conciso hasta el punto.
Charlie Parker
publicación cruzada
Charlie Parker

Respuestas:

186

Hay pocas operaciones en Tensor en PyTorch que realmente no cambian el contenido del tensor, sino solo cómo convertir índices en tensor en ubicación de bytes. Estas operaciones incluyen:

narrow(), view(), expand()Ytranspose()

Por ejemplo: cuando llamas transpose(), PyTorch no genera un nuevo tensor con un nuevo diseño, solo modifica la metainformación en el objeto Tensor para que el desplazamiento y la zancada sean para una nueva forma. ¡El tensor transpuesto y el tensor original comparten la memoria!

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

Aquí es donde entra en juego el concepto de contiguo . Lo anterior xes contiguo, pero yno porque su diseño de memoria sea diferente a un tensor de la misma forma hecho desde cero. Tenga en cuenta que la palabra "contiguo" es un poco engañosa porque no es que el contenido del tensor se extienda alrededor de bloques de memoria desconectados. Aquí los bytes todavía se asignan en un bloque de memoria, ¡pero el orden de los elementos es diferente!

Cuando llama contiguous(), en realidad hace una copia del tensor, por lo que el orden de los elementos sería el mismo que si el tensor de la misma forma se creara desde cero.

Normalmente, no necesita preocuparse por esto. Si PyTorch espera un tensor contiguo, pero si no es así, lo obtendrá RuntimeError: input is not contiguousy luego simplemente agregará una llamada a contiguous().

Shital Shah
fuente
Me encontré con esto de nuevo. ¡Tu explicación es muy buena! Me pregunto: si los bloques en la memoria no están muy extendidos, ¿cuál es el problema con un diseño de memoria que es "diferente a un tensor de la misma forma hecho desde cero" ? ¿Por qué ser contiguo es el único requisito para algunas operaciones?
MBT
4
No puedo responder definitivamente a esto, pero supongo que parte del código de PyTorch usa una implementación vectorizada de alto rendimiento de las operaciones implementadas en C ++ y este código no puede usar desplazamientos / pasos arbitrarios especificados en la metainformación de Tensor. Sin embargo, esto es solo una suposición.
Shital Shah
1
¿Por qué el destinatario no puede simplemente llamar contiguous()por sí mismo?
information_interchange
muy posiblemente, porque no lo quieres de forma contigua, y siempre es bueno tener control sobre lo que haces.
shivam13juna
2
Otra operación tensorial popular es permute, que también puede devolver un tensor no "contiguo".
Oleg
31

De la [documentación de pytorch] [1]:

contiguo () → Tensor

Returns a contiguous tensor containing the same data as self 

tensor. Si el auto tensor es contiguo, esta función devuelve el auto tensor.

Donde contiguousaquí significa no solo contiguo en la memoria, sino también en el mismo orden en la memoria que el orden de los índices: por ejemplo, hacer una transposición no cambia los datos en la memoria, simplemente cambia el mapa de índices a punteros de memoria, si luego aplicarlo contiguous()cambiará los datos en la memoria para que el mapa de índices a la ubicación de la memoria sea el canónico. [1]: http://pytorch.org/docs/master/tensors.html

patapouf_ai
fuente
1
¡Gracias por su respuesta! ¿Puede decirme por qué / cuándo necesito que los datos sean contiguos? ¿Es solo actuación o alguna otra razón? ¿PyTorch requiere datos contiguos para algunas operaciones? ¿Por qué los objetivos deben ser contiguos y las entradas no?
MBT
Es solo por rendimiento. No sé por qué los códigos lo hacen para objetivos pero no para entradas.
patapouf_ai
2
Entonces, aparentemente, pytorch requiere que los objetivos en la pérdida sean constantes en la memoria, pero las entradas de la red neuronal no necesitan satisfacer este requisito.
patapouf_ai
2
¡Genial gracias! Creo que esto tiene sentido para mí, he notado que contiguous () también se aplica a los datos de salida (que por supuesto antes era la entrada) en la función de avance, por lo que tanto las salidas como los objetivos son contiguos al calcular la pérdida. ¡Muchas gracias!
MBT
1
@patapouf_ai No. Tu explicación es incorrecta. Como se señaló en la respuesta correcta, no se trata en absoluto de bloques contiguos de memoria.
Akaisteph7
14

tensor.contiguous () creará una copia del tensor, y el elemento de la copia se almacenará en la memoria de forma contigua. La función contiguous () generalmente se requiere cuando primero transponemos () un tensor y luego lo reformamos (vemos). Primero, creemos un tensor contiguo:

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

El retorno de stride () (3,1) significa que: cuando nos movemos a lo largo de la primera dimensión en cada paso (fila por fila), necesitamos mover 3 pasos en la memoria. Al movernos a lo largo de la segunda dimensión (columna por columna), necesitamos mover 1 paso en la memoria. Esto indica que los elementos del tensor se almacenan de forma contigua.

Ahora intentamos aplicar las funciones come al 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 encontrar que transponer (), estrecho () y corte de tensor, y expandir () harán que el tensor generado no sea contiguo. Curiosamente, repeat () y view () no lo hacen descontiguo. Entonces ahora la pregunta es: ¿qué sucede si uso un tensor no contiguo?

La respuesta es que la función view () no se puede aplicar a un tensor no contiguo. Esto probablemente se deba a que view () requiere que el tensor se almacene de forma contigua para que pueda realizar una remodelación rápida en la memoria. p.ej:

bbb.view(-1,3)

obtendremos el error:

---------------------------------------------------------------------------
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 esto, simplemente agregue contiguous () a un tensor no contiguo, para crear una copia contigua y luego aplique view ()

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

Como en la respuesta anterior contigous () asigna trozos de memoria contigous , será útil cuando estemos pasando el tensor al código de backend c o c ++ donde los tensores se pasan como punteros

pag. vignesh
fuente
3

Las respuestas aceptadas fueron geniales, y traté de engañar al transpose()efecto de la función. Creé las dos funciones que pueden verificar el samestorage()y el 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")

Revisé y obtuve este resultado como una tabla:

funciones

Puede revisar el código de verificación a continuación, pero demos un ejemplo cuando el tensor no es contiguo . No podemos simplemente llamar view()a ese tensor, lo necesitaríamos reshape()o también podríamos llamar .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)

Además, hay que tener en cuenta que existen métodos que crean tensores contiguos y no contiguos al final. Hay métodos que pueden operar en un mismo almacenamiento , y algunos métodos flip()crearán un nuevo almacenamiento (léase: clonar el tensor) antes de regresar.

El código del 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
fuente
0

Por lo que entiendo, esta es una respuesta más resumida:

Contiguo es el término utilizado para indicar que el diseño de la memoria de un tensor no se alinea con sus metadatos o información de forma anunciados.

En mi opinión, la palabra contiguo es un término confuso / engañoso ya que en contextos normales significa cuando la memoria no se distribuye en bloques desconectados (es decir, su "contiguo / conectado / continuo").

Algunas operaciones pueden necesitar esta propiedad contigua por alguna razón (probablemente la eficiencia en gpu, etc.).

Tenga en cuenta que .viewes otra operación que podría causar este problema. Mire el siguiente código que solucioné simplemente llamando a contiguous (en lugar del típico problema de transposición que lo causa, aquí hay un ejemplo que es la causa cuando un RNN no está contento con su 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))

Error que solía obtener:

RuntimeError: rnn: hx is not contiguous


Fuentes / recurso:

Charlie Parker
fuente