OneFlow 源码解析:Op、Kernel 与解释器
撰文|郑建华
更新|赵露阳
1 Op 与 Kernel 的注册
继续追踪执行流程会发现,ReluFunctor 在构造 UserOpExpr 时会用到 UserOpRegistryMgr 管理的 Op 与 Kernel。Op 表示算子的描述信息,Kernel 在不同设备上实现计算。
注册信息保存在私有的 map 变量中。UserOpRegistryMgr 的头文件
(hhttps://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/user_op_registry_manager.h)中定义了 3 个宏,REGISTER_USER_OP
、REGISTER_USER_OP_GRAD
、REGISTER_USER_KERNEL
分别用于注册 op、grad_op、kernel。
1.1 ReluOp 的注册
REGISTER_USER_OP 负责 UserOp 的注册。通过检索代码可以找到这个宏的使用场景。ReluOp 相关的源代码在这 3 个文件中:
class 定义:
build/oneflow/core/framework/op_generated.h
注册 op、op 的部分实现:
build/oneflow/core/framework/op_generated.cpp
主要实现:
oneflow/oneflow/user/ops/relu_op.cpp
REGISTER_USER_OP
宏在op_generated.cpp
中展开后代码如下:
调用流程如下:
CheckAndGetOpRegistry
(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/user_op_registry_manager.cpp#L33)会创建一个 OpRegistry(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/user_op_registry.h#L91)对象,这个类和 UserOpRegisterTrigger(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/user_op_registry_manager.h#L63)类一样,只是为构造 OpRegistryResult(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/user_op_registry.h#L62)用的中间类型。
OpRegistry
会暂存中间结果并在Finish
中设置一些默认推导逻辑。UserOpRegisterTrigger
的构造函数会调用注册逻辑。静态变量就是为了触发构造函数从而调用注册逻辑,将构造好的OpRegistryResult
保存到 UserOpRegistryMgr(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/user_op_registry_manager.h#L29)(key 是 op_type,如relu
)。
ReluOp 表示一个具体的 op_type,负责为 OpRegistryResult 提供 Op 特有的方法。
OpRegistryResult 把不同的 Op 抽象为一个通用的结构(便于统一注册管理),主要包含描述信息,保存了 op 的输入输出描述,以及数据类型、sbp 等的推导逻辑函数。对于 relu 来说,主要是记录了几个推导函数要调用 ReluOp 的静态方法;op_def 主要包含 input/output 的名字。
1.2 ReluKernel 的注册
ReluKernel 在 relu_kernel.cpp 中注册,过程和 Op 的注册类似。REGISTER_USER_KERNEL
宏产开后如下所示:
注意 SetCreateFn 只是把一个如下的 lambda 表达式赋值给result_.create_fn
,这个字段很重要,后续执行就是通过它获取 kernel。
对于 relu 来说,NewOpKernel 就是 new 一个 UnaryPrimitiveKernel 对象并返回函数指针。
最终注册的结果,会把 OpKernelRegistryResult 保存到 UserOpRegistryMgr(key 是 op_type_name,如"relu")。
1.3 Op 和 Kernel 注册相关的类关系图
2、UserOpExpr 的构造
上一篇提到,functional_api.yaml.cpp 中的functional::Relu
函数通过find("Relu")
获取预先注册的PackedFunctor<impl::ReluFunctor>
,调用其call
方法会执行impl::ReluFunctor
。
ReluFunctor(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/functional/impl/activation_functor.cpp#L38)的核心代码如下:
ReluFunctor
(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/functional/impl/activation_functor.cpp#L40)的构造函数中,主要是构造 UserOpExpr(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_expr.h#L131)。
每一个user op
通过OpBuilder的Build()
后,都会生成相应的UserOpExpr
,用于存储属性、类型/shape/设备等推导方法,用于接下来 op/kernel 的实际计算。UserOpExpr
包含以下成员:
base_attrs_
tensor_desc_infer_fn_
dtype_infer_fn_
device_and_stream_infer_fn_
它们分别用于存储该 user op 相关 attrs 属性、input/output tensor shape 推导方法、数据类型 data type 推导方法、设备及计算流推导方法等。除了常用的 UserOpExpr、还有一些用于系统 op 的 BuiltinOpExpr。
OpBuilder
的Input/Output
调用主要是操作UserOpConf
的 proto 对象,Build 函数内会修改 UserOpConf 对象,比如根据OpRegistryResult::op_def
补充默认值到 attr。
之后构造UserOpExpr
对象,UserOpConf
对象被保存到UserOpExpr
的父类BuiltinOpExprImpl<UserOpConf>
的op_proto_
字段,对于 relu 来说,op_proto_
主要保存 input, output 等信息。UserOpExpr
初始化时会从 OpRegistryResult 拷贝函数变量。
3 、Functor 的执行
ReluFunctor
执行的核心逻辑是调用OpInterpUtil::Dispatch
。调运顺序如下:
整个链路很长,本篇笔记只以 Eager Local Mode 下,对主要执行流程做一些说明。
3.1 根据环境和输入选择解释器
Dispatch 调用的 GetInterpreter(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_interpreter/op_interpreter_util.cpp#L147)返回的是一个 AutogradInterpreter(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_interpreter.h#L168)对象,这个类是在其内含的OpExprInterpreter
成员变量基础之上增加了 autograd 的功能。GetInterpreter
内实际构造的是以下 3 种Interpreter
,在 Build 函数返回时转为 AutogradInterpreter。
LazyInterpreter: 用于 lazy mode 下的分布式静态图执行模式
EagerLocalInterpreter: 用于 eager local mode 本地单卡执行模式(和 pytorch 单卡或 DDP 对齐)
EagerGlobalInterpreter: 用于 eager global mode,的分布式动态图执行模式
各个 Interpreter 的关系如下:
GetInterpreter
的作用是根据输入和环境等信息,选择一个合适的解释器。
接着在 Dispatch 中调用解释器的AutogradInterpreter::Apply
方法,在这个方法内调用 internal_->Apply(...)(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_interpreter/op_interpreter.cpp#L111),也就是上述 3 个解释器的Apply
方法。
3.2 Apply
通过上面我们知道,EagerLocalInterpreter、EagerGlobalnterpreter 和 LazyInterpreter 都将为其包裹上 AutogradInterpreter 的壳,通过 AutogradInterpreter 触发 Apply 的调用。顾名思义,AutogradInterpreter 的作用主要是和 autograd 相关,其主要为 eager mode 下前向的 op 节点插入对应的,用于反向计算 grad 的节点。
下面以最常用的(Eager Mode)模式,讲解 Apply 的执行方法。在 Eager Mode(无论是 eager local 还是 eager consistent)模式下,实际都会走到 EagerInterpreter 的 Apply(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_interpreter/op_interpreter.cpp#L51)方法:
这里通过宏定义 APPLY_IF,增加了对不同类型 op 的分支处理,将 op_expr dynamic_cast 成相应子类 op 实现的 Expr,如对于大多数用户来说,用到的 op 都是 UserOp 类型,所以这里实际上会走到这个分支中:
再看看 EagerLocalInterpreter::ApplyImpl(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_interpreter/eager_local_op_interpreter.cpp#L209):
其最终实现是 NaiveInterpret(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_interpreter/eager_local_op_interpreter.cpp#L88)。
3.3 NaiveInterpret
NaiveInterpret 简单来说,主要用于做以下四件事:
check input tensor 的 device 是否一致
生成 output tensor
为 output tensor 推导和检查 shape/stride/dtype
构建 op 执行指令,并派发至 vm
简化版的代码如下:
PhysicalRun 接受一个 lambda functor 作为参数,这里即 InstructionsBuilder->Call 方法,该方法接受 kernel、input/output 的 eager blob object、kernel 执行的上下文作为参数。Call 方法实际会完成OpCall
指令的构建,并最终将其派发至 vm 指令列表中,等待 VM 实际调度执行。
参考资料
OneFlow 学习笔记:Op 注册
(https://mp.weixin.qq.com/s/eF-c2irraxnH4iAesURy0Q)
https://github.com/Oneflow-Inc/oneflow/tree/v0.8.1
https://zhuanlan.zhihu.com/p/523884650
(本文经授权后发布,原文https://segmentfault.com/a/1190000041844858)
其他人都在看
欢迎下载体验 OneFlow v0.8.0 最新版本:https://github.com/Oneflow-Inc/oneflow/
版权声明: 本文为 InfoQ 作者【OneFlow】的原创文章。
原文链接:【http://xie.infoq.cn/article/ca5d12fd609da6a55f9c3e208】。文章转载请联系作者。
评论