Usar strides para expressar padding e layout de memória

Tensores DirectML, que são apoiados por buffers Direct3D 12, são descritos por propriedades conhecidas como sizes e strides do tensor. Os tamanhos do tensor descrevem as dimensões lógicas do tensor. Por exemplo, um tensor 2D pode ter uma altura de 2 e uma largura de 3. Logicamente, o tensor tem 6 elementos distintos, embora os tamanhos não especifiquem como esses elementos são armazenados na memória. Os strides do tensor descrevem o layout de memória física dos elementos do tensor.

Matrizes bidimensionais (2D)

Considere um tensor 2D que tenha uma altura de 2 e uma largura de 3; os dados são compostos por caracteres textuais. Em C/C++, isso pode ser expresso usando uma matriz multidimensional.

constexpr int rows = 2;
constexpr int columns = 3;
char tensor[rows][columns];
tensor[0][0] = 'A';
tensor[0][1] = 'B';
tensor[0][2] = 'C';
tensor[1][0] = 'D';
tensor[1][1] = 'E';
tensor[1][2] = 'F';

A exibição lógica do tensor acima é visualizada abaixo.

A B C
D E F

Em C/C++, uma matriz multidimensional é armazenada em ordem de linha principal. Em outras palavras, os elementos consecutivos ao longo da dimensão largura são armazenados contíguamente no espaço de memória linear.

Deslocamento: 0 1 2 3 4 5
Valor: A B C D E F

O stride de uma dimensão é o número de elementos a serem ignorados para acessar o próximo elemento nessa dimensão. Os strides expressam a disposição do tensor na memória. Com uma ordem de linha maior, o stride da dimensão de largura é sempre 1, uma vez que os elementos adjacentes ao longo da dimensão são armazenados de forma contígua. O stride da dimensão de altura depende do tamanho da dimensão de largura; no exemplo acima, a distância entre elementos consecutivos ao longo da dimensão de altura (por exemplo, A a D) é igual à largura do tensor (que é 3 nesse exemplo).

Para ilustrar um layout diferente, considere a ordem principal das colunas. Em outras palavras, os elementos consecutivos ao longo da dimensão de altura são armazenados contíguamente no espaço de memória linear. Nesse caso, a altura-passada é sempre 1, e a largura-passada é 2 (o tamanho da dimensão de altura).

Deslocamento: 0 1 2 3 4 5
Valor: Um D B E C F

Dimensões mais altas

Quando se trata de mais de duas dimensões, é difícil se referir a um layout como linha maior ou coluna maior. Portanto, o restante deste tópico usa termos e rótulos como estes.

  • 2D: "HW": altura é a dimensão de ordem mais alta (linha principal).
  • 2D: "WH": largura é a dimensão de ordem mais alta (coluna principal).
  • 3D: "DHW": a profundidade é a dimensão de ordem mais alta, seguida pela altura e depois pela largura.
  • 3D: "WHD": a largura é a dimensão de ordem mais alta, seguida pela altura e depois pela profundidade.
  • 4D: "NCHW": o número de imagens (tamanho do lote), depois o número de canais, a altura e a largura.

Em geral, a etapa empacotada de uma dimensão é igual ao produto dos tamanhos das dimensões de ordem inferior. Por exemplo, com um layout "DHW", o stride D é igual a H * W; o stride H é igual a W; e o stride W é igual a 1. Afirma-se que os strides são empacotados quando o tamanho físico total do tensor é igual ao tamanho lógico total do tensor; em outras palavras, não há espaço extra nem elementos sobrepostos.

Vamos estender o exemplo 2D para três dimensões, de modo que tenhamos um tensor com profundidade 2, altura 2 e largura 3 (para um total de 12 elementos lógicos).

A B C
D E F

G H I
J K L

Com um layout "DHW", esse tensor é armazenado da maneira a seguir.

Deslocamento: 0 1 2 3 4 5 6 7 8 9 10 11
Valor: A B C D E F G H I J K L
  • Stride D = altura (2) * largura (3) = 6 (por exemplo, a distância entre 'A' e 'G').
  • Stride H = largura (3) = 3 (por exemplo, a distância entre 'A' e 'D').
  • Stride W = 1 (por exemplo, a distância entre 'A' e 'B').

O produto ponto dos índices/coordenadas de um elemento e os strides fornece o deslocamento para esse elemento no buffer. Por exemplo, o deslocamento do elemento H (d=1, h=0, w=1) é 7.

{1, 0, 1} ⋅ {6, 3, 1} = 1 * 6 + 0 * 3 + 1 * 1 = 7

Tensores empacotados

Os exemplos acima ilustram tensores empacotados. Afirma-se que um tensor está empacotado quando o tamanho lógico do tensor (em elementos) é igual ao tamanho físico do buffer (em elementos) e cada elemento tem um endereço/deslocamento exclusivo. Por exemplo, um tensor 2x2x3 é compactado se o buffer tem 12 elementos de comprimento e nenhum par de elementos compartilha o mesmo deslocamento no buffer. Tensores empacotados são o caso mais comum; mas os avanços permitem layouts de memória mais complexos.

Transmissão com strides

Se o tamanho do buffer de um tensor (em elementos) for menor que o produto de suas dimensões lógicas, segue-se que deve haver alguma sobreposição de elementos. O caso usual para isso é conhecido como transmissão, em que os elementos de uma dimensão são duplicados de outra dimensão. Vamos revisitar o exemplo 2D. Digamos que queremos um tensor que seja logicamente 2x3, mas a segunda linha é idêntica à primeira. Veja como é a aparência.

A B C
A B C

Isso pode ser armazenado como um tensor HW/linha maior compactado. Porém, um armazenamento mais compacto conteria apenas 3 elementos (A, B e C) e usaria uma altura de 0 em vez de 3. Nesse caso, o tamanho físico do tensor é de 3 elementos, mas o tamanho lógico é de 6 elementos.

Em geral, se o stride de uma dimensão é 0, então todos os elementos nas dimensões de ordem inferior são repetidos ao longo da dimensão transmitida; por exemplo, se o tensor é NCHW e o tride C é 0, então cada canal tem os mesmos valores ao longo de H e W.

Preenchimento com strides

Diz-se que um tensor é preenchido se seu tamanho físico é maior do que o tamanho mínimo necessário para ajustar seus elementos. Quando não há elementos de transmissão nem sobreposição, o tamanho mínimo do tensor (em elementos) é simplesmente o produto de suas dimensões. Você pode usar a função auxiliar DMLCalcBufferTensorSize (consulte Funções auxiliares DirectML para obter uma listagem dessa função) para calcular o tamanho mínimo do buffer para seus tensores DirectML.

Digamos que um buffer contenha os valores a seguir (os elementos 'x' indicam valores de preenchimento).

0 1 2 3 4 5 6 7 8 9
A B C x x D E F x x

O tensor preenchido pode ser descrito usando uma altura-stride de 5 em vez de 3. Em vez de passar por 3 elementos para chegar à próxima linha, a etapa é de 5 elementos (3 elementos reais mais 2 elementos de preenchimento). O preenchimento é comum em computação gráfica, por exemplo, para garantir que uma imagem tenha um alinhamento de potência de dois.

A B C
D E F

Descrições do tensor do buffer DirectML

O DirectML pode funcionar com uma variedade de layouts de tensores físicos, já que a estrutura DML_BUFFER_TENSOR_DESC tem membros Sizes e Strides. Algumas implementações de operador podem ser mais eficientes com um layout específico, portanto, não é incomum alterar a forma como os dados do tensor são armazenados para obter melhor desempenho.

A maioria dos operadores DirectML requer tensores 4D ou 5D, e a ordem dos valores de sizes e strides é fixa. Corrigindo a ordem de sizes e valores de stride em uma descrição de tensor, é possível para o DirectML inferir diferentes layouts físicos.

4D

5D

  • DML_BUFFER_TENSOR_DESC::Tamanhos = { N-size, C-size, D-size, H-size, W-size }
  • DML_BUFFER_TENSOR_DESC::Strides = { N-stride, C-stride, D-stride, H-stride, W-stride }

Se um operador DirectML exigir um tensor 4D ou 5D, mas os dados reais tiverem uma classificação menor (por exemplo, 2D), as dimensões principais deverão ser preenchidas com 1s. Por exemplo, um tensor "HW" é definido usando DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, H, W }.

Se os dados do tensor forem armazenados em NCHW/NCDHW, não será necessário definir DML_BUFFER_TENSOR_DESC::Strides, a menos que você queira transmitir ou preencher. Você pode definir o campo de strides como nullptr. No entanto, se os dados do tensor forem armazenados em outro layout, como NHWC, você precisará de strides para expressar a transformação de NCHW para esse layout.

Para um exemplo simples, considere a descrição de um tensor 2D com altura 3 e largura 5.

NCHW empacotado (strides implícitos)

  • DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, 3, 5 }
  • DML_BUFFER_TENSOR_DESC::Strides = nullptr

NCHW empacotado (strides explícitos)

  • N-stride = C-size * H-size * W-size = 1 * 3 * 5 = 15
  • C-stride = H-size * W-size = 3 * 5 = 15
  • H-stride = W-size = 5
  • W-stride = 1
  • DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, 3, 5 }
  • DML_BUFFER_TENSOR_DESC::Strides = { 15, 15, 5, 1 }

NHWC empacotado

  • N-stride = H-size * W-size * C-size = 3 * 5 * 1 = 15
  • H-stride = W-size * C-size = 5 * 1 = 5
  • W-stride = C-size = 1
  • C-stride = 1
  • DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, 3, 5 }
  • DML_BUFFER_TENSOR_DESC::Strides = { 15, 1, 5, 1 }

Confira também