欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

OneFlow 源代码分析:基本计算界面基元

最编程 2024-06-11 09:42:30
...


作者|郑建华

此前, OneFlow 版本更新 博客的第 5 节对框架的“多设备适配”作了说明,原文摘录如下:

OneFlow 提供简洁高效易扩展的硬件抽象层 EP(Execution Provider),以应对适配不同硬件的复杂性。 引入硬件抽象层之后,用户无需关注底层硬件和框架的具体实现细节,框架的各个模块无需改动便可以适配新的硬件设备,同时,用户只需按照硬件抽象接口的约定和硬件设备的实际情况,实现一系列接口,便可以完成硬件的适配工作。

EP 还定义了一组基础计算接口 Primitive,基于 Primitive 接口重新实现了 Kernel。 相比 EP 提供的运行时接口,Primitive 提供的接口更加灵活,不同接口之间相互独立,每一个接口表示了某种硬件设备可以提供的特定的计算能力。

ep 模块主要包括两部分。一部分是之前讨论的设备管理,根据用户提供的信息能获取设备实例,将设备抽象出 Stream、Event、内存管理等接口。

 

另一部分就是基础计算接口 Primitive。这里只简要介绍一下 Primitive 的概念,包含哪些内容。不会涉及具体计算的设计和实现。


Primitive 是什么?


粗略地说,基础计算接口是指 P rimitive 目录下定义的二十来个基础计算接口类。它们都是 Primitive 的子类。这些接口类型通常只声明一个 Launch 方法,实际支持哪些计算是由针对具体设备的实现决定的。
各基础计算接口如下表所示:

Primitive 接口类型
设备实现
支持的操作
补充说明
Add
CPU , CUDA , OneDnn
DataType

BatchMatmul
BatchMatmulImpl
是否转置
转发给 BroadcastMatmul
BroadcastElementwiseBinary
CPU , CUDA , OneDnn
BinaryOp
支持 BinaryOp 操作
BroadcastElementwiseUnary
CPU , CUDA
UnaryOp
支持 UnaryOp 操作
BroadcastMatmul
BroadcastMatmulImpl
是否转置
CPU 和 CUDA 实现都是基于模版类
BroadcastMatmulImpl
Cast
CPU , CUDA
DataType

ConstantPad
CPU , CUDA
DataType

CopyNd
CPU , CUDA
DimSize

ElementwiseUnary
CPU , CUDA
UnaryOp
支持 UnaryOp 操作
Fill
CPU , CUDA
DataType

LogSoftmax
Backward
CPU , CUDA , OneDnn
DataType
与 SoftmaxBackward 复用实现
LogSoftmax
CPU , CUDA , OneDnn
DataType
与Softmax 复用实现。
SoftmaxImpl 的基类 SoftmaxBase 可以是 Softmax 或 LogSoftmax
Matmul
MatmulImpl
是否转置
转发给 BatchMatmul
Memcpy
CPU , CUDA
设备拷贝方向
Host2Device、Device2Host ……
Memset
CPU , CUDA


Permute
CPU , CUDA , OneDnn
DimSize

SoftmaxBackward
CPU , CUDA , OneDnn

与 LogSoftmaxBackward 复用实现
Softmax
CPU , CUDA , OneDnn

与 LogSoftmax 复用实现。
TensorFill
CPU , CUDA
DataType


部分计算接口的说明

2.1 ElementwiseUnary

2.1.1 relu kernel 的执行过程

relu kernel 就是通过 ElementwiseUnary 执行计算的。注册 relu kernel 的 SetCreateFn 函数执行类似如下代码的操作。UnaryPrimitiveKernel 构造时会 保存 primitive_factory_func_
  
  
  

auto primitive_factory_func_ = [](user_op::KernelComputeContext* ctx) {   const user_op::TensorDesc* src = ctx->TensorDesc4ArgNameAndIndex("x", 0);   const user_op::TensorDesc* dst = ctx->TensorDesc4ArgNameAndIndex("y", 0);   return ep::primitive::NewPrimitive<ep::primitive::ElementwiseUnaryFactory>(       ctx->device_type(), ep::primitive::UnaryOp::kRelu, src->data_type(),       dst->data_type()); }; OpKernel* ptr = new UnaryPrimitiveKernel("y", "x", primitive_factory_func_);

在调用 UnaryPrimitiveKernel::Compute 执行 kernel 计算时,执行如下操作:

  • 调用 primitive_factory_func_ 获取一个 primitive 实例。
    • NewPrimitive
      • 调用 NewObjUniquePtr 获取 ElementwiseUnaryFactoryImpl 实例( CPU CUDA )。
      • 调用 ElementwiseUnaryFactoryImpl::New 返回 ElementwiseUnaryImpl 实例( CPU , CUDA )。
  • 调用 primitive->Launch 执行计算。

上述类之间的关系如下:


2.1.2 ElementwiseUnary 支持哪些操作?

ElementwiseUnaryFactoryImpl::New 中的宏展开后,代码如下。根据 UnaryOp 的操作类别、数据类型查到 New 函数,传递对应的模版参数给 New 函数并创建 ElementwiseUnaryImpl 实例。

ElementwiseUnary 在 CPU 环境支持的 <操作, 数据类型> 组合都在这个 map 中注册。这个就是“常规”意义上的“Primitive 接口”的一部分(支持哪些操作、数据类型等),操作的输入参数由 Launch 函数的接口决定。

  
  
  
static const std::map<     std::tuple<UnaryOp, DataType, DataType>,     std::function<std::unique_ptr<ElementwiseUnary>(Scalar, Scalar)>>   new_elementwise_unary_handle {     {std::make_tuple((UnaryOp::kRelu), DataType::kFloat, DataType::kFloat), NewElementwiseUnary<(UnaryOp::kRelu), float, float>},     {std::make_tuple((UnaryOp::kRelu), DataType::kDouble, DataType::kDouble), NewElementwiseUnary<(UnaryOp::kRelu), double, double>},     {std::make_tuple((UnaryOp::kElu), DataType::kFloat, DataType::kFloat), NewElementwiseUnary<(UnaryOp::kElu), float, float>},     {std::make_tuple((UnaryOp::kLogicalNot), DataType::kDouble, DataType::kBool), NewElementwiseUnary<(UnaryOp::kLogicalNot), double, bool>},     // ......   }; const auto it =     new_elementwise_unary_handle.find(std::make_tuple(unary_op, src_type, dst_dtype)); if (it != new_elementwise_unary_handle.end()) {   return it->second(attr0, attr1); } else {   return nullptr; }

2.1.3 ElementwiseUnaryImpl::Launch 的实现

Primitive 不同子类的 Launch 方法,其实现方式和输入参数各不一样。ElementwiseUnaryImpl::Launch 通过 primitive::UnaryFunctor 实现计算逻辑( CPU CUDA )。

primitive::UnaryFunctor 是一个 模版类 ,其特化版本分布在如下文件:

  • 各设备通用的 UnaryFunctor 实现 。其中包括 relu 的实现
  • CPU 的 UnaryFunctor 实现 。通过 cpu_stream->ParallelFor 并行加速。
  • CUDA 的 UnaryFunctor 实现 。后续通过 cuda::elementwise::Unary 调用设备计算。


2.2 BroadcastElementwiseBinary

BroadcastElementwiseBinary 也定义了 CUDA 的工厂实现 New 函数 的 map 中定义了 CUDA 下支持的所有操作组合,每个都是一个 NewBroadcastElementwiseBinary 模版函数的特化实例的引用。这些模版函数的特化定义在下面几个文件中: