通过 PrivateUse1 促进新的后端集成¶
创建于:2023 年 10 月 03 日 | 最后更新:2024 年 05 月 07 日 | 最后验证:2024 年 11 月 05 日
在本教程中,我们将逐步介绍通过 PrivateUse1
集成 pytorch/pytorch
仓库外部的新后端的一些必要步骤。请注意,本教程假设您已经对 PyTorch 有基本的了解,并且是 PyTorch 的高级用户。
注意
本教程仅涉及与 PrivateUse1 机制相关的部分,该机制有助于集成新设备,其他部分将不涵盖。同时,并非本教程中涉及的所有模块都是必需的,您可以根据您的实际需求选择对您有帮助的模块。
什么是 PrivateUse1?¶
在 Pytorch 2.0 之前,PyTorch 提供了三个保留的分派键(及其对应的 Autograd 键),用于原型设计树外后端扩展,这三个分派键如下:
PrivateUse1/AutogradPrivateUse1
PrivateUse2/AutogradPrivateUse2
PrivateUse3/AutogradPrivateUse3
在原型验证通过后,您可以为新的后端申请私钥,例如 CUDA、XLA、MPS 等。
然而,随着 PyTorch 的快速发展,越来越多的硬件制造商尝试将其后端集成到 PyTorch 中,这可能会导致以下问题:
每次新的后端集成都涉及大量文件修改
目前分派键的数量存在硬性限制(
DispatchKeySet
64 位限制)
注意
通过 PrivateUse1 键将新后端集成到 PyTorch 中也存在问题,因为不可能同时集成多个后端。幸运的是,这些树外后端很少同时使用。
鉴于上述原因,社区开始建议通过 PrivateUse1
将新后端集成到 PyTorch 中。
然而,之前的 PrivateUse1
机制并不完全能够与新后端集成,因为它在某些模块(例如 Storage、AMP、Distributed 等)中缺乏一些相关支持。
随着 Pytorch 2.1.0 的到来,针对 PrivateUse1
在新后端集成方面进行了一系列优化和增强,现在可以快速有效地支持新设备的集成。
如何通过 PrivateUse1 集成新后端?¶
在本节中,我们将讨论通过 PrivateUse1
将新后端集成到 Pytorch 的详细信息,主要包括以下部分:
为新后端注册内核。
为新后端注册生成器。
为新后端注册设备保护器。
为新后端元数据注册序列化和反序列化函数。
其他模块。
为新后端注册内核¶
新后端可能具有操作符的一些高性能实现,可以通过 在 C++ 中注册分派运算符 中描述的 TORCH_LIBRARY_IMPL
API 注册到分派器。这涉及几种情况:
将新后端支持的所有前向运算符注册到分派器,并同时注册回退,以便当新后端不支持某些运算符时,这些运算符可以回退到 CPU 进行执行,以确保功能的可用性。
at::Tensor wrapper_Custom_Tensor_add(const at::Tensor & self, const at::Tensor & other, const at::Scalar & alpha) {
// Implementation of add kernel in new backend
...
}
TORCH_LIBRARY_IMPL(aten, PrivateUse1, m) {
...
m.impl("add.Tensor", TORCH_FN(wrapper_Custom_Tensor_add));
...
}
void custom_cpu_fallback(const c10::OperatorHandle& op, torch::jit::Stack* stack) {
// Add some hints about new devices that do not support and need to fall back to cpu
at::native::cpu_fallback(op, stack);
}
TORCH_LIBRARY_IMPL(_, PrivateUse1, m) {
m.fallback(torch::CppFunction::makeFromBoxedFunction<&custom_cpu_fallback>());
}
如果新后端需要覆盖
PyTorch Autograd 层
,则通过AutogradPrivateUse1
将来自torch::autograd::Function
的内核注册到分派器,分派器和 autograd 系统将自动调用这些运算符的前向和反向实现。
class CumtomSeluFunction : public torch::autograd::Function<CumtomSeluFunction> {
// Implementation of selu kernel in new backend
}
at::Tensor wrapper_AutogradCumstom__selu(const at::Tensor & self) {
return CumtomSeluFunction::apply(self);
}
TORCH_LIBRARY_IMPL(aten, AutogradPrivateUse1, m) {
...
m.impl("selu", TORCH_FN(wrapper_AutogradCustom__selu));
...
}
通过
AutocastPrivateUse1
注册想要支持 自动混合精度 (AMP) 和回退机制的内核到分派器,autocast 系统将在需要时自动调用这些内核。
TORCH_LIBRARY_IMPL(aten, AutocastPrivateUse1, m) {
...
KERNEL_PRIVATEUSEONE(<operator>, <policy>)
...
}
TORCH_LIBRARY_IMPL(_, AutocastPrivateUse1, m) {
m.fallback(torch::CppFunction::makeFallthrough());
}
需要添加的是,如果要在新后端中支持 AMP,则需要通过 torch._register_device_module("backend_name", BackendModule)
注册新的 BackendModule
,并且 BackendModule
需要具有以下 API:
get_amp_supported_dtype() -> List[torch.dtype]
获取 AMP 中新后端上支持的 dtype,这可能会支持一个或多个
dtype
。
is_autocast_enabled() -> bool
检查 AMP 是否在新后端上启用。
get_autocast_dtype() -> torch.dtype
获取 AMP 中新后端上支持的
dtype
,该 dtype 由set_autocast_dtype
或默认dtype
设置,默认dtype
为torch.float16
。
set_autocast_enabled(bool) -> None
在新后端上启用或禁用 AMP。
set_autocast_dtype(dtype) -> None
设置 AMP 中新后端上支持的
dtype
,并且dtype
包含在从get_amp_supported_dtype
获取的dtypes
中。
为新后端注册生成器¶
有必要支持与新设备对应的生成器。目前,PrivateUse1
可以动态注册自定义生成器,这主要分为以下步骤:
继承
GeneratorImpl
类以实现与新后端对应的生成器类,并实现各种通用方法。定义一个新的后端
builder
,带有一个参数:device index
。调用
REGISTER_GENERATOR_PRIVATEUSE1
宏以完成动态注册。
struct CustomGeneratorImpl : public c10::GeneratorImpl {
// Implementation of generator in new backend
}
at::Generator make_custom_generator(c10::DeviceIndex device_index) {
return at::make_generator<CustomGeneratorImpl>(device_index);
}
REGISTER_GENERATOR_PRIVATEUSE1(make_cumstom_generator)
为新后端注册设备保护器¶
PyTorch 通过 DeviceGuard
提供与设备、流和事件切换相关的功能。此功能也适用于 PrivateUse1
键。
继承
DeviceGuardImplInterface
类以实现与新后端对应的各种通用方法。调用
C10_REGISTER_GUARD_IMPL
宏以完成动态注册。
struct CustomGuardImpl final : public c10::impl::DeviceGuardImplInterface {
// Implementation of guard in new backend
}
C10_REGISTER_GUARD_IMPL(PrivateUse1, CustomGuardImpl);
为新后端元数据注册序列化和反序列化函数¶
PyTorch 目前能够动态注册序列化/反序列化函数,以支持类 TensorImpl.ExtraMeta
中名为 backend_meta_
的新后端附加元数据的序列化和反序列化。您可以参考以下步骤:
继承
BackendMeta
类以实现与新后端对应的CustomBackendMetadata
,并且可以在类中自定义新后端的各种字段。实现新后端的序列化和反序列化函数,函数签名为
void(const at::Tensor&, std::unordered_map<std::string, bool>&)
。调用
TensorBackendMetaRegistry
宏以完成动态注册。
struct CustomBackendMetadata : public c10::BackendMeta {
// Implementation of backend metadata in new backend
}
void for_serialization(const at::Tensor& t, std::unordered_map<std::string, bool>& m) {
// Implementation of serialization
}
void for_deserialization(const at::Tensor& t, std::unordered_map<std::string, bool>& m) {
// Implementation of deserialization
}
TensorBackendMetaRegistry(c10::DeviceType::PrivateUse1, &for_serialization, &for_deserialization);
其他模块¶
除了上述部分外,还有一些其他模块可以通过 PrivateUse1
进行扩展,例如 distributed collective communication
、benchmark timer
等,这些将在未来添加。关于 PrivateUse1
集成的一个示例是 Ascend NPU。
如何通过 Privateuse1 改善用户体验?¶
通过 PrivateUse1
集成新设备的主要目标是满足基本功能需求,接下来要做的就是提高可用性,这主要涉及以下几个方面:
向 Pytorch 注册新的后端模块。
将 PrivateUse1 重命名为新后端的自定义名称。
生成与新后端相关的方法和属性。
向 Pytorch 注册新的后端模块¶
PyTorch 中一些与 CUDA 相关的接口可以通过以下形式调用:torch.cuda.xxx
。因此,为了符合用户习惯,通过 PrivateUse1
机制实现的新后端也应提供类似的接口。
例如,使用 Ascend NPU
torch._register_device_module('npu', torch_npu.npu)
完成上述操作后,用户可以通过 torch.npu.xxx
调用 Ascend NPU
的一些独有 API
将 PrivateUse1 重命名为新后端的自定义名称¶
PrivateUse1
键是将新后端集成到 PyTorch 中的内部机制。对于用户而言,与 PrivateUse1
相比,与新后端密切相关的自定义名称应该更友好。
以 Ascend NPU
为例,第一种用法将对用户更友好。
torch.rand((2,2),device='npu:0')
torch.rand((2,2),device='privateuse1:0')
现在,PyTorch 为自命名的 PrivateUse1
后端提供了一个新的 C++/Python API,使用起来非常简单。
torch.rename_privateuse1_backend("npu")
c10::register_privateuse1_backend("npu")
未来工作¶
PrivateUse1
机制的改进仍在进行中,因此将依次添加新模块的 PrivateUse1
集成方法。以下是我们正在积极开展的几个项目:
添加
distributed collective communication
的集成方法。添加
benchmark timer
的集成方法。
结论¶
本教程引导您完成了通过 PrivateUse1
将新后端集成到 PyTorch 的过程,包括但不限于运算符注册、生成器注册、设备保护器注册等。同时,介绍了一些改善用户体验的方法。