使用步幅来表示填充和内存布局

DirectML 张量(由 Direct3D 12 缓冲区提供支持)由称为张量 的大小步幅 的属性描述。 张量 的大小 描述张量的逻辑维度。 例如,2D 张量的高度可能为 2,宽度为 3。 从逻辑上讲,张量有 6 个不同的元素,尽管这些大小不指定这些元素在内存中的存储方式。 张量的步幅描述张量元素的物理内存布局。

二维 (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
值: 一个 B C D E F

维度的步幅是为了访问该维度中的下一个元素而要跳过的元素数。 步幅表示内存中的张量布局。 使用行主序时,宽度维度的步幅始终为 1,因为维度上的相邻元素是连续存储的。 高度尺寸的步幅取决于宽度尺寸的大小;在上面的示例中,沿高度维度(例如,A 到 D)的连续元素之间的距离等于张量(在本示例中为 3)。

要演示不同的布局,请考虑列主序。 换句话说,沿高度维度的连续元素连续存储在线性内存空间中。 在这种情况下,高度步幅始终为 1,宽度步幅为 2(对应于高度维度的大小)。

偏移量: 0 1 2 3 4 5
值: 一个 D B E C F

更高维度

超过两个维度时,以行主序或列主序方式引用布局会很不方便。 因此,本主题的其余部分将使用这些术语和标签。

  • 2D:“HW”— 高度是最高序维度(行主序)。
  • 2D:“WH”— 宽度是最高序维度(列主序)。
  • 3D:“DHW”:深度是最高阶维度,后面是高度,然后是宽度。
  • 3D:“WHD”-宽度是最高阶维度,后跟高度,然后是深度。
  • 4D:“NCHW”-图像数(批大小),然后通道数,高度,宽度。

一般情况下,维度的打包步幅等于低序维度大小的乘积。 例如,使用“DHW”布局时,D 步幅等于 H * W;H 步幅等于 W;和 W 步幅等于 1。 如果张量的总物理大小等于张量的总逻辑大小,则认为步幅是打包的;换言之,不存在任何额外的空间,也不存在重叠的元素。

让我们将 2D 示例扩展到三个维度,以便具有深度为 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
值: 一个 B C D E F G H 上执行碎片合并 J K L
  • D 步幅 = 高度 (2) * 宽度 (3) = 6(例如,“A”到“G”的距离)。
  • H 步幅 = 宽度 (3) = 3(例如,“A”到“G”的距离)。
  • 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

打包的张量

以上示例演示了打包的张量。 一个张量被称为打包,当其逻辑大小(以元素为单位)等于缓冲区的物理大小(以元素为单位),并且每个元素都有唯一的地址/偏移量时。 例如,如果缓冲区的长度为 12 个元素,并且没有任何一对元素在缓冲区中使用相同的偏移量,则 2x2x3 张量是打包的。 打包的张量是最常见的情况;但步幅允许更复杂的内存布局。

使用步幅进行广播

如果张量的缓冲区大小(以元素为单位)小于其逻辑维度的乘积,则随后必须有一些元素重叠。 这种常见情况称为“广播”;其中某个维度的元素是另一个维度的重复项。 例如,让我们再次回顾一下 2D 实例。 假设我们想要获得一个逻辑维度为 2x3 的张量,但第二行与第一行相同。 布局如下。

A B C
A B C

此张量可存储为打包的 HW/行主序张量。 但是,更紧凑存储只包含 3 个元素(A、B 和 C),并使用高度步幅 0 而不是 3。 在这种情况下,张量的物理大小为 3 个元素,但逻辑大小为 6 个元素。

一般情况下,如果维度的步幅为 0,则低阶维度中的所有元素沿广播维度重复;例如,如果张量为 NCHW 且 C 步幅为 0,则每个通道沿 H 和 W 具有相同的值。

使用步幅进行填充

如果某个张量的物理大小大于拟合其元素所需的最小大小,则认为该张量是填充的。 如果既不存在广播也不存在重叠的元素,则(元素中)张量的最小大小仅是其维度的积。 可以使用辅助函数 DMLCalcBufferTensorSize(查看 DirectML 辅助函数 了解该函数的列表)来计算 DirectML 张量的最小缓冲区大小。

假设缓冲区包含以下值(“x”元素指示填充值)。

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

可以使用高度步幅 5 而不是 3 来描述填充的张量。 不是按 3 个元素步进到下一行,而步幅为 5(3 个真实元素加上 2 个填充元素)。 例如,填充在计算机图形中很常见,目的是确保图像采用 2 次幂对齐。

A B C
D E F

DirectML 缓冲区张量说明

DirectML 可以处理各种物理张量布局,因为 DML_BUFFER_TENSOR_DESC 结构 同时具有成员 SizesStrides 成员。 某些运算符实现在特定的布局中可能更高效,因此更改张量数据的存储方式并不罕见,以提高性能。

大多数 DirectML 运算符需要 4D 或 5D 张量,大小和步幅值的顺序是固定的。 通过在张量说明中调整大小和步幅值的顺序,DirectML 可以推断出不同的物理布局。

4D

5D

  • DML_BUFFER_TENSOR_DESC::Size = { N-size, C-size, D-size, H-size, W-size }
  • DML_BUFFER_TENSOR_DESC::Strides = { N 步幅, C 步幅, D 步幅, H 步幅, W 步幅 }

如果 DirectML 运算符需要 4D 或 5D 张量,但实际数据的秩更小(例如 2D),则应使用 1s 填充前导维度。 例如,使用 DML_BUFFER_TENSOR_DESC::Sizes = { 1, 1, H, W } 设置“HW”张量。

如果张量数据存储在 NCHW/NCDHW 中,则无需设置 DML_BUFFER_TENSOR_DESC::Strides,除非你想要广播或填充。 可以将步幅字段设置为 nullptr。 但是,如果张量数据存储在另一个布局中(例如 NHWC),则需要使用步幅来表示从 NCHW 到该布局的转换。

对于简单的例子,可以考虑一个描述为高度 3 和宽度 5 的二维张量。

打包的 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 }

另请参阅