[pytorch][CPU] 在 ATen 中重构随机数生成器 - 第 1/n 部分

2024-03-20 863 views
0

此 PR 源自https://github.com/pytorch/pytorch/pull/13070

概括:

此 PR 的目的是重构 ATen 中的随机数生成器 (RNG) 设计。目前,PyTorch 中的 RNG 具有不对称设计,即 CPU 生成器使用 ATen 类,而 CUDA 生成器使用旧版 THC 代码(THCRNGState, THCState, THCRandom_Init等)。此外,从目前的设计来看,ATen 中的发电机概念并不明确。此 PR 是围绕 RNG 的更多重构工作的第一部分,目前仅处理 PyTorch 前端和 CPU 后端。它执行以下操作:

  • 通过回顾 Generator、CPUGenerator 和 CUDAGenerator 类来阐明生成器概念。
  • 将 mt19937 从 TH 移动到 aten 作为 MT19937RNGEngine.h,并将分布从 THRandom.cpp 移动到 DistributionsHelper.h。添加 PhiloxRNGEngine.h 引擎并为其添加单元测试。
  • 修复了几个用于代码生成的 python 文件中硬编码的生成器相关代码,例如function_wrapper.py等。
  • 修复了生成器前端 python 绑定以包括设备 kwarg 和默认 kwarg
  • 从类型中删除生成器的创建。
  • 更新文档和注释并添加torch.Generatorapi 文档。

回答

4

这不再是 WIP 了吧?在这种情况下最好删除[WIP]标签;)

3

抄送 @cpuhrsch 进行代码生成更改

9

这看起来相当不错,比之前有明显的进步!然而,有一些 C++ 的东西需要首先修复。

2

是的,Syed 计划在后续 PR 中这样做。

0

@syed-ahmed 如果您不介意,我将直接向您的分支提交一些小的缺陷修复。

7

测试失败看起来是真实的;也许我们扰乱了随机数生成的顺序,这正在影响您。

0

@ezyang 感谢您添加更改!

1

看着

--- a/build/aten/src/ATen/CUDAShortType.cpp
+++ b/build/aten/src/ATen/CUDAShortType.cpp
@@ -1847,32 +1847,24 @@ Tensor & CUDAShortType::s__th_addcdiv_(Tensor & self, const Tensor & tensor1, co
 Tensor & CUDAShortType::_th_random_(Tensor & self, int64_t from, int64_t to, Generator * generator) const {
     // DeviceGuard omitted
     auto self_ = checked_tensor_unwrap(self,"self",1, false, Backend::CUDA, ScalarType::Short);
-    auto generator_ = check_generator<CUDAGenerator>(generator, &globalContext().defaultGenerator(device_type()));
-    (void) generator_; //silence unused warning
     THCudaShortTensor_clampedRandom(globalContext().getTHCState(), self_, from, to);
     return self;
 }

值得研究一下它的check_generator作用以及是否需要继续成为我们代码生成的一部分。

9

@cpuhrsch https://github.com/pytorch/pytorch/blob/222a07863fc6f997ac695315c3047c1169e7ff60/aten/src/ATen/CheckGenerator.h#L9-L17

check_generator 做了两件事:

  • 如果将空生成器提供给张量,则确保使用默认生成器
  • 它对输入生成器进行dynamic_cast以获取请求的子类类型

我的 check_generator 版本如下: https://github.com/pytorch/pytorch/blob/2854c732e5292a0dfee7c48df4a8b85fb48a79a5/aten/src/ATen/Utils.h#L137-L158

我的版本的不同之处在于我尽量不使用dynamic_cast。@ezyang 要求在运行时使用 Generator 基类上的标签来检查类型,因为 RTTI 在某些平台上可能不可用。因为,我在 Generator 基类中有设备信息,所以我修改了这个函数,首先根据模板创建一个临时生成器,然后使用 getDevice 函数检查类型,然后执行 static_cast 以避免动态_casting(这感觉就像一个黑客,但我真的不知道在这里应该做什么是正确的)。

check_generator 相对于 code-gen 似乎不应该在那里(https://github.com/pytorch/pytorch/pull/16604/files#diff-ff8fdb5222085eb1036fbebb7cc4f4a9L294)并且应该是用户在张量实现中的责任。也就是说,例如,我们当前在代码生成调度中使用检查生成器,这将为您提供 CPUGenerator或 CUDAGenerator,然后将其传递给具有 Generator* 的函数签名(例如:`void THTensor (clampedRandom )(THTensor self, at::Generator _generator, int64_t min, int64_t max) { ). In my PR, I would have to do another check_generator insideTHTensor_(clampedRandom)` 获取具有 CPUGenerator 特定方法 (random(), random64()) 的 CPUGenerator。当前上游代码不需要强制转换为 CPU/CUDAGenerator,因为所有 CPU/CUDA 函数都在 Generator 基类中可用。

7

当测试通过时驳回我的评论

4

@syed-ahmed 你还想修复测试吗,还是现在就想进行审查?

8

@ezyang 如果您可以对算法进行评论,那就太好了。特别是 ATen/core/DistributionsHelper.h 和 ATen/core/PhiloxRNGEngine.h。根据对这些文件的审查,测试可能需要进行调整。

7

@ngimel @ezyang 我已将均匀分布算法更改为除以 max uint32/uint64 并将 1.0s 压缩为std::nextafter(1.0, 0.0). 这是我们 slack 讨论中的方法 2。我说过我将保持当前的 TH 算法不变,但事实证明,当与 Philox(松弛讨论中的方法 1)结合使用时,更多的测试会失败。我不想处理这些失败,因为无论如何我都会在将来将方法 2 设置为默认值。我推测,由于Philox的统计特性不如mt19937,因此损失熵的影响更为突出。现在大部分测试都已通过(仅处理 1 个 rocm 故障)。如果您不介意,请再看一下此 PR,并让我知道是否需要进行其他更改/改进。

@drnikolaev 感谢您的评论!我很快就会跟进。

0

这看起来没问题吗(我只测试了测试/分布式?注意,这个 PR 仅适用于 CPU。这是我用来运行测试的 docker 镜像:docker pull tousif111/pytorch:generator-refactor-cpu

大多数分布测试已经在 PyTorch 中进行,而 Pyro 中的测试主要针对形状语义,这些语义不应受到 PRNG 任何更改的影响。最好执行所有测试 ( make test) 以确保不会出现任何意外故障。

8

这个公关正在成型!我设法对完整的 PR 进行了完整的审查,上面评论的问题是我唯一未解决的问题。看起来你还得哄骗 CI 通过。

在此之前,最好重写 #13070 中的 PR 描述,以准确地表示此 PR 中的更改,以帮助将来的提交读者。

0

@ezyang我已经在此提交中解决了您的最新评论:https://github.com/pytorch/pytorch/pull/16604/commits/c3f0093e8980d285eed915d95d506a417aeed508 我们尚未达成协议的两件事是

  • 是否返回原始指针或 unique_ptr at::detail::getDefaultCPUGenerator(我在上面的线程中留下了回复:https://github.com/pytorch/pytorch/pull/16604#discussion_r273722767)。在此提交中,我更改了 check_generator_with_default 函数以采用 unique_ptr 并在多个地方删除了 .get() 。
  • 为 Generator 类创建default构造函数参数。我带回了杰出的default_generator并且仍然用作default构造函数参数。唯一的区别是tp_newTHPGenerator 类的方法现在使用一些PyImport_ImportModulePyObject_GetAttrString来加载在初始化期间创建的杰出生成器。现在torch.Generator(default=True) == torch.Generator(default=True)成立,我已将其添加到测试中。
0

为 Generator 类设置默认构造函数参数。我带回了杰出的 default_generator 并仍然使用 default 作为构造函数参数。唯一的区别是 THPGenerator 类的 tp_new 方法现在使用一些 PyImport_ImportModule 和 PyObject_GetAttrString 来加载在初始化期间创建的杰出生成器。torch.Generator(default=True) == torch.Generator(default=True) 现在成立,我已将其添加到测试中。

我还没有读过您为这种情况编写的代码,但您所做的描述听起来很可疑。你能向我解释一下为什么你真的想要默认值作为构造函数参数吗?我不认为这是一个好的 API 设计。我期望 Python 对象构造的一个常见不变量是,对象的每次构造都会给我一个新的实例,但现在default=True违反了这个假设(在我原来的评论中,我建议你完全删除它。你还没有这样做,并且我想知道为什么;)

4

我还没有读过您为这种情况编写的代码,但您所做的描述听起来很可疑。你能向我解释一下为什么你真的想要默认值作为构造函数参数吗?我不认为这是一个好的 API 设计。我期望 Python 对象构造的一个常见不变量是,对象的每次构造都会给我一个新的实例,但现在default=True违反了这个假设(在我原来的评论中,我建议你完全删除它。你还没有这样做,并且我想知道为什么;)

我现在同意你提到的 Python 对象构造不变量。我认为default=Truetorch.Generator('cpu', default=true)使用torch.default_cpu_generator().

5

我认为在像 torch.Generator('cpu', default=true) 这样的调用中使用 default=True 是一种比使用 torch.default_cpu_generator() 更干净的获取默认生成器的方法。

是的,如果你想让它起作用,你应该把它变成一个函数,而不是一个对象构造函数。

9

@ezyang 我已经讨论了你的最新评论。新的变化是:

  • 更改了 getDefaultCPUGenerator 以返回原始 CPUGenerator 指针而不是 unique_ptr。
  • 使正态分布对下一个浮点和双精度值使用 c10::可选
  • 从 torch.Generator api 中删除了默认参数
  • 更新了文档
8

请注意,我需要进行一些内部构建调整。

3

内部 CI 正在触发此警告,这看起来是合法的:

caffe2/aten/src/TH/generic/THTensorRandom.cpp:487:10: error: destination for this 'memcpy' call is a pointer to dynamic class 'CPUGenerator'; vtable pointer will be overwritten [-Werror,-Wdynamic-class-memaccess]
  memcpy(rng_state, cast_generator, size);
  ~~~~~~ ^
caffe2/aten/src/TH/generic/THTensorRandom.cpp:487:10: note: explicitly cast the pointer to silence this warning
  memcpy(rng_state, cast_generator, size);
         ^
         (void*)
caffe2/aten/src/TH/generic/THTensorRandom.cpp:505:10: error: destination for this 'memcpy' call is a pointer to dynamic class 'CPUGenerator'; vtable pointer will be overwritten [-Werror,-Wdynamic-class-memaccess]
  memcpy(cast_generator, rng_state, size);
  ~~~~~~ ^
caffe2/aten/src/TH/generic/THTensorRandom.cpp:505:10: note: explicitly cast the pointer to silence this warning
  memcpy(cast_generator, rng_state, size);
         ^
         (void*)
1

我正在做。

4

@ezyang我在此提交中添加了 get_rng_state/set_rng_state 函数的补丁:https://github.com/pytorch/pytorch/pull/16604/commits/1f3675e28b4f3a40036416cb96d0167f868ab95a 主要更改是:

  • 确保可以通过在 THGenerator.hpp 中保留旧的 THGeneratorState 结构来加载旧的检查点。测试使用:https://gist.github.com/syed-ahmed/fc2f3635a02cba12342c204d54754960
  • 在 THGenerator.hpp 中创建了一个新的 POD 类来保存 CPUGenerator 数据 - THGeneratorNew 结构
  • 在 CPUGenerator 中添加了引擎的 getter/setter,现在由 get_rng_state/set_rng_state 使用。
8

重新测试一下:RNG状态并没有那么大吧?我们是否可以在测试中包含一个十六进制编码的二进制文件,以便我们实际上可以将其保留在测试套件中?

9

我认为 POD 问题和 THGeneratorState{,New} 之间缺乏静态大小断言是土地阻塞。

6

@ezyang我已经解决了你的最新评论变化是

  • 修复了 POD 问题,添加了mt19937_data_pod用于输入和输出 mt19937 数据的函数,将 c10::Optional 分解为 bool 和 float
  • 添加了大小的静态断言(当我们可以在 ATen 中本地移动 get_rng_state、set_rng_state 时,让我们在将来进行标头解决方案)并在错误消息中添加诊断
  • 在 get/set_rng_state 中添加了对 CPUGenerator 的检查。

关于旧检查点的测试:rng 二进制文件看起来很大:https://gist.github.com/syed-ahmed/fc2f3635a02cba12342c204d54754960#gistcomment-2897557 让我知道您是否仍然希望在测试套件中使用它。

8

嘿@syed-ahmed,这里有关于登陆此 PR 的状态更新:diff 本身正在通过 PyTorch/Caffe2 测试,但有一些 C++ BC 破坏性更改,我需要在上游进行修复。不幸的是,我下周就要去 PTO,所以要到下周才能开始。

TensorImpl.cpp:44:28: error: ‘class at::Context’ has no member named ‘defaultGenerator’
     torch::globalContext().defaultGenerator(torch::kCPU);
                            ^~~~~~~~~~~~~~~~
TensorImpl.cpp:45:11: error: ‘struct at::Generator’ has no member named ‘manualSeed’; did you mean ‘manualSeedAll’?
   cpu_gen.manualSeed(seed);
           ^~~~~~~~~~
TensorImpl.cpp:44:28: error: ‘class at::Context’ has no member named ‘defaultGenerator’
     torch::globalContext().defaultGenerator(torch::kCPU);
                            ^~~~~~~~~~~~~~~~
TensorImpl.cpp:45:11: error: ‘struct at::Generator’ has no member named ‘manualSeed’; did you mean ‘manualSeedAll’?
   cpu_gen.manualSeed(seed);
           ^~~~~~~~~~

此外,RNG 的变化也扰乱了一些测试。我不记得这个 PR 是否应该产生完全相同的随机数。我们可能应该修复这些测试(尤其是因为您计划更改 RNG 引擎),但如果此差异不是预期的,我们应该进行更多调查。

tensor1 (torch.Size([4, 4])) and tensor2 (torch.Size([4, 4])) are not close enough. 
max rtol: 0.39336044    max atol: 0.30065852

    self.assertAlmostEqual(output, -25.48, places=2)
AssertionError: -26.65626863950947 != -25.48 within 2 places
3

@syed-ahmed 我认为一个好主意是编写“如何迁移代码”发行说明(如果我们不能以合理的方式编写这些迁移说明,我们应该为 BC 添加一些方法)。要涵盖的内容:

  • 现在类型torch::Generator不同了,对吗?torch::Generator& x = ...如果你之前在代码中写过,我现在应该写什么?
  • 如果删除了 at::Context 中的任何方法,请记录它们的替代方法
  • 如果之前torch::Generator更改了任何方法,请记录其替换内容

让这件事变得棘手的一件事是这个 PR 只涉及 CPU,而不涉及 CUDA。所以我不确定仅部分重构会引入多少 BC 问题。

3

此外,RNG 的变化也扰乱了一些测试。我不记得这个 PR 是否应该产生完全相同的随机数。我们可能应该修复这些测试(尤其是因为您计划更改 RNG 引擎),但如果此差异不是预期的,我们应该进行更多调查。

这个 PR 应该产生完全相同的随机数。测试失败的原因之一可能是我如何从 /dev/random 读取数据。如果失败的测试中没有torch.manual_seed(),则意味着种子是由 getNonDeterministicRandom() 设置的

uint64_t getNonDeterministicRandom() {
  std::random_device rd;
  uint32_t random1;
  uint32_t random2;
  if (rd.entropy() != 0) {
    random1 = rd();
    random2 = rd();
    return make64BitsFrom32Bits(random1, random2);
  }
  else {
    random1 = std::chrono::high_resolution_clock::now().time_since_epoch().count();
    random2 = std::chrono::high_resolution_clock::now().time_since_epoch().count();
    return make64BitsFrom32Bits(random1, random2);
  }
}

之前,我们使用以下方法执行此操作: https: //github.com/pytorch/pytorch/blob/master/aten/src/TH/THRandom.cpp#L62,它只是从 time(0) 开始静态转换一个 32 位数字,而我在函数中连接两个 32 位数字。我可以只更改 getNonDeterministicRandom() 来执行相同的静态转换,然后您可以再次运行内部 CI,或者内部 CI 中失败测试的解决方案可能是在测试开始时设置 manual_seed。

我正在考虑写迁移笔记,并将再次 ping 棘手的部分 -So I'm not sure how many BC problems being only partially refactored introduces

3

我可以只更改 getNonDeterministicRandom() 来执行相同的静态转换,然后您可以再次运行内部 CI,或者内部 CI 中失败测试的解决方案可能是在测试开始时设置 manual_seed。

我可以运行这个测试(如果您在此处粘贴差异,我可以应用它),但您给我的解释对我来说没有意义。如果内部测试之前是根据当前时间进行播种的,我预计它们也会同样不稳定(假设“坏”种子均匀分布在 64 位整数的空间中)。我想它们实际上可能是这样的。 re 不是以这种方式分发的,但这看起来仍然很奇怪)。

感谢您的迁移笔记;它还将帮助修复内部使用站点:)(或者他们也可能会向您建议,我们可以通过重新引入一些旧功能但只是弃用它们来改进 BC。)

2

我快速检查了一下我们是否生成了相同的随机数。

掌握

(/home/ezyang/Dev/pytorch-tmp-env) [ezyang@devgpu005.ash6 ~/Dev/pytorch-tmp] python                     
Python 3.7.2 (default, Dec 29 2018, 06:19:36)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.                                  
>>> import torch
>>> torch.manual_seed(0)
<torch._C.Generator object at 0x7f21d1694af0>
>>> torch.randn(10)
tensor([ 1.5410, -0.2934, -2.1788,  0.5684, -1.0845, -1.3986,  0.4033,  0.8380,                         
        -0.7193, -0.4033])

你的公关

(/home/ezyang/Dev/pytorch-tmp-env) [ezyang@devgpu005.ash6 ~/Dev/pytorch-tmp] python
Python 3.7.2 (default, Dec 29 2018, 06:19:36) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> torch.manual_seed(0)
<torch._C.Generator object at 0x7f5b08b74ab0>
>>> torch.randn(10)
tensor([ 1.5410, -0.2934, -2.1788,  0.5684, -1.0845, -1.3986,  0.4033,  0.8380,
        -0.7193, -0.4033])

所以,至少正常一代看起来还不错。(这可能是其他 RNG 生成器之一的问题吗?很难说......)