通过 PrivateUse1 便捷整合新后端¶
Created On: Oct 03, 2023 | Last Updated: May 07, 2024 | Last Verified: Nov 05, 2024
在本教程中,我们将展示一些必要的步骤,以通过 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 和分布式等。
随着 PyTorch 2.1.0 的到来,PrivateUse1
在新后端集成方面进行了一系列优化和增强,现在可以快速高效地支持新设备的集成。
如何通过 PrivateUse1 集成新后端¶
在本节中,我们将讨论通过 PrivateUse1
将新后端集成到 PyTorch 中的细节,这主要由以下几个部分组成:
为新后端注册内核。
为新后端注册生成器。
为新后端注册设备管理器。
为新后端的元数据注册序列化和反序列化功能。
其他模块。
为新后端注册内核¶
新后端可能有一些高性能的运算符实现,这些可以通过使用 TORCH_LIBRARY_IMPL
API(描述于 Registering a Dispatched Operator in C++)注册到分派器中。这涉及几种情况:
将新后端支持的所有前向运算符注册到分派器,同时注册后备机制,这样在新后端不支持某些运算符时,这些运算符可以回退到 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>());
}
通过
AutogradPrivateUse1
将torch::autograd::Function
的内核注册到分派器中,如果新后端需要覆盖PyTorch 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) 和后备机制的内核注册到分派器中,当需要时,自动转换系统将自动调用这些内核。
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
。
is_autocast_enabled() -> bool
检查新后端的 AMP 是否启用。
get_autocast_dtype() -> torch.dtype
获取新后端的 AMP 中支持的
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
类以实现与新后端对应的生成器类,并实现各种通用方法。定义一个参数为
device index
的新后端builder
。调用
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
扩展,例如 分布式集体通信
、基准计时器
等,这些模块将在未来添加。关于 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 提供了一个简单易用的新 C++/Python API,用于自行命名 PrivateUse1
后端。
torch.rename_privateuse1_backend("npu")
c10::register_privateuse1_backend("npu")
未来工作¶
PrivateUse1
机制的改进仍在进行中,因此新模块的``PrivateUse1`` 集成方法将依次添加。以下是我们正在积极开展的工作项目:
添加
分布式集体通信
的集成方法。添加
基准计时器
的集成方法。
总结¶
本教程向您介绍了通过 PrivateUse1
集成新后端到 PyTorch 的过程,包括但不限于算子注册、生成器注册、设备保护注册等。同时介绍了一些方法以提升用户体验。