• 一个算子在深度学习框架中的旅程


    8c5be85f71ff46c25454e8d9f085425d.png

    撰文|赵露阳

    算子即Operator,这里简称op。op是深度学习的基础操作,任意深度学习框架中都包含了数百个op,这些op用于各种类型的数值、tensor运算。

    深度学习中,通过nn.Module这样搭积木的方式搭建网络,而op就是更基础的,用于制作积木的配方和原材料。

    譬如如下的一个demo网络:

    1. import oneflow as torch
    2. class TinyModel(torch.nn.Module):
    3. def __init__(self):
    4. super(TinyModel, self).__init__()
    5. self.linear1 = torch.nn.Linear(100, 200)
    6. self.activation = torch.nn.ReLU()
    7. self.linear2 = torch.nn.Linear(200, 10)
    8. self.softmax = torch.nn.Softmax()
    9. def forward(self, x):
    10. x = self.linear1(x)
    11. x = self.activation(x)
    12. x = self.linear2(x)
    13. x = self.softmax(x)
    14. return xtinymodel = TinyModel()print('The model:')print(tinymodel)

    从结构来看,这个网络是由各种nn.Module如Linear、ReLU、Softmax搭建而成,但从本质上,这些nn.Module则是由一个个基础op拼接,从而完成功能的。这其中就包含了Matmul、Relu、Softmax等op。 在OneFlow中,对于一个已有op,是如何完成从Python层->C++层的调用、流转和执行过程?本文将以

     
     
    output = flow.relu(input)
    为例,梳理一个op从Python -> C++执行的完整过程。

    首先,这里给出一个流程示意图:

    f8d41740f153920f595b99c27a7f1189.png

    下面,将分别详细从源码角度跟踪其各个环节。

    1

    Binding

    这里,binding是指Python和C++代码的绑定。通常,我们用Python搭建网络,训练模型,调用函数完成各种操作。实际上,这些函数通常在Python层只是一层wrapper,底层实现还是通过C++代码完成的,那么Python -> C++是如何调用的?这就需要用到Python和C++的绑定。

    在深度学习框架的实现中,即可以用Python原生的C API,也可以通过pybind11来完成函数绑定,在OneFlow中,二者均有使用,譬如:

    • oneflow/api/python/framework/tensor.cpp

    • oneflow/api/python/framework/tensor_functions.cpp

    中涉及到的 tensor.xxx 方法都是通过Python C API完成了函数绑定;

    • oneflow/core/functional/functional_api.yaml

    中定义的诸多 flow.xxx 方法则是通过pybind实现的绑定。这里关于Python C API和pybind不做过多介绍,具体用法可以参考相应文档:

    • https://docs.python.org/zh-cn/3.8/c-api/index.html

    • https://pybind11.readthedocs.io/en/stable/index.html

    下面我们回到flow.relu方法,我们在Python层调用的flow.relu实际是调用了在

     
     
    python/oneflow/__init__.py

    中定义的oneflow._C.relu。 _C表示其实现位于底层C++。和PyTorch类似,我们也基于.yaml定义了一套接口导出及code gen的规则,譬如在 functional_api.yaml 中,我们可以看到Relu的导出接口的函数签名:

    1. - name: "relu"
    2. signature: "Tensor (Tensor x, Bool inplace=False) => Relu"
    3. bind_python: True

    从yaml定义可以看出,flow._C.relu 接收两个参数,tensor和一个bool值,其绑定了C++的Relu方法,函数返回值也是tensor。实际上,在OneFlow编译时,会通过执行

     
     
    tools/functional/generate_functional_api.py

    这个文件,对 functional_api.yaml 进行解析和代码生成,动态生成C++的.h和.cpp文件。

    • build/oneflow/core/functional/functional_api.yaml.h

    • build/oneflow/core/functional/functional_api.yaml.cpp

    并在.cpp文件中调用相应的functor完成C++层面的函数调用。这里,还是以flow._C.relu为例,其对应的functor定义位于oneflow/core/functional/impl/activation_functor.cpp:

    1. class ReluFunctor {
    2. public:
    3. ReluFunctor() { op_ = CHECK_JUST(one::OpBuilder("relu").Input("x", 1).Output("y", 1).Build()); }
    4. Maybe<Tensor> operator()(const std::shared_ptr<Tensor>& x, bool inplace) const {
    5. ...
    6. }
    7. private:
    8. std::shared_ptr<OpExpr> op_;
    9. };

    ReluFunctor通过

    1. ONEFLOW_FUNCTION_LIBRARY(m) {
    2. m.add_functor<impl::ReluFunctor>("Relu");
    3. ...
    4. }

    完成functor的注册,注册成functional接口后,在Python层flow._C.relu就完成了和“Relu”的绑定。同时,这个函数在C++中也可以通过functional::Relu直接调用。

    2

    Functor

    Functor不仅是Python -> C++交互的核心,也是op调用、输入参数推导和检查的第一站。通常,各种op在functor层需要完成对输入tensor的shape、dtype、维度、元素个数等各种check,以及对op特有的逻辑进行解析和处理。Relu Functor代码如下:

    1. class ReluFunctor {
    2. public:
    3. ReluFunctor() { op_ = CHECK_JUST(one::OpBuilder("relu").Input("x", 1).Output("y", 1).Build()); }
    4. Maybe<Tensor> operator()(const std::shared_ptr<Tensor>& x, bool inplace) const {
    5. if (inplace) {
    6. JUST(CheckInplaceValid(x));
    7. std::shared_ptr<TensorTuple> outputs = std::make_shared<TensorTuple>(1);
    8. outputs->at(0) = x;
    9. JUST(OpInterpUtil::Dispatch(*op_, {x}, outputs.get(), AttrMap{}));
    10. return outputs->at(0);
    11. } else {
    12. return OpInterpUtil::Dispatch<Tensor>(*op_, {x});
    13. }
    14. }
    15. private:
    16. std::shared_ptr<OpExpr> op_;
    17. };

    可以看见,ReluFunctor是比较简单的,其定义了一个私有变量

     
     
    std::shared_ptr<OpExpr> op_;

    这个op_即需要执行的Relu op,通过OpBuilder进行构建;functor的operator()内部,根据是否inplace走到2个不同分支,并最终通过OpInterpUtil::Dispatch()将op、输入tensor和参数派发至Interpreter处理。

    3

    Dispatch

    各种op在functor中完成check和逻辑处理后,大多需要通过OpInterpUtil::Dispatch() 进行派发,其目的地是Interpreter。在Interpreter中,将会对op进行更进一步的处理。在oneflow/core/framework/op_interpreter/op_interpreter_util.h 中,我们可以看见多种重载的Dispatch模板代码:

    1. class OpInterpUtil {
    2. public:
    3. template<typename T>
    4. static Maybe<T> Dispatch(const OpExpr& op_expr, const TensorTuple& inputs, const AttrMap& attrs) {
    5. return Dispatch<T>(op_expr, inputs, OpExprInterpContext(attrs));
    6. }
    7. template<typename T>
    8. static Maybe<T> Dispatch(const OpExpr& op_expr, const TensorTuple& inputs) {
    9. return Dispatch<T>(op_expr, inputs, OpExprInterpContext(AttrMap{}));
    10. }
    11. template<typename T>
    12. static Maybe<T> Dispatch(const OpExpr& op_expr, const TensorTuple& inputs,
    13. const OpExprInterpContext& ctx);
    14. static Maybe<void> Dispatch(const OpExpr& op_expr, const TensorTuple& inputs,
    15. TensorTuple* outputs, const AttrMap& attrs) {
    16. return Dispatch(op_expr, inputs, outputs, OpExprInterpContext(attrs));
    17. }
    18. static Maybe<void> Dispatch(const OpExpr& op_expr, const TensorTuple& inputs,
    19. TensorTuple* outputs) {
    20. return Dispatch(op_expr, inputs, outputs, OpExprInterpContext(AttrMap{}));
    21. }
    22. static Maybe<void> Dispatch(const OpExpr& op_expr, const TensorTuple& inputs,
    23. TensorTuple* outputs, const OpExprInterpContext& ctx);

    这些重载,是为了应对不同的输入、输出以及OpExprInterpContext的情况。譬如这个OpExprInterpContext是op在Interpreter中所需的上下文,可能携带op计算所需要的属性(如conv2d op所需要的kernel_size、padding等)、device、sbp、parallel等描述信息。这些重载的Dispatch最终都会走到:

    1. /* static */ Maybe<void> OpInterpUtil::Dispatch(
    2. const OpExpr& op_expr,
    3. const TensorTuple& inputs,
    4. TensorTuple* outputs,
    5. const OpExprInterpContext& ctx) {
    6. return JUST(GetInterpreter(inputs, ctx, op_expr))->Apply(op_expr, inputs, outputs, ctx);
    7. }

    Dispatch至此,剩下的就要交给Interpreter了。

    4

    Interpreter

    Get Interpreter

    这里先看看GetInterpreter,这里其实就是获取所需的Interpreter,来负责op接下来的执行。省略check相关的逻辑,主要代码如下:oneflow/core/framework/op_interpreter/op_interpreter_util.cpp

    1. Maybe<AutogradInterpreter> GetInterpreter(const TensorTuple& inputs, const OpExprInterpContext& ctx,
    2. const OpExpr& op_expr) {
    3. static const auto& g_lazy_interpreter = BuildLazyInterpreter();
    4. static const auto& g_eager_consistent_interpreter = BuildEagerInterpreter(/*is_mirrored=*/false);
    5. static const auto& g_eager_mirrored_interpreter = BuildEagerInterpreter(/*is_mirrored=*/true);
    6. if (!LazyMode::is_enabled()) {
    7. if (inputs.empty()) {
    8. if (ctx.parallel_desc.has_value()) {
    9. JUST(ctx.nd_sbp);
    10. CHECK_OR_RETURN(!ctx.device.has_value());
    11. return g_eager_consistent_interpreter;
    12. } else {
    13. CHECK_OR_RETURN(!ctx.nd_sbp.has_value());
    14. return g_eager_mirrored_interpreter;
    15. }
    16. } else {
    17. if (inputs.at(0)->is_consistent()) {
    18. ...
    19. return g_eager_consistent_interpreter;
    20. } else {
    21. ...
    22. return g_eager_mirrored_interpreter;
    23. }
    24. }
    25. UNIMPLEMENTED_THEN_RETURN();
    26. }
    27. return g_lazy_interpreter;
    28. }

    通过上面的逻辑可以看出,Interpreter大体上分为Eager Interpteter和Lazy Interpreter;其中Eager Interpteter又根据Eager Mirrored和Eager Consistent有所区别。具体就是以下3种子类实现:

    • EagerMirroredInterpreter

    • EagerConsistentInterpreter

    • LazyInterpreter

    普通的Eager mode下(无论是单卡还是DDP的情况)都会走到 EagerMirroredInterpreter 的逻辑;在普通Eager Mode之外,为输入tensor设置了sbp、placement则会进入到EagerConsistentInterpreter的逻辑;在Lazy Mode时(使用nn.Graph),则会进入到LazyInterpreter

    下面,我们看下这3种Interpreter的构建:

    1. std::shared_ptr<AutogradInterpreter> BuildEagerInterpreter(const bool& is_mirrored) {
    2. std::shared_ptr<OpExprInterpreter> internal;
    3. if (is_mirrored) {
    4. internal = std::make_shared<EagerMirroredInterpreter>();
    5. } else {
    6. internal = std::make_shared<EagerConsistentInterpreter>();
    7. }
    8. return std::make_shared<AutogradInterpreter>(internal);
    9. }
    10. std::shared_ptr<AutogradInterpreter> BuildLazyInterpreter() {
    11. auto internal = std::make_shared<LazyInterpreter>();
    12. return std::make_shared<AutogradInterpreter>(internal);
    13. }

    可见,这3种Interpreter构建完成后,都会以私有变量internal的形式,参与AutogradInterpreter的构建,并最终返回AutogradInterpreter

    1. class AutogradInterpreter {
    2. public:
    3. AutogradInterpreter() = delete;
    4. AutogradInterpreter(const std::shared_ptr<OpExprInterpreter>& internal) : internal_(internal) {}
    5. virtual ~AutogradInterpreter() = default;
    6. Maybe<void> Apply(const OpExpr& op_expr, const TensorTuple& inputs, TensorTuple* outputs,
    7. const AttrMap& attrs) const {
    8. return Apply(op_expr, inputs, outputs, OpExprInterpContext(attrs));
    9. }
    10. Maybe<void> Apply(const OpExpr& op_expr, const TensorTuple& inputs, TensorTuple* outputs) const {
    11. return Apply(op_expr, inputs, outputs, OpExprInterpContext(AttrMap{}));
    12. }
    13. Maybe<void> Apply(const OpExpr& op_expr, const TensorTuple& inputs, TensorTuple* outputs,
    14. const OpExprInterpContext& ctx) const;
    15. private:
    16. std::shared_ptr<OpExprInterpreter> internal_;
    17. };

    Apply()

    通过上面我们知道,EagerMirroredInterpreterEagerConsistentInterpreterLazyInterpreter都将为其包裹上AutogradInterpreter的壳,通过AutogradInterpreter触发Apply的调用。顾名思义,AutogradInterpreter的作用主要是和autograd相关,其主要为eager mode下前向的op节点插入对应的用于反向计算grad的节点。

    我们看看这部分代码,关键部分的作用在注释里给出:

    1. Maybe<void> AutogradInterpreter::Apply(const OpExpr& op_expr, const TensorTuple& inputs,
    2. TensorTuple* outputs, const OpExprInterpContext& ctx) const {
    3. // 判断是否需要计算梯度,如果处于GradMode的作用域切改op注册时没有禁用梯度
    4. // 则requires_grad的值根据输入tensor的requires_grad属性判断
    5. // any of input tensors requires_grad==True,则表示需要计算梯度
    6. bool requires_grad = false;
    7. if (autograd::GradMode::is_enabled() && !JUST(op_expr.IsGradDisabled())) {
    8. requires_grad =
    9. std::any_of(inputs.begin(), inputs.end(),
    10. [](const std::shared_ptr<Tensor>& tensor) { return tensor->requires_grad(); });
    11. }
    12. // 这一坨逻辑比较丑陋,是因为近期支持了oneflow系统中支持了stride&&view机制
    13. // 而大部分op尚未注册stride推导、尚未支持non-contiguous的输入tensor
    14. // 所以需要在这对这部分op的输入进行强制转换,将其变为contiguous的
    15. // NOTE: if this op not support stride, then need to tensor->contiguous()
    16. #define HANDLE_NON_CONTIGUOUS_INPUT(tensor_tuple_ptr) \
    17. TensorTuple tmp_inputs; \
    18. if (!LazyMode::is_enabled() && !JUST(op_expr.SupportNonContiguous())) { \
    19. tmp_inputs.resize(inputs.size()); \
    20. for (size_t i = 0; i < inputs.size(); i++) { tmp_inputs[i] = inputs[i]->contiguous(); } \
    21. tensor_tuple_ptr = &tmp_inputs; \
    22. }
    23. const TensorTuple* inputs_ptr = &inputs;
    24. HANDLE_NON_CONTIGUOUS_INPUT(inputs_ptr);
    25. // 这里是进行实际Interpreter执行的主要过程
    26. {
    27. autograd::AutoGradMode mode(false);
    28. JUST(internal_->Apply(op_expr, *inputs_ptr, outputs, ctx));
    29. }
    30. // 这里主要是为了eager mode下,且requires_grad==True的op,
    31. // 插入反向节点(AddNode)用于autograd,该节点包含反向梯度计算的方法(backward_fn)
    32. // Lazy mode will construct backward compute graph in passes, so disable autograd if lazy mode.
    33. std::shared_ptr<OpExprGradClosure> grad_closure(nullptr);
    34. if (requires_grad && !LazyMode::is_enabled()) {
    35. grad_closure = JUST(op_expr.GetOrCreateOpGradClosure());
    36. auto backward_fn = std::make_shared<BackwardFunction>();
    37. backward_fn->body = [=](const TensorTuple& out_grads, TensorTuple* in_grads,
    38. bool create_graph) -> Maybe<void> {
    39. autograd::AutoGradMode mode(create_graph);
    40. JUST(grad_closure->Apply(out_grads, in_grads));
    41. return Maybe<void>::Ok();
    42. };
    43. backward_fn->status = [=]() { return grad_closure->state()->SavedTensors().size() > 0; };
    44. JUST(GetThreadLocalAutogradEngine()->AddNode(op_expr.op_type_name() + "_backward", backward_fn,
    45. *inputs_ptr, outputs));
    46. }
    47. // Update outputs autograd meta
    48. // Note: if requires_grad is True, we will create a new autograd meta for each output
    49. // in `AddBackwardFuncPtr` to support inplace operation, so the update should after
    50. // `AddBackwardFuncPtr`
    51. for (auto& output : *outputs) {
    52. output->set_is_leaf(inputs_ptr->size() == 0 || !requires_grad);
    53. ...
    54. if (!output->requires_grad()) {
    55. JUST(output->set_requires_grad(
    56. requires_grad && IsSupportRequireGradDataType(output->dtype()->data_type())));
    57. }
    58. }
    59. // 捕获前向的inputs outputs,反向计算时可能用到
    60. if (requires_grad && !LazyMode::is_enabled()) {
    61. // Capture inputs and outputs after `AddBackwardFuncPtr` because of that grad function
    62. // node has been attached to them.
    63. JUST(grad_closure->Capture(*inputs_ptr, *outputs, ctx));
    64. }
    65. return Maybe<void>::Ok();
    66. }

    上面一坨逻辑有点多,让我们看一下重点,对于简单的Relu op,我们只需关注这部分代码:

    1. // 这里是进行实际Interpreter执行的主要过程
    2. {
    3. autograd::AutoGradMode mode(false);
    4. JUST(internal_->Apply(op_expr, *inputs_ptr, outputs, ctx));
    5. }

    这里,还是以上面的flow.relu为例,由于是简单的Eager Mode,所以实际会走到EagerInterpreter的Apply方法:

    1. Maybe<void> EagerInterpreter::Apply(const OpExpr& op_expr, const TensorTuple& inputs,
    2. TensorTuple* outputs, const OpExprInterpContext& ctx) const {
    3. #define APPLY_IF(op_type) \
    4. if (const auto* op = dynamic_cast<const op_type##Expr*>(&op_expr)) { \
    5. return ApplyImpl(*op, inputs, outputs, ctx); \
    6. }
    7. APPLY_IF(UserOp);
    8. APPLY_IF(VariableOp);
    9. APPLY_IF(CastToMirroredOp);
    10. APPLY_IF(CastFromMirroredOp);
    11. APPLY_IF(ConsistentToConsistentOp);
    12. APPLY_IF(CastToConsistentOp);
    13. APPLY_IF(CastFromConsistentOp);
    14. APPLY_IF(DistributeSplitOp);
    15. APPLY_IF(DistributeCloneOp);
    16. APPLY_IF(DistributeConcatOp);
    17. APPLY_IF(DistributeAddOp);
    18. APPLY_IF(FunctionOp);
    19. APPLY_IF(SelectTopNOp)
    20. #undef APPLY_IF
    21. OF_UNIMPLEMENTED() << "The type " << op_expr.op_type_name()
    22. << " has not been supported in EagerInterpreter::Apply.";
    23. }

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

    1. if (const auto* op = dynamic_cast<const UserOpExpr*>(&op_expr)) {
    2. return ApplyImpl(*op, inputs, outputs, ctx);
    3. }

    再看看EagerMirroredInterpreter::ApplyImpl,位于

    oneflow/core/framework/op_interpreter/eager_mirrored_op_interpreter.cpp

    1. Maybe<void> EagerMirroredInterpreter::ApplyImpl(const UserOpExpr& op_expr,
    2. const TensorTuple& inputs, TensorTuple* outputs,
    3. const OpExprInterpContext& ctx) const {
    4. return NaiveInterpret(op_expr, inputs, outputs, ctx);
    5. }

    其最终实现是NaiveInterpret。

    NaiveInterpret

    NaiveInterpret简单来说,主要用于做以下几件事:

    • check input tensor的device是否一致

    • 生成output tensor

    • 为output tensor推导和检查shape/stride/dtype

    • 构建op执行指令,并派发至vm

    简化版的代码如下:

    1. Maybe<void> NaiveInterpret(const UserOpExpr& user_op_expr, const TensorTuple& inputs,
    2. const Symbol<Device>& default_device, TensorTuple* outputs,
    3. const OpExprInterpContext& ctx) {
    4. const auto& attrs = ctx.attrs;
    5. std::shared_ptr<EagerBlobObjectList> input_eager_blob_objects =
    6. std::make_shared<EagerBlobObjectList>(inputs.size());
    7. // check devices
    8. for (int i = 0; i < inputs.size(); i++) {
    9. const auto& input_device = JUST(inputs.at(i)->device());
    10. if (i > 0) {
    11. CHECK_OR_RETURN(*default_device == *input_device)
    12. << Error::RuntimeError()
    13. << "Expected all tensors to be on the same device, but found at least two devices, "
    14. << default_device->ToString() << " (positional 0) and " << input_device->ToString()
    15. << " (positional " << i << ")!";
    16. }
    17. input_eager_blob_objects->at(i) = JUST(inputs.at(i)->eager_blob_object());
    18. }
    19. // make output tensors
    20. std::shared_ptr<EagerBlobObjectList> output_eager_blob_objects =
    21. std::make_shared<EagerBlobObjectList>(outputs->size());
    22. auto* output_tensor_metas = ThreadLocalDefaultOutputMutTensorMetas(outputs->size());
    23. for (int i = 0; i < outputs->size(); i++) {
    24. if (!outputs->at(i)) {
    25. const auto& tensor_impl = std::make_shared<EagerMirroredTensorImpl>();
    26. outputs->at(i) = std::make_shared<MirroredTensor>(tensor_impl);
    27. output_tensor_metas->at(i) = tensor_impl->mut_tensor_meta();
    28. } else {
    29. bool has_eager_blob_object = JUST(outputs->at(i)->has_eager_blob_object());
    30. CHECK_OR_RETURN(has_eager_blob_object);
    31. output_eager_blob_objects->at(i) = JUST(outputs->at(i)->eager_blob_object());
    32. }
    33. }
    34. Symbol<Stream> stream;
    35. bool need_check_mem_case = true;
    36. // Infer devices
    37. ...
    38. // Infer shapes strides dtype
    39. ...
    40. // 构建op执行指令,并派发至vm
    41. JUST(PhysicalRun([&](InstructionsBuilder* builder) -> Maybe<void> {
    42. return builder->LocalCallOpKernel(kernel, input_eager_blob_objects, output_eager_blob_objects,
    43. ctx, stream);
    44. }));
    45. return Maybe<void>::Ok();
    46. }

    Interpreter的终点是虚拟机(vm)。vm部分,是OneFlow比较独特的设计,内容很多,这里暂不展开了:) 可以简单理解,派发至vm后,此op将进入一个任务执行的队列,将会等待其vm的调度、执行。

    5

    Compute

    在Interpreter将op执行指令派发至vm后,经过调度逻辑处理后,将会在

     
     
    oneflow/core/eager/opkernel_instruction_type.cpp

    被触发执行,核心代码如下:

    1. static inline void OpKernelCompute(
    2. LocalCallOpKernelPhyInstrOperand* operand,
    3. DeviceCtx* device_ctx, user_op::OpKernelState* state,
    4. const user_op::OpKernelCache* cache) {
    5. auto* opkernel = operand->mut_opkernel();
    6. auto* compute_ctx =
    7. opkernel->UpdateComputeContext(operand->inputs().get(), operand->outputs().get(),
    8. operand->consistent_tensor_infer_result().get(), device_ctx);
    9. ...
    10. operand->user_opkernel()->Compute(compute_ctx, state, cache);
    11. opkernel->UpdateComputeContext(nullptr, nullptr, nullptr, nullptr);
    12. }

    其中,

     
     
    operand->user_opkernel()->Compute(compute_ctx, state, cache);

    将触发op kernel的实际执行。通常来说,op的kernel实现根据device的不同,会派发到不同的实现,其一般都位于:

     
     
    oneflow/user/kernels/xxx_kernel.cpp

     
     
    oneflow/user/kernels/xxx_kernel.cu

    这里的Relu op相对比较特殊,是用primitive实现的(primitive也是oneflow中一种独特的设计,有着良好的抽象和可组合性),具体这个UnaryPrimitive就是elementwise unary的模板+UnaryFunctor的组合。其调用链如下:

    c7f34126021fd211c0bfbd7b90f914c8.png

    UnaryPrimitiveKernel

    1. class UnaryPrimitiveKernel final : public user_op::OpKernel, public user_op::CudaGraphSupport {
    2. public:
    3. OF_DISALLOW_COPY_AND_MOVE(UnaryPrimitiveKernel);
    4. UnaryPrimitiveKernel() = default;
    5. ~UnaryPrimitiveKernel() = default;
    6. using PrimitiveFactoryFuncType = std::function<std::unique_ptr<ep::primitive::ElementwiseUnary>(
    7. user_op::KernelComputeContext*)>;
    8. UnaryPrimitiveKernel(const std::string& output_name, const std::string& input_name,
    9. PrimitiveFactoryFuncType fn)
    10. : output_name_(output_name),
    11. input_name_(input_name),
    12. primitive_factory_func_(std::move(fn)) {}
    13. private:
    14. using user_op::OpKernel::Compute;
    15. void Compute(user_op::KernelComputeContext* ctx) const override {
    16. auto primitive = primitive_factory_func_(ctx);
    17. CHECK(primitive);
    18. const user_op::Tensor* input_tensor = ctx->Tensor4ArgNameAndIndex(input_name_, 0);
    19. ...
    20. const int64_t elem_cnt = input_shape.elem_cnt();
    21. if (elem_cnt != 0) {
    22. primitive->Launch(ctx->stream(), input_tensor->dptr(), output_tensor->mut_dptr(), elem_cnt);
    23. }
    24. }
    25. bool AlwaysComputeWhenAllOutputsEmpty() const override { return false; }
    26. std::string output_name_;
    27. std::string input_name_;
    28. PrimitiveFactoryFuncType primitive_factory_func_;
    29. };

    ep::primitive::ElementwiseUnary

    1. template<UnaryOp unary_op, typename Src, typename Dst>
    2. class ElementwiseUnaryImpl : public ElementwiseUnary {
    3. public:
    4. OF_DISALLOW_COPY_AND_MOVE(ElementwiseUnaryImpl);
    5. ElementwiseUnaryImpl(Scalar attr0, Scalar attr1) : attr0(attr0), attr1(attr1) {}
    6. ~ElementwiseUnaryImpl() override = default;
    7. void Launch(Stream* stream, const void* src_ptr, void* dst_ptr, size_t count) override {
    8. CpuStream* cpu_stream = stream->As<CpuStream>();
    9. Dst* dst = reinterpret_cast<Dst*>(dst_ptr);
    10. const Src* src = reinterpret_cast<const Src*>(src_ptr);
    11. auto functor = UnaryFunctor<DeviceType::kCPU, unary_op, Dst, Src>(attr0, attr1);
    12. cpu_stream->ParallelFor(0, count, [functor, src, dst](int64_t begin, int64_t end) {
    13. for (int64_t i = begin; i < end; i++) { dst[i] = functor(src[i]); }
    14. });
    15. }
    16. protected:
    17. Scalar attr0, attr1;
    18. };

    UnaryFunctor

    这个UnaryFuntor根据不同的Unaray op类型,特化出不同的具体functor实现,具体到Relu op,其实现位于

    oneflow/core/ep/common/primitive/unary_functor.h:

    1. template<DeviceType device, typename Dst, typename Src>
    2. struct UnaryFunctor<device, UnaryOp::kRelu, Dst, Src> {
    3. UnaryFunctor(Scalar attr0, Scalar attr1) {}
    4. OF_DEVICE_FUNC Dst operator()(Src src) const {
    5. const Src zero_val = static_cast<Src>(0.0);
    6. if (src <= zero_val) {
    7. return static_cast<Dst>(zero_val);
    8. } else {
    9. return static_cast<Dst>(src);
    10. }
    11. }
    12. };

    至此,我们已经完成了一个op的Python -> C++ 之旅。从细节上看,是相对复杂的,但从整体流程上看,其实是比较简单的,排除了binding,vm调度机制等细节,其主要过程其实就4个环节: Functor -> Dispatch -> Interpreter -> Kernel Compute。

    实现/新增一个op,通常也不需要管中间的Dispatch以及Interpreter,我们只需重点关注和该op强相关的部分——Functor层面的参数、op逻辑检查,以及Kernel Compute部分的实际op运算。

    (参考代码:

    https://github.com/Oneflow-Inc/oneflow/commit/1dbdf8faed988fa7fd1a9034a4d79d5caf18512d)

    其他人都在看

    欢迎下载体验OneFlow v0.7.0:GitHub - Oneflow-Inc/oneflow: OneFlow is a performance-centered and open-source deep learning framework.OneFlow is a performance-centered and open-source deep learning framework. - GitHub - Oneflow-Inc/oneflow: OneFlow is a performance-centered and open-source deep learning framework.https://github.com/Oneflow-Inc/oneflow/

  • 相关阅读:
    基于区块链技术的域名系统设计与实现
    Gumroad如何使用美国虚拟visa卡购买图集教程?gumroad国内银行卡能付款吗?gumroad付款教程?
    【Front Plant Sci】BrMYB2调控大白菜和拟南芥发育过程中花青素生物的合成机制
    自动化立体仓库AS/RS货架|分离式仓库货架与整体式仓库货架如何运用?
    LCR 078. 合并 K 个升序链表
    含文档+PPT+源码等]精品微信小程序ssm驾校教培服务系统小程序+后台管理系统|前后分离VUE[包运行成功]微信小程序项目源码Java毕业设计
    layui实现学生数据的展示(查找)和分页
    21【JDBC操作数据库元数据】
    使用Java和NLP技术实现AI伪原创文章自动生成:一个详细的编程指南
    高精度与高精度的乘法---基础算法
  • 原文地址:https://blog.csdn.net/OneFlow_Official/article/details/125289349