• 一个算子在 MindSpore 框架中的执行流程


    一个算子在 MindSpore 框架中的执行流程

    本文分析了一个算子在 MindSpore 框架中的执行流程。MindSpore 中设计了 Primitive 对算子进行了封装和抽象,一般来说封装和抽象是出于差异,这种差异来自于底层执行设备的差异,比如有 CPU,GPU,Ascend 等执行设备,每种执行设备上的计算逻辑,内存分配,通信逻辑各不相同。“没有什么问题是加一层抽象不能解决的”,如果有,咱们加两层哈哈。本文着重分析一个算子在 MindSpore 框架中的执行流程,对 Primitive 的设计论述相对较少,但是通过观察一个算子在框架中的执行流程,我们可以形象的感知到 Primitive 的作用。后续将会写一篇文章分析 Primitive 的设计。

    原文发布在 GitLink 上的 MindSpore 评注解读上面,可以的话点一点链接!

    链接:https://forum.gitlink.org.cn/forums/7330/detail

    Python ReLU 代码

    首先写一个最简单的算子,计算一个向量的 ReLU。

    import mindspore
    import mindspore.ops as P
    from mindspore import Tensor
    from mindspore import dtype as mstype
    import numpy as np
    
    input_x = Tensor(np.random.randn(2,3), mstype.float32)
    relu = P.ReLU()
    output_x = relu(input_x)
    print(input_x)
    print(output_x)
    

    输出如下,大于 0 的部分保留,小于 0 的部分设为 0:

    [[ 1.5206318  -0.35908994 -0.54122275]
     [ 0.32850873 -0.6513135  -2.8261368 ]]
    [[1.5206318  0.         0.        ]
     [0.32850873 0.         0.        ]]
    

    Python 端源码分析

    ReLU 继承了 Primitive,Primitive 又继承自 C++ 导出的 Primitive_

    调用 ReLU 的背后,实际上执行的是 _run_op 这个函数

    # mindspore/python/mindspore/ops/primitive.py
    def __call__(self, *args):
        should_elim, output = self.check_elim(*args)
        for arg in args:
            if isinstance(arg, Parameter) and arg.has_init:
                arg.init_data()
        if should_elim:
            return output
        return _run_op(self, self.name, args)
    

    _run_op 调用了 C++ 导出的 real_run_op

    # mindspore/python/mindspore/ops/primitive.py
    @_wrap_func
    def _run_op(obj, op_name, args):
        """Single op execution function supported by ge in PyNative mode."""
        output = real_run_op(obj, op_name, args)
        return output
    

    由此我们进入到 C++ 端的实现中。

    C++ 端源码分析

    首先看看 pybind11 导出的函数。

    // mindspore/ccsrc/pipeline/jit/init.cc
    (void)m.def("real_run_op", &mindspore::pynative::RealRunOp, "Run op pynatively.");
    

    RealRunOp 内部实现如下,先构造 info,然后再执行。info 里面会保存参数,保存 Python 端的 Primitive 对象。

    // mindspore/ccsrc/pipeline/pynative/pynative_execute.cc
    py::object RealRunOp(const py::args &args) {
      CheckPyNativeContext();
      const auto &executor = PynativeExecutor::GetInstance();
      MS_EXCEPTION_IF_NULL(executor);
      OpExecInfoPtr op_exec_info = executor->forward_executor()->GenerateOpExecInfo(args);
      MS_EXCEPTION_IF_NULL(op_exec_info);
      py::object ret = py::none();
      PynativeExecutorTry(executor->forward_executor()->RunOpS, &ret, op_exec_info);
      return ret;
    }
    

    传递到 ForwardExecutor 内部的 RunOpInner。首先执行检查,判断参数正确性,判断算子是否存在,特殊处理混合精度算子。因为是 PyNative,所以是动态图,也就是执行的时候构图,可以从下面看到 ConstructForwardGraph,最后通过 GetOpOutput 执行算子。

    std::function<void(py::object *, const OpExecInfoPtr &)> RunOpS = [this](auto &&PH1, auto &&PH2) {
      RunOpInner(std::forward<decltype(PH1)>(PH1), std::forward<decltype(PH2)>(PH2));
    };
    
    void ForwardExecutor::RunOpInner(py::object *ret, const OpExecInfoPtr &op_exec_info) {
      MS_EXCEPTION_IF_NULL(ret);
      MS_EXCEPTION_IF_NULL(op_exec_info);
      MS_LOG(DEBUG) << "RunOp name: " << op_exec_info->op_name;
      if (kSummaryOperators.count(op_exec_info->op_name)) {
        MS_LOG(DEBUG) << "PyNative not support Operator " << op_exec_info->op_name;
        return;
      }
      if (op_exec_info->op_name == prim::kPrimMixedPrecisionCast->name()) {
        RunMixedPrecisionCastOp(op_exec_info, ret);
        return;
      }
    
      // 1.Set cast for inputs
      SetCastForInputs(op_exec_info);
      // 2.Construct graph, first step abs will update by node
      auto cnode = ConstructForwardGraph(op_exec_info);
      // 3.Get inputs abstract
      abstract::AbstractBasePtrList args_spec_list;
      GetInputsArgsSpec(op_exec_info, &args_spec_list);
      // 4.Get output abstract
      bool prim_cache_hit = false;
      GetOpOutputAbstract(op_exec_info, args_spec_list, &prim_cache_hit);
      // 5.Get output
      GetOpOutput(op_exec_info, args_spec_list, cnode, prim_cache_hit, ret);
    }
    

    GetOpOutput 内部还要处理反向梯度,需要将算子的信息记录下来。最终执行到 RunOpWithInitBackendPolicy 里面,使用指定的后端来运行算子。后端指的是运行引擎,比如 vm,ge,tsd 等,不同的运行引擎应该有不同的优化策略。

    void ForwardExecutor::GetOpOutput(const OpExecInfoPtr &op_exec_info,
                                      const abstract::AbstractBasePtrList &args_spec_list, const CNodePtr &cnode,
                                      bool prim_cache_hit, py::object *ret) {
      MS_EXCEPTION_IF_NULL(op_exec_info);
      const auto &prim = op_exec_info->py_primitive;
      MS_EXCEPTION_IF_NULL(prim);
      // Infer output value by constant folding
      MS_EXCEPTION_IF_NULL(ret);
      py::dict output = abstract::ConvertAbstractToPython(op_exec_info->abstract, true);
      if (!output[ATTR_VALUE].is_none()) {
        *ret = output[ATTR_VALUE];
        grad()->RecordGradOpInfo(op_exec_info);
        MS_LOG(DEBUG) << "Get output by constant folding, output is " << py::str(*ret);
        return;
      } else if (prim->is_const_prim()) {
        *ret = py::cast("");
        grad()->RecordGradOpInfo(op_exec_info);
        MS_LOG(DEBUG) << "Get const prim";
        return;
      }
    
      // Add output abstract info into cache, the const value needs to infer evert step
      if (grad()->enable_op_cache() && !prim_cache_hit && !IsDynamicShape(op_exec_info)) {
        AbsCacheKey key{prim->name(), prim->Hash(), prim->attrs()};
        auto &out = prim_abs_list_[key];
        out[args_spec_list].abs = op_exec_info->abstract;
        out[args_spec_list].attrs = prim->evaluate_added_attrs();
      }
    
      // Run op with selected backend, nop is no need run backend
      ValuePtr out_real_value = nullptr;
      if (op_exec_info->is_nop_prim) {
        DoNopOutput(op_exec_info, &out_real_value);
        *ret = BaseRefToPyData(out_real_value);
      } else {
        auto result = RunOpWithInitBackendPolicy(op_exec_info);
        py::object out_real = result;
        if (result.size() == 1 && op_exec_info->abstract != nullptr &&
            !op_exec_info->abstract->isa<abstract::AbstractSequence>()) {
          out_real = result[0];
        }
        // Get output value
        if (grad()->grad_flag()) {
          out_real_value = PyObjToValue(out_real);
        }
        *ret = out_real;
      }
    
      if (grad()->need_construct_graph() && !grad()->in_cell_with_custom_bprop_()) {
        MS_EXCEPTION_IF_NULL(cnode);
        const auto &obj_id = GetId(*ret);
        cnode->set_abstract(op_exec_info->abstract);
        node_abs_map_[obj_id] = op_exec_info->abstract;
        grad()->SaveOutputNodeMap(obj_id, *ret, cnode);
        grad()->DoOpGrad(op_exec_info, cnode, out_real_value);
        // Dynamic shape should update to top cell
        if (IsDynamicShape(op_exec_info)) {
          grad()->top_cell()->set_dynamic_shape(true);
        }
      } else {
        node_abs_map_.clear();
      }
      // Record op info for judge whether the construct of cell has been changed
      grad()->RecordGradOpInfo(op_exec_info);
      grad()->UpdateForwardTensorInfoInBpropGraph(op_exec_info->op_info, out_real_value);
    }
    

    接下来进入到根据策略执行算子的地方。如果跟进去看,RunOpInVM,最后的逻辑是运行了 python 的计算函数。如果要往 C++ 走,应该还要看 RunOpInMs,即在 MindSpore 中执行算子。

    py::object ForwardExecutor::RunOpWithBackendPolicy(MsBackendPolicy backend_policy, const OpExecInfoPtr &op_exec_info) {
      py::object result;
      if (backend_policy == kMsBackendVmOnly) {
    #ifndef ENABLE_TEST
        if (kVmOperators.find(op_exec_info->op_name) != kVmOperators.end()) {
          result = RunOpInVM(op_exec_info);
        } else {
          result = RunOpInMs(op_exec_info);
        }
    #else
        result = RunOpInVM(op_exec_info);
    #endif
      }
    
      return result;
    }
    

    在 RunOpInMs 里面,可以看到运行时 Runtime 又分为了 Ms 和 MindRT 两种,我们暂且看 Ms 这一条分支。先获取一个 Session,然后运行算子。Session 需要通过 MS_REG_SESSION 宏来注册一个设备对应的 Session 类。这个类是一个注册工厂,在启动的时候,创建对应的类,注册到工厂当中,后面可以通过 GetCurrentSession 运行时获取对应的 Session 类。

    VectorRef outputs;
    if (!enable_mind_rt) {
        auto cur_session = GetCurrentSession(cur_target, device_id);
        MS_EXCEPTION_IF_NULL(cur_session);
        cur_session->RunOp(&op_run_info, &outputs);
    } else {
        auto cur_mind_rt_backend = GetMindRtBackend(cur_target, device_id);
        MS_EXCEPTION_IF_NULL(cur_mind_rt_backend);
        mindspore::ScopedLongRunning long_running;
        cur_mind_rt_backend->RunOp(&op_run_info, &outputs);
    }
    
    // mindspore/ccsrc/backend/common/session/session_factory.h
    #define MS_REG_SESSION(DEVICE_NAME, SESSION_CLASS)                           \
      static const SessionRegistrar g_session_registrar__##DEVICE_NAME##_##_reg( \
        DEVICE_NAME, []() { return std::make_shared<SESSION_CLASS>(); });
    

    通过搜索使用了 MS_REG_SESSION 宏的地方,我们可以看到有 cpu, gpu, ascend 还有其他 session 类调用了。我们专注于看 cpu 这一条分支。上面分析到了,通过跟踪 CPUSession 的父类 SessionBasic,再看它的类成员 Executor,我们可以看到最后其实调用的是每个 Session 的 RunOpImpl 这个函数。于是我们只要专注看 CPUSession 的 RunOpImpl 即可。

    调用 ProcessInputTensorsForHeterogeneous 如果数据不在 CPU 上,那么将数据同步到 CPU 上。构建 Op 计算图,创建输出向量,再将输入和输出向量绑定到 device::cpu::CPUKernelRuntime 对象上,最后调用 Run 方法执行。

    
    void CPUSession::RunOpImpl(const GraphInfo &graph_info, OpRunInfo *op_run_info,
                               std::vector<tensor::TensorPtr> *input_tensors, VectorRef *outputs,
                               const std::vector<int64_t> &tensors_mask) {
      MS_EXCEPTION_IF_NULL(input_tensors);
      MS_EXCEPTION_IF_NULL(op_run_info);
      ProcessInputTensorsForHeterogeneous("CPU", *input_tensors);
      const auto &kernel_graph = BuildOpImpl(*op_run_info, graph_info, *input_tensors, tensors_mask);
      EraseValueNodeTensor(tensors_mask, input_tensors);
    
      // Remove reorder after PS feature finish adapting push/pull in auto_monad.
      auto execution_order = kernel_graph->execution_order();
      Reorder(&execution_order);
      kernel_graph->set_execution_order(execution_order);
    
      // runtime init
      if (!runtime_.Init()) {
        MS_LOG(EXCEPTION) << "Kernel runtime init error.";
      }
      runtime_.AssignKernelGraphAddress(kernel_graph.get());
      std::map<tensor::TensorPtr, session::KernelWithIndex> tensor_to_node;
      runtime_.CreateOutputTensors(kernel_graph.get(), *input_tensors, outputs, &tensor_to_node);
      runtime_.BindInputOutput(kernel_graph.get(), *input_tensors, outputs);
    
      bool ret = runtime_.Run(*kernel_graph, false);
      if (!ret) {
        MS_LOG(EXCEPTION) << "Run Op failed";
      }
      UpdateDynamicOutputShape(tensor_to_node);
      // update output abstract of dynamic op to op_run_info
      if (op_run_info->output_is_dynamic_shape) {
        UpdateOutputAbstract(kernel_graph, op_run_info);
      }
      SetOutputFlags(*outputs);
      runtime_.RunOpClearMemory(*kernel_graph);
    }
    

    device::cpu::CPUKernelRuntime 是 MindSpore Primitive 设计的一部分,Primitive 通过对 “计算设备” 进行封装和抽象,抽象出了 KernelRuntime 算子执行的逻辑就在这里面,我们已经快接近真相了。CPUKernelRuntime 的 Run 方法里面为了处理 Profile,安全,调试等,写了不少条件编译,我们只看最核心的逻辑。前面我们可以看到 kernel_graph 已经设置好了执行顺序,因此在这里我们只需要依次获取 kernel,然后逐个执行,调用 Kernel 的 Launch 方法。

    for (const auto &kernel : kernels) {
        auto kernel_mod = AnfAlgo::GetKernelMod(kernel);
        MS_EXCEPTION_IF_NULL(kernel_mod);
        // akg kernel do not support dynamic shape by now
        kernel::NativeCpuKernelMod *cpu_kernel = nullptr;
        if (session::AnfRuntimeAlgorithm::GetKernelType(kernel) != KernelType::AKG_KERNEL) {
            cpu_kernel = dynamic_cast<kernel::NativeCpuKernelMod *>(kernel_mod);
            MS_EXCEPTION_IF_NULL(cpu_kernel);
        }
        if (common::AnfAlgo::IsDynamicShape(kernel)) {
            AnfAlgo::InferShape(kernel);
            auto args = kernel::GetArgsFromCNode(kernel);
            if (cpu_kernel != nullptr && cpu_kernel->Resize(args->op, args->inputs, args->outputs, args->depend_tensor_map) ==
                                            kernel::KRET_RESIZE_FAILED) {
            MS_LOG(EXCEPTION) << "Node " << kernel->fullname_with_scope() << " Resize failed!";
            }
        }
        std::vector<kernel::AddressPtr> kernel_inputs;
        std::vector<kernel::AddressPtr> kernel_workspaces;
        std::vector<kernel::AddressPtr> kernel_outputs;
        GetRuntimeAddressFromNode(kernel, &kernel_inputs, &kernel_outputs, &kernel_workspaces);
        bool ret = true;
        try {
            ret = kernel_mod->Launch(kernel_inputs, kernel_workspaces, kernel_outputs, 0);
        } catch (std::exception &e) {
            MS_LOG(EXCEPTION) << e.what() << trace::DumpSourceLines(kernel);
        }
        if (!ret) {
            MS_LOG(EXCEPTION) << "Launch kernel failed." << trace::DumpSourceLines(kernel);
        }
        static_cast<CPUMemoryManager *>(mem_manager_.get())->DecreaseAddressRefCount(kernel);
    }
    

    再往下翻,我们找到 ReLU 的 CPU kernel 实现,其实就是判断是否大于 0,大于 0 的保留,小于 0 的设置为 0。

    template <typename T>
    void Relu(ArithmeticSelfCpuKernelFunc *content, const T *in, T *out, size_t size) {
      auto task = [in, out](size_t start, size_t end) {
        for (size_t i = start; i < end; i++) {
          out[i] = std::greater<T>()(in[i], 0) ? in[i] : 0;
        }
      };
      ParallelLaunchAutoSearch(task, size, content, &content->parallel_search_info_);
    }
    
    MS_KERNEL_FACTORY_REG_BY_CREATOR(NativeCpuKernelMod, ReLU,
                                     []() { return std::make_shared<ArithmeticSelfCpuKernelMod>(kReLU); });
    

    总结

    自此,我们分析了一个算子从 Python 前端最终运行到 C++ CPU Kernel 的全流程,其他分支流程大同小异,基本遵循着 “构图,图优化,分配内存,绑定输入输出,运行” 这样一个模式。

  • 相关阅读:
    求助:多个参数的存储过程
    经典回顾:哲学家进餐问题(The Dinning Philosophers Problem)
    杨辉三角
    开发公共课选修系统之二
    关于即时消息系统
    用户 'NT AUTHORITY/NETWORK SERVICE' 登录失败 的解决方法(转)
    细说反射API
    东莞哪家公司可以提供实习的机会?
    使用Gmail发送email时出现Must issue a STARTTLS command first错误!!
    Revolution
  • 原文地址:https://www.cnblogs.com/zzk0/p/16411058.html
Copyright © 2020-2023  润新知