写点什么

OneFlow 源码解析:Op、Kernel 与解释器

作者:OneFlow
  • 2022 年 8 月 01 日
  • 本文字数:6449 字

    阅读完需:约 21 分钟

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_OPREGISTER_USER_OP_GRADREGISTER_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中展开后代码如下:

static UserOpRegisterTrigger<OpRegistry> g_register_trigger715 =  ::oneflow::user_op::UserOpRegistryMgr::Get()  .CheckAndGetOpRegistry("relu")  .Input("x")  .Output("y")  .SetGetSbpFn(&ReluOp::GetSbp)  .SetLogicalTensorDescInferFn(&ReluOp::InferLogicalTensorDesc)  .SetPhysicalTensorDescInferFn(&ReluOp::InferPhysicalTensorDesc)  .SetDataTypeInferFn(&ReluOp::InferDataType);
复制代码


调用流程如下:


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宏产开后如下所示:

static UserOpRegisterTrigger<OpKernelRegistry> g_register_trigger0 =  UserOpRegistryMgr::Get().    CheckAndGetOpKernelRegistry("relu").    .SetCreateFn(...)    .SetIsMatchedHob(UnaryPrimitiveExists(ep::primitive::UnaryOp::kRelu, "y", "x"))    .SetInplaceProposalFn([](const user_op::InferContext&,                             const user_op::AddInplaceArgPair& AddInplaceArgPairFn) -> Maybe<void> {      OF_RETURN_IF_ERROR(AddInplaceArgPairFn("y", 0, "x", 0, true));      return Maybe<void>::Ok();    });
复制代码


注意 SetCreateFn 只是把一个如下的 lambda 表达式赋值给result_.create_fn,这个字段很重要,后续执行就是通过它获取 kernel。


[]() {    return user_op::NewOpKernel<UnaryPrimitiveKernel>(        "y", "x", [](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());        });}
复制代码


对于 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)的核心代码如下:


class ReluFunctor { public:  ReluFunctor() { op_ = CHECK_JUST(one::OpBuilder("relu").Input("x", 1).Output("y", 1).Build()); }  Maybe<Tensor> operator()(const std::shared_ptr<Tensor>& x, bool inplace) const {    // 忽略inplace相关逻辑    return OpInterpUtil::Dispatch<Tensor>(*op_, {x});  } private:  std::shared_ptr<OpExpr> op_;};
复制代码


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。

 

OpBuilderInput/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


通过上面我们知道,EagerLocalInterpreterEagerGlobalnterpreter 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)方法:


Maybe<void> EagerInterpreter::Apply(const OpExpr& op_expr, const TensorTuple& inputs,                                    TensorTuple* outputs, const OpExprInterpContext& ctx) const {#define APPLY_IF(op_type)                                              \  if (const auto* op = dynamic_cast<const op_type##Expr*>(&op_expr)) { \    return ApplyImpl(*op, inputs, outputs, ctx);                       \  }  APPLY_IF(UserOp);  APPLY_IF(VariableOp);  APPLY_IF(CastToLocalOp);  APPLY_IF(CastFromLocalOp);  APPLY_IF(GlobalToGlobalOp);  APPLY_IF(CastToGlobalOp);  APPLY_IF(CastFromGlobalOp);  APPLY_IF(DistributeSplitOp);  APPLY_IF(DistributeCloneOp);  APPLY_IF(DistributeConcatOp);  APPLY_IF(DistributeAddOp);  APPLY_IF(FunctionOp);  APPLY_IF(SelectTopNOp)#undef APPLY_IF  OF_UNIMPLEMENTED() << "The type " << op_expr.op_type_name()                     << " has not been supported in EagerInterpreter::Apply.";}
复制代码


这里通过宏定义 APPLY_IF,增加了对不同类型 op 的分支处理,将 op_expr dynamic_cast 成相应子类 op 实现的 Expr,如对于大多数用户来说,用到的 op 都是 UserOp 类型,所以这里实际上会走到这个分支中:


if (const auto* op = dynamic_cast<const UserOpExpr*>(&op_expr)) {    return ApplyImpl(*op, inputs, outputs, ctx);}
复制代码


再看看 EagerLocalInterpreter::ApplyImpl(https://github.com/Oneflow-Inc/oneflow/blob/v0.8.1/oneflow/core/framework/op_interpreter/eager_local_op_interpreter.cpp#L209):


Maybe<void> EagerLocalInterpreter::ApplyImpl(const UserOpExpr& op_expr, const TensorTuple& inputs,                                             TensorTuple* outputs,                                             const OpExprInterpContext& ctx) const {  return NaiveInterpret(op_expr, inputs, outputs, ctx);}
复制代码


其最终实现是 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


简化版的代码如下:


Maybe<void> NaiveInterpret(const UserOpExpr& user_op_expr, const TensorTuple& inputs,                           const Symbol<Device>& default_device, TensorTuple* outputs,                           const OpExprInterpContext& ctx) {  const auto& attrs = ctx.attrs;  // 检查input tensor是否位于相同device上  ...  // 推导outout tensor的设备类型  // Infer devices  if (!user_op_expr.has_device_and_stream_infer_fn()) {    stream = JUST(GetDefaultStreamByDevice(default_device));    for (int i = 0; i < outputs->size(); i++) {      auto* tensor_impl = JUST(TensorImpl4Tensor(outputs->at(i)));      *JUST(tensor_impl->mut_device()) = default_device;    }  } else {    need_check_mem_case = false;    stream = JUST(user_op_expr.InferDeviceAndStream(attrs, inputs, outputs));  }  // 推导outout tensor的形状、数据类型  // Infer shapes and dtypes  const auto& device_tag = stream->device()->type();  JUST(user_op_expr.InferPhysicalTensorDesc(      attrs, device_tag,      [&](int32_t i) -> const TensorMeta* {        return CHECK_JUST(TensorImpl4Tensor(inputs[i]))->mut_tensor_meta();      },      [&](int32_t i) -> TensorMeta* {        // using thread_local TensorMeta pointer if inplace.        // using tensor_impl TensorMeta pointer if not inplace.        return output_tensor_metas->at(i);      }));  // 为output tensor初始化eager_blob_object  for (int i = 0; i < output_eager_blob_objects->size(); i++) {    auto* tensor_impl = JUST(TensorImpl4Tensor(outputs->at(i)));    if (!output_eager_blob_objects->at(i)) {      if (!JUST(user_op_expr.SupportNonContiguous())) {        std::shared_ptr<Stride> stride(new Stride(*tensor_impl->shape()));        tensor_impl->mut_tensor_meta()->set_stride(stride);      }      const auto& dep_object = NewLocalDepObject();      JUST(tensor_impl->InitEagerBlobObject(dep_object));      output_eager_blob_objects->at(i) = JUST(tensor_impl->eager_blob_object());    } else {      // output i is inplaced.      // check thread_local TensorMeta and tensor_impl TensorMeta.      CHECK_OR_RETURN(tensor_impl->tensor_meta()->shape() == output_tensor_metas->at(i)->shape());      CHECK_OR_RETURN(tensor_impl->tensor_meta()->dtype() == output_tensor_metas->at(i)->dtype());    }  }  // 从user_op_expr中取出kernel  const auto& kernel = JUST(user_op_expr.MutKernel4Stream(stream));  kernel->set_need_check_mem_case(need_check_mem_case);  for (int64_t index : kernel->output_tuple_indexes4mut2_obns()) {    output_eager_blob_objects->at(index)->set_is_shape_synced(false);  }  // kernel dispatch至VM,等待后续实际的调度执行  JUST(PhysicalRun([&](InstructionsBuilder* builder) -> Maybe<void> {    return builder->Call(kernel, input_eager_blob_objects, output_eager_blob_objects, ctx, stream);  }));  return Maybe<void>::Ok();}
复制代码

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

  • 从Functor到OpExprInterpreter

  • 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/

发布于: 刚刚阅读数: 2
用户头像

OneFlow

关注

不至于成为世界上最快的深度学习框架。 2022.03.23 加入

★ OneFlow深度学习框架:github.com/Oneflow-Inc/oneflow ★ OF云平台:oneflow.cloud

评论

发布
暂无评论
OneFlow源码解析:Op、Kernel与解释器_深度学习_OneFlow_InfoQ写作社区