Pytorch: [RFC] 内存格式(又名布局又名 NHWC)支持

创建于 2019-04-10  ·  68评论  ·  资料来源: pytorch/pytorch

问题陈述

CNN 算子利用张量维度的规范顺序并为其分配语义。 对于今天 PyTorch 中的 2D 案例,torch.nn.Conv2d 的输入必须是 NCHW 顺序的 4d 张量 -.

出于性能原因,以不同方式对维度重新排序通常是有益的,这样特定操作访问的内存将被连续布置并更好地利用局部性。 最常见的选择是将尺寸移到最后 - NHWC。 可以有更复杂的内存格式,将一维平铺成块,例如.

使用它的示例库包括:

  • cudnn 在 NHWC 中的 Volta 上具有更快的性能
  • fbgemm 和 qnnpack 不支持 NCHW。
  • libxsmm 确实支持 NCHW,但性能损失大约为 50% (IIRC)。

挑战在于转换维度顺序本身是昂贵的,因此在连续执行多个 CNN 操作的情况下(例如conv(relu(conv))) ),最好一次转换为不同的内存格式,执行操作并重新排序它们背部。

因此,重要的是让 PyTorch 了解不同的维度顺序,并能够Eager 和 JIT 模式下的操作之间

我们努力构建能够表示的 API:

  • Eager 和 JIT 中的 PyTorch 中存在具有不同内存格式(一开始,只是维度顺序)的张量。 阻塞布局的优先级较低,但仍然很好。
  • 用于查询和更改内存格式的用户公开 API
  • 核心 CNN 操作能够处理具有不同内存格式的输入张量并路由到相应的更快实现
  • 能够推断和优化 JIT 传递中的内存格式

术语:上面的问题通常被称为“布局”(mxnet)、“data_format”(tf)、“image_format”(keras)、“order”(caffe2)。 我们建议在 PyTorch 中使用名称“memory format”或“memory_format”。 不幸的是,“布局”这个名称在 PyTorch 中被采用,其值为“strided”和“sparse_coo”,因此命名选项不可用。

受影响的运营商

以下操作符至少应该是内存格式感知的。 除了产生正确的结果外,他们还需要从底层库提供最佳性能保留输出的

  • 卷积
  • 不同类型的池化
  • 批范数、层范数、实例范数(通常,任何范数)
  • 上采样/插值
  • 特征丢失
  • softmax 在较小程度上 - 可以在那里手动指定维度,但有效的实现仅适用于隐式 nchw 布局
  • 填充
  • 逐元素(一元和二元)运算
  • 继承内存格式的张量构造函数,例如 empty_like。

API 和行为变化

在 PyTorch 中定义内存格式的概念:

  • torch.memory_format.channels_first这样的常量。 它们没有指定的类型,可以是任意可比较的对象(可能以枚举开头,但将来可能是其他对象与命名张量的概念互操作)

    • 替代方法:直接使用torch.channels_first

  • 值为channels_firstchannels_last (允许使用较少的常量)
  • 对于 1D 图像 / 3D 张量,值表示 NCW、NWC,对于 2D 图像 / 4D 张量 - NCHW、NHWC,对于 3D 图像 / 5D 张量 - NCDHW、NDHWC

将以下方法添加到 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)

操作保留内存格式行为:

  • 卷积、池化等(见上文)以与输入相同的内存格式返回输出,并在内部分派到最佳实现
  • 一元元素操作保留相同的内存格式,并且需要像在连续张量上一样快地运行
  • 二进制元素操作为保留内存格式提供了一些合理的保证 - 可能可以定义更广泛,但最小值是:

    • NHWC + 标量 → NHWC

    • NHWC + 列向量 → NHWC

  • 核心 CNN 操作的后向操作保留与前向路径相同的内存格式。 (可能需要明确强制执行,因为输出的传入梯度可以采用不同的内存格式)

内存格式是通过序列化/反序列化保留的张量的属性(如果张量是参数)。

跨步实施

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

这种方法的优点

  • 利用现有 PyTorch 的 strides 概念,无需添加新的顶级想法或 API 参数
  • 以规范的 NCHW 顺序保留张量的逻辑行为
  • 适用于输入维度的任意重新排序
  • 现有的序列化例程已经保留了张量的步幅
  • 能够重用许多操作来处理不同的内存布局

缺点

  • 调用.contiguous()相当于切换到 NCHW,可能是用户偶然发生的,也可能是其中一个 ops 内部发生的

    • 需要对操作员进行显式审计以确保他们保留内存格式

  • 不适用于阻塞/平铺格式 - 需要不同的方法

    • 可以考虑在 PyTorch 中将它们添加为一等公民,但这是一个更大的变化

    • 另一种方法是将它们视为不透明的句柄,例如 MKLDNN 张量

  • 底层实现的性能特征对最终用户不太明显

最大的潜在问题是用户意图不明确。 无法区分用户是否真的想要不同的内存格式或输入张量只是碰巧以这种方式跨越。 具体来说,它会导致现有操作的行为发生变化——今天,即使输入是任意跨步的,卷积也只能产生 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

性能工具

为了调试性能,我们应该向分析器添加以下支持:

  • 查看程序中实际内存重新排序发生的位置 - 即跟踪对 .contiguous() 的调用
  • 跟踪调用了哪个实现
  • 在例如二进制操作中发出有关内存格式更改的警告(其中“软”注释很有用)

此功能可以内置到按需分析工具中。

Autograd处理

期望向后传递应该以与向前传递相同的内存格式运行是合乎逻辑的。 它不会总是自动发生,因为传入的梯度可能是任意跨步的。 因此,前向传递必须明确识别内存格式,将其存储在 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(...)

JIT 中的表示

目前的提议是:

  • 目前还没有对类型注释中内存格式的一流处理。 相反,我们可以为操纵内存格式的通道维护一个必要形状的后备图
  • 产生 per-Value 格式注释的推理过程(类似于 shape_inference)
  • 内存格式转换过程(手动或自动)找到必要的to(memory_format)调用需要插入以获得最佳性能

出于强制目的,我们还可以使用像assert x.is_contiguous(channels_last)这样的语句。

注意:存在一个问题,即特定设备具有首选内存格式组合的信息存储在何处(例如,x86 路由上的 qconv 到仅实现 NHWC 的 fbgemm)。 一种选择是将其置于 op 注册级别,但是,内存格式注释感觉更像是一种辅助信息。 我们可以首先在 JIT pass 中的某处维护一个全局映射,该映射表示首选内存格式和相关的启发式方法。 如果它变得不整洁 - 我们可以切换到基于注册的机制。

超越:阻塞的布局

当我们决定添加更复杂的张量包装时,使用一流的 PyTorch 张量可能不合理,因为实现成本和复杂性都很高。 可能有两种选择:

  • 不透明的表示,如自定义 C 类型绑定。 这是一个选择在推理中打包的选项,其中在性能优化方面的多样性更高
  • 像 MKLDNNTensor 这样的一流张量类型,其中一些(但不是全部)操作绑定在这种新类型上

另一种选择是在核心 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 的内容)。

基于这个答案,有几个选项是可能的:

  • 选项 A - 跨步(如上所示)。 张量布局是完全内部的表示。 像实现一样,它最方便地通过 strides 来完成。

    • .size(1) 返回“通道”,但内部存储器的布局不同

    • 亲:不改模型代码,我的模型还是可以直接做维数运算的。 事实上,公共 API 没有任何变化

    • 缺点:在大步实现中,许多运算符调用 .contiguous() 并且可能会意外地将布局还原回来

    • 缺点:从用户的角度来看,理解 op 回报的保证是最重要的。 这个 IMO 消除了 strides-only 方法,因为很难理解它们将返回的格式,并且没有 API 可以说“忽略我的 strides,实际上只是返回 NCHW-contiguous 的东西。” 这是对上述限制的补充。

  • 选项 B - 显式 NHWC 张量。 用户显式操作具有不同维度顺序的张量,但张量本身对此一无所知。 我们需要一些操作员级别的注释来弄清楚用户的期望。

    • .size(1) 返回“高度”

    • 亲:没有魔法,非常可预测

    • 缺点:将模型从一种布局更改为另一种布局成为一项复杂的操作,需要跟踪对 .size() 和 .reshape() 的所有访问(或者您需要在 API 中明确表示?)

  • 选项 B' - 带有布局标志的显式 NHWC 张量。 与上面相同,但我们允许将注释附加到张量以标记操作员在其实现中使用的语义布局。 则不需要操作员级别的注释 - 操作员可以根据输入的布局标志进行调度。
  • 选项 C - 命名张量。 ( https://docs.google.com/document/d/1ynu3wA2hcjwOtEng04N904gJjEbZWcINXO_ardX6hxc/edit#heading =h.2gbe5xpga3w9)

    • .size(1) 返回“height”,但我们要求人们不要使用这个 API 而是使用 .size('channel')

    • pro:非常明确,用户想要什么

    • con:不能解决转换问题,我们需要强制所有以布局感知编写的代码使用命名张量。 如果不是 - 与上述相同的问题适用

  • 选项 D-Layout 是不透明的张量类型。 像对待 MKLDNN 或 SparseTensor 一样对待 NHWC - 具有不同 DispatchID 的单独张量类型。 这就像选项 A,但在默认行为上有不同的权衡——未实现的操作将失败而不是恢复到 NCHW。

    • .size(1) 仍然返回“通道”

    • 优点:没有魔法和明确的,单独的调度允许操作员决定他们想要什么

    • 优点/缺点:所有必要的操作符都需要在不同的布局上实现,如果缺少某些操作,用户会得到一个不支持的明确错误

    • 缺点:我们可能需要禁止对其进行许多操作,例如视图,因为预期结果难以预测

internals mkldnn triaged

最有用的评论

顺便说一句,为什么我们必须创建一个新概念而不是仅仅坚持layout ? 我认为稀疏表示没有像“channels_last”这样的布局的定义明确的概念,所以我们不需要表示memory_formats * layouts的乘积( layouts指的是当前的用法),但只有memory_format + layouts意味着使用与以前相同的参数应该没问题? 对我来说,它更短、更好,并且可以让我们避免将工厂的签名扩展到一千个参数。

所有68条评论

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 张量为例。 为了描述张量,我们有sizesstridesstride_indexes

尺寸(n, c, h, w)
按物理顺序大步前进,即

  • 如果格式为 nchw,则为 (n, c, h, w) 的步幅
  • 如果格式为 nhwc,则为 (n, h, w, c) 的步幅。

stride_indexes

  • (0, 1, 2, 3) 如果格式是 nchw,
  • (0, 2, 3, 1) 如果格式是 nhwc。

对于 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 连续了。 不糊涂吗?

很难理解您的示例,因为您在口语中使用了一些术语并且需要精确。 以下是我如何解释你所说的:

  • 一个“nhwc”张量,根据这个提议,“张量的物理布局是 NHWC,但被跨步使得逻辑布局是 NCHW。”
  • 要将(逻辑布局为 NCHW 的张量)张量置换为(逻辑布局)NWHC”是运行y = x.permute(0, 2, 3, 1) ,因为您正在置换逻辑布局,而不是物理布局。 (我怀疑这不是你的意思,因为在你原来的帖子中你提到了置换x.permute(0, 3, 1, 2)
  • 然后将(逻辑布局)NWHC 张量进一步置换到(逻辑布局)NHWC 是应用置换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哦,我刚刚发现上面的回复中有一个错字。 (对此我很抱歉。但是原始示例仍然是正确的。)让我重述如下:

  1. 我有一个 NCHW 张量(物理上,连续)。
  2. 然后我将它置换为 NWHC(逻辑上)。
  3. 我想通过 contiguous() 调用将它进一步置换为 NHWC。
  4. 将其用作 NHWC(物理上)。

但是我在第 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) 来指定步幅的维度索引。

此外, stridesstride_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种维度信息:

  • 形状:大小
  • 逻辑顺序:以步幅记录的顺序信息(通常用于支持转置或置换)
  • 物理顺序:NCHW 或 NHWC(可以按照我的建议作为 stride_index 寻址)。

目前物理顺序与形状/尺寸相同。 所以我们只是大步放下逻辑顺序。 考虑到我们正在解耦形状和物理顺序,我们也可以只删除逻辑顺序但保留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 连续的。

两个问题:

  • 如果将 NHWC 张量添加到 NCHW 张量会发生什么?
  • 通过在张量上创建诸如 t.channel_dim() 之类的方法来解决(B)的缺点,该方法返回指示维度物理位置的整数值? 甚至可能需要这种方法来允许在不更改网络的情况下选择其他格式,例如块格式。

如果我们用最后一个要点解决(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我们可以通过传播每个维度的标志(或名称)来跟踪布局传播逻辑,然后它就相当于具有命名约定的命名张量

如果我们可以将内存标签逻辑和命名张量逻辑准确对齐,即使我们在开始时将它们作为两个单独的实现路径,也会很整洁。

阶段1

扩展了两个张量函数.is_contiguous.contiguous (python 和 c++ api)的功能。

注意:我们曾多次抱怨.to(memory_format)函数,并决定不支持它。

  1. .contiguous现在支持可选的仅关键字参数 - memory_format ,可以是torch.contiguous_formattorch.channels_last

    • 使用torch.contiguous_format将保留现有的.contiguous()行为。

    • 调用x.contiguous(memory_format=torch.channels_last)返回保持相同语义布局(NCHW)但具有不同内存分配模式的新张量。

      x.contiguous(memory_format=torch.channels_last)期望输入张量是 3d、4d 或 5d; 否则失败。

  2. .is_contiguous现在支持可选的仅关键字参数 - memory_format ,它可以是torch.contiguous_formattorch.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)将在每次调用时计算张量的状态。 此功能将在稍后更新。

阶段2

为特定操作保留内存格式:

  1. 一元元素运算符保留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
    
  2. 二元元素操作符( addsubmuldiv )保留了 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
    
  3. 任何超过 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
    

未定

  1. 重塑(和类似)操作的结果,如果输出是“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

  2. NHWC + NCHW 操作的结果。 是NHWC吗?

    注:目前NHWC + NCHW -> NHWC 和NCHW + NHWC -> NHWC

像 cat/split 这样的操作呢? 保留内存格式对他们很有用。

@ezyang - 关于索引我认为我们应该停在某个地方。 不同的内存布局不是完全透明的,应该允许一些操作忽略它们。 我认为应该允许x[0]擦除标签,包括x[0].unsqueeze(0)

正如 Raghu 提到的, cat/split 应该尽可能保留标签,因为它是一种非常常见的用法。 我认为一般的经验法则应该是,只要操作不会奇怪地改变排名或重新排序轴,我们就应该保留标签。 如果排名发生变化 - 所有赌注都将关闭。

我同意在某些情况下我们会丢失标签。 但我不同意x[0] 。 对我来说,这似乎是从NCHWCHW一种非常常见的方式。

在多次讨论让 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 内核。
  • 除非它也有nhwc=True选项,否则下一个操作员将不得不再次重新输入(连续)。
  • 要在整个网络中使用 NHWC,每个运营商都需要nhwc=True选项。

附注。 如果您担心 CudNN Ex函数,我们希望公开cudnn_batch_norm_nhwc和类似的运算符。

@VitalyFedyunin ,我们看到 PyTorch 1.3 支持命名张量。 这能解决(或部分解决)对 NHWC(甚至被阻止)格式支持的担忧吗? 有没有计划基于命名张量推进 NHWC 状态?

我们正在推进渠道最后的支持,我将在本周在这里和松弛的渠道中发布路线图。 我们不会很快考虑添加被阻止的格式(因为它需要重写所有运算符)。

谢谢。 那会很好!

此页面是否有帮助?
0 / 5 - 0 等级