Pytorch: Suporte ao formato de memória [RFC] (também conhecido como layout, também conhecido como NHWC)

Criado em 10 abr. 2019  ·  68Comentários  ·  Fonte: pytorch/pytorch

Declaração do problema

Os operadores CNN utilizam a ordem canônica das dimensões do tensor e atribuem a elas significado semântico. Para o caso 2D em PyTorch hoje, uma entrada para torch.nn.Conv2d deve ser um tensor 4d na ordem NCHW -.

Por motivos de desempenho, muitas vezes é benéfico reordenar as dimensões de maneira diferente, para que a memória acessada por operações específicas seja disposta de forma contígua e a localidade seja melhor utilizada. A opção mais comum é mover as dimensões em direção ao final - NHWC. Pode haver formatos de memória ainda mais complexos que agrupam uma dimensão em blocos, por exemplo.

Bibliotecas de exemplo que o utilizam incluem:

  • cudnn tem desempenho mais rápido no Volta em NHWC
  • fbgemm e qnnpack não suportam NCHW.
  • libxsmm oferece suporte a NCHW, mas a penalidade de desempenho é algo como 50% (IIRC).

O desafio é que transformar a ordem da dimensão em si é caro, portanto, nos casos em que várias operações CNNs são realizadas em uma linha (por exemplo, conv(relu(conv))) ), é benéfico transformar para o formato de memória diferente uma vez, realizar as operações e reordená-las de volta.

Portanto, é importante tornar o PyTorch ciente das diferentes ordens de dimensões e ser capaz de passar tensores com diferentes formatos de memória entre as operações no modo ansioso e JIT. Além disso, é benéfico ter passes de otimização JIT automáticos que tentam aplicar heurísticas ou técnicas de pesquisa para descobrir se alterar o formato da memória é benéfico para o desempenho e onde no modelo faz sentido fazê-lo.

Nós nos esforçamos para construir uma API capaz de representar:

  • Tensor com formato de memória diferente (no início, apenas ordem de dimensão) presente no PyTorch no Eager e no JIT. Layouts bloqueados são de baixa prioridade, mas ainda assim são bons.
  • APIs expostas pelo usuário para consultar e alterar o formato da memória
  • As operações principais do CNN são capazes de lidar com tensores de entrada com diferentes formatos de memória e roteamento para uma implementação mais rápida correspondente
  • Capacidade de inferir e otimizar formatos de memória em passes JIT

Terminologia : o problema acima é frequentemente referido como “layout” (mxnet), “data_format” (tf), “image_format” (keras), “order” (caffe2). Propomos utilizar o nome “formato de memória” ou “formato_memória” no PyTorch. O nome “layout” infelizmente é usado no PyTorch com os valores 'strided' vs 'sparse_coo', de modo que a opção de nomenclatura não está disponível.

Operadores afetados

Os operadores a seguir devem, no mínimo, reconhecer o formato da memória. Além de produzir o resultado correto, eles precisam fornecer o melhor desempenho das bibliotecas subjacentes E preservar o formato de

  • convolução
  • diferentes tipos de pool
  • norma de lote, norma de camada, norma de instância (geralmente, quaisquer normas)
  • upsampling / interpolação
  • queda de recurso
  • softmax em um grau menor - a dimensão pode ser especificada manualmente lá, mas implementações eficientes estão presentes apenas para o layout nchw implícito
  • preenchimento
  • operações elementares (unárias e binárias)
  • construtores de tensores que herdam o formato da memória, por exemplo, empty_like.

API e mudanças de comportamento

Defina o conceito de formato de memória em PyTorch:

  • Constantes como torch.memory_format.channels_first . Eles não têm um tipo especificado e podem ser objetos comparáveis ​​arbitrários (provavelmente começam com enum, mas no futuro podem ser outros objetos para interoperar com o conceito de tensor nomeado)

    • Alternativa: use torch.channels_first diretamente

  • Os valores são channels_first e channels_last (para permitir menos constantes)
  • Para imagens 1D / tensores 3D, os valores significam NCW, NWC, para imagens 2D / tensores 4D - NCHW, NHWC, para imagens 3D / tensores 5D - NCDHW, NDHWC

Adicione os seguintes métodos ao Tensor:

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

Nota : não há função x.get_memory_format() por enquanto, apenas verificações explícitas - permite uma gama mais ampla de implementações possíveis. Podemos querer adicioná-lo.

O layout semântico do tensor sempre permanece o mesmo - NCHW! x.size() sempre retorna (n,c,h,w)

As operações preservam o comportamento do formato de memória:

  • convolução, pooling, etc, (veja acima) retorna a saída no mesmo formato de memória que a entrada e despacha internamente para a melhor implementação
  • as operações elementares unárias preservam o mesmo formato de memória e precisam ser executadas tão rápido quanto no tensor contíguo
  • As operações binárias de elementos fornecem algumas garantias razoáveis ​​sobre a preservação do formato da memória - provavelmente podem ser definidas de forma mais ampla, mas o mínimo é:

    • NHWC + escalar → NHWC

    • NHWC + vetor coluna → NHWC

  • as operações de retrocesso para operações CNN centrais preservam o mesmo formato de memória do caminho de encaminhamento. (pode ser necessário aplicar explicitamente porque os gradientes de entrada para a saída podem estar em diferentes formatos de memória)

O formato da memória é uma propriedade de um tensor que é preservada por meio da serialização / desserialização (caso o tensor seja um parâmetro).

Implementação Strided

O Tensor em PyTorch hoje tem o conceito de passadas que especificam como o tensor memória . Especificamente, cada tensor tem um vetor strides do mesmo comprimento que sizes . Para indexar elementos na indexação lógica (i1, i2, .., ik) um faz o produto pontilhado com passos largos e procura na memória offset + i0*stride0 + i1*stride1 + ... * ik * stridek . Tensores contíguos, portanto, têm passadas que são produtos cumulativos reversos de tamanhos. Por exemplo, tensor 4D com tamanhos (n,c,h,w) tem passos largos (c*h*w, h*w, w, 1) .

Strides pode ser usado para representar diferentes formatos de memória (que são reordenamento de dimensão) fisicamente enquanto preserva a ordem NCHW padrão lógica. Ele fornece uma definição eficaz de transformação de formato de memória 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()

No formato NHWC, o vetor de passos é (c*h*w, 1, c*w, c) . Assim, no buffer de memória, os pesos estão em ordem contínua para NHWC.

O Strides pode ser usado para testar:

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

Prós desta abordagem:

  • Utiliza o conceito PyTorch existente de avanços sem adicionar novas ideias de nível superior ou parâmetros de API
  • Preserva o comportamento lógico do tensor na ordem NCHW canônica
  • Funciona para reordenamento arbitrário de dimensões de entrada
  • As rotinas de serialização existentes já preservam avanços do tensor
  • Capacidade de reutilizar muitas operações para trabalhar em diferentes layouts de memória

Contras :

  • Chamar .contiguous() é equivalente a mudar para NCHW e pode ocorrer por acidente do usuário ou dentro de uma das operações

    • A auditoria explícita dos operadores é necessária para garantir que preservem o formato da memória

  • Não funciona para formatos bloqueados / lado a lado - uma abordagem diferente é necessária

    • É possível considerar adicioná-los como cidadãos de primeira classe em PyTorch, mas é uma mudança muito maior

    • A alternativa é tratá-los como alças opacas, por exemplo, tensores MKLDNN

  • As características de desempenho das implementações subjacentes são menos óbvias para o usuário final

O maior problema potencial é com a intenção do usuário pouco clara . Não há como distinguir se o usuário realmente deseja um formato de memória diferente ou o tensor de entrada apenas acontece desta forma. Especificamente, isso leva a uma mudança de comportamento para as operações existentes - hoje a convolução só pode produzir tensores contíguos de NCHW, mesmo se a entrada for de passos largos arbitrários, em um novo mundo ela poderia reconhecer a entrada como NHWC e, portanto, também retornaria NHWC. Isso não muda a semântica, mas leva a problemas de desempenho difíceis de depurar. A solução possível pode ser marcar os tensores explicitamente com o sinalizador memory_format especificado pelo usuário e apenas seguir esta anotação (além de passos).

Para resolver o problema acima, a proposta inicial é introduzir um tag de formato de memória “soft” no tensor que registre a última to(memory_format) chamada feita no tensor. Os operadores precisariam propagar essa anotação para as saídas. A anotação é “suave”, portanto, não faremos erros físicos em anotações incompatíveis, mas sim produziremos avisos no modo de criação de perfil.

Implementações do operador

A assinatura dos operadores existentes não muda. Os operadores podem fazer despacho codificado dentro do operador para encaminhar para uma implementação mais rápida. Se a implementação não estiver disponível, o round-trip através de diferentes formatos de memória é possível. A alternativa seria gerar uma mensagem de erro.

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

É preferível usar um único símbolo como 'conv' para se referir aos operadores no JIT IR em vez de criar operadores separados como 'conv_nhwc'. A razão para isso é a simplicidade e manter o RI no nível da representação semântica.

Operações elementares

Temos que garantir que as operações principais, como o elemento inteligente, preservem o formato da memória e sejam eficientes.

As operações unárias podem ser tratadas genericamente verificando se um bloco de memória é “denso” - ou seja, se os elementos abrangem uma área sem lacunas e cada local da memória é usado exatamente uma vez. Pode ser verificado com um algoritmo simples

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

Ferramentas de desempenho

Para desempenho de depuração, devemos adicionar suporte ao criador de perfil para:

  • ver onde no programa os reordenamentos de memória reais ocorrem - ou seja, rastrear chamadas para .contiguous ()
  • rastrear qual implementação é invocada
  • emitir avisos sobre mudanças de formato de memória em, por exemplo, operações binárias (onde a anotação “suave” é útil)

Essa funcionalidade pode ser incorporada a uma ferramenta de criação de perfil sob demanda.

Manipulação de autogradação

É lógico esperar que a passagem para trás seja executada com o mesmo formato de memória que para a frente. Isso nem sempre acontecerá automaticamente, pois os gradientes de entrada podem ter passos arbitrários. Assim, a passagem para a frente tem que reconhecer explicitamente o formato da memória, armazená-lo no fechamento do autograd e aplicar ao tensor grad antes da função de retrocesso.

Implementação possível:

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

Representação em JIT

A proposta atual é ter:

  • Ainda não há manipulação de primeira classe para formato de memória em anotações de tipo. Em vez disso, podemos manter um mapa lookaside na forma necessária para passagens que manipulam o formato de memória
  • Passagem de inferência (semelhante a shape_inference) que produz anotações de formato por valor
  • Passos de transformação do formato de memória (manual ou automático) que encontram, quando necessário, to(memory_format) chamadas precisam ser inseridas para um desempenho ideal

Para fins de aplicação, também podemos utilizar declarações como assert x.is_contiguous(channels_last) .

Observação: há uma questão de onde armazenar as informações de que determinado dispositivo possui uma combinação de formato de memória preferencial (por exemplo, qconv em rotas x86 para fbgemm que implementa apenas NHWC). Uma opção é colocá-lo no nível de registro operacional, no entanto, a anotação no formato de memória parece mais uma informação secundária. Podemos começar mantendo um mapa global em algum lugar no passo JIT que denota formatos de memória preferidos e heurísticas associadas. Se ficar desordenado - podemos mudar para o mecanismo baseado em registro.

Além: layouts bloqueados

Conforme decidimos adicionar pacotes mais complexos de tensores, usar o tensor PyTorch de primeira classe para isso pode não ser plausível devido ao alto custo de implementação e complexidade. Duas alternativas são possíveis:

  • Representações opacas, como associações de tipo C personalizadas. Esta é uma opção a ser escolhida para empacotar na inferência onde a diversidade é maior em termos de otimizações de desempenho
  • Tipo de tensor de primeira classe como MKLDNNTensor com algumas (mas não todas) das operações vinculadas a este novo tipo

Ainda outra alternativa é implementar suporte nativo para bloqueio / tiling na classe PyTorch Tensor principal.

Relação de tensor nomeada

A proposta existente para NamedTensor é estruturada como um mecanismo de verificação de tipo em tensores - no momento não atribui nenhum significado semântico aos nomes das dimensões. Assim, a única maneira de inferir o significado do tensor de ativação é continuar usando o formato NCHW predeterminado. Ele torna o NamedTensor e as propostas atuais ortogonais.

Se estivermos dispostos a especificar os significados de alguns nomes (como “canais”, “larguras”), os operadores podem utilizar essas informações para encaminhar para uma implementação mais rápida. Seria uma mudança semântica, pois os tensores de entrada teriam logicamente o formato de memória NHWC (não NCHW como hoje).

Arte anterior

O TensorFlow é compatível com NHWC e NCHW no nível do operador, por meio do parâmetro data_format ; os valores aceitáveis ​​são (“NHWC”, “NCHW”) para entradas 4-d, (“NDHWC”, “NCDHW”) para entradas 5-d ou channels_first / channels_last independente da entrada dimensionalidade. Cabe ao usuário lidar com a configuração correta do parâmetro, ou seja, ele não é rastreado automaticamente pelo tensor.

Caffe2 chama esse parâmetro de order vez de data_format , mas ainda é aplicado explicitamente no nível do operador individual.


Apêndice: Outras opções consideradas

Pergunta decisiva: o que o código a seguir imprime: tensor_in_nhwc_layout.size(1) - o número de canais (porque o padrão é NCHW em PyTorch) ou altura (porque é o que está no layout NHWC na posição 1).

Com base nesta resposta, várias opções são possíveis:

  • Opção A - Strides (apresentada acima). O layout do tensor é uma representação totalmente interna. Semelhante à implementação, é mais convenientemente feito com passos largos.

    • .size (1) retorna "canais", mas a memória interna é disposta de forma diferente

    • pro: não muda o código do modelo, meu modelo ainda pode fazer aritmética de dimensão diretamente. Na verdade, nenhuma das mudanças de API públicas

    • contras: na implementação rápida, muitos operadores chamam .contiguous () e podem reverter o layout acidentalmente

    • contras: da perspectiva do usuário, entender quais são as garantias do retorno operacional são fundamentais. Este IMO elimina abordagens apenas de passos largos, porque se torna muito difícil entender o formato em que seu op será retornado, e não há API para dizer "ignore meus passos, na verdade, apenas retorne a coisa contígua de NCHW." Isso é uma adição às limitações acima.

  • Opção B - tensor NHWC explícito. O usuário manipula explicitamente o tensor que possui uma ordem de dimensão diferente, mas o próprio tensor não sabe nada sobre isso. Precisaríamos de algumas anotações no nível do operador para descobrir o que o usuário espera.

    • .size (1) retorna “altura”

    • pro: sem mágica e muito previsível

    • contras: mudar o modelo de um layout para outro torna-se uma operação complexa que precisa rastrear todos os acessos a .size () e .reshape () (ou você precisa torná-lo explícito na API?)

  • Opção B '- tensor NHWC explícito com sinalizador de layout . O mesmo que acima, mas permitimos anexar uma anotação ao tensor para marcar o layout semântico que os ops consomem em sua implementação. Não há necessidade de anotação em nível de operador - um operador pode fazer o despacho com base no sinalizador de layout das entradas.
  • Opção C - Tensor nomeado . ( https://docs.google.com/document/d/1ynu3wA2hcjwOtEng04N904gJjEbZWcINXO_ardX6hxc/edit#heading = h.2gbe5xpga3w9)

    • .size (1) retorna “altura”, mas pedimos às pessoas que NÃO usem esta API e, em vez disso, usem .size ('canal')

    • pro: muito explícito e o que o usuário deseja

    • con: não resolve o problema de transição, precisaríamos forçar todo o código escrito com reconhecimento de layout a usar tensores nomeados. Caso contrário - os mesmos problemas acima se aplicam

  • Opção D - O layout é do tipo tensor opaco . Trate NHWC como tratamos MKLDNN ou SparseTensor - tipo tensor separado com DispatchID diferente. É como a Opção A, mas com diferentes compensações no comportamento padrão - as operações não implementadas falhariam em vez de reverter para NCHW.

    • .size (1) ainda retorna “canais”

    • pro: sem mágica e explícito, despacho separado permite que os ops decidam o que querem

    • prós / contras: todos os operadores necessários precisam ser implementados em um layout diferente, se alguma operação estiver faltando, o usuário receberá um erro explícito de que não é compatível

    • contras: provavelmente precisaríamos proibir muitas operações nele, por exemplo, visualizações porque os resultados esperados são difíceis de prever

internals mkldnn triaged

Comentários muito úteis

BTW, por que temos que criar um novo conceito em vez de apenas nos limitarmos a layout ? Não acho que representações esparsas tenham um conceito bem definido de layout como "channels_last", então não precisamos representar um produto de memory_formats * layouts ( layouts refere-se ao uso atual ), mas apenas memory_format + layouts o que significa que não há problema em usar o mesmo argumento que usamos? Para mim é mais curto, mais agradável e nos permitirá evitar estender assinaturas de fábricas para mil argumentos.

Todos 68 comentários

Há um problema com empty_like ; a semântica atualmente definida é que você descarta todas as informações de passada, portanto, não é possível preservar o layout e ser BC.

@VitalyFedyunin se inscreveu para implementar os bits .contiguous() e torch.memory_layout

Uma pergunta - para um tensor 4D x com tamanhos (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)

Temos uma permutação estranha

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

Agora verificamos se ele é contíguo para o formato NHWC. Seguindo sua lógica como abaixo

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 os casos is_nhwc_contiguous(y) retornará Verdadeiro?

Isto está correto. No entanto, não podemos retransmitir apenas a passos largos, pois queremos evitar quaisquer conversões para frente e para trás durante a cópia, para e operações semelhantes.

E se os passos tiverem a mesma ordem do formato da memória? Vamos usar o tensor 4D como exemplo. Para descrever um tensor, temos sizes , strides e stride_indexes :

tamanhos em (n, c, h, w)
passos largos na ordem física, ou seja,

  • passos de (n, c, h, w) se o formato for nchw
  • passos de (n, h, w, c) se o formato for nhwc.

stride_indexes mapeia os avanços para o tamanho nchw:

  • (0, 1, 2, 3) se o formato for nchw,
  • (0, 2, 3, 1) se o formato for nhwc.

Para o formato nchw, é o mesmo que antes. Para nhwc, será semelhante.

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)

Isso também pode ser estendido para suportar o formato bloqueado. Use nChw16c como exemplo,

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)

Mais detalhes podem ser explorados posteriormente.

Para OPs que aceitam apenas tensores contíguos nchw, haverá algum trabalho aqui.

Alternativamente, também podemos mudar ligeiramente o protótipo, digamos

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

Portanto, por padrão, ele assume que apenas nchw é contíguo. Desta forma, você não precisa reescrever esses OPs, eles serão reordenados para nchw automaticamente.

Nós nos esforçamos para construir uma API capaz de representar:

  • Tensor com formato de memória diferente (no início, apenas ordem de dimensão) presente no PyTorch no Eager e no JIT. Layouts bloqueados são de baixa prioridade, mas ainda assim são bons.
  • APIs expostas pelo usuário para consultar e alterar o formato da memória
  • As operações principais do CNN são capazes de lidar com tensores de entrada com diferentes formatos de memória e roteamento para uma implementação mais rápida correspondente
  • Capacidade de inferir e otimizar formatos de memória em passes JIT

Ótima proposta! Posso explicar meu entendimento para ver se está certo (incluindo propostas para manipulação de formatos MKL-DNN):

Permitam-me pensar que houve uma implementação desta proposta como uma aula de "formato". Contanto que forneça consulta e alteração da API como virtual, poderíamos fazer a herança / extensões que se ajustam aos formatos complexos MKL-DNN. Ou outros métodos, desde que forneça uma estrutura para lidar com formatos, transferindo esses detalhes essenciais para nós.

Sobre a implementação de OPs, cada OP poderia ter um formato preferencial que maximizasse seu desempenho e um formato compatível e funcional. O operador elemento-a-elemento (ou, falando de maneira mais geral, OPs limitados por memória) supõe não ter preferência. OP produz seu tensor de resultados com um objeto de "formato", este objeto de formato garante consulta / alteração semântica compatível com a expectativa de pytorch padrão, bem como pode manipular formatos específicos se chamados de séries de funções otimizadas (como conv2d (ReLU (conv2d)) caso)

@uyongw , quero esclarecer um pouco mais sobre seu primeiro exemplo. Você configurou o exemplo como, "Eu tenho um tensor NCHW, que transpus de uma maneira estranha (agora parece NWCH); agora eu quero saber se é NHWC contíguo." Mas essa é a maneira errada de ver as coisas. Uma formulação melhor é: "Eu tenho um tensor NHWC, que transpus para um tensor NCHW."

Em outras palavras, não há significado intrínseco para as dimensões físicas de um tensor (quando ignoramos os passos). Só damos sentido a eles quando consideramos como os referimos com respeito aos passos largos.

Para descrever um tensor, temos tamanhos, passadas e stride_indexes

Acho que stride_indexes é uma maneira conveniente de pensar sobre o problema, mas é estritamente redundante com passadas, porque tudo o que você está dizendo é "Aplique esta permutação (reversa?) A passadas e, em seguida, trate-a como o verdadeiros passos.) @VitalyFedyunin e eu estávamos conversando sobre como ainda pode ser uma boa ideia armazenar em cache essas informações de alguma forma, porque é uma dor reconstruir as informações a partir dos próprios passos. Mas isso está fora do escopo desta proposta.

Portanto, por padrão, ele assume que apenas nchw é contíguo.

Sim, essa é a minha leitura do plano.

@CaoZhongZ

Permitam-me pensar que houve uma implementação desta proposta como uma aula de "formato". Contanto que forneça consulta e alteração da API como virtual, poderíamos fazer a herança / extensões que se ajustam aos formatos complexos MKL-DNN. Ou outros métodos, desde que forneça uma estrutura para lidar com formatos, transferindo esses detalhes essenciais para nós.

Na verdade, não acho que seja uma descrição precisa da proposta. O suporte de layout de memória que a proposta aqui oferece são apenas layouts que podem ser expressos por meio de passos largos. Qualquer coisa que seja inexprimível dessa forma (por exemplo, layout de bloco) não funcionará dessa forma e deve ser suportada por nosso mecanismo de "layout" mais pesado.

Em outras palavras, não há significado intrínseco para as dimensões físicas de um tensor (quando ignoramos os passos). Só damos sentido a eles quando consideramos como os referimos com respeito aos passos largos.

Concordo parcialmente :-) Mas não neste problema específico. Digamos que eu já tenha um tensor nhwc. Então eu permuto para nwhc. Desejo permutar ainda mais para nhwc e, em seguida, fazer um contíguo (). Mas eu já entendi nhwc contíguo. Não é confuso?

Acho que stride_indexes é uma maneira conveniente de pensar sobre o problema, mas é estritamente redundante com passadas, porque tudo o que você está dizendo é "Aplique esta permutação (reversa?) Às passadas e, em seguida, trate-a como passadas verdadeiras.)

IMHO, não será redundante com passadas, se você tiver passadas em nhwc (físico). Porque você precisa de um mapeamento correto com tamanhos (lógica). Caso contrário, não há como saber a ordem real.

BTW, há uma abordagem mais direta usando mapeamento reverso. Digamos que para nchw seja (0, 1, 2, 3), para nhwc, seja (0, 3, 1, 2) em vez de (0, 2, 3, 1). Isso diz que o próprio stride_index é sempre NCHW também. Mas o problema é que ele não pode ser estendido para formatos bloqueados como nChw16c ou OIhw16i16o.

Os formatos bloqueados requerem um conjunto completamente diferente de implementação de operadores; por esse motivo, preferimos não misturá-los com o 'formato de memória', que por definição é considerado amigável com todos os operadores existentes e funciona com o mesmo ou melhor desempenho.

Concordo parcialmente :-) Mas não neste problema específico. Digamos que eu já tenha um tensor nhwc. Então eu permuto para nwhc. Desejo permutar ainda mais para nhwc e, em seguida, fazer um contíguo (). Mas eu já entendi nhwc contíguo. Não é confuso?

É difícil entender seu exemplo porque você está usando alguns termos coloquialmente e é necessária precisão. Aqui está como estou interpretando o que você disse:

  • Um tensor "nhwc" para ser conforme esta proposta, "Tensor cujo layout físico é NHWC, mas é alongado de forma que o layout lógico seja NCHW."
  • Para "permutar um tensor (tensor cujo layout lógico é NCHW) para (layout lógico) NWHC" é executar y = x.permute(0, 2, 3, 1) , uma vez que você está permutando o layout lógico , não o layout físico. (Suspeito que não foi isso que você quis dizer, porque em sua postagem original você mencionou a permutação x.permute(0, 3, 1, 2)
  • Para, então, permutar um tensor NWHC (layout lógico) para (layout lógico) NHWC é aplicar a permutação z = y.permute(0, 2, 3, 1) . Portanto, agora você tem um tensor cujo layout lógico coincide com o layout físico. Isso significa que, se perguntarmos z.contiguous() , seremos verdadeiros (e, confusamente, z.contiguous(memory_layout=NCHW) também o será.) Mas NÃO será contíguo ao NHWC.

Não creio que este seja o exemplo que você tinha em mente, caso em que terá que ser mais preciso sobre o que entende por "permutar".

IMHO, não será redundante com passadas, se você tiver passadas em nhwc (físico). Porque você precisa de um mapeamento correto com tamanhos (lógica). Caso contrário, não há como saber a ordem real.

Este é o ponto crucial da proposta: privilegiamos o NCHW como o layout lógico, sempre . Portanto, se tenho um tensor 4D sobre o qual nada sei, presumo que seu layout lógico seja NCHW. Isso remove a ambigüidade. Se você quiser lidar com tensores cujo layout lógico não é NCHW, acho que a API conforme declarada torna a vida um pouco difícil para você.

@dzhulgakov

As operações preservam o comportamento do formato de memória

Se tensores NHWC físicos podem ocorrer puramente por meio de passadas, isso é tecnicamente uma quebra de BC, a menos que você faça com que eles preservem o formato de memória apenas quando a tag de formato de memória estiver presente (mas parece que você não quer que isso tenha significado semântico, então eu não tenho certeza do que a proposta está sugerindo no momento.) Não tenho certeza se isso realmente quebra o código de alguém na prática.

Se tensores NHWC físicos podem ocorrer puramente por meio de passadas, isso é tecnicamente uma quebra de BC, a menos que você faça com que eles preservem o formato de memória apenas quando a tag de formato de memória estiver presente (mas parece que você não quer que isso tenha significado semântico, então eu não tenho certeza do que a proposta está sugerindo no momento.) Não tenho certeza se isso realmente quebra o código de alguém na prática.

Supondo que possamos tornar o formato da memória 'aderente'. O tensor de op sobre memória formatado produzirá tensor de memória formatado. Isso resolverá o problema do BC.

No entanto, precisamos definir um comportamento de operações binárias (ou mais membros) quando tensores têm formatos de memória diferentes.

@ezyang Oh, acabei de descobrir que há um erro de digitação na minha resposta acima. (Lamento por isso. No entanto, o exemplo original ainda está correto.) Deixe-me reformulá-lo como abaixo:

  1. Eu tenho um tensor NCHW (fisicamente, contíguo).
  2. Então eu permuto para NWHC (logicamente).
  3. Eu quero permutá-lo ainda mais para NHWC com uma chamada contígua () seguida.
  4. Use-o como NHWC (fisicamente).

Mas eu entendi NHWC contíguo já após a etapa 2. Então, posso pular a etapa 3 e usá-lo como NHWC diretamente na etapa 4. Mas isso certamente não está correto porque a ordem física do tensor não muda em nada.

Os formatos bloqueados requerem um conjunto completamente diferente de implementação de operadores; por esse motivo, preferimos não misturá-los com o 'formato de memória', que por definição é considerado amigável com todos os operadores existentes e funciona com o mesmo ou melhor desempenho.

Sim, podemos habilitar o NHWC como a primeira etapa. No entanto, não acho que o formato bloqueado seja algo totalmente diferente. Pode ser expresso naturalmente (com alguma boa abstração). Se houver uma descrição geral do formato, outros podem apenas registrar novos formatos com bloqueios / passadas arbitrárias.

Mais, se já tivermos bloqueado o suporte, não nos preocupamos em criar algumas construções ocultas para executar tudo subjacente, o que cria um mundo implícito dentro e o de / para entre os dois mundos pode se tornar um problema.

De qualquer forma, pode estar muito longe para pensar em um formato bloqueado. Mas eu acho que, se possível, é melhor tornar o design extensível.

Mas eu entendi NHWC contíguo já após a etapa 2. Então, posso pular a etapa 3 e usá-lo como NHWC diretamente na etapa 4. Mas isso certamente não está correto porque a ordem física do tensor não muda em nada.

OK, eu entendo seu exemplo agora. Você pode realmente parar na etapa 2 e usá-lo como se fosse um tensor NCHW; nesse caso, você interpretará incorretamente W como C, etc. Esta é definitivamente uma desvantagem com a implementação baseada em passos ( @dzhulgakov , provavelmente deveríamos adicionar isso à proposta). A proposta contém algumas disposições para este caso:

Para resolver o problema acima, a proposta inicial é introduzir um tag de formato de memória “soft” no tensor que registre a última chamada (memory_format) feita no tensor. Os operadores precisariam propagar essa anotação para as saídas. A anotação é “suave”, portanto, não faremos erros físicos em anotações incompatíveis, mas sim produziremos avisos no modo de criação de perfil.

A tag de formato de memória macia permitiria distinguir de um tensor NCHW que você permutou, versus um tensor que é, na verdade, fisicamente, NHWC. Mas a soft tag em sua forma atual não é vinculativa, então não tenho certeza de quão útil ela realmente seria para este caso.

Outra maneira de resolver o problema é com tensores nomeados. Com tensores nomeados, podemos usar os nomes nas dimensões (lógicas) para descobrir se estamos vendo um tensor como NCHW (o padrão assumido) ou outra coisa.

No entanto, não acho que o formato bloqueado seja algo totalmente diferente. Pode ser expresso naturalmente (com alguma boa abstração). Se houver uma descrição geral do formato, outros podem apenas registrar novos formatos com bloqueios / passadas arbitrárias.

Há mais comentários sobre o tópico aqui: https://github.com/pytorch/pytorch/issues/16038#issuecomment -454490374

@ezyang Obrigado pela resposta. Sim, a etiqueta de formato suave pode ajudar. A preocupação é que pode não ser flexível o suficiente, pois a ordem das dimensões pode ser arbitrária. Além disso, ele próprio não é computável. Tensor nomeado tem significado semântico para cada dimensão, mas pode precisar de mais alguns recursos para oferecer suporte, eu duvido.

Pessoalmente, acho que isso pode ser resolvido com a introdução de um mapa da ordem dos passos (física) para a ordem dos tamanhos NCHW (lógico). Como propus acima, para NCHW é quase igual ao design atual; para NHWC, sizes ainda é NCHW, strides estará na ordem (N, H, W, C). E usamos stride_index = (0, 2, 3, 1) para especificar o índice de dimensão das passadas.

Além disso, a combinação de strides e stride_index pode ser usada para representar qualquer formato de tensor. Isso pode dar flexibilidade a outras pessoas para registrar um novo formato de dados.

@ezyang

As operações preservam o comportamento do formato de memória

Se tensores NHWC físicos podem ocorrer puramente por meio de passadas, isso é tecnicamente uma quebra de BC, a menos que você faça com que eles preservem o formato de memória apenas quando a tag de formato de memória estiver presente (mas parece que você não quer que isso tenha significado semântico, então eu não tenho certeza do que a proposta está sugerindo no momento.) Não tenho certeza se isso realmente quebra o código de alguém na prática.

Quando as operações aritméticas e o limite foram movidos para TensorIterator, isso era tecnicamente uma quebra de BC (porque o formato de memória dos operandos costumava não ser preservado e o TensorIterator o preserva). O status quo agora é muito inconsistente - o limite preserva o layout, todas as outras operações unárias não, torch.where não, as operações aritméticas preservam o layout se ambos os operandos tiverem o mesmo layout, mas seriam padronizados para "nchw" ou tensor que é contiguous no entendimento atual, se houver uma incompatibilidade, não tenho certeza do que acontece com a transmissão.
Você também está fazendo uma boa observação sobre empty_like e coisas semelhantes, preservando o layout de não ser BC. Talvez também precise de um argumento de layout, como is_contiguous na proposta

x.is_contiguous(torch.memory_format.channels_first)

@ezyang @ngimel

Existe um problema com empty_like; a semântica atualmente definida é que você descarta todas as informações de passada, portanto, não é possível preservar o layout e ser BC.

Você também está fazendo uma boa observação sobre empty_like e similares que preservam o layout não sendo BC.

Se não dependermos de passos largos para expressar ordem física, empty_like não quebra necessariamente o BC. Existem 3 tipos de informações de dimensão no tensor:

  • forma: tamanhos
  • ordem lógica: informações de ordem registradas em passadas (normalmente usadas para suportar transpor ou permutar)
  • ordem física: NCHW ou NHWC (pode ser endereçado como stride_index como propus).

Atualmente, a ordem física é igual à forma / aos tamanhos. Então, simplesmente eliminamos a ordem lógica em passos largos. Considerando que estamos desacoplando a forma e a ordem física, também podemos simplesmente descartar a ordem lógica, mas preservar a forma e a ordem física por empty_like . Isso significa que size() e stride_index() serão preservados, mas stride() serão redefinidos. Especialmente, empty_like de um tensor NHWC retornará um tensor contíguo NHWC com a mesma informação de forma especificada.

@uyongw Não tenho certeza se seria uma boa ideia mudar empty_like ; no momento, sua semântica corresponde ao empty_like numpy .

O status quo agora é muito inconsistente - o limite preserva o layout, todas as outras operações unárias não, torch.onde não, as operações aritméticas preservam o layout se ambos os operandos tiverem o mesmo layout, mas seriam padronizados como "nchw" ou tensor que é contíguo em entendimento atual, se houver uma incompatibilidade, não tenho certeza do que acontece com a transmissão.

@ngimel , sim, eles não são muito consistentes agora. Acho que uma parte do trabalho de representar o formato da memória é fazer com que nossos operadores tenham um estado consistente

@ zou3519 numpy's empty_like que você vinculou tem order argumento cujo padrão é "corresponder ao layout do protótipo o mais próximo possível.". Não é isso que empty_like em pytorch faz atualmente (retorna "nchw" - tensor contíguo, mesmo se o protótipo for descontínuo)

Oh, entendo, eu estava lendo isso rápido demais. Nesse caso, seria bom ter o nosso empty_like match numpy's também e (provavelmente?) Seria bom ter um layout de memória aqui também

@ zou3519 Sim, o que estou tentando dizer é manter a semântica atual (descartar a ordem lógica como @ezyang e @ngimel mencionaram) e ao mesmo tempo preservar o layout físico como os padrões do numpy. Assim, para o protótipo NCHW, o comportamento será o mesmo de antes. Para o protótipo NHWC, seu comportamento ainda será compatível, ou seja, o novo tensor será NHWC contíguo, em vez de NCHW contíguo se você não alterar a implementação atual.

Duas questões:

  • O que acontece se um tensor NHWC for adicionado a um tensor NCHW?
  • Que tal abordar a desvantagem de (B) criando métodos como t.channel_dim () em um tensor que retorna o valor inteiro indicando onde a dimensão está fisicamente? Essa abordagem pode até ser necessária para permitir que outros formatos, como formatos de bloco, sejam escolhidos sem alterações na rede.

Se abordarmos a desvantagem de (B) com o último marcador, então (B) parece preferível para mim. É intuitivamente claro e os erros lógicos devem ser fáceis de detectar. Todos os ops existentes podem trabalhar no tensor também, já que se parece com qualquer outro tensor contíguo. As operações que podem entender a semântica (análoga à proposta do tensor nomeado) também funcionarão conforme o esperado.

@ zou3519 numpy's empty_like que você vinculou tem order argumento cujo padrão é "corresponder ao layout do protótipo o mais próximo possível.". Não é isso que empty_like em pytorch faz atualmente (retorna "nchw" - tensor contíguo, mesmo se o protótipo for descontínuo)

Estamos planejando manter o formato em tais casos (para tensores formatados por memória)

O que acontece se um tensor NHWC for adicionado a um tensor NCHW?
A operação com tensor formatado por memória retornará o tensor formatado por memória. Se ambos os tensores forem formatados em memória, o formato de saída será determinado pelo primeiro tensor.

Duas coisas que eu acrescentaria:

Estamos planejando manter o formato em tais casos (para tensores formatados por memória)

Precisaríamos auditar os usos existentes, porque frequentemente os operadores chamarão empty_like e então presumirão que eles são NCHW contíguos. E não sei como lidaríamos com o código de terceiros. Parece que precisaríamos de um padrão diferente de numpy se quisermos preservar o BC.

A operação com tensor formatado em memória retornará o tensor formatado em memória. Se ambos os tensores forem formatados em memória, o formato de saída será determinado pelo primeiro tensor.

Eu também acrescentaria, se você realmente se importa com o formato de sua saída - passe um tensor de saída.

Concordo em empty_like, existem alguns casos em que o resultado de empty_like / zeros_like etc é assumido como nchw-contíguo (fisicamente contíguo, devo dizer, em muitos casos não são operações de imagem).
Passar tensor de saída não é uma opção na maioria dos casos, porque funções com out kwarg não são diferenciáveis.

Muitos de nossos problemas vêm da inconsistência dos layouts de saída esperados. Não podemos resolvê-los todos de uma vez, mas podemos tentar bloquear o estado atual (pelo menos para passadas) e resolvê-los um por um. Então aqui está a proposta.

API Python

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

O tensor exigirá conversão de formato de memória 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 'etiquetá-los' com um 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)

Agora sobre empty_like e semelhantes:

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

Porque na verdade é:

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

Se quisermos manter o 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

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

Isso significa que podemos definir lentamente cada padrão de função memory_format para o estado atual do mundo, classificando-os e estar atento como vamos alterá-los no futuro.

Se você especificar o tensor, os TensorOptions são atualmente ignorados (na melhor das hipóteses, eles lançam uma exceção é, por exemplo, uma incompatibilidade de opção de dispositivo passada com out dispositivo tensor).

O formato da memória é suposto ser leve, portanto, quaisquer permutações irão perdê-lo.

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)

Não tenho certeza sobre o preenchimento, agradecemos a ajuda aqui.

No entanto, podemos fazer x.to (memory_format = torch.memory_format.nhwc) 'tag' tensor com o formato adequado e retornar self

Multiprocessamento

Preservará o formato de memória 'tag'

Formatos de memória de bloco

API acima não depende de dimensões / avanços / tamanhos, o que significa que podemos estender a funcionalidade no futuro mantendo a mesma API.

APIs internas

Os operadores seriam capazes de ramificar com base no formato da memória

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

Se fizermos memory_format como TensorOptions, podemos pensar em ramificação no nível de despacho (semelhante ao dispositivo, layout)

Pequeno feedback da proposta de @VitalyFedyunin - acho que requer tensores 4D aqui

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

é muito restritivo (porque também queremos lidar com 1D e 3D além de 2D), e channels_first/channels_last da proposta original foram mais adequados para este propósito.

Concordo, precisamos de uma nomenclatura melhor. channels_first soa quase certo, exceto que o lote vai primeiro =)

Eu gosto de sua última proposta. O tratamento de .contiguous () mudaria? Você exigiria .contiguous (memory_format = <...>)? Nesse caso, e muitos ops simplesmente chamam .contiguous (), eles ainda podem estar formatando a memória incorretamente. Muitas operações hoje também alocam saídas como empty_like (), o que teria o mesmo efeito. O plano seria atualizá-los para detectar o formato de memória das entradas e fazer as chamadas contíguas e empty_like corretas?

No momento, nossos usuários (e todas as bibliotecas) esperam que .contiguous() retorne o tensor contíguo de memória com passos em ordem decrescente.

Não podemos quebrar este contrato. No entanto, a boa notícia é: assim que oferecermos suporte à opção memory_format, o JIT será capaz de entender quando é mais eficiente chamar .contiguous(memory_format=...) vez do formato clássico.

@VitalyFedyunin Presumimos que operações como as abaixo não são 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)

Mais uma variante seria:

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 que o usuário chamaria .permute(0,2,3,1) em primeiro lugar? Todos os tensores nesta proposta têm tamanho semântico de (n, c, h, w), o que significa que size (1) retorna seus canais. É isso que a biblioteca padrão do PT assume hoje e o que assumiria também nesta proposta. Então, provavelmente ninguém chamaria .permute

Um gerenciador de contexto pode ser útil para permitir que o usuário substitua o formato de memória de tensores alocados dentro do escopo do gerenciador para um 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(...)

Não gosto da ideia de gerenciador de contexto, pois vai afrouxar o controle do memory_format.

Por exemplo:

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

Quando memory_format explícito deixa 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

Se necessário, podemos adicionar sintaxe para permitir:

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

@ raghuramank100 não há necessidade de permutar.

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

Fará todo o trabalho sujo para você, mantendo a ordem dos dimmer como em x.

Então:

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)

E você pode continuar abordando nhwc neste formato

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

@VitalyFedyunin Isso faz sentido.

Do ponto de vista do usuário, a nomenclatura do método (se continuar assim) me parece enganosa, pois "para" já é a forma recomendada para transferir o Tensor para diferentes dispositivos.

Além disso, que tal algo como o do Numpy para converter matrizes C_ORDER e F_ORDER?

numpy.asfortranarray()
numpy.ascontiguousarray()

Pode-se facilmente imaginar algo como:

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

@VitalyFedyunin : Eu entendo que a conversão para um memory_format diferente elimina a necessidade de os usuários permutarem manualmente. No entanto, uma vez que essa funcionalidade esteja disponível na tocha, o que aconteceria se os usuários chamavam as funções na sequência que descrevi acima? Devemos ter pelo menos uma mensagem de aviso / erro informando que a transformação do layout falhou.

@VitalyFedyunin : Eu entendo que a conversão para um memory_format diferente elimina a necessidade de os usuários permutarem manualmente. No entanto, uma vez que essa funcionalidade esteja disponível na tocha, o que aconteceria se os usuários chamavam as funções na sequência que descrevi acima? Devemos ter pelo menos uma mensagem de aviso / erro informando que a transformação do layout falhou.

Isso só será possível quando implementarmos tensores nomeados. Porque agora:

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

Ninguém pode me dizer se acabei de criar nchw ou nhwc.

Talvez eu tenha entendido mal a proposta original, mas a etiqueta de formato de memória gravada não deveria eliminar a ambigüidade dessa situação?

@VitalyFedyunin Faz sentido, precisamos nos certificar de que isso seja comunicado aos usuários finais quando a API se estabilizar.

@dzhulgakov @VitalyFedyunin Depois de revisar o # 19975, tenho algumas novas preocupações sobre a marca de formato de memória gravada no tensor. Meu problema básico é: como decidir se as operações devem preservar o tag de memória? Originalmente, eu pensava que apenas os operadores com "layout alternativo" precisariam ter essa inteligência. Mas olhando para o patch de Vitaly, acho que alguns operadores principais também vão precisar de ajustes. Por exemplo, considere x[0] ; se x era anteriormente um tensor NHWC, devo obter um tensor HWC depois de fazer isso. Tenho quase certeza de que o patch de Vitaly não lida com isso corretamente e aposto que ficaria muito confuso para os usuários. Talvez os únicos operadores afetados sejam aqueles que mexem com passadas (nesse caso, não há muitos deles e podemos auditá-los manualmente), mas parece que devemos fazer isso. O que você acha?

Espere, os tensores ainda permanecem indexados na ordem de: 0-dim N; 1o-dim C; 2-dim H; 3. dim W. Assim, x [0] retorna tensor com 0-dim C; 1o-dim H; 2 ° dim W. Independentemente se x era canal_primeiro ou canal_último layout de memória.

Caso contrário, memory_format simplesmente não faz sentido e precisamos apenas permutar o tensor.

Meu ponto é que a etiqueta de formato de memória não é preservada. Se o tensor de entrada foi marcado channels_last , o novo tensor está marcado any

cc @ zou3519 , a lógica de propagação de layout aqui me lembra muito da propagação de dimensão nomeada no trabalho de tensor nomeado.

Ainda estou atualizando essa proposta. Mas @ezyang poderíamos acompanhar a lógica de propagação do layout propagando um sinalizador por dimensão (ou nome) e então seria equivalente a ter tensores nomeados com convenções de nome

Seria ótimo se pudéssemos alinhar exatamente a lógica do tag de memória e a lógica do tensor nomeado, mesmo se os tivermos como dois caminhos de implementação separados no início.

Fase 1

Expande a funcionalidade de duas funções de tensor .is_contiguous e .contiguous (ambos python e c ++ api).

Observação: recebemos várias reclamações sobre a função .to(memory_format) e decidimos não apoiá-la.

  1. .contiguous agora oferece suporte a argumentos opcionais apenas de palavra-chave - memory_format , que pode ser torch.contiguous_format ou torch.channels_last .

    • Usar torch.contiguous_format preservará o comportamento existente de .contiguous() .

    • Chamar x.contiguous(memory_format=torch.channels_last) retorna um novo tensor que mantém o mesmo layout semântico (NCHW), mas tem um padrão de alocação de memória diferente.

      x.contiguous(memory_format=torch.channels_last) espera que o tensor de entrada seja 3d, 4d ou 5d; e falha de outra forma.

  2. .is_contiguous agora suporta o argumento opcional somente de palavra-chave - memory_format , que pode ser torch.contiguous_format ou torch.channels_last .

    • x.is_contiguous(memory_format=torch.contiguous_format) preserva a mesma funcionalidade de x.is_contiguous() e permanece inalterado.

    • x.is_contiguous(memory_format=torch.channels_last) retorna verdadeiro se A) tensor de entrada é contíguo na memória E B) alocado na memória no formato NWHC (ou similar para 3d, 5d).

Nota: Ao final da fase um x.is_contiguous(memory_format=torch.channels_last) calculará o estado do Tensor em cada chamada. Esta funcionalidade será atualizada posteriormente.

Fase 2

Preserve o formato da memória para operações específicas:

  1. Os operadores elementares unários preservam o formato de memória dos canais_último.

    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. Operadores de elementos binários ( add , sub , mul , div ) preservam o formato de memória dos canais_último.

    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. Quaisquer operações sobre tamanhos, passos e escurecimento reiniciam o formato da memória.

    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 da operação de remodelagem (e semelhante), se a saída for 'channels_last' legível

    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: Atualmente memory_format não preservado

  2. Resultado da operação NHWC + NCHW. É NHWC?

    Nota: Atualmente NHWC + NCHW -> NHWC e NCHW + NHWC -> NHWC

E quanto a operações como cat / split? Será útil para eles preservar o formato da memória.

@ezyang - com relação à indexação, acho que devemos parar em algum lugar. Os diferentes layouts de memória não são totalmente transparentes e alguns ops devem ser autorizados a desconsiderá-los. Eu diria que x[0] deve ter permissão para apagar a tag, incluindo x[0].unsqueeze(0)

Como Raghu mencionou, cat / split deve preservar a tag se possível, pois é um uso bastante comum. Acho que a regra geral deve ser que, contanto que a operação não mude a classificação ou reordene o eixo de maneira estranha, devemos preservar a tag. Se a classificação mudar - todas as apostas serão canceladas.

Concordo que em alguns casos perderemos a marca. Mas eu discordaria sobre x[0] . Essa me parece uma maneira muito comum de passar de NCHW para CHW .

Depois de várias conversas sobre como é confuso ter tensores para carregar (ou não) a 'tag' de channels_last, decidimos correr o risco de introduzir a mudança de quebra de bc e promover tensores automaticamente para o formato channels_last.

O que isso significa para a API:

Quaisquer tensores 3d, 4d, 5d com avanços como N, 1, H, [W, [D]] obterão automaticamente o formato de memória channels_last.

Para fazer isso funcionar, tomaremos precauções especiais para garantir que os operadores em tensores channels_last que geram tensores channels_last terão pelo menos um desempenho semelhante aos operadores em tensores contíguos.

No caso do pior cenário:
1) Os usuários podem chamar .contiguous () na saída.
2) Escreveremos código de promoção automática de forma que seja quase trivial alterar esse comportamento.

Os efeitos colaterais dessa promoção automática são:

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

Por outro lado, pode resolver o caso (após pequenas modificações):

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 conversões de folga, por solicitação de @ezyang

Natalia Gimelshein [14h19]
Portanto, presumo que não haveria conceito de tag.

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 AM]
z vai ser channels_last

Vitaly Fedyunin [8:25 AM]
se x1 não for channels_last em qualquer uma das variantes propostas (a menos que alteremos a função chunk para não retornar visualizações), então a convolução irá convertê-lo para o formato contíguo (channels_first) e retornar contíguo também

Vitaly Fedyunin [9:12]
@ngimel obrigado pelo feedback, acho que podemos chegar a uma definição mais significativa de channels_last para cobrir a maioria dos casos quando operações semelhantes a visualização estão envolvidas. Irá mantê-lo informado.

Natalia Gimelshein [9:36]
respondeu a um tópico:
Então parece um problema, não? Chunking através da dimensão de canais é uma coisa relativamente comum, por exemplo, em redes do tipo iniciação. Portanto, se o tensor for o primeiro tensor dos canais em blocos, a saída da convolução será os canais primeiro (o que é um comportamento intuitivo e muito provavelmente o que o usuário deseja), se o tensor for os canais em blocos por último, a saída da convolução será mais uma vez os canais primeiro?

Natalia Gimelshein [9:39]
respondeu a um tópico:
Mas apenas devido ao comportamento de adição não comutativa e y sendo o primeiro argumento e os canais por último, certo? Qual seria o resultado para x1+y ? Temos regras de propagação de layout para operações binárias em algum lugar?

Vitaly Fedyunin [10:44 AM]
1) Sim, é um problema que vamos resolver com uma proposta alternativa. Vou fazer alguns testes agora e vou anotar esta semana (em um ou dois dias).
2) x1 + y - também deve produzir channels_last, caso contrário, é confuso e, sim, teremos regras de propagação de layout escritas.

Acho que a observação que fiz a @VitalyFedyunin quando conversamos sobre isso pessoalmente (mas acho que não me lembrei de escrever isso em lugar nenhum) é que há um grau de liberdade na convolução, que é quando começa um argumento cujo layout de memória não corresponde a nenhum que ele saiba como implementar de forma eficiente, a qual layout ele deve contigüificar? Por motivos de BC, a contiguidade para os canais primeiro é necessária, mas tomamos uma decisão arbitrária aqui - provavelmente você também pode contiguificar para os canais por último. Talvez devêssemos ter algum tipo de alternância local de thread que diga quais são os padrões?

Mas parece que há muitos detalhes aqui para descobrir, e não tenho certeza se funcionará no final.

Portanto, a nebulosidade da convolução (e outros operadores com reconhecimento de layout, por exemplo, por exemplo, upsampling que eu olhei recentemente começa chamando .contiguous () na entrada - então o que isso quer dizer?) Foi o motivo principal para apresentar a tag, iirc.

Sim, estou bem em abrir o design da tag novamente, mas então nós
tem que resolver seriamente os problemas de como propagar essas tags,
mesmo quando você perde o layout (como teria sido o caso com o chunking
em canais). Gosto muito mais de criar um "layout atual"
tipo de gerenciador de contexto, do que torná-lo dependente de dados.

Trechos da mensagem de ngimel de 19/06/2019 12:43:45 -0700:

Portanto, a nebulosidade da convolução (e outros operadores com reconhecimento de layout, por exemplo, por exemplo, upsampling que eu olhei recentemente começa chamando .contiguous () na entrada - então o que isso quer dizer?) Foi o motivo principal para apresentar a tag, iirc.

BTW, por que temos que criar um novo conceito em vez de apenas nos limitarmos a layout ? Não acho que representações esparsas tenham um conceito bem definido de layout como "channels_last", então não precisamos representar um produto de memory_formats * layouts ( layouts refere-se ao uso atual ), mas apenas memory_format + layouts o que significa que não há problema em usar o mesmo argumento que usamos? Para mim é mais curto, mais agradável e nos permitirá evitar estender assinaturas de fábricas para mil argumentos.

a opção de layout foi considerada (verifique o apêndice), mas descobrimos que isso levará a muitas duplicações de código, bem como impedirá a conversão automática de tensores para um memory_format diferente instantaneamente

afinal, memory_format é uma maneira de tensor stride e de fácil escolha de kernels otimizados e saídas que são propriedade de tensor strided, não uma classe completamente diferente

Em certo sentido, layouts esparsos também são uma maneira de escolher facilmente kernels otimizados para matrizes que são em sua maioria zero 😄 Você pode falar sobre a parte "e também não permitir a conversão automática de tensores em um memory_format on fly diferente", por favor?

Esta pode ser uma pergunta ingênua, mas por que PyTorch está considerando esta API em vez de apenas expor uma opção para usar NHWC nas próprias operações, o que chamaria diretamente o kernel CuDNN subjacente quando disponível?

Parece que, para um caso de uso comum (misturar operações de imagem como conv e pooling com arquiteturas LM), essa seria uma solução fácil. Como desenvolvedor, tudo que eu quero é Conv2d(..., nhwc=True) . Existe algum motivo pelo qual isso não faz sentido?

@rewonc , consideramos a abordagem semelhante (adicionar opções aos operadores em vez do kernel derivado do striding) e achamos difícil de aplicar pelos seguintes motivos:

  • Essa abordagem exigirá que o kernel faça a reestruturação do tensor contíguo para aplicar o kernel NHWC.
  • O próximo operador terá que restringir a entrada novamente (para contíguo), a menos que também tenha a opção nhwc=True .
  • Para ter NHWC em toda a rede, cada operadora precisaria da opção nhwc=True .

PS. Se você está preocupado com as funções CudNN Ex , estamos procurando expor cudnn_batch_norm_nhwc e operadores semelhantes.

Olá, @VitalyFedyunin , vimos que o tensor nomeado era compatível com o PyTorch 1.3. Isso pode resolver (ou resolver parcialmente) as preocupações sobre o suporte ao formato NHWC (ou mesmo bloquear)? Existe algum plano para avançar o estado NHWC com base no tensor nomeado?

Estamos avançando com o último suporte dos canais, irei publicar o roadmap esta semana aqui e nos canais slack. Não estamos considerando adicionar formatos bloqueados tão cedo (pois isso exigirá a reescrita de TODOS os operadores).

Obrigado. Isso vai ser bom!

Acompanhamento de tarefas e progresso em https://github.com/pytorch/pytorch/issues/28619

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

bartvm picture bartvm  ·  3Comentários

negrinho picture negrinho  ·  3Comentários

eliabruni picture eliabruni  ·  3Comentários

soumith picture soumith  ·  3Comentários

Coderx7 picture Coderx7  ·  3Comentários