CNN 算子利用张量维度的规范顺序并为其分配语义。 对于今天 PyTorch 中的 2D 案例,torch.nn.Conv2d 的输入必须是 NCHW 顺序的 4d 张量 -
出于性能原因,以不同方式对维度重新排序通常是有益的,这样特定操作访问的内存将被连续布置并更好地利用局部性。 最常见的选择是将尺寸移到最后 - NHWC。 可以有更复杂的内存格式,将一维平铺成块,例如
使用它的示例库包括:
挑战在于转换维度顺序本身是昂贵的,因此在连续执行多个 CNN 操作的情况下(例如conv(relu(conv)))
),最好一次转换为不同的内存格式,执行操作并重新排序它们背部。
因此,重要的是让 PyTorch 了解不同的维度顺序,并能够在Eager 和 JIT 模式下的操作之间
我们努力构建能够表示的 API:
术语:上面的问题通常被称为“布局”(mxnet)、“data_format”(tf)、“image_format”(keras)、“order”(caffe2)。 我们建议在 PyTorch 中使用名称“memory format”或“memory_format”。 不幸的是,“布局”这个名称在 PyTorch 中被采用,其值为“strided”和“sparse_coo”,因此命名选项不可用。
以下操作符至少应该是内存格式感知的。 除了产生正确的结果外,他们还需要从底层库提供最佳性能并保留输出的
在 PyTorch 中定义内存格式的概念:
torch.memory_format.channels_first
这样的常量。 它们没有指定的类型,可以是任意可比较的对象(可能以枚举开头,但将来可能是其他对象与命名张量的概念互操作)torch.channels_first
channels_first
和channels_last
(允许使用较少的常量)将以下方法添加到 Tensor:
x.is_contiguous(torch.memory_format.channels_first)
x.to(memory_format=torch.memory_format.channels_first)
注意:目前没有x.get_memory_format()
函数,只有显式检查 - 它允许更广泛的可能实现。 不过,我们可能想添加它。
张量语义布局始终保持不变 - NCHW! x.size()
总是返回(n,c,h,w)
操作保留内存格式行为:
内存格式是通过序列化/反序列化保留的张量的属性(如果张量是参数)。
今天 PyTorch 中的张量有 strides 的概念,它指定了逻辑张量在内存中的布局方式。 具体来说,每个张量都有一个与sizes
长度相同的strides
向量。 为了在逻辑索引(i1, i2, .., ik)
对元素进行索引,可以使用 strides 进行点积并在offset + i0*stride0 + i1*stride1 + ... * ik * stridek
处查找内存。 因此,连续张量的步幅是大小的反向累积乘积。 例如4D张量大小(n,c,h,w)
有进步(c*h*w, h*w, w, 1)
。
Strides 可用于物理表示不同的内存格式(即维度重新排序),同时保留逻辑默认 NCHW 顺序。 它给出了内存格式转换的有效定义为:
# 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()
在 NHWC 格式中,步幅向量是(c*h*w, 1, c*w, c)
。 因此,在内存缓冲区中,NHWC 的权重是连续的。
strides 可用于测试:
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()
这种方法的优点:
缺点:
.contiguous()
相当于切换到 NCHW,可能是用户偶然发生的,也可能是其中一个 ops 内部发生的最大的潜在问题是用户意图不明确。 无法区分用户是否真的想要不同的内存格式或输入张量只是碰巧以这种方式跨越。 具体来说,它会导致现有操作的行为发生变化——今天,即使输入是任意跨步的,卷积也只能产生 NCHW 连续的张量,在新世界中,它可能会将输入识别为 NHWC,因此也会返回 NHWC。 它不会改变语义,但会导致难以调试的性能问题。 可能的解决方案可能是使用用户指定的 memory_format 标志显式标记张量,并且仅遵循此注释(除了 strides)。
为了解决上述问题,最初的提议是在张量上引入“软”内存格式标签,记录最后在张量上完成的to(memory_format)
调用。 操作员需要将此注释传播到输出。 注释是“软”的,所以我们不会在不匹配的注释上出现硬错误,而是在分析模式下产生警告。
现有运营商的签名不会改变。 运营商可以在运营商内部进行硬编码调度,以实现更快的实现。 如果实现不可用,则可以通过不同的内存格式进行往返。 另一种方法是引发错误消息。
def maxpool(x: Tensor):
if x.is_contiguous(torch.layout.NHWC):
return max_pool_impl_nhwc(x)
return max_pool_impl_default(x.contiguous())
最好使用像“conv”这样的单个符号来引用 JIT IR 中的运算符,而不是创建像“conv_nhwc”这样的单独运算符。 这样做的原因是简单并将 IR 保持在语义表示级别。
我们必须确保像逐元素这样的核心操作保留内存格式并且是高效的。
一元运算一般可以通过验证内存块是否“密集”来处理——即元素是否跨越一个区域而没有间隙,并且每个内存位置只使用一次。 可以用简单的算法验证
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
为了调试性能,我们应该向分析器添加以下支持:
此功能可以内置到按需分析工具中。
期望向后传递应该以与向前传递相同的内存格式运行是合乎逻辑的。 它不会总是自动发生,因为传入的梯度可能是任意跨步的。 因此,前向传递必须明确识别内存格式,将其存储在 autograd 闭包中,并在后向函数之前应用于 grad 张量。
可能的实现:
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(...)
目前的提议是:
to(memory_format)
调用需要插入以获得最佳性能出于强制目的,我们还可以使用像assert x.is_contiguous(channels_last)
这样的语句。
注意:存在一个问题,即特定设备具有首选内存格式组合的信息存储在何处(例如,x86 路由上的 qconv 到仅实现 NHWC 的 fbgemm)。 一种选择是将其置于 op 注册级别,但是,内存格式注释感觉更像是一种辅助信息。 我们可以首先在 JIT pass 中的某处维护一个全局映射,该映射表示首选内存格式和相关的启发式方法。 如果它变得不整洁 - 我们可以切换到基于注册的机制。
当我们决定添加更复杂的张量包装时,使用一流的 PyTorch 张量可能不合理,因为实现成本和复杂性都很高。 可能有两种选择:
另一种选择是在核心 PyTorch Tensor 类中实现对阻塞/平铺的原生支持。
NamedTensor 的现有提议被构造为张量的类型检查机制 - 目前它没有为维度名称分配任何语义含义。 因此,推断激活张量含义的唯一方法是继续使用预定的 NCHW 格式。 它使 NamedTensor 和当前的提案正交。
如果我们愿意硬指定某些名称(如“通道”、“宽度”)的含义,操作员可以利用这些信息来更快地实现。 这将是一个语义变化,因为输入张量在逻辑上将具有 NHWC(不是今天的 NCHW)内存格式。
TensorFlow 通过data_format
参数在操作员级别同时支持 NHWC 和 NCHW; 可接受的值为(“NHWC”、“NCHW”)用于 4 维输入,(“NDHWC”、“NCDHW”)用于 5 维输入,或channels_first
/ channels_last
独立于输入维度。 正确设置参数取决于用户,即它不会被张量自动跟踪。
Caffe2 将此参数称为order
而不是data_format
,但它仍然明确地应用于单个操作员级别。
试金石问题:以下代码打印什么内容: tensor_in_nhwc_layout.size(1)
- 通道数(因为默认是 PyTorch 中的 NCHW)或高度(因为这是 NHWC 布局中位置 1 的内容)。
基于这个答案,有几个选项是可能的:
empty_like
有一个问题; 当前定义的语义是您删除所有步幅信息,因此,不可能保留布局并成为 BC。
@VitalyFedyunin已签约实施.contiguous()
和torch.memory_layout
位
一个问题 - 对于大小(n, c, h, w)
的 4D 张量x
(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)
我们有一个奇怪的排列
y = x.permute(0, 3, 1, 2)
# y.size(): (n, w, c, h)
# y.stride(): (c*h*w, 1, h*w, w)
现在我们检查NHWC格式是否连续。 按照你的逻辑如下
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)
对于这两种情况, is_nhwc_contiguous(y)
将返回 True?
这是对的。 但是,我们不能仅在步幅上进行中继,因为我们希望避免在复制、到和类似操作期间进行任何来回转换。
如果 strides 与内存格式的顺序相同怎么办? 让我们以 4D 张量为例。 为了描述张量,我们有sizes
、 strides
和stride_indexes
:
尺寸(n, c, h, w)
按物理顺序大步前进,即
stride_indexes将
对于 nchw 格式,这与以前相同。 对于 nhwc,它将是相似的。
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)
这也可以扩展为支持阻止格式。 以 nChw16c 为例,
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)
稍后可以进一步探索更多细节。
对于仅接受 nchw 连续张量的 OP,这将是一些工作。
或者我们也可以稍微改变原型,比如
def is_contiguous(format=nchw):
...
def contiguous(format=nchw)
...
因此,默认情况下,它假定只有 nchw 是连续的。 这样你就不需要重写那些 OP,它会自动重新排序到 nchw。
我们努力构建能够表示的 API:
- Eager 和 JIT 中的 PyTorch 中存在具有不同内存格式(一开始,只是维度顺序)的张量。 阻塞布局的优先级较低,但仍然很好。
- 用于查询和更改内存格式的用户公开 API
- 核心 CNN 操作能够处理具有不同内存格式的输入张量并路由到相应的更快实现
- 能够推断和优化 JIT 传递中的内存格式
伟大的提议! 我可以明确我的理解,看看它是否正确(包括 MKL-DNN 格式处理的建议):
请允许我认为该提案是作为“格式”类实现的。 只要它提供查询和更改 API 为虚拟的,我们就可以进行适合 MKL-DNN 复杂格式的继承/扩展。 或者其他方法,只要它提供处理格式的框架,将那些细节交给我们。
关于 OP 的实现,每个 OP 都可以有一个首选格式,以最大限度地提高其性能和兼容格式。 元素操作符(或者更一般地说,内存有界 OPs)假设没有偏好。 OP 使用“格式”对象生成其结果张量,该格式对象保证查询/更改语义与默认 pytorch 期望兼容,并且如果调用优化函数的序列(如 conv2d(ReLU(conv2d)),则它可以处理特定格式案件)
@uyongw我想进一步说明你的第一个例子。 您将示例设置为“我有一个 NCHW 张量,然后我以一种奇怪的方式对其进行了转置(所以现在它看起来像 NWCH);现在我想知道它是否与 NHWC 相邻。” 但这是错误的看待方式。 更好的表述是,“我有一个 NHWC 张量,然后我将其转置为 NCHW 张量。”
换句话说,张量的物理维度没有内在意义(当我们忽略步幅时)。 只有当我们考虑如何在步幅方面引用它们时,我们才赋予它们意义。
为了描述张量,我们有尺寸、步幅和 stride_indexes
我确实认为stride_indexes
是一种考虑问题的便捷方式,但它对于 strides 来说是完全多余的,因为您所说的只是“将这个(反向?)排列应用于 strides,然后将其视为真正的步幅。) @VitalyFedyunin和我正在谈论以某种方式缓存这些信息可能仍然是一个好主意,因为从步幅本身重建信息是一种痛苦。但这超出了本提案的范围。
因此,默认情况下,它假定只有 nchw 是连续的。
是的,这是我对计划的解读。
@曹中Z
请允许我认为该提案是作为“格式”类实现的。 只要它提供查询和更改 API 为虚拟的,我们就可以进行适合 MKL-DNN 复杂格式的继承/扩展。 或者其他方法,只要它提供处理格式的框架,将那些细节交给我们。
实际上,我认为这不是对提案的准确描述。 这里的提案支持的内存布局支持只是可以通过 strides 来表达的布局。 任何无法通过这种方式表达的东西(例如,块布局)都不会以这种方式工作,并且必须得到我们更重量级的“布局”机制的支持。
换句话说,张量的物理维度没有内在意义(当我们忽略步幅时)。 只有当我们考虑如何在步幅方面引用它们时,我们才赋予它们意义。
部分同意:-) 但不是在这个特定问题上。 假设我已经有了一个 nhwc 张量。 然后我将它置换为 nwhc。 我想进一步置换到 nhwc 然后做一个连续的()。 但是我已经将它 nhwc 连续了。 不糊涂吗?
我确实认为 stride_indexes 是一种考虑问题的便捷方式,但它对于 strides 来说是完全多余的,因为您所说的只是“将这个(反向?)排列应用于 strides,然后将其视为真正的 strides。)
恕我直言,如果你在 nhwc(物理)中有大步,大步不会是多余的。 因为您需要具有大小(逻辑)的正确映射。 否则没有办法说出真正的顺序。
顺便说一句,使用反向映射有一种更直接的方法。 比如说,对于 nchw,它是 (0, 1, 2, 3),对于 nhwc,它是 (0, 3, 1, 2) 而不是 (0, 2, 3, 1)。 也就是说 stride_index 本身也总是 NCHW。 但问题是,它不能扩展到像 nChw16c 或 OIhw16i16o 这样的阻塞格式。
阻塞格式需要一套完全不同的运算符实现; 出于这个原因,我们不希望将它们与“内存格式”混合使用,根据定义,它应该与所有现有运算符友好并以相同或更好的性能工作。
部分同意:-) 但不是在这个特定问题上。 假设我已经有了一个 nhwc 张量。 然后我将它置换为 nwhc。 我想进一步置换到 nhwc 然后做一个连续的()。 但是我已经将它 nhwc 连续了。 不糊涂吗?
很难理解您的示例,因为您在口语中使用了一些术语并且需要精确。 以下是我如何解释你所说的:
y = x.permute(0, 2, 3, 1)
,因为您正在置换逻辑布局,而不是物理布局。 (我怀疑这不是你的意思,因为在你原来的帖子中你提到了置换x.permute(0, 3, 1, 2)
z = y.permute(0, 2, 3, 1)
。 所以现在你有一个张量,其逻辑布局与物理布局一致。 这意味着,如果我们询问z.contiguous()
我们将得到 true(并且令人困惑的是, z.contiguous(memory_layout=NCHW)
也将是 true。)但它不会是 NHWC 连续的。我不认为这是您想到的示例,在这种情况下,您必须更准确地了解“置换”的含义。
恕我直言,如果你在 nhwc(物理)中有大步,大步不会是多余的。 因为您需要具有大小(逻辑)的正确映射。 否则没有办法说出真正的顺序。
这是该提案的症结所在:我们的特权NCHW的逻辑布局,始终。 所以如果我有一个我一无所知的 4D 张量,我假设它的逻辑布局是 NCHW。 这消除了歧义。 如果您想处理逻辑布局不是 NCHW 的张量,我确实认为上述 API 对您来说有点困难。
@dzhulgakov
操作保留内存格式行为
如果物理 NHWC 张量可以完全通过 strides 出现,这在技术上是 BC-breaking,除非你让它们只在内存格式标签存在时保留内存格式(但听起来你不希望它具有语义意义,所以我我不确定提案目前的建议是什么。)但我不确定这是否实际上破坏了任何人的代码。
如果物理 NHWC 张量可以完全通过 strides 出现,这在技术上是 BC-breaking,除非你让它们只在内存格式标签存在时保留内存格式(但听起来你不希望它具有语义意义,所以我我不确定提案目前的建议是什么。)但我不确定这是否实际上破坏了任何人的代码。
假设我们可以使内存格式“粘性”。 对内存格式化张量进行运算将产生内存格式化张量。 这将解决BC问题。
但是,当张量具有不同的内存格式时,我们需要定义二进制(或更多成员)操作的行为。
@ezyang哦,我刚刚发现上面的回复中有一个错字。 (对此我很抱歉。但是原始示例仍然是正确的。)让我重述如下:
但是我在第 2 步之后已经得到了 NHWC 连续。然后我可以跳过第 3 步,直接在第 4 步中将它用作 NHWC。但这肯定是不正确的,因为张量的物理顺序根本没有改变。
阻塞格式需要一套完全不同的运算符实现; 出于这个原因,我们不希望将它们与“内存格式”混合使用,根据定义,它应该与所有现有运算符友好并以相同或更好的性能工作。
是的,我们可以启用 NHWC 作为第一步。 但是,我实际上并不认为阻止格式真的是完全不同的东西。 它可以自然地表达(有一些很好的抽象)。 如果有一般的格式描述,那么其他人可以注册具有任意阻塞/步幅的新格式。
更多的是,如果我们已经阻止了支持,我们就不会费心创建一些隐藏的构造来运行底层的一切,这会在内部创建一个隐式世界,并且两个世界之间的 from/to 可能会成为一个问题。
无论如何,考虑阻止格式可能太远了。 但我想如果可能的话,最好使设计具有可扩展性。
但是我在第 2 步之后已经得到了 NHWC 连续。然后我可以跳过第 3 步,直接在第 4 步中将它用作 NHWC。但这肯定是不正确的,因为张量的物理顺序根本没有改变。
好的,我现在明白你的例子了。 你确实可以在第 2 步停下来,把它当作 NCHW 张量来使用; 在这种情况下,您会将 W 错误地解释为 C 等等。这绝对是基于 stride 的实现的缺点( @dzhulgakov ,我们可能应该将其添加到提案中)。 该提案对这种情况有一些规定:
为了解决上述问题,最初的建议是在张量上引入“软”内存格式标签,记录最后一次在张量上完成的 to(memory_format) 调用。 操作员需要将此注释传播到输出。 注释是“软”的,所以我们不会在不匹配的注释上出现硬错误,而是在分析模式下产生警告。
软内存格式标签可以让你区分你排列的 NCHW 张量和实际上是 NHWC 的张量。 但是当前形式的软标签不具有约束力,因此我不确定它对于这种情况实际上有多大用处。
另一种解决问题的方法是使用命名张量。 使用命名张量,我们可以使用(逻辑)维度上的名称来确定我们是否将张量视为 NCHW(假定的默认值)或其他东西。
但是,我实际上并不认为阻止格式真的是完全不同的东西。 它可以自然地表达(有一些很好的抽象)。 如果有一般的格式描述,那么其他人可以注册具有任意阻塞/步幅的新格式。
这里有更多关于这个话题的评论: https :
@ezyang感谢您的回复。 是的,软格式标签可能会有所帮助。 问题是它可能不够灵活,因为维度顺序可以是任意的。 它本身也是不可计算的。 命名张量对每个维度都有语义,但我怀疑可能需要更多的工具来支持。
我个人认为这可以通过引入从步幅顺序(物理)到 NCHW 大小顺序(逻辑)的映射来解决。 正如我上面提出的,对于 NCHW,它与当前的设计几乎相同; 对于 NHWC, sizes
仍然是 NCHW, strides
将按 (N, H, W, C) 顺序排列。 并且我们使用stride_index
= (0, 2, 3, 1) 来指定步幅的维度索引。
此外, strides
和stride_index
可用于表示任何张量格式。 这可以为其他人提供注册新数据格式的灵活性。
@ezyang
操作保留内存格式行为
如果物理 NHWC 张量可以完全通过 strides 出现,这在技术上是 BC-breaking,除非你让它们只在内存格式标签存在时保留内存格式(但听起来你不希望它具有语义意义,所以我我不确定提案目前的建议是什么。)但我不确定这是否实际上破坏了任何人的代码。
当算术运算和阈值移到 TensorIterator 时,这在技术上是打破 BC 的(因为以前不保留操作数的内存格式,而 TensorIterator 保留了它)。 现在的现状非常不一致 - 阈值保留布局,所有其他一元运算不保留,torch.where 不保留,如果两个操作数具有相同布局,算术运算保留布局,但默认为“nchw”或张量,即contiguous
在目前的理解中,如果不匹配,我不确定广播会发生什么。
您也很好地说明了empty_like
等保留布局不是 BC。 也许它还需要一个布局参数,比如提案中的 is_contiguous
x.is_contiguous(torch.memory_format.channels_first)
@ezyang @ngimel
empty_like 有一个问题; 当前定义的语义是您删除所有步幅信息,因此,不可能保留布局并成为 BC。
您还对 empty_like 等保留布局不是 BC 提出了很好的观点。
如果我们不依赖步幅来表达物理顺序, empty_like
就没有必要打破 BC。 张量中有3种维度信息:
目前物理顺序与形状/尺寸相同。 所以我们只是大步放下逻辑顺序。 考虑到我们正在解耦形状和物理顺序,我们也可以只删除逻辑顺序但保留empty_like
形状和物理顺序。 这意味着size()
和stride_index()
都将被保留,但stride()
将被重置。 特别是,NHWC 张量的empty_like
将返回具有相同形状信息的 NHWC 连续张量。
@uyongw我不确定改变empty_like
是否是个好主意; 现在它的语义匹配numpy 的empty_like
。
现在的现状非常不一致 - 阈值保留布局,所有其他一元运算不保留,torch.where 不保留,算术运算保留布局,如果两个操作数具有相同的布局,但默认为“nchw”或连续的张量目前的理解是否存在不匹配,我不确定广播会发生什么。
@ngimel ,是的,这些现在不是很一致。 我认为研究如何表示内存格式的一部分是让我们的操作员处于一致的状态
@zou3519你链接的 numpy 的 empty_like 有order
参数,默认为“尽可能匹配原型的布局。”。 这不是 pytorch 中的empty_like
当前所做的(它返回“nchw”-连续张量,即使原型是不连续的)
哦,我明白了,我读得太快了。 在那种情况下,让我们的 empty_like 匹配 numpy 也很好,并且(可能?)在这里也有用于内存布局
@zou3519是的,我想说的是保持当前的语义(删除@ezyang和@ngimel提到的逻辑顺序),同时保留物理布局,如 numpy 的默认值。 因此,对于 NCHW 原型,行为将与以前相同。 对于 NHWC 原型,它的行为将仍然兼容,即,如果您不更改当前实现,新张量将是 NHWC 连续的,而不是 NCHW 连续的。
两个问题:
如果我们用最后一个要点解决(B)的缺点,那么(B)似乎对我更可取。 它直观清晰,逻辑错误应该很容易检测到。 所有现有的操作也可以在张量上工作,因为它看起来像任何其他连续的张量。 可以理解语义(类似于命名张量提议)的操作也将按预期执行。
@zou3519你链接的 numpy 的 empty_like 有
order
参数,默认为“尽可能匹配原型的布局。”。 这不是 pytorch 中的empty_like
当前所做的(它返回“nchw”-连续张量,即使原型是不连续的)
我们计划在这种情况下保持格式(对于内存格式张量)
如果将 NHWC 张量添加到 NCHW 张量会发生什么?
使用内存格式化张量操作将返回内存格式化张量。 如果两个张量都是内存格式的输出格式将由第一个张量确定。
我要补充的两件事:
我们计划在这种情况下保持格式(对于内存格式张量)
我们需要审核现有的用法,因为操作员通常会调用empty_like
,然后假设它们是 NCHW 连续的。 我不知道我们将如何处理第三方代码。 如果我们想保留 BC,似乎我们需要一个与 numpy 不同的默认值。
使用内存格式化张量的操作将返回内存格式化张量。 如果两个张量都是内存格式的输出格式将由第一个张量确定。
我还要补充一点,如果你真的关心你的输出是什么格式——传入一个输出张量。
同意empty_like,在很多情况下,empty_like/zeros_like 等的结果被假定为 nchw-contiguous(我应该说物理上是连续的,在很多情况下它不是图像操作)。
在大多数情况下,传递输出张量不是一种选择,因为带有out
kwarg 的函数是不可微的。
我们的许多问题来自预期输出布局的不一致。 我们无法一次解决所有问题,但我们可以尝试锁定当前状态(至少对于 strides)并一一确定。 所以这里是提案。
蟒蛇API
引入新的 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
张量将需要显式内存格式转换
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
要使用特定格式“标记”它们:
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)
现在关于 empty_like 和类似的:
z = torch.empty_like(y)
z.is_contiguous() == True # For BC
因为它实际上是:
z = torch.empty_like(y, memory_format=torch.memory_format.any )
如果我们想保持格式:
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
相似地:
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
这意味着我们可以慢慢定义每个函数 memory_format 默认为当前世界的状态,对它们进行分类并注意我们将来如何更改它们。
如果你指定了张量 TensorOptions 当前被忽略(在最好的情况下,它们抛出异常是例如传递的设备选项与out
张量设备不匹配)。
内存格式应该是轻量级的,所以任何排列都会丢失它。
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)
不确定填充,将在此处感谢帮助。
但是我们可以使用正确的格式制作 x.to(memory_format=torch.memory_format.nhwc) 'tag' 张量并返回 self
多处理
将保留内存格式“标签”
块内存格式
上面的 API 不依赖于维度/步幅/大小,这意味着我们可以在未来扩展功能并保持相同的 API。
内部 API
操作员将能够根据内存格式进行分支
if (self.memory_format(nhwc)) {
// fast path
} else
{
// classic implementation
}
如果我们把 memory_format 做为 TensorOptions,我们可以考虑在 dispatch level 上进行分支(类似于 device、layout)
@VitalyFedyunin的建议的一小部分反馈 - 我认为这里需要 4D 张量
torch.memory_format.nchw # requires 4D tensor, contiguous memory
torch.memory_format.nhwc # requires 4D tensor, restrided/permuted memory
限制太多(因为除了 2D 之外,我们还想处理 1D 和 3D),而原始提案中的channels_first/channels_last
更适合此目的。
同意,我们需要更好的命名。 channels_first
听起来几乎是正确的,除了批处理先行=)
我喜欢你的最新提议。 .contiguous() 的处理会改变吗? 你需要 .contiguous(memory_format=<...>) 吗? 如果是这样,并且很多操作只是调用 .contiguous(),它们仍然可能不正确地格式化内存。 今天的许多操作也将输出分配为 empty_like(),这将具有相同的效果。 计划是更新这些以检测输入的内存格式并进行正确的连续和 empty_like 调用吗?
至于现在我们的用户(和所有库)期望.contiguous()
以降序返回内存连续张量。
我们不能破坏这个合同。 然而,好消息是:一旦我们支持 memory_format 选项,JIT 将能够理解何时调用.contiguous(memory_format=...)
而不是经典格式更有效。
@VitalyFedyunin我们是否假设不允许以下操作?
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)
另一种变体是:
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 - 为什么用户会首先调用.permute(0,2,3,1)
? 此提案中的所有张量的语义大小为 (n,c,h,w),这意味着 size(1) 返回您的通道。 这就是 PT 的标准库今天的假设以及它在本提案中的假设。 所以人们可能永远不会调用 .permute
上下文管理器是否有助于用户将管理器范围内已分配张量的内存格式覆盖为特定格式?
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(...)
我不喜欢上下文管理器的想法,因为它会放松对 memory_format 的控制。
例如:
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
当显式 memory_format 明确表示时:
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
如有必要,我们可以添加语法以允许:
x = torch.randn(10,3,32,32, memory_format=torch.channels_last)
@raghuramank100无需置换。
y = x.to(memory_format=torch.channels_last)
将为您完成所有肮脏的工作,保持与 x 中相同的昏暗顺序。
所以:
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)
你可以继续以这种格式寻址 nhwc
nhwc[N][C][H][W]
@VitalyFedyunin这是有道理的。
从用户的角度来看,方法的命名(如果它保持这样)似乎误导了我,因为“to”已经是将 Tensor 转移到不同设备的推荐方式。
另外,像Numpy 那样的用于转换 C_ORDER 和 F_ORDER 数组的东西怎么样?
numpy.asfortranarray()
numpy.ascontiguousarray()
人们很容易想象这样的事情:
torch.randn(32, 3, 64, 64).to(device).as_nhwc()
@VitalyFedyunin :我知道转换为不同的 memory_format 消除了用户手动排列的需要。 然而,一旦这个功能在 Torch 中可用,如果用户按照我上面概述的顺序调用函数会发生什么? 我们至少应该有一条警告/错误消息,指出布局转换失败。
@VitalyFedyunin :我知道转换为不同的 memory_format 消除了用户手动排列的需要。 然而,一旦这个功能在 Torch 中可用,如果用户按照我上面概述的顺序调用函数会发生什么? 我们至少应该有一条警告/错误消息,指出布局转换失败。
只有当我们实现命名张量时,这才有可能。 因为现在:
x.zeros(10,10,10,10)
x = x.permute(0,2,3,1)
没有人能告诉我我是刚刚创建了 nchw 还是 nhwc。
也许我误解了最初的提议,但是记录的内存格式标签不是应该消除这种情况吗?
@VitalyFedyunin有道理,我们需要确保在此 API 稳定
@dzhulgakov @VitalyFedyunin在回顾 #19975 之后,我对张量中记录的内存格式标签有了一些新的担忧。 我的基本问题是,我们如何决定操作是否应该保留内存标签? 最初,我认为只有“替代布局感知”操作员才需要拥有这些智能。 但是看看 Vitaly 的补丁,我认为一些核心运营商也需要调整。 例如,考虑x[0]
; 如果 x 以前是 NHWC 张量,那么我应该在执行此操作后取出 HWC 张量。 我相当确定 Vitaly 的补丁没有正确处理这个问题,我敢打赌这会让用户感到非常困惑。 也许唯一受影响的操作员是那些大步前进的操作员(在这种情况下,他们没有太多,我们可以手动审核他们),但这似乎是我们应该做的事情。 你怎么认为?
等等,张量仍然按以下顺序索引:0-dim N; 1-dim C; 2nd-dim H; 3rd-dim W。所以 x[0] 返回带有 0-dim C 的张量; 1-dim H; 2nd-dim W。无论 x 是 channels_first 还是 channels_last 内存布局。
否则 memory_format 就没有意义,我们只需要置换张量。
我的观点是不保留内存格式标签。 如果输入张量被标记channels_last
,则新张量被标记any
cc @zou3519 ,这里的布局传播逻辑让我想起了很多命名张量工作中的命名维度传播。
我还在追赶这个提议。 但是@ezyang我们可以通过传播每个维度的标志(或名称)来跟踪布局传播逻辑,然后它就相当于具有命名约定的命名张量
如果我们可以将内存标签逻辑和命名张量逻辑准确对齐,即使我们在开始时将它们作为两个单独的实现路径,也会很整洁。
扩展了两个张量函数.is_contiguous
和.contiguous
(python 和 c++ api)的功能。
注意:我们曾多次抱怨.to(memory_format)
函数,并决定不支持它。
.contiguous
现在支持可选的仅关键字参数 - memory_format
,可以是torch.contiguous_format
或torch.channels_last
。
使用torch.contiguous_format
将保留现有的.contiguous()
行为。
调用x.contiguous(memory_format=torch.channels_last)
返回保持相同语义布局(NCHW)但具有不同内存分配模式的新张量。
x.contiguous(memory_format=torch.channels_last)
期望输入张量是 3d、4d 或 5d; 否则失败。
.is_contiguous
现在支持可选的仅关键字参数 - memory_format
,它可以是torch.contiguous_format
或torch.channels_last
。
x.is_contiguous(memory_format=torch.contiguous_format)
保留与x.is_contiguous()
相同的功能并保持不变。
x.is_contiguous(memory_format=torch.channels_last)
如果 A) 输入张量在内存中是连续的,并且 B) 以 NWHC(或类似的 3d,5d)格式分配在内存中,则
注意:在阶段结束时, x.is_contiguous(memory_format=torch.channels_last)
将在每次调用时计算张量的状态。 此功能将在稍后更新。
为特定操作保留内存格式:
一元元素运算符保留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
二元元素操作符( add
、 sub
、 mul
、 div
)保留了 channels_last 内存格式。
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
任何超过 size、strides 和 dims 的操作都会重置内存格式。
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
未定
重塑(和类似)操作的结果,如果输出是“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) # ?
注意:当前未保留 memory_format
NHWC + NCHW 操作的结果。 是NHWC吗?
注:目前NHWC + NCHW -> NHWC 和NCHW + NHWC -> NHWC
像 cat/split 这样的操作呢? 保留内存格式对他们很有用。
@ezyang - 关于索引我认为我们应该停在某个地方。 不同的内存布局不是完全透明的,应该允许一些操作忽略它们。 我认为应该允许x[0]
擦除标签,包括x[0].unsqueeze(0)
正如 Raghu 提到的, cat/split 应该尽可能保留标签,因为它是一种非常常见的用法。 我认为一般的经验法则应该是,只要操作不会奇怪地改变排名或重新排序轴,我们就应该保留标签。 如果排名发生变化 - 所有赌注都将关闭。
我同意在某些情况下我们会丢失标签。 但我不同意x[0]
。 对我来说,这似乎是从NCHW
到CHW
一种非常常见的方式。
在多次讨论让 Tensor 携带(或不携带)channels_last '标签'是多么令人困惑之后,我们决定冒险将 bc-breaking 更改和自动提升张量引入到 channels_last 格式。
它对 API 意味着什么:
任何步长为 N,1,H,[W,[D]] 的 3d,4d,5d 张量都将自动获得 channels_last 内存格式。
为了使它工作,我们将采取特殊的预防措施来保证输出 channels_last 张量的 channels_last 张量上的算子至少与连续张量上的算子具有相似的性能。
在最坏的情况下:
1) 用户可以在输出上调用 .contiguous()。
2) 我们将以这样一种方式编写自动提升代码,改变这种行为几乎是微不足道的。
这种自动促销的副作用是:
import torch
x = torch.randn(10,16,16,3).permute(0,3,1,2)
x.is_contiguous(memory_format=torch.channels_last) == True
另一方面它可以解决这个问题(经过光照修改):
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
根据@ezyang的要求,来自松弛转换
Natalia Gimelshein [2:19 PM]
所以我认为没有标签的概念。
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?```
维塔利·费尤宁 [8:23 AM]
z 将是 channels_last
维塔利·费尤宁 [8:25 AM]
如果在任何提议的变体中 x1 不是 channels_last(除非我们更改块函数以不返回视图),则卷积会将其转换为连续(channels_first)格式并返回连续的
维塔利·费尤宁 [9:12 AM]
@ngimel感谢您的反馈,我认为我们可以对
Natalia Gimelshein [9:36 AM]
回复一个话题:
所以这似乎是一个问题,不是吗? 跨渠道维度分块是一种相对常见的事情,例如在类似 inception 的网络中。 因此,如果张量是分块通道第一张量,则卷积输出将是通道优先(这是直观的行为,并且很可能是用户想要的),如果张量是分块通道最后那么卷积输出将再次成为通道第一?
Natalia Gimelshein [9:39 AM]
回复一个话题:
但仅由于非交换加法行为和y
是第一个参数而通道最后,对吗? x1+y
的结果是什么? 我们在某处是否有二元运算的布局传播规则?
维塔利·费尤宁 [10:44 AM]
1) 是的,这是我们要用替代方案解决的问题。 我现在正在进行一些测试,并将在本周(一两天内)写下来。
2) x1+y - 也应该产生 channels_last 否则它会令人困惑,是的,我们将写下布局传播规则。
我认为当我们面对面谈论这个时我对
但似乎这里有很多细节需要解决,我不确定它最终是否有效。
因此,卷积的模糊(以及其他布局感知运算符,就此而言,例如我最近通过在输入上调用 .contiguous() 开始查看的上采样 - 那么它应该是什么意思?)是主要原因用于引入标签,iirc。
是的,所以我可以再次打开标签设计,但是我们
必须认真解决如何传播这些标签的问题,
即使你失去了布局(就像分块的情况一样)
在频道上)。 我更喜欢使“当前布局”一些
某种上下文管理器,而不是让它依赖于数据。
摘自ngimel 2019-06-19 12:43:45 -0700 的留言:
因此,卷积的模糊(以及其他布局感知运算符,就此而言,例如我最近通过在输入上调用 .contiguous() 开始查看的上采样 - 那么它应该是什么意思?)是主要原因用于引入标签,iirc。
顺便说一句,为什么我们必须创建一个新概念而不是仅仅坚持layout
? 我认为稀疏表示没有像“channels_last”这样的布局的定义明确的概念,所以我们不需要表示memory_formats * layouts
的乘积( layouts
指的是当前的用法),但只有memory_format + layouts
意味着使用与以前相同的参数应该没问题? 对我来说,它更短、更好,并且可以让我们避免将工厂的签名扩展到一千个参数。
考虑了布局选项(检查附录),但我们发现它会导致大量代码重复,并且不允许将张量自动转换为不同的内存格式
毕竟 memory_format 是一种跨越张量的方法,并且可以轻松选择优化的内核和输出,这是 strided 张量的属性,而不是一个完全不同的类
从某种意义上说,稀疏布局也是一种为大部分为零的数组轻松选择优化内核的方法
这可能是一个幼稚的问题,但是为什么 PyTorch 考虑这个 API 而不是仅仅公开一个在操作本身中使用 NHWC 的选项,这将直接调用可用的底层 CuDNN 内核?
对于常见的用例(将 conv 和池化等图像操作与 LM 架构混合),这似乎是一个简单的解决方案。 作为开发人员,我想要的只是Conv2d(..., nhwc=True)
。 有什么理由为什么这没有意义吗?
@rewonc我们已经考虑过类似的方法(向运算符添加选项而不是从跨步派生内核),并且发现由于以下原因很难申请:
nhwc=True
选项,否则下一个操作员将不得不再次重新输入(连续)。nhwc=True
选项。附注。 如果您担心 CudNN Ex
函数,我们希望公开cudnn_batch_norm_nhwc
和类似的运算符。
嗨@VitalyFedyunin ,我们看到 PyTorch 1.3 支持命名张量。 这能解决(或部分解决)对 NHWC(甚至被阻止)格式支持的担忧吗? 有没有计划基于命名张量推进 NHWC 状态?
我们正在推进渠道最后的支持,我将在本周在这里和松弛的渠道中发布路线图。 我们不会很快考虑添加被阻止的格式(因为它需要重写所有运算符)。
谢谢。 那会很好!
最有用的评论
顺便说一句,为什么我们必须创建一个新概念而不是仅仅坚持
layout
? 我认为稀疏表示没有像“channels_last”这样的布局的定义明确的概念,所以我们不需要表示memory_formats * layouts
的乘积(layouts
指的是当前的用法),但只有memory_format + layouts
意味着使用与以前相同的参数应该没问题? 对我来说,它更短、更好,并且可以让我们避免将工厂的签名扩展到一千个参数。