Pytorch: [RFC] Compatibilidad con formato de memoria (también conocido como diseño, NHWC)

Creado en 10 abr. 2019  ·  68Comentarios  ·  Fuente: pytorch/pytorch

Planteamiento del problema

Los operadores de CNN utilizan el orden canónico de las dimensiones de los tensores y les asignan un significado semántico. Para el caso 2D en PyTorch actual, una entrada para torch.nn.Conv2d tiene que ser un tensor 4d en orden NCHW -.

Por motivos de rendimiento, a menudo es beneficioso reordenar las dimensiones de manera diferente para que la memoria a la que se accede mediante operaciones particulares se disponga de forma contigua y se utilice mejor la localidad. La opción más común es mover las dimensiones hacia el final: NHWC. Puede haber formatos de memoria aún más complejos que agrupan una dimensión en bloques, p. Ej..

Las bibliotecas de ejemplo que lo utilizan incluyen:

  • cudnn tiene un rendimiento más rápido en Volta en NHWC
  • fbgemm y qnnpack no son compatibles con NCHW.
  • libxsmm es compatible con NCHW, pero la penalización del rendimiento es algo así como el 50% (IIRC).

El desafío es que transformar el orden de dimensión en sí es costoso, por lo que en los casos en que se realizan varias operaciones de CNN seguidas (por ejemplo, conv(relu(conv))) ) es beneficioso transformar al formato de memoria diferente una vez, realizar operaciones y reordenarlas. espalda.

Por lo tanto, es importante hacer que PyTorch sea consciente de los diferentes órdenes de dimensiones y poder pasar tensores con diferentes formatos de memoria entre operaciones tanto en modo ansioso como en modo JIT. Además, es beneficioso tener pases de optimización JIT automáticos que intenten aplicar técnicas heurísticas o de búsqueda para determinar si cambiar el formato de memoria es beneficioso en términos de rendimiento y en qué parte del modelo tiene sentido hacerlo.

Nos esforzamos por construir una API capaz de representar:

  • Tensor con diferente formato de memoria (al principio, solo orden de dimensión) presente en PyTorch en Eager y JIT. Los diseños bloqueados tienen menor prioridad, pero siguen siendo agradables.
  • API expuestas al usuario para consultar y cambiar el formato de la memoria
  • Las operaciones centrales de CNN pueden manejar tensores de entrada con diferentes formatos de memoria y enrutamiento para una implementación más rápida correspondiente.
  • Capacidad para inferir y optimizar sobre formatos de memoria en pases JIT

Terminología : el problema anterior a menudo se denomina "diseño" (mxnet), "formato_datos" (tf), "formato_imagen" (keras), "orden" (caffe2). Proponemos utilizar el nombre "formato de memoria" o "formato_memoria" en PyTorch. Desafortunadamente, el nombre "diseño" se toma en PyTorch con los valores 'strided' vs 'sparse_coo', por lo que la opción de nombrar no está disponible.

Operadores afectados

Los siguientes operadores, como mínimo, deben tener en cuenta el formato de la memoria. Además de producir el resultado correcto, necesitan ofrecer el mejor rendimiento de las bibliotecas subyacentes Y preservar el formato de

  • circunvolución
  • diferentes tipos de agrupaciones
  • norma de lote, norma de capa, norma de instancia (generalmente, cualesquiera que sean las normas)
  • muestreo / interpolación
  • abandono de funciones
  • softmax en menor grado: la dimensión se puede especificar manualmente allí, pero las implementaciones eficientes están presentes solo para el diseño nchw implícito
  • relleno
  • operaciones de elementos (unarios y binarios)
  • constructores de tensores que heredan el formato de memoria, por ejemplo, empty_like.

Cambios de comportamiento y API

Defina el concepto de formato de memoria en PyTorch:

  • Constantes como torch.memory_format.channels_first . No tienen un tipo especificado y pueden ser objetos comparables arbitrarios (probablemente comiencen con enum, pero en el futuro podrían haber otros objetos para interoperar con el concepto de tensor con nombre)

    • Alternativa: use torch.channels_first directamente

  • Los valores son channels_first y channels_last (para permitir menos constantes)
  • Para imágenes 1D / tensores 3D, los valores significan NCW, NWC, para imágenes 2D / tensores 4D - NCHW, NHWC, para imágenes 3D / tensores 5D - NCDHW, NDHWC

Agrega los siguientes métodos a Tensor:

  • x.is_contiguous(torch.memory_format.channels_first)
  • x.to(memory_format=torch.memory_format.channels_first)

Nota : no hay una función x.get_memory_format() por ahora, solo verificaciones explícitas; permite una gama más amplia de posibles implementaciones. Sin embargo, es posible que queramos agregarlo.

El diseño semántico del tensor siempre es el mismo: ¡NCHW! x.size() siempre devuelve (n,c,h,w)

Las operaciones conservan el comportamiento del formato de la memoria:

  • convolución, agrupación, etc. (ver arriba) devuelve la salida en el mismo formato de memoria que la entrada y envía internamente a la mejor implementación
  • Las operaciones unarias basadas en elementos conservan el mismo formato de memoria y deben ejecutarse tan rápido como en un tensor contiguo
  • Las operaciones binarias basadas en elementos proporcionan algunas garantías razonables sobre la preservación del formato de la memoria; probablemente se puede definir de manera más amplia, pero el mínimo es:

    • NHWC + escalar → NHWC

    • NHWC + vector de columna → NHWC

  • Las operaciones hacia atrás para las operaciones centrales de CNN conservan el mismo formato de memoria que en la ruta de avance. (es posible que sea necesario aplicarlo explícitamente porque los gradientes entrantes para la salida pueden tener un formato de memoria diferente)

El formato de memoria es una propiedad de un tensor que se conserva a través de la serialización / deserialización (en caso de que el tensor sea un parámetro).

Implementación acelerada

El tensor en PyTorch hoy tiene un concepto de pasos que especifica cómo se presenta el tensor lógico en la memoria . Específicamente, cada tensor tiene un vector strides de la misma longitud que sizes . Para indexar elementos en la indexación lógica (i1, i2, .., ik) uno hace un producto puntual con pasos y busca la memoria en offset + i0*stride0 + i1*stride1 + ... * ik * stridek . Por tanto, los tensores contiguos tienen pasos que son productos acumulativos invertidos de tamaños. Por ejemplo, el tensor 4D con tamaños (n,c,h,w) tiene pasos (c*h*w, h*w, w, 1) .

Los pasos se pueden usar para representar diferentes formatos de memoria (que son reordenamiento de dimensiones) físicamente mientras se conserva el orden lógico predeterminado de NCHW. Proporciona una definición efectiva de la transformación del formato de memoria como:

# implementation of x.to(channels_last)
def to_mem_format_nhwc(x):
    return x.permute(0,2,3,1).contiguous().permute(0,3,1,2)

# implementation of x.to(channels_first)
def to_mem_format_nchw(x):
    return x.contiguous()

En formato NHWC, el vector de pasos es (c*h*w, 1, c*w, c) . Por lo tanto, en el búfer de memoria, los pesos están en orden contiguo para NHWC.

Strides se puede utilizar para realizar pruebas:

def is_nhwc_contiguous(x):
    return x.permute(0,2,3,1).is_contiguous()

# or alteratively
def is_nhwc_contiguous(x):
    n,c,h,w = x.size() # in any case the sizes remain in NCHW order
    return x.stride() == (c*h*w, 1, c*w, c)

def is_nchw_contiguous(x):
    return x.is_contiguous()


# operator implementations can just check contiguity and carry on directly on data pointer
def my_sample_op(x):
    if x.is_contiguous(nhwc):
        float* p = x.data();
        # Do we need to go to c++ here? 
        # can we have an example in python?
        n,c,h,w = x.size()
        # operate on `p` as it's guaranteed to be (n,h,w,c) array
        y=my_nhwc_op(p)
        # Do we need to convert the layout of y?

    else:
        # Need to convert x to nhwc layout
        x = x.permute(0,2,3,1).contiguous()
        float *p = x.data();
        # Is this needed?
        y = my_nhwc_op(p)
        return y.permute(0,3,1,2).contiguous()

Ventajas de este enfoque:

  • Utiliza el concepto existente de PyTorch de pasos sin agregar nuevas ideas de nivel superior o parámetros de API
  • Conserva el comportamiento lógico del tensor en el orden canónico NCHW
  • Funciona para el reordenamiento arbitrario de las dimensiones de entrada.
  • Las rutinas de serialización existentes ya conservan pasos de tensor
  • Posibilidad de reutilizar muchas operaciones para trabajar en diferentes diseños de memoria.

Contras :

  • Llamar a .contiguous() equivale a cambiar a NCHW y puede ocurrir por accidente del usuario o dentro de una de las operaciones

    • Se necesita una auditoría explícita de los operadores para garantizar que conserven el formato de la memoria.

  • No funciona para formatos bloqueados / en mosaico; se necesita un enfoque diferente

    • Es posible considerar agregarlos como ciudadanos de primera clase en PyTorch, pero es un cambio mucho mayor.

    • La alternativa es tratarlos como mangos opacos, por ejemplo, tensores MKLDNN

  • Las características de rendimiento de las implementaciones subyacentes son menos obvias para el usuario final.

El mayor problema potencial es la intención del usuario poco clara . No hay forma de distinguir si el usuario realmente quería un formato de memoria diferente o si el tensor de entrada se aceleró de esta manera. Específicamente, conduce a un cambio de comportamiento para las operaciones existentes; hoy en día, la convolución solo puede producir tensores contiguos a NCHW incluso si la entrada es arbitraria, en un mundo nuevo podría reconocer la entrada como NHWC y, por lo tanto, también devolvería NHWC. No cambia la semántica, pero genera problemas de rendimiento difíciles de depurar. Una posible solución podría ser etiquetar los tensores explícitamente con el indicador memory_format especificado por el usuario y seguir solo esta anotación (además de las zancadas).

Para resolver el problema anterior, la propuesta inicial es introducir una etiqueta de formato de memoria "suave" en el tensor que registre la última llamada to(memory_format) realizada en el tensor. Los operadores necesitarían propagar esta anotación a las salidas. La anotación es "suave", por lo que no cometeremos errores graves en las anotaciones que no coincidan, sino que produciremos advertencias en el modo de creación de perfiles.

Implementaciones de operador

La firma de los operadores existentes no cambia. Los operadores pueden realizar un envío codificado dentro del operador para encaminar a una implementación más rápida. Si la implementación no está disponible, es posible realizar un recorrido de ida y vuelta a través de diferentes formatos de memoria. La alternativa sería generar un mensaje de error.

def maxpool(x: Tensor):
    if x.is_contiguous(torch.layout.NHWC):
        return max_pool_impl_nhwc(x)
    return max_pool_impl_default(x.contiguous())

Se prefiere usar un solo símbolo como 'conv' para referirse a los operadores en JIT IR en lugar de crear operadores separados como 'conv_nhwc'. La razón de esto es la simplicidad y mantener la RI en el nivel de representación semántica.

Operaciones por elementos

Tenemos que asegurarnos de que las operaciones centrales, como las de elementos, conserven el formato de la memoria y sean eficientes.

Las operaciones unarias se pueden manejar de forma genérica verificando si un bloque de memoria es "denso", es decir, si los elementos abarcan un área sin espacios y cada ubicación de la memoria se usa exactamente una vez. Se puede verificar con un algoritmo simple.

def is_dense_format(x):
    p = 1
    for s, d in sorted(zip(x.stride(), x.size())):
        if s != p:
            return False
        p *= d
    return True

def my_unary(x):
    if is_dense_format(x):
        return contig_memory_impl(x.data(), x.numel())
    return default_strided_impl(x)

# is_dense_format can be used in implementations of e.g. empty_like too

Herramientas de rendimiento

Para el rendimiento de la depuración, debemos agregar soporte al generador de perfiles para:

  • ver en qué parte del programa ocurren los reordenamientos reales de la memoria, es decir, rastrear las llamadas a .contiguous ()
  • seguimiento de qué implementación se invoca
  • emitir advertencias sobre cambios de formato de memoria en, por ejemplo, operaciones binarias (donde la anotación "suave" es útil)

Esta funcionalidad se puede integrar en una herramienta de creación de perfiles bajo demanda.

Manejo de autograduación

Es lógico esperar que el pase hacia atrás se ejecute con el mismo formato de memoria que hacia adelante. No siempre sucederá automáticamente, ya que los gradientes entrantes pueden tener pasos arbitrarios. Por lo tanto, el pase hacia adelante tiene que reconocer explícitamente el formato de la memoria, almacenarlo en cierre de autogrado y aplicarlo al tensor de graduación antes de la función hacia atrás.

Posible implementación:

def conv_backward(input, weight, grad_output, grad_weight, grad_input):
  if input.is_contiguous(torch.memory_format.channels_last):
    grad_output = grad_output.to(torch.memory_format.channels_last)
    return conv_backward_nhwc(...)
  else:
    grad_output = grad_output.contiguous()
    return conv_backward_nchw(...)

Representación en JIT

La propuesta actual es tener:

  • Todavía no hay un manejo de primera clase para el formato de memoria en anotaciones de tipo. En cambio, podemos mantener un mapa de mirar a un lado en la forma necesaria para pases que manipulan el formato de la memoria.
  • Pase de inferencia (similar a shape_inference) que produce anotaciones de formato por valor
  • Pases de transformación de formato de memoria (manuales o automáticos) que encuentran donde es necesario insertar llamadas to(memory_format) para un rendimiento óptimo

Para fines de cumplimiento, también podemos utilizar declaraciones como assert x.is_contiguous(channels_last) .

Nota: Existe la cuestión de dónde almacenar la información de que el dispositivo en particular tiene una combinación de formato de memoria preferida (por ejemplo, qconv en rutas x86 a fbgemm que implementa NHWC solamente). Una opción es ponerlo en el nivel de registro operativo, sin embargo, la anotación de formato de memoria se siente más como una información secundaria. Podemos empezar por mantener un mapa global en algún lugar de la pasada JIT que denota los formatos de memoria preferidos y las heurísticas asociadas. Si se pone desordenado, podemos cambiar al mecanismo basado en el registro.

Más allá: diseños bloqueados

A medida que decidimos agregar paquetes de tensores más complejos, usar un tensor de PyTorch de primera clase podría no ser plausible debido al alto costo de implementación y la complejidad. Son posibles dos alternativas:

  • Representaciones opacas como enlaces de tipo C personalizados. Esta es una opción a elegir para empaquetar en inferencia donde la diversidad es mayor en términos de optimizaciones de rendimiento.
  • Tipo de tensor de primera clase como MKLDNNTensor con algunas (pero no todas) de las operaciones vinculadas a este nuevo tipo

Otra alternativa más es implementar soporte nativo para bloqueo / mosaico en la clase principal de PyTorch Tensor.

Relación tensorial con nombre

La propuesta existente para NamedTensor está estructurada como un mecanismo de verificación de tipos en tensores; por el momento, no asigna ningún significado semántico a los nombres de las dimensiones. Por lo tanto, la única forma de inferir el significado del tensor de activación es continuar usando el formato NCHW predeterminado. Hace que NamedTensor y las propuestas actuales sean ortogonales.

Si estamos dispuestos a especificar los significados de algunos nombres (como "canales", "anchos"), los operadores pueden utilizar esta información para encaminarlos hacia una implementación más rápida. Sin embargo, sería un cambio semántico ya que los tensores de entrada lógicamente tendrían formato de memoria NHWC (no NCHW como hoy).

Estado de la técnica

TensorFlow admite tanto NHWC como NCHW a nivel de operador, a través del parámetro data_format ; Los valores aceptables son (“NHWC”, “NCHW”) para entradas de 4 d, (“NDHWC”, “NCDHW”) para entradas de 5 d, o channels_first / channels_last independientemente de la entrada dimensionalidad. Depende del usuario manejar la configuración del parámetro correctamente, es decir, el tensor no lo rastrea automáticamente.

Caffe2 llama a este parámetro order lugar de data_format , pero aún se aplica explícitamente a nivel de operador individual.


Apéndice: Otras opciones consideradas

Pregunta de tornasol: ¿qué imprime el siguiente código: tensor_in_nhwc_layout.size(1) - el número de canales (porque el valor predeterminado es NCHW en PyTorch) o la altura (porque eso es lo que está en el diseño de NHWC en la posición 1).

En función de esta respuesta, son posibles varias opciones:

  • Opción A - Zancadas (presentada arriba). El diseño del tensor es una representación completamente interna. La implementación, como si se hiciera más convenientemente con pasos.

    • .size (1) me devuelve "canales", pero la memoria interna se distribuye de manera diferente

    • pro: no cambia el código del modelo, mi modelo todavía puede hacer aritmética de dimensiones directamente. De hecho, ninguna de las API públicas cambia

    • contras: en la implementación de pasos, muchos operadores llaman a .contiguous () y pueden revertir accidentalmente el diseño

    • contras: desde la perspectiva del usuario, comprender cuáles son las garantías del retorno de la operación son primordiales. Esta OMI elimina los enfoques de solo pasos, porque se vuelve muy difícil entender el formato en el que se devolverá su operación, y no hay una API que diga "ignore mis pasos, en realidad solo devuelva lo contiguo a NCHW". Esto se suma a las limitaciones anteriores.

  • Opción B: tensor NHWC explícito. El usuario manipula explícitamente el tensor que tiene un orden de dimensión diferente, pero el tensor en sí no sabe nada al respecto. Necesitaríamos alguna anotación a nivel de operador para averiguar qué espera el usuario.

    • .size (1) devuelve "altura"

    • pro: sin magia y muy predecible

    • contras: cambiar el modelo de un diseño a otro se convierte en una operación compleja que necesita rastrear todos los accesos a .size () y .reshape () (¿o necesita hacerlo explícito en la API?)

  • Opción B ': tensor NHWC explícito con marca de diseño . Igual que el anterior, pero permitimos adjuntar una anotación al tensor para marcar su diseño semántico que las operaciones consumen en su implementación. Entonces, no es necesario realizar anotaciones a nivel de operador: un operador puede realizar el despacho en función del indicador de diseño de las entradas.
  • Opción C - Tensor con nombre . ( https://docs.google.com/document/d/1ynu3wA2hcjwOtEng04N904gJjEbZWcINXO_ardX6hxc/edit#heading = h.2gbe5xpga3w9)

    • .size (1) devuelve "altura" pero le pedimos a la gente que NO use esta API y en su lugar use .size ('canal')

    • pro: muy explícito y lo que quiere el usuario

    • con: no resuelve el problema de la transición, tendríamos que forzar a todo el código escrito con conocimiento del diseño a usar tensores con nombre. Si no es así, se aplican los mismos problemas que los anteriores.

  • Opción D: el diseño es de tipo tensor opaco . Trate NHWC como tratamos MKLDNN o SparseTensor - tipo de tensor separado con diferente DispatchID. Es como la Opción A pero con diferentes compensaciones en el comportamiento predeterminado: las operaciones no implementadas fallarían en lugar de volver a NCHW.

    • .size (1) todavía devuelve "canales"

    • pro: sin magia y explícita, el envío separado permite a las operaciones decidir lo que quieren

    • pros / contras: todos los operadores necesarios deben implementarse en un diseño diferente, si falta alguna operación, el usuario obtendría un error explícito de que no es compatible

    • contras: probablemente tendríamos que prohibir muchas operaciones en él, por ejemplo, vistas porque los resultados esperados son difíciles de predecir

internals mkldnn triaged

Comentario más útil

Por cierto, ¿por qué tenemos que crear un nuevo concepto en lugar de limitarnos a layout ? No creo que las representaciones dispersas tengan un concepto bien definido de un diseño como "channels_last", por lo que no necesitamos representar un producto de memory_formats * layouts ( layouts refiere al uso actual ), pero solo memory_format + layouts lo que significa que debería estar bien usar el mismo argumento que solíamos usar. Para mí es más breve, más agradable y nos permitirá evitar extender las firmas de las fábricas a mil argumentos.

Todos 68 comentarios

Hay un problema con empty_like ; la semántica definida actualmente es que descarta toda la información de zancada, por lo que no es posible preservar el diseño y ser BC.

@VitalyFedyunin está registrado para implementar los bits .contiguous() y torch.memory_layout

Una pregunta: para un tensor 4D x con tamaños (n, c, h, w)

x = torch.randn(n,c,h,w)
# x.size(): (n, c, h, w)
# x.stride(): (c*h*w, h*w, w, 1)

Tenemos una permutación extraña

y = x.permute(0, 3, 1, 2)
# y.size(): (n, w, c, h)
# y.stride(): (c*h*w, 1, h*w, w)

Ahora comprobamos si es contiguo para el formato NHWC. Siguiendo tu lógica como se muestra a continuación

def is_nhwc_contiguous(x):
    return x.permute(0,2,3,1).is_contiguous()

# or alternatively
def is_nhwc_contiguous(x):
    n,c,h,w = x.size() # in any case the sizes remain in NCHW order
    return x.stride() == (c*h*w, 1, c*w, c)

Para ambos casos is_nhwc_contiguous(y) devolverá True?

Esto es correcto. Sin embargo, no podemos transmitir solo los avances, ya que queremos evitar conversiones hacia atrás y hacia adelante durante operaciones de copia, a y similares.

¿Qué pasa si los pasos tienen el mismo orden que el formato de la memoria? Usemos el tensor 4D como ejemplo. Para describir un tensor, tenemos sizes , strides y stride_indexes :

tamaños en (n, c, h, w)
zancadas en orden físico, es decir

  • pasos de (n, c, h, w) si el formato es nchw
  • pasos de (n, h, w, c) si el formato es nhwc.

stride_indexes asigna los pasos al tamaño de nchw:

  • (0, 1, 2, 3) si el formato es nchw,
  • (0, 2, 3, 1) si el formato es nhwc.

Para el formato nchw, esto es igual que antes. Para nhwc, será similar.

def is_nhwc_contiguous(x):
     n,c,h,w = x.size()
     return x.stride() == (h*w*c, w*c, c, 1)

def is_nchw_contiguous(x):
    n,c,h,w = x.size()
    return x.stride() == (c*h*w, h*w, w, 1)

def is_nchw_format(x):
    return x.stride_index() == (0, 1, 2, 3) 

def is_nhwc_format(x):
    return x.stride_index == (0, 2, 3, 1)

def is_contiguous(x):
    if (is_nchw_format(x)):
        return is_nchw_contiguous(x)
    else if (is_nhwc_format(x)):
        return  is_nhwc_contiguous(x)
    else:
        warning_not_support()

# or, to use stride_index
def is_contiguous(x):
    return x.stride() == (x.size[x.stride_index[1]]*x.size[x.stride_index[2]]*x.size[x.stride_index[3]], x.size[x.stride_index[2]] * x.size[x.stride_index[3]], x.size[x.stride_index[3]], 1)

Esto también se puede ampliar para admitir el formato bloqueado. Utilice nChw16c como ejemplo,

sizes: (n, c, h, w)
block_sizes: (n, c/16, h, w, 16)
strides: strides of (n, c/16, h, w, 16)
stride_indexes: (0, 1, 2, 3, 1)  # assume blocked dimension is always in dense (i.e. on the right side of major dimension)

Se pueden explorar más detalles más adelante.

Para los OP que aceptan solo el tensor contiguo nchw, eso será algo de trabajo aquí.

Alternativamente, también podemos cambiar ligeramente el prototipo, digamos

def is_contiguous(format=nchw):
    ...
def contiguous(format=nchw)
    ...

Por lo tanto, de forma predeterminada, asume que solo nchw es contiguo. De esta manera, no es necesario que vuelva a escribir esos OP, se reordenará a nchw automáticamente.

Nos esforzamos por construir una API capaz de representar:

  • Tensor con diferente formato de memoria (al principio, solo orden de dimensión) presente en PyTorch en Eager y JIT. Los diseños bloqueados tienen menor prioridad, pero siguen siendo agradables.
  • API expuestas al usuario para consultar y cambiar el formato de la memoria
  • Las operaciones centrales de CNN pueden manejar tensores de entrada con diferentes formatos de memoria y enrutamiento para una implementación más rápida correspondiente.
  • Capacidad para inferir y optimizar sobre formatos de memoria en pases JIT

¡Gran propuesta! ¿Puedo explicitar mi comprensión para ver si es correcto (incluidas las propuestas para el manejo de formatos MKL-DNN):

Permítanme pensar que hubo una implementación de esta propuesta como una clase de "formato". Siempre que proporcione consultas y cambie la API como virtual, podríamos hacer la herencia / extensiones que se ajusten a los formatos complejos MKL-DNN. U otros métodos siempre que proporcionen un marco para manejar formatos, descargándonos esos detalles esenciales.

Acerca de la implementación de los OP, cada OP podría tener formatos preferidos que maximicen su rendimiento y un formato compatible que funcione. Se supone que el operador de elementos (o, más en general, OP delimitados por memoria) no tiene preferencia. OP produce su tensor de resultados con un objeto de "formato", este objeto de formato garantiza la consulta / cambio de semántica compatible con la expectativa predeterminada de pytorch, así como que puede manejar formatos específicos si se llaman series de funciones optimizadas (como conv2d (ReLU (conv2d)) caso)

@uyongw Quiero aclarar un poco más tu primer ejemplo. Configura el ejemplo como, "Tengo un tensor NCHW, que luego transpuse de una manera extraña (por lo que ahora se ve como NWCH); ahora quiero saber si es NHWC contiguo". Pero esa es la forma incorrecta de verlo. Una mejor formulación es: "Tengo un tensor NHWC, que luego transpuse a un tensor NCHW".

Para decirlo de otra manera, no hay un significado intrínseco a las dimensiones físicas de un tensor (cuando ignoramos los pasos). Solo les damos significado cuando consideramos cómo los hacemos referencia con respecto a los pasos.

Para describir un tensor, tenemos tamaños, zancadas y stride_indexes

Creo que stride_indexes es una forma conveniente de pensar en el problema, pero es estrictamente redundante con las zancadas, porque todo lo que estás diciendo es "Aplicar esta permutación (¿inversa?) A las zancadas, y luego tratar eso como el verdaderas zancadas.) @VitalyFedyunin y yo estábamos hablando de cómo podría ser una buena idea almacenar en caché esta información de alguna manera, porque es una molestia reconstruir la información a partir de las propias zancadas, pero esto está fuera del alcance de esta propuesta.

Por lo tanto, de forma predeterminada, asume que solo nchw es contiguo.

Sí, esa es mi lectura del plan.

@CaoZhongZ

Permítanme pensar que hubo una implementación de esta propuesta como una clase de "formato". Siempre que proporcione consultas y cambie la API como virtual, podríamos hacer la herencia / extensiones que se ajusten a los formatos complejos MKL-DNN. U otros métodos siempre que proporcionen un marco para manejar formatos, descargándonos esos detalles esenciales.

De hecho, no creo que sea una descripción precisa de la propuesta. El soporte de diseño de memoria que admite la propuesta aquí son solo diseños que se pueden expresar a través de pasos. Cualquier cosa que sea inexpresable de esta manera (por ejemplo, el diseño de bloques) no funcionará de esta manera, y debe ser compatible con nuestro mecanismo de "diseño" más pesado.

Para decirlo de otra manera, no hay un significado intrínseco a las dimensiones físicas de un tensor (cuando ignoramos los pasos). Solo les damos significado cuando consideramos cómo los hacemos referencia con respecto a los pasos.

Parcialmente de acuerdo :-) Pero no en este problema específico. Digamos que ya tengo un tensor nhwc. Luego lo permuto a nwhc. Quiero permutar aún más a nhwc y luego hacer un contiguous (). Pero ya lo tengo nhwc contiguo. ¿No es confuso?

Creo que stride_indexes es una forma conveniente de pensar en el problema, pero es estrictamente redundante con los pasos, porque todo lo que estás diciendo es "Aplicar esta permutación (¿inversa?) A los pasos, y luego tratar eso como los verdaderos pasos).

En mi humilde opinión, no será redundante con zancadas, si tiene zancadas en nhwc (físico). Porque necesitas el mapeo correcto con tamaños (lógica). De lo contrario, no hay forma de saber el orden real.

Por cierto, hay un enfoque más sencillo mediante el uso de mapeo inverso. Digamos, para nchw, es (0, 1, 2, 3), para nhwc, es (0, 3, 1, 2) en lugar de (0, 2, 3, 1). Eso dice que stride_index en sí mismo es siempre NCHW también. Pero el problema es que no se puede extender a formatos bloqueados como nChw16c u OIhw16i16o.

Los formatos bloqueados requieren la implementación de un conjunto de operadores completamente diferente; por esa razón, preferimos no mezclarlos con 'formato de memoria', que por definición se supone que es amigable con todos los operadores existentes y trabaja con el mismo o mejor desempeño.

Parcialmente de acuerdo :-) Pero no en este problema específico. Digamos que ya tengo un tensor nhwc. Luego lo permuto a nwhc. Quiero permutar aún más a nhwc y luego hacer un contiguous (). Pero ya lo tengo nhwc contiguo. ¿No es confuso?

Es difícil entender su ejemplo porque está usando algunos términos coloquialmente y se necesita precisión. Así es como interpreto lo que ha dicho:

  • Un tensor "nhwc" debe ser, según esta propuesta, "Tensor cuyo diseño físico es NHWC, pero está escalonado de modo que el diseño lógico sea NCHW".
  • "Permutar un (tensor cuyo diseño lógico es NCHW) tensor a (diseño lógico) NWHC" es ejecutar y = x.permute(0, 2, 3, 1) , ya que estás permutando el diseño lógico , no el diseño físico. (Sospecho que esto no es lo que quisiste decir, porque en tu publicación original mencionaste la permutación x.permute(0, 3, 1, 2)
  • Para luego permutar aún más un tensor NWHC (diseño lógico) a (diseño lógico) NHWC es aplicar la permutación z = y.permute(0, 2, 3, 1) . Entonces ahora tiene un tensor cuyo diseño lógico coincide con el diseño físico. Esto significa que si preguntamos z.contiguous() obtendremos la verdad (y, confusamente, z.contiguous(memory_layout=NCHW) también será verdad). Pero NO será contiguo al NHWC.

No creo que este sea el ejemplo que tenías en mente, en cuyo caso tendrás que ser más preciso sobre lo que quieres decir con "permutar".

En mi humilde opinión, no será redundante con zancadas, si tiene zancadas en nhwc (físico). Porque necesitas el mapeo correcto con tamaños (lógica). De lo contrario, no hay forma de saber el orden real.

Este es el quid de la propuesta: privilegiamos NCHW como el diseño lógico, siempre . Entonces, si tengo un tensor 4D del que no sé nada, supongo que su diseño lógico es NCHW. Eso elimina la ambigüedad. Si desea lidiar con tensores cuyo diseño lógico no es NCHW, creo que la API como se indica le hace la vida un poco difícil.

@dzhulgakov

Las operaciones conservan el comportamiento del formato de la memoria

Si los tensores físicos NHWC pueden ocurrir puramente a través de zancadas, esto es técnicamente una ruptura de BC, a menos que haga que solo conserven el formato de memoria cuando la etiqueta de formato de memoria está presente (pero parece que no quiere que esto tenga un significado semántico, así que No estoy seguro de lo que sugiere la propuesta actualmente). Sin embargo, no estoy seguro de si esto realmente rompe el código de alguien en la práctica.

Si los tensores físicos NHWC pueden ocurrir puramente a través de zancadas, esto es técnicamente una ruptura de BC, a menos que haga que solo conserven el formato de memoria cuando la etiqueta de formato de memoria está presente (pero parece que no quiere que esto tenga un significado semántico, así que No estoy seguro de lo que sugiere la propuesta actualmente). Sin embargo, no estoy seguro de si esto realmente rompe el código de alguien en la práctica.

Suponiendo que podamos hacer que el formato de la memoria sea "pegajoso". Op sobre el tensor con formato de memoria producirá un tensor con formato de memoria. Eso resolverá el problema de BC.

Sin embargo, necesitamos definir un comportamiento de operaciones binarias (o más miembros) cuando los tensores tienen diferentes formatos de memoria.

@ezyang Oh, acabo de descubrir que hay un error tipográfico en mi respuesta anterior. (Lo siento por eso. Sin embargo, el ejemplo original sigue siendo correcto). Permítanme reafirmarlo como se muestra a continuación:

  1. Tengo un tensor NCHW (físicamente, contiguo).
  2. Luego lo permuto a NWHC (lógicamente).
  3. Quiero permutarlo aún más a NHWC con una llamada contigua () seguida.
  4. Úselo como NHWC (físicamente).

Pero ya lo obtuve NHWC contiguo después del paso 2. Entonces puedo omitir el paso 3 y usarlo como NHWC directamente en el paso 4. Pero esto seguramente no es correcto porque el orden físico del tensor no cambia en absoluto.

Los formatos bloqueados requieren la implementación de un conjunto de operadores completamente diferente; por esa razón, preferimos no mezclarlos con 'formato de memoria', que por definición se supone que es amigable con todos los operadores existentes y trabaja con el mismo o mejor desempeño.

Sí, podemos habilitar NHWC como primer paso. Sin embargo, no creo que el formato bloqueado sea algo totalmente diferente. Puede expresarse de forma natural (con una buena abstracción). Si hay una descripción de formato general, otros pueden simplemente registrar nuevos formatos con bloqueos / pasos arbitrarios.

Más si ya hemos bloqueado el soporte, no nos molestamos en crear algunas construcciones ocultas para ejecutar todo lo subyacente, lo que crea un mundo implícito en el interior y el desde / hacia entre los dos mundos puede convertirse en un problema.

De todos modos, puede estar demasiado lejos para pensar en el formato bloqueado. Pero creo que, si es posible, es mejor hacer extensible el diseño.

Pero ya lo obtuve NHWC contiguo después del paso 2. Entonces puedo omitir el paso 3 y usarlo como NHWC directamente en el paso 4. Pero esto seguramente no es correcto porque el orden físico del tensor no cambia en absoluto.

Bien, ahora entiendo tu ejemplo. De hecho, puede detenerse en el paso 2 y usarlo como si fuera un tensor NCHW; en cuyo caso, interpretará incorrectamente W como C, etc. Esto es definitivamente una desventaja con la implementación basada en stride ( @dzhulgakov , probablemente deberíamos agregar esto a la propuesta). La propuesta tiene alguna disposición para este caso:

Para resolver el problema anterior, la propuesta inicial es introducir una etiqueta de formato de memoria "suave" en el tensor que registre la última llamada a (memory_format) realizada en el tensor. Los operadores necesitarían propagar esta anotación a las salidas. La anotación es "suave", por lo que no cometeremos errores graves en las anotaciones que no coincidan, sino que produciremos advertencias en el modo de creación de perfiles.

La etiqueta de formato de memoria flexible le permitiría distinguir de un tensor NCHW que permutó, frente a un tensor que en realidad es, físicamente, NHWC. Pero la etiqueta flexible en su forma actual no es vinculante, por lo que no estoy seguro de cuán útil sería realmente para este caso.

Otra forma de resolver el problema es con tensores con nombre. Con tensores con nombre, podemos usar los nombres en las dimensiones (lógicas) para averiguar si estamos viendo un tensor como NCHW (el valor predeterminado asumido) o algo más.

Sin embargo, no creo que el formato bloqueado sea algo totalmente diferente. Puede expresarse de forma natural (con una buena abstracción). Si hay una descripción de formato general, otros pueden simplemente registrar nuevos formatos con bloqueos / pasos arbitrarios.

Hay más comentarios sobre el tema aquí: https://github.com/pytorch/pytorch/issues/16038#issuecomment -454490374

@ezyang Gracias por la respuesta. Sí, la etiqueta de formato suave puede ayudar. La preocupación es que puede no ser lo suficientemente flexible ya que el orden de las dimensiones puede ser arbitrario. Además, él mismo no es computable. El tensor con nombre tiene un significado semántico para cada dimensión, pero dudo que necesite más facilidades para respaldarlo.

Personalmente, creo que esto se puede resolver introduciendo un mapa desde el orden de pasos (físico) hasta el orden de tamaños NCHW (lógico). Como propuse anteriormente, para NCHW es casi lo mismo que el diseño actual; para NHWC, sizes sigue siendo NCHW, strides estará en orden (N, H, W, C). Y usamos stride_index = (0, 2, 3, 1) para especificar el índice de dimensión de las zancadas.

Además, la combinación de strides y stride_index se puede usar para representar cualquier formato de tensor. Esto puede dar flexibilidad a otros para registrar nuevos formatos de datos.

@ezyang

Las operaciones conservan el comportamiento del formato de la memoria

Si los tensores físicos NHWC pueden ocurrir puramente a través de zancadas, esto es técnicamente una ruptura de BC, a menos que haga que solo conserven el formato de memoria cuando la etiqueta de formato de memoria está presente (pero parece que no quiere que esto tenga un significado semántico, así que No estoy seguro de lo que sugiere la propuesta actualmente). Sin embargo, no estoy seguro de si esto realmente rompe el código de alguien en la práctica.

Cuando las operaciones aritméticas y el umbral se movieron a TensorIterator, eso fue técnicamente un rompimiento de BC (porque el formato de memoria de los operandos solía no conservarse, y TensorIterator lo conserva). El status quo ahora es muy inconsistente: el umbral conserva el diseño, todas las demás operaciones unarias no, antorcha. Donde no, las operaciones aritméticas conservan el diseño si ambos operandos tienen el mismo diseño, pero el valor predeterminado es "nchw" o el tensor es contiguous según el conocimiento actual, si hay una discrepancia, no estoy seguro de qué sucede con la transmisión.
También está haciendo un buen punto acerca de que empty_like y similares preservando el diseño no son BC. Quizás también necesite un argumento de diseño, como is_contiguous en la propuesta

x.is_contiguous(torch.memory_format.channels_first)

@ezyang @ngimel

Hay un problema con empty_like; la semántica definida actualmente es que descarta toda la información de zancada, por lo que no es posible preservar el diseño y ser BC.

También está haciendo un buen punto acerca de que empty_like y similares preservando el diseño no son BC.

Si no confiamos en los pasos para expresar el orden físico, empty_like no necesariamente rompe el BC. Hay 3 tipos de información de dimensión en tensor:

  • forma: tamaños
  • orden lógico: información de la orden registrada en pasos (normalmente se usa para admitir la transposición o permutar)
  • orden físico: NCHW o NHWC (se puede abordar como stride_index como propuse).

Actualmente, el orden físico es el mismo que la forma / tamaño. Así que abandonamos el orden lógico poco a poco. Considere que estamos desacoplando la forma y el orden físico, también podemos simplemente eliminar el orden lógico pero preservar la forma y el orden físico por empty_like . Eso significa que se conservarán tanto size() como stride_index() , pero se restablecerá stride() . Especialmente, empty_like de un tensor NHWC devolverá un tensor contiguo NHWC con la misma información de forma especificada.

@uyongw No estoy seguro de que sea una buena idea cambiar empty_like ; ahora mismo su semántica coincide con empty_like numpy .

El status quo ahora es muy inconsistente: el umbral conserva el diseño, todas las demás operaciones unarias no, antorcha. Donde no, las operaciones aritméticas conservan el diseño si ambos operandos tienen el mismo diseño, pero por defecto sería "nchw" o el tensor contiguo en comprensión actual si hay una discrepancia, no estoy seguro de qué sucede con la transmisión.

@ngimel , sí, estos no son muy consistentes en este momento. Creo que una parte de resolver cómo representar el formato de la memoria es lograr que nuestros operadores alcancen un estado consistente

@ zou3519 numpy empty_like que vinculó tiene order argumento que por defecto es "coincidir con el diseño del prototipo lo más cerca posible". Eso no es lo que empty_like en pytorch hace actualmente (devuelve "nchw" - tensor contiguo, incluso si el prototipo no es contiguo)

Oh, ya veo, estaba leyendo eso demasiado rápido. En ese caso, sería bueno tener nuestro número de coincidencia vacío_como también y sería (¿probablemente?) Bueno tenerlo para el diseño de memoria aquí también.

@ zou3519 Sí, lo que estoy tratando de decir es mantener la semántica actual (eliminar el orden lógico como mencionan @ezyang y @ngimel ) y al mismo tiempo preservar el diseño físico como los valores predeterminados de numpy. Por lo tanto, para el prototipo NCHW, el comportamiento será el mismo que antes. Para el prototipo NHWC, su comportamiento seguirá siendo compatible, es decir, el nuevo tensor será NHWC contiguo, en lugar de NCHW contiguo si no cambia la implementación actual.

Dos preguntas:

  • ¿Qué sucede si se agrega un tensor NHWC a un tensor NCHW?
  • ¿Qué hay de abordar la desventaja de (B) mediante la creación de métodos como t.channel_dim () en un tensor que devuelve el valor entero que indica dónde está físicamente la dimensión? Este enfoque puede incluso ser necesario para permitir que se elijan otros formatos, como los formatos de bloque, sin cambios en la red.

Si abordamos la estafa de (B) con la última viñeta, entonces (B) me parece preferible. Es intuitivamente claro y los errores lógicos deberían ser fáciles de detectar. Todas las operaciones existentes también pueden funcionar en el tensor, ya que se parece a cualquier otro tensor contiguo. Las operaciones que pueden entender la semántica (análoga a la propuesta de tensor con nombre) también funcionarán como se esperaba.

@ zou3519 numpy empty_like que vinculó tiene order argumento que por defecto es "coincidir con el diseño del prototipo lo más cerca posible". Eso no es lo que empty_like en pytorch hace actualmente (devuelve "nchw" - tensor contiguo, incluso si el prototipo no es contiguo)

Estamos planeando mantener el formato en tales casos (para tensores formateados en memoria)

¿Qué sucede si se agrega un tensor NHWC a un tensor NCHW?
La operación con tensor con formato de memoria devolverá el tensor con formato de memoria. Si ambos tensores tienen formato de memoria, el formato de salida vendría determinado por el primer tensor.

Dos cosas que agregaría:

Estamos planeando mantener el formato en tales casos (para tensores formateados en memoria)

Necesitaríamos auditar los usos existentes, porque a menudo los operadores llamarán empty_like y luego asumirán que son contiguos a NCHW. Y no sé cómo trataríamos con el código de terceros. Parece que necesitaríamos un valor predeterminado diferente a numpy si queremos preservar BC.

La operación con tensor con formato de memoria devolverá el tensor con formato de memoria. Si ambos tensores tienen formato de memoria, el formato de salida vendría determinado por el primer tensor.

También agregaría, si realmente le importa en qué formato viene su salida, pase un tensor de salida.

De acuerdo en empty_like, hay bastantes casos en los que el resultado de empty_like / zeros_like, etc. se asume como nchw-contiguous (físicamente contiguo, debería decir, en muchos casos no son operaciones de imagen).
Pasar el tensor de salida no es una opción en la mayoría de los casos, porque las funciones con out kwarg no son diferenciables.

Muchos de nuestros problemas provienen de la inconsistencia de los diseños de salida esperados. No podemos resolverlos todos a la vez, pero podemos intentar bloquear el estado actual (al menos para los pasos) y concretarlos uno por uno. Entonces aquí está la propuesta.

API de Python

Introducir nuevo torch.memory_format

torch_memory_format.any # default value
torch_memory_format.preserve
torch.memory_format.contiguous # what most of the functions now behave as default
torch.memory_format.nchw # requires 4D tensor, contiguous memory
torch.memory_format.nhwc # requires 4D tensor, restrided/permuted memory

El tensor requerirá una conversión de formato de memoria explícita

x = torch.zeros((10,3,32,32)) # NCHW
x.permute(0,2,3,1).is_contiguous(memory_format=torch.memory_format.nhwc) == False # because memory still layed out as NCHW

Para 'etiquetarlos' con un formato específico:

y = x.to(memory_format=torch.memory_format.nhwc)
y.is_contiguous(memory_format=torch.memory_format.nhwc) == True # We got new tensor with proper memory layout
y.is_contiguous() == False # Required for back compatibility
y.stride() == (3072, 3, 1, 96)

Ahora sobre empty_like y similar:

z = torch.empty_like(y) 
z.is_contiguous() == True # For BC

Porque en realidad es:

z = torch.empty_like(y, memory_format=torch.memory_format.any ) 

Si queremos mantener el formato:

z = torch.empty_like(y, memory_format=torch_memory_format.preserve) 
z.is_contiguous() == False 
z.is_contiguous(memory_format=torch.memory_format.nhwc) == True

Similar:

z = torch.empty_like(y, memory_format=memory_format=torch.memory_format.nhwc) 
z.is_contiguous() == False 
z.is_contiguous(memory_format=torch.memory_format.nhwc) == True

Eso significa que podemos definir lentamente cada función memory_format predeterminada al estado actual del mundo, clasificarlas y ser conscientes de cómo las cambiamos en el futuro.

Si especifica el tensor, las TensorOptions se ignoran actualmente (en el mejor de los casos, arrojan una excepción, por ejemplo, si la opción del dispositivo pasa no coincide con el dispositivo tensor out ).

Se supone que el formato de memoria es ligero, por lo que cualquier permutacion lo perderá.

x.zeros((10,3,32,32), memory_format=torch.memory_format.nhwc)
x = x.permute(0,1,3,2).permute(0,1,3,2)
x.is_contiguous(memory_format=torch.memory_format.nhwc) == False (even if strides are similar)

No estoy seguro sobre el relleno, agradeceré ayuda aquí.

Sin embargo, podemos hacer x.to (memory_format = torch.memory_format.nhwc) 'tag' tensor con el formato adecuado y devolver self

Multiprocesamiento

Conservará el formato de memoria 'etiqueta'

Formatos de memoria en bloque

La API anterior no depende de las dimensiones / zancadas / tamaños, lo que significa que podemos ampliar la funcionalidad en el futuro manteniendo la misma API.

API internas

Los operadores podrían bifurcarse según el formato de memoria

if (self.memory_format(nhwc)) {
 // fast path
} else
{
 // classic implementation
}

Si hacemos memory_format como TensorOptions, podemos pensar en la ramificación en el nivel de despacho (de manera similar al dispositivo, diseño)

Un pequeño comentario sobre la propuesta de

torch.memory_format.nchw # requires 4D tensor, contiguous memory
torch.memory_format.nhwc # requires 4D tensor, restrided/permuted memory

es demasiado restrictivo (porque también queremos manejar 1D y 3D además de 2D), y channels_first/channels_last de la propuesta original fueron más acomodaticios para este propósito.

De acuerdo, necesitamos una mejor denominación. channels_first suena casi bien excepto que el lote va primero =)

Me gusta tu última propuesta. ¿Cambiaría el manejo de .contiguous ()? ¿Necesitaría .contiguous (memory_format = <...>)? Si es así, y muchas operaciones simplemente llaman a .contiguous (), aún podrían estar formateando la memoria de manera incorrecta. Hoy en día, muchas operaciones también asignan salidas como empty_like (), lo que tendría el mismo efecto. ¿El plan sería actualizarlos para detectar el formato de memoria de las entradas y realizar las llamadas contiguas y similares a vacías correctas?

En cuanto a ahora, nuestros usuarios (y todas las bibliotecas) esperan que .contiguous() devuelva el tensor contiguo de memoria con pasos en orden descendente.

No podemos romper este contrato. Sin embargo, la buena noticia es: tan pronto como admitamos la opción memory_format, JIT podrá entender cuándo es más eficiente llamar a .contiguous(memory_format=...) lugar del formato clásico.

@VitalyFedyunin ¿Asumimos que las operaciones como las siguientes no están permitidas?

x.zeros(10,3,32,32)
# x is in nchw (default)
# x.size() is [10,3,32,32]
# x.stride() is [3*32*32, 32*32, 32,1]
x = x.permute(0,2,3,1)
# At this point 
# x.size() is [10,32,32,3], size is not in nchw order
# x.stride() is [3*32*32, 32,1,32*32]

# How can this be supported?
y = x.to(memory_format=torch.memory_format.nhwc)

Una variante más sería:

x.zeros(10,3,32,32)
# `x` is in nchw (default)
# x.size() is [10,3,32,32]
# x.stride() is [3*32*32, 32*32, 32,1]
x = x.permute(0,2,3,1)
x=x.contiguous()
# At this point 
# x.size() is [10,32,32,3], size is not in nchw order
# x.stride() is [32*32*3, 32*3,3,1]

# How can this be supported?
y = x.to(memory_format=torch.memory_format.nhwc)

@ raghuramank100 - ¿por qué el usuario llamaría .permute(0,2,3,1) en primer lugar? Todos los tensores de esta propuesta tienen un tamaño semántico de (n, c, h, w), lo que significa que el tamaño (1) le devuelve los canales. Eso es lo que asume la biblioteca estándar de PT hoy y lo que supondría también en esta propuesta. Por lo que probablemente nunca llamaría .permute en absoluto

¿Puede un administrador de contexto ser útil para permitir al usuario anular el formato de memoria de los tensores asignados dentro del alcance del administrador a un formato específico?

with torch.memory_format(torch.memory_format.nhwc):
    # a will be allocated with the context managed memory format   
    a = torch.randn(...)

# b will be allocated matching some assumed default format
b = torch.randn(...)

No me gusta la idea del administrador de contexto, ya que aflojará el control de memory_format.

Por ejemplo:

with torch.memory_format(torch.channels_last):
  x = torch.randn(10,3,32,32) # this one is NHWC
  y = torch.randn(10,10) @ this one is not

Cuando el memory_format explícito lo deja claro:

x = torch.randn(10,3,32,32).to(memory_format=torch.channels_last) # this one is NHWC
y = torch.randn(10,10).to(memory_format=torch.channels_last) # This is errors out as dim == 2

Si es necesario, podemos agregar sintaxis para permitir:

x = torch.randn(10,3,32,32, memory_format=torch.channels_last)

@ raghuramank100 no es necesario permutar.

y = x.to(memory_format=torch.channels_last)

Hará todo el trabajo sucio por usted, manteniendo el orden de atenuación igual que en x.

Entonces:

x = torch.randn(10, 3, 32, 32)
nhwc = x.to(memory_format=torch.channels_last)
self.assertFalse(nhwc.is_contiguous())
self.assertTrue(nhwc.is_contiguous(memory_format=torch.channels_last))
self.assertEqual(nhwc, x)

Y puede seguir direccionando nhwc en este formato

nhwc[N][C][H][W]

@VitalyFedyunin Eso tiene sentido.

Desde el punto de vista del usuario, el nombre del método (si se mantiene así) me parece engañoso, ya que "a" ya es la forma recomendada para transferir Tensor a diferentes dispositivos.

Además, ¿qué pasa con algo como el de Numpy para convertir matrices C_ORDER y F_ORDER?

numpy.asfortranarray()
numpy.ascontiguousarray()

Uno puede imaginarse fácilmente algo como:

torch.randn(32, 3, 64, 64).to(device).as_nhwc()

@VitalyFedyunin : Entiendo que la conversión a un formato de memoria diferente elimina la necesidad de que los usuarios permuten manualmente. Sin embargo, una vez que esta funcionalidad esté disponible en la antorcha, ¿qué pasaría si los usuarios llamaran a las funciones en la secuencia que describí anteriormente? Deberíamos tener al menos un mensaje de advertencia / error que indique que la transformación del diseño falló.

@VitalyFedyunin : Entiendo que la conversión a un formato de memoria diferente elimina la necesidad de que los usuarios permuten manualmente. Sin embargo, una vez que esta funcionalidad esté disponible en la antorcha, ¿qué pasaría si los usuarios llamaran a las funciones en la secuencia que describí anteriormente? Deberíamos tener al menos un mensaje de advertencia / error que indique que la transformación del diseño falló.

Esto solo será posible cuando implementemos tensores con nombre. Porque en este momento:

x.zeros(10,10,10,10)
x = x.permute(0,2,3,1)

Nadie puede decirme si acabo de crear nchw o nhwc.

Quizás entendí mal la propuesta original, pero ¿no se supone que la etiqueta de formato de memoria grabada elimina la ambigüedad de esta situación?

@VitalyFedyunin Tiene sentido, debemos asegurarnos de que esto se comunique a los usuarios finales cuando esta API se estabilice.

@dzhulgakov @VitalyFedyunin Después de revisar # 19975, tengo algunas inquietudes nuevas sobre la etiqueta de formato de memoria grabada en tensor. Mi problema básico es, ¿cómo vamos a decidir si las operaciones deben preservar la etiqueta de memoria? Originalmente, había pensado que solo los operadores "conscientes del diseño alternativo" necesitarían tener esta inteligencia. Pero mirando el parche de Vitaly, creo que algunos operadores centrales también necesitarán ajustes. Por ejemplo, considere x[0] ; si x era anteriormente un tensor NHWC, entonces debería sacar un tensor HWC después de hacer esto. Estoy bastante seguro de que el parche de Vitaly no maneja esto correctamente, y apuesto a que sería muy confuso para los usuarios. Quizás los únicos operadores que se ven afectados son los que se mueven con zancadas (en cuyo caso, no hay demasiados y podemos auditarlos manualmente), pero parece algo que deberíamos hacer. ¿Qué piensas?

Espere, los tensores aún permanecen indexados en el orden de: 0-dim N; 1st-dim C; 2ª atenuación H; 3rd-dim W. Entonces x [0] devuelve un tensor con 0-dim C; 1ª atenuación H; 2nd-dim W. Independientemente de si x era el diseño de memoria de channels_first o de channels_last.

De lo contrario, memory_format simplemente no tiene sentido y solo necesitamos permutar el tensor.

Mi punto es que la etiqueta de formato de memoria no se conserva. Si el tensor de entrada se etiquetó como channels_last , el nuevo tensor se etiquetará como any

cc @ zou3519 , la lógica de propagación del diseño aquí me recuerda mucho a la propagación de la dimensión con nombre en el trabajo del tensor con nombre.

Todavía me estoy poniendo al día con esta propuesta. Pero @ezyang podríamos realizar un seguimiento de la lógica de propagación del diseño propagando una bandera por dimensión (o nombre) y luego sería equivalente a tener tensores nombrados con convenciones de nombres

Sería genial si pudiéramos alinear la lógica de la etiqueta de memoria y la lógica del tensor con nombre exactamente, incluso si las tenemos como dos rutas de implementación separadas al principio.

Fase 1

Expande la funcionalidad de dos funciones tensoras .is_contiguous y .contiguous (tanto python como c ++ api).

Nota: Tuvimos varias quejas sobre la función .to(memory_format) y decidimos no admitirla.

  1. .contiguous ahora admite el argumento opcional solo de palabra clave - memory_format , que puede ser torch.contiguous_format o torch.channels_last .

    • El uso de torch.contiguous_format preservará el comportamiento actual de .contiguous() .

    • Llamar a x.contiguous(memory_format=torch.channels_last) devuelve un nuevo tensor que mantiene el mismo diseño semántico (NCHW), pero tiene un patrón de asignación de memoria diferente.

      x.contiguous(memory_format=torch.channels_last) espera que el tensor de entrada sea 3d, 4d o 5d; y falla de otra manera.

  2. .is_contiguous ahora admite el argumento opcional solo de palabras clave - memory_format , que puede ser torch.contiguous_format o torch.channels_last .

    • x.is_contiguous(memory_format=torch.contiguous_format) conserva la misma funcionalidad que x.is_contiguous() y permanece sin cambios.

    • x.is_contiguous(memory_format=torch.channels_last) devuelve verdadero si A) el tensor de entrada es contiguo en la memoria Y B) está asignado en la memoria en formato NWHC (o similar para 3d, 5d).

Nota: Al final de la fase uno, x.is_contiguous(memory_format=torch.channels_last) calculará el estado del tensor en cada llamada. Esta funcionalidad se actualizará más adelante.

Fase 2

Conservar el formato de la memoria para operaciones específicas:

  1. Los operadores de elementos unarios conservan el formato de memoria de channels_last.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b.sin()
    c.is_contiguous(memory_format=torch.channels_last) == True
    
  2. Los operadores binarios basados ​​en elementos ( add , sub , mul , div ) conservan el formato de memoria del último canal.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b * torch.randn(H,W)
    c.is_contiguous(memory_format=torch.channels_last) == True
    
  3. Cualquier operación sobre tamaños, pasos y atenuaciones restablece el formato de la memoria.

    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b.permute(0,2,3,1).permute(0,3,1,2)
    c.is_contiguous(memory_format=torch.channels_last) == False
    

Permanece indeciso

  1. Resultado de la operación de remodelación (y similar), si la salida es legible 'channels_last'

    import torch
    a = torch.randn(N,C,H,W)
    b = a.contiguous(memory_format=torch.channels_last)
    c = b.reshape(N,C,-1)
    c.is_contiguous(memory_format=torch.channels_last) # ?
    

    Nota: Actualmente memory_format no se conserva

  2. Resultado de la operación NHWC + NCHW. ¿Es NHWC?

    Nota: Actualmente NHWC + NCHW -> NHWC y NCHW + NHWC -> NHWC

¿Qué pasa con operaciones como cat / split? Les será útil conservar el formato de la memoria.

@ezyang : con respecto a la indexación, creo que deberíamos detenernos en algún lugar. Los diferentes diseños de memoria no son completamente transparentes y algunas operaciones deberían poder ignorarlos. Yo diría que x[0] debería poder borrar la etiqueta, incluido x[0].unsqueeze(0)

Como mencionó Raghu, cat / split debería conservar la etiqueta si es posible, ya que es un uso bastante común. Creo que la regla general debería ser que mientras el funcionamiento no cambie de rango o reordene el eje de forma extraña, deberíamos conservar la etiqueta. Si el rango cambia, todas las apuestas están canceladas.

Estoy de acuerdo en que en algunos casos perderemos la etiqueta. Pero no estaría de acuerdo con x[0] . Eso me parece una forma muy común de pasar de NCHW a CHW .

Después de varias conversaciones sobre lo confuso que es tener tensores para llevar (o no) la 'etiqueta' de channels_last, decidimos correr el riesgo de introducir un cambio que rompa bc y auto-promover tensores en el formato de channels_last.

Qué significa para la API:

Todos los tensores 3d, 4d, 5d con pasos como N, 1, H, [W, [D]] obtendrán automáticamente el formato de memoria de channels_last.

Para que funcione, tomaremos precauciones especiales para garantizar que los operadores en canales_últimos tensores que generan canales_últimos tensores tendrán un rendimiento al menos similar al de los operadores en tensores contiguos.

En el caso del peor escenario:
1) Los usuarios pueden llamar a .contiguous () en la salida.
2) Escribiremos código de promoción automática de tal manera que sería casi trivial cambiar este comportamiento.

Los efectos secundarios de dicha promoción de automóviles son:

import torch
x = torch.randn(10,16,16,3).permute(0,3,1,2) 
x.is_contiguous(memory_format=torch.channels_last) == True

Por otro lado, puede resolver el caso (después de ligeras modificaciones):

import torch
x = torch.randn(10,3,16,16).contiguous(memory_format=torch.channels_last)
x = x[0].unsqueeze(0)
x.is_contiguous(memory_format=torch.channels_last) == True

De conversiones flojas, según la solicitud de @ezyang

Natalia Gimelshein [2:19 p. M.]
Así que supongo que no habría ningún concepto de etiqueta.

import torch
#batch = 10, channels = 4, spatial dimensions = 16
x = torch.randn(10,16,16,4).permute(0,3,1,2)
x.is_contiguous(memory_format=torch.channels_last) == True
y = torch.randn(10,16,16,2).permute(0,3,1,2)
x1,x2 = x.chunk(2, dim=1) #chunk along channels dimension, no longer contiguous
x1.is_contiguous(memory_format=torch.channels_last) == False #right? So, if a tensor like this comes into e.g. convolution, what am I supposed to do with it? Did it want to be NHWC? Did it want to be nchw?
z=y+x1 #y is channels_last, x1 is something, what is the z layout?```

Vitaly Fedyunin [8:23 a. M.]
z va a ser channels_last

Vitaly Fedyunin [8:25 a. M.]
si x1 no es channels_last en ninguna de las variantes propuestas (a menos que cambiemos la función de fragmento para no devolver vistas), entonces la convolución lo convertirá al formato contiguo (channels_first) y devolverá contiguo también

Vitaly Fedyunin [9:12 a. M.]
@ngimel, gracias por los comentarios. Creo que podemos obtener una definición más significativa de channels_last para cubrir la mayoría de los casos en los que están involucradas operaciones de visualización. Te mantendrá informado.

Natalia Gimelshein [9:36 a. M.]
respondió a un hilo:
Entonces parece ser un problema, ¿no? La división entre canales es algo relativamente común, por ejemplo, en redes de tipo inicial. Entonces, si el tensor es el primer tensor de canales fragmentados, la salida de convolución será canales primero (que es un comportamiento intuitivo, y muy probablemente lo que el usuario quiera), si el tensor es canales fragmentados, la salida de convolución será nuevamente canales primero.

Natalia Gimelshein [9:39 a. M.]
respondió a un hilo:
Pero solo debido al comportamiento de suma no conmutativo y y siendo el primer argumento y los canales al final, ¿verdad? ¿Cuál sería el resultado para x1+y ? ¿Tenemos reglas de propagación de diseño para operaciones binarias en algún lugar?

Vitaly Fedyunin [10:44 a. M.]
1) Sí, es un problema que vamos a solucionar con propuesta alternativa. Voy a hacer algunas pruebas ahora y lo escribiré esta semana (en un día o dos).
2) x1 + y - también debería producir channels_last, de lo contrario es confuso, y sí, tendremos reglas de propagación de diseño escritas.

Creo que la observación que le hice a @VitalyFedyunin cuando charlamos sobre esto en persona (pero no creo que me acordé de escribir esto en ninguna parte), es que hay un grado de libertad en la convolución, que es cuando se pone un argumento cuyo diseño de memoria no coincide con ninguno que sepa cómo implementar de manera eficiente, ¿a qué diseño debería estar contiguo? Por razones de BC, primero se requiere la contiguificación a los canales, pero hemos tomado una decisión arbitraria aquí; posiblemente, usted también podría continuar con la contiguación a los canales. ¿Quizás deberíamos tener algún tipo de alternancia local de subprocesos que diga cuáles son los valores predeterminados?

Pero parece que hay muchos detalles aquí para discutir, y no estoy seguro de si al final funciona.

Entonces, la confusión de la convolución (y otros operadores conscientes del diseño, para el caso, por ejemplo, el muestreo superior que he visto recientemente comienza llamando a .contiguous () en la entrada, entonces, ¿qué se supone que significa?) Fue la razón principal para introducir la etiqueta, iirc.

Sí, estoy de acuerdo con abrir el diseño de la etiqueta nuevamente, pero luego
tienen que resolver seriamente los problemas de cómo propagar estas etiquetas,
incluso cuando pierde el diseño (como hubiera sido el caso con fragmentación
en canales). Me gusta mucho más hacer un "diseño actual"
una especie de administrador de contexto, que hacerlo dependiente de los datos.

Extractos del mensaje de ngimel de 2019-06-19 12:43:45 -0700:

Entonces, la confusión de la convolución (y otros operadores conscientes del diseño, para el caso, por ejemplo, el muestreo superior que he visto recientemente comienza llamando a .contiguous () en la entrada, entonces, ¿qué se supone que significa?) Fue la razón principal para introducir la etiqueta, iirc.

Por cierto, ¿por qué tenemos que crear un nuevo concepto en lugar de limitarnos a layout ? No creo que las representaciones dispersas tengan un concepto bien definido de un diseño como "channels_last", por lo que no necesitamos representar un producto de memory_formats * layouts ( layouts refiere al uso actual ), pero solo memory_format + layouts lo que significa que debería estar bien usar el mismo argumento que solíamos usar. Para mí es más breve, más agradable y nos permitirá evitar extender las firmas de las fábricas a mil argumentos.

Se consideró la opción de diseño (consulte el apéndice), pero descubrimos que conducirá a una gran cantidad de duplicación de código y no permitirá la conversión automática de tensores a un formato de memoria diferente al instante.

después de todo, memory_format es una forma de stride tensor y de elegir fácilmente kernels optimizados y salidas que son propiedad del tensor strided, no una clase completamente diferente

En cierto sentido, los diseños dispersos también son una forma de elegir fácilmente kernels optimizados para matrices que en su mayoría son cero.

Esta podría ser una pregunta ingenua, pero ¿por qué PyTorch está considerando esta API en lugar de simplemente exponer una opción para usar NHWC en las propias operaciones, que llamaría directamente al kernel CuDNN subyacente cuando esté disponible?

Parece que para un caso de uso común (mezclar operaciones de imágenes como conv y combinar con arquitecturas LM) esta sería una solución fácil. Como desarrollador, todo lo que quiero es un Conv2d(..., nhwc=True) . ¿Hay alguna razón por la que esto no tenga sentido?

@rewonc hemos considerado un enfoque similar (agregar opciones a los operadores en lugar del kernel derivado de striding), y nos resultó difícil de aplicar por las siguientes razones:

  • Este enfoque requerirá que el kernel restrinja el tensor contiguo para aplicar el kernel NHWC.
  • El siguiente operador tendrá que volver a restringir la entrada (a contiguo) a menos que también tenga la opción nhwc=True .
  • Para tener NHWC en la red, cada operador necesitaría la opción nhwc=True .

PD. Si le preocupan las funciones CudNN Ex , estamos buscando exponer cudnn_batch_norm_nhwc y operadores similares.

Hola @VitalyFedyunin , vimos que el tensor con nombre era compatible con PyTorch 1.3. ¿Puede eso resolver (o resolver parcialmente) las preocupaciones sobre la compatibilidad con el formato NHWC (o incluso bloqueado)? ¿Hay algún plan para avanzar en el estado de NHWC en función del tensor con nombre?

Estamos avanzando con el último soporte de canales, publicaré la hoja de ruta esta semana aquí y en canales flojos. No estamos considerando agregar formatos bloqueados en el corto plazo (ya que requerirá reescribir TODOS los operadores).

Gracias. ¡Eso será bueno!

Seguimiento de tareas y progreso dentro de https://github.com/pytorch/pytorch/issues/28619

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

soumith picture soumith  ·  3Comentarios

eliabruni picture eliabruni  ·  3Comentarios

bartvm picture bartvm  ·  3Comentarios

rajarshd picture rajarshd  ·  3Comentarios

miguelvr picture miguelvr  ·  3Comentarios