ストライドを使用してパディングとメモリ レイアウトを表現する
DirectML のテンソル (Direct3D 12 バッファーによってバッキングされます) は、テンソルの サイズ および ストライド と呼ばれるプロパティによって記述されます。 テンソルの "サイズ" は、テンソルの論理的な寸法を表します。 たとえば、ある 2D テンソルの高さは 2、幅は 3 であるとします。 論理的には、このテンソルには 6 つの異なる要素がありますが、サイズはこれらの要素がどのようにメモリ内に格納されるかを指定するものでありません。 テンソルの "ストライド" は、テンソルの要素の物理的なメモリ レイアウトを表します。
2 次元 (2D) 配列
高さが 2 で幅が 3 の 2D テンソルについて考えます。データはテキストの文字で構成されます。 C/C++ では、多次元配列を使ってこれを表すことができます。
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 B C
D E F
C/C++ では、多次元配列が格納されるときの順序は行優先です。 つまり、幅次元に沿って並ぶ要素が、線形のメモリ空間内に連続して格納されます。
オフセット: | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
値: | A | B | 貸方 | 借方 | E | F |
次元の "ストライド" は、その次元内の次の要素にアクセスするためにスキップされる要素の数です。 ストライドによって、メモリ内でのテンソルのレイアウトが表現されます。 行優先順序では、幅次元のストライドは常に 1 となります。その次元に沿って隣接する要素は連続して格納されるからです。 高さ次元のストライドは、幅次元のサイズによって決まります。上の例では、高さ次元に沿って連続する要素間の距離は (たとえば、A から D)、テンソルの幅と等しくなります (この例では 3)。
別のレイアウトの例として、列優先順序について考えます。 つまり、高さ次元に沿って連続する要素が、線形のメモリ空間内に連続して格納されます。 この場合は、高さのストライドは常に 1 であり、幅のストライドは 2 (高さ次元のサイズ) です。
オフセット: | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
値: | A | D | B | E | C | F |
高次元
2 次元よりも高い次元では、行優先や列優先という呼び名ではレイアウトを表現するのが難しくなります。 そのため、このトピックの残りの部分では、次のような用語とラベルを使います。
- 2D: "HW" — 高さが最高位の次元です (行優先)。
- 2D: "WH" — 幅が最高位の次元です (列優先)。
- 3D: "DHW" — 深さが最高位の次元で、その次が高さ、最後が幅です。
- 3D: "WHD" — 幅が最高位の次元で、その次が高さ、最後が深さです。
- 4D: "NCHW" — イメージの数 (バッチ サイズ)、チャネルの数、高さ、幅の順です。
一般に、ある次元の "パックされた" ストライドは、下位の次元のサイズの積と等しくなります。 たとえば、"DHW" レイアウトでは、D のストライドは H * W と等しく、H のストライドは W と等しく、W のストライドは 1 です。 ストライドが "パックされた" と呼ばれるのは、テンソルの総物理サイズがテンソルの総論理サイズに等しいときです。つまり、つまり、余分なスペースや要素の重複がない状態です。
では、2D の例を 3 次元に拡張してみましょう。深さ 2、高さ 2、幅 3 のテンソルがあるとします (論理要素は全部で 12 個)。
A B C
D E F
G H I
J K L
"DHW" レイアウトでは、このテンソルは次のように格納されます。
オフセット: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
値: | A | B | 貸方 | 借方 | E | 金 | G | 平 | I | J | K | L |
- D のストライド = 高さ (2) * 幅 (3) = 6 (たとえば、"A" と "G" の間の距離)。
- H のストライド = 幅 (3) = 3 (たとえば、"A" と "D" の間の距離)。
- W のストライド = 1 (たとえば、"A" と "B" の間の距離)。
要素のインデックス/座標とストライドのドット積が、バッファー内でのその要素までのオフセットとなります。 たとえば、H 要素 (d=1、h=0、w=1) のオフセットは 7 です。
{1, 0, 1} ⋅ {6, 3, 1} = 1 * 6 + 0 * 3 + 1 * 1 = 7
パックされたテンソル
上の例で示したのは "パックされた" テンソルです。 テンソルが "パックされた" と呼ばれるのは、テンソルの論理サイズ (要素数) がバッファーの物理サイズ (要素数) と等しく、かつ各要素に一意のアドレス/オフセットがあるときです。 たとえば、2x2x3 のテンソルが "パックされた" 状態になるのは、バッファーの長さが 12 要素で、かつバッファー内の同じオフセットを共有している要素のペアがない場合です。 パックされたテンソルは最も一般的なケースですが、ストライドを利用するとさらに複雑なメモリ レイアウトも可能になります。
ストライドでのブロードキャスト
テンソルのバッファー サイズ (要素数) がその論理的な次元の積より小さい場合は、要素の重複が存在していることになります。 これが正常であるケースを "ブロードキャスト" といいます。これは、ある次元の要素が別の次元の複製である状態です。 例として、2D の例に戻ってみましょう。 論理的に 2x3 のテンソルが必要ですが、2 番目の行が 1 番目の行と同一であるとします。 この場合は、次のようになります。
A B C
A B C
これは、パックされた HW/行優先テンソルとして格納できます。 しかし、ストレージをさらに小さくすることもできます。それには 3 つの要素 (A、B、C) だけを格納し、高さのストライドとして 3 ではなく 0 を使用します。 この場合のテンソルの物理サイズは 3 要素ですが、論理サイズは 6 要素です。
一般に、ある次元のストライドが 0 の場合は、それより下位の次元内のすべての要素がブロードキャスト次元に沿って繰り返されます。たとえば、テンソルが NCHW で C のストライドが 0 の場合は、H と W に沿った値は各チャネルで同じになります。
ストライドでのパディング
テンソルが "パディング" されるのは、テンソルの物理サイズが、その要素を収容するのに必要な最小サイズより大きい場合です。 要素のブロードキャストも重複もないときは、テンソルの最小サイズ (要素数) はその次元の積になります。 ヘルパー関数 DMLCalcBufferTensorSize
を使用して (関数の一覧については「DirectML ヘルパー関数」を参照) DirectML テンソルの "最小" バッファー サイズを計算することができます。
たとえば、バッファーの中に次の値があるとします ("x" 要素はパディング値を示します)。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
A | B | C | x | x | D | E | F | x | x |
このパディングされたテンソルを表現するには、高さのストライドとして 3 ではなく 5 を使用します。 次の行に到達するために 3 要素分進むのではなく、5 要素分進みます ("実際の" 3 要素に加えてパディングの 2 要素)。 パディングは、コンピューター グラフィックでよく行われます。たとえば、イメージが確実に 2 のべき乗アラインメントとなるようにする場合です。
A B C
D E F
DirectML のバッファーでのテンソルの記述
DirectML では、さまざまな物理テンソル レイアウトを扱うことができます。DML_BUFFER_TENSOR_DESC 構造体に Sizes
と Strides
の両方のメンバーがあるからです。 演算子の実装によっては、特定のレイアウトのときに効率が向上することがあるため、パフォーマンス向上のためにテンソル データの格納方法を変更することは珍しくありません。
ほとんどの DirectML 演算子では 4D または 5D のテンソルが必要であり、サイズとストライドの値の順序は固定されています。 テンソルの記述でのサイズとストライドの値の順序が固定されているので、DirectML がさまざまな物理レイアウトを推論することができます。
4D
- DML_BUFFER_TENSOR_DESC::Sizes = { N のサイズ, C のサイズ, H のサイズ, W のサイズ }
- DML_BUFFER_TENSOR_DESC::Strides = { N のストライド, C のストライド, H のストライド, W のストライド }
5D
- DML_BUFFER_TENSOR_DESC::Sizes = { N のサイズ, C のサイズ, D のサイズ, H のサイズ, W のサイズ }
- DML_BUFFER_TENSOR_DESC::Strides = { N のストライド, C のストライド, D のストライド, H のストライド, W のストライド }
DirectML の演算子が 4D または 5D のテンソルを必要としている場合に、実際のデータの階数がそれより小さい場合は (たとえば 2D)、先行する次元を 1 で埋める必要があります。 たとえば、"HW" テンソルを設定するには DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, H, W } を使います。
テンソル データが NCHW/NCDHW で格納されている場合は、DML_BUFFER_TENSOR_DESC::Strides を設定する必要はありません (ブロードキャストまたはパディングを行う場合を除きます)。 ストライド フィールドを nullptr
に設定できます。 ただし、テンソル データが別のレイアウト (たとえば NHWC) で格納されている場合は、NCHW からそのレイアウトへの変換を表すためにストライドが必要になります。
単純な例として、高さが 3 で幅が 5 の 2D テンソルの記述について考えます。
パックされた NCHW (暗黙のストライド)
- DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, 3, 5 }
- DML_BUFFER_TENSOR_DESC::Strides =
nullptr
パックされた NCHW (明示的なストライド)
- N のストライド = C のサイズ * H のサイズ * W のサイズ = 1 * 3 * 5 = 15
- C のストライド = H のサイズ * W のサイズ = 3 * 5 = 15
- H のストライド = W のサイズ = 5
- W のストライド = 1
- DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, 3, 5 }
- DML_BUFFER_TENSOR_DESC::Strides = { 15, 15, 5, 1 }
パックされた NHWC
- N のストライド = H のサイズ * W のサイズ * C のサイズ = 3 * 5 * 1 = 15
- H のストライド = W のサイズ * C のサイズ = 5 * 1 = 5
- W のストライド = C のサイズ = 1
- C のストライド = 1
- DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, 3, 5 }
- DML_BUFFER_TENSOR_DESC::Strides = { 15, 1, 5, 1 }