• OneFlow学习笔记:从Python到C++调用过程分析


    9cae8c5d7f9eab06d4e0e58390735065.png

    撰文|月踏

    在OneFlow中,从Python端我们可以使用各种Op进行相关操作,下面是一个最最简单的relu op的使用示例:

    1. >>> import oneflow as of
    2. >>> x=of.tensor([-3,-2,-1,0,1,2,3], dtype=of.float)
    3. >>> of.relu(x)
    4. tensor([0., 0., 0., 0., 1., 2., 3.], dtype=oneflow.float32)

    虽然调用在Python端,但具体的实现是在C++端,那么OneFlow是怎么样一步步从Python端调到C++中的呢,本文以最最简单的Relu这个Op作为例子,来追溯一下在OneFlow中从Python端到C++中的大致调用过程,具体过程大概总结为Python wrapper和C++ glue functor两部分,下面是两部分的具体细节。

    1

    Python wrapper

    Python的代码都在python/oneflow文件夹中,在分析Python wrapper的过程中,也会涉及很多C++代码,主要是和pybind11绑定相关的,也一并归类到Python wrapper这部分了。

    先看本文开头示例中的relu接口的直接来源,在python/oneflow/__init__.py中可以找到下面这一行:

    from oneflow._C import relu

    可以看到relu是从_C这个module中导出来的,所以继续看oneflow/_C/__init__.py这个文件:

    from oneflow._oneflow_internal._C import *

    可见relu接口来自_oneflow_internal这个module,_oneflow_internal是pybind11定义的一个module,位于oneflow/api/python/init.cpp:

    1. PYBIND11_MODULE(_oneflow_internal, m) {
    2. ...
    3. ::oneflow::cfg::Pybind11ModuleRegistry().ImportAll(m);
    4. ::oneflow::OneflowModuleRegistry().ImportAll(m);
    5. }

    继续看上面代码中的OneflowModuleRegistry,它是注册过的Op暴露到Python层的关键,它位于oneflow/api/python/of_api_registry.h:

    1. class OneflowModuleRegistry {
    2. ...
    3. void Register(std::string module_path, std::function<void(pybind11::module&)> BuildModule);
    4. void ImportAll(pybind11::module& m);
    5. };

    这个类提供了一个Register接口,被封装进了下面这个注册宏里,代码位于oneflow/api/python/of_api_registry.h:

    1. #define ONEFLOW_API_PYBIND11_MODULE(module_path, m) \
    2.   struct OfApiRegistryInit {                                                    \
    3. OfApiRegistryInit() { \
    4.       ::oneflow::OneflowModuleRegistry()                                        \
    5. .Register(module_path, &OF_PP_CAT(OneflowApiPythonModule, __LINE__)); \
    6.     }                                                                           \
    7. }; \
    8. OfApiRegistryInit of_api_registry_init; \
    9. static void OF_PP_CAT(OneflowApiPythonModule, __LINE__)(pybind11::module & m)

    知道了ONEFLOW_API_PYBIND11_MODULE这个宏,继续搜哪里会用到它,在build/oneflow/api/python/functional/functional_api.yaml.pybind.cpp这个自动生成的文件中,可以搜到它被用到:

    1. ONEFLOW_API_PYBIND11_MODULE("_C", m) {
    2. py::options options;
    3.   options.disable_function_signatures();
    4. ...
    5. m.def("relu", &functional::PyFunction<functional::ReluSchema_TTB>);
    6. ...
    7. options.enable_function_signatures();
    8. }

    由此可知本节刚开头的from oneflow._C import relu这句代码中的_C这个module和Relu这个算子是从哪来的了,在这里Relu被映射到了functional::PyFunction<functional::ReluSchema_TTB>这个函数,这是一个模板函数,先看其中的模板参数ReluSchema_TTB的定义:

    1. struct ReluSchema_TTB {
    2. using FType = Maybe<one::Tensor>(const std::shared_ptr<one::Tensor>& x, bool inplace);
    3. using R = Maybe<one::Tensor>;
    4. static constexpr FType* func = &functional::Relu;
    5. static constexpr size_t max_args = 2;
    6. static constexpr size_t max_pos_args = 2;
    7. static constexpr char const* signature = "Tensor (Tensor x, Bool inplace=False)";
    8. static FunctionDef function_def;
    9. };

    可以看到里面最和调用流程相关的是一个指向functional::Relu的函数指针成员,functional::Relu这个系列的函数非常重要,它是一个自动生成的全局C++ 接口,可以认为是Python和C++之间的分水岭,细节在第二节会详细讲,下面继续来看functional::PyFunction<functional::ReluSchema_TTB>这个模板函数,是它决定了怎么样去调用functional::ReluSchema_TTB中的func这个指向functional::Relu的函数指针,functional::PyFunction模板函数定义位于oneflow/api/python/functional/py_function.h:

    1. template<typename... SchemaT>
    2. inline py::object PyFunction(const py::args& args, const py::kwargs& kwargs) {
    3. static PyFunctionDispatcher<SchemaT...> dispatcher;
    4. return dispatcher.call(args, kwargs, std::make_index_sequence<sizeof...(SchemaT)>{});
    5. }

    这里又继续调用了PyFunctionDispatcher中的call函数:

    1. template<typename... SchemaT>
    2. class PyFunctionDispatcher {
    3. ...
    4. template<size_t I0, size_t... I>
    5. py::object call(const py::args& args, const py::kwargs& kwargs,
    6. std::index_sequence<I0, I...>) const {
    7. std::cout << I0 << std::endl;
    8. using T = schema_t<I0>;
    9. std::vector<PythonArg> parsed_args(T::max_args);
    10.     if (ParseArgs(args, kwargs, &parsed_args, T::function_def, T::max_pos_args, schema_size_ == 1)) {
    11. return detail::unpack_call(*T::func, parsed_args);
    12. }
    13. return call(args, kwargs, std::index_sequence<I...>{});
    14. }
    15. ...
    16. };

    这里把functional::ReluSchema_TTB中的func这个指向functional::Relu的函数指针作为参数,继续调用了oneflow/api/python/functional/unpack_call.h中的unpack_call:

    1. template<typename F>
    2. py::object unpack_call(const F& f, const std::vector<PythonArg>& args) {
    3.   constexpr size_t nargs = function_traits<F>::nargs;
    4. using R = typename function_traits<F>::return_type;
    5. return CastToPyObject(
    6. unpack_call_dispatcher<F, R>::apply(f, args, std::make_index_sequence<nargs>{}));
    7. }

    这里又把functional::ReluSchema_TTB中的func这个指向functional::Relu的函数指针作为参数,继续调用了同一个文件中的unpack_call_dispatcher<F, R>::apply:

    1. template<typename F, typename R>
    2. struct unpack_call_dispatcher {
    3. template<size_t... I>
    4. static R apply(const F& f, const std::vector<PythonArg>& args, std::index_sequence<I...>) {
    5.     return f(args[I].As<oneflow::detail::remove_cvref_t<typename std::tuple_element<I, typename function_traits<F>::args_type>::type>>()...);
    6. }
    7. };

    至此完成了对全局C++接口functional::Relu的调用,下一节具体讲functional::Relu这个全局C++接口怎么生成的。

    2

    C++ glue functor

    先看oneflow/core/functional/impl/activation_functor.cpp中的一个类,它对下是通往Relu底层实现的大门,通往底层的实现是OneFlow框架的精髓,我还没有往里看,以后时机到了会继续总结出来,对上则提供了上层调用的接口,本文只关注接口部分:

    1. class ReluFunctor {
    2. ...
    3. Maybe<Tensor> operator()(const std::shared_ptr<Tensor>& x, bool inplace) const {
    4.     ...
    5.     return OpInterpUtil::Dispatch<Tensor>(*op_, {x});
    6.   }
    7. };

    ReluFunctor提供了一个函数调用符的重载函数,所以它对应的对象是可调用对象,它会被下面的代码进行注册:

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

    继续看ONEFLOW_FUNCTION_LIBRARY的定义,它通过定义一个静态变量的办法来在OneFlow的初始化阶段把上面的类似ReluFunctor的这些funtor通过add_functor接口全部注册到FunctionLibrary这个单例类中:

    1. #define ONEFLOW_FUNCTION_LIBRARY(m) ONEFLOW_FUNCTION_LIBRARY_IMPL(m, __COUNTER__)
    2. #define ONEFLOW_FUNCTION_LIBRARY_IMPL(m, uuid)                                  \
    3. static int OF_PP_CAT(_oneflow_function_library_dummy_, uuid) = []() { \
    4. FunctionLibrary* library = FunctionLibrary::Global(); \
    5. OF_PP_CAT(_oneflow_function_library_, uuid)(*library); \
    6. return 0; \
    7. }(); \
    8. void OF_PP_CAT(_oneflow_function_library_, uuid)(FunctionLibrary & m)

    FunctionLibrary的主要数据结构和接口如下,其中PackedFuncMap是一个用于存放注册对象的数据结构,add_functor用于注册,find用于查找已经注册过的对象, Global是单例接口:

    1. class FunctionLibrary {
    2. template<typename R, typename... Args>
    3. struct PackedFuncMap<R(Args...)> {
    4. static HashMap<std::string, FunctorCreator>* Get() {
    5. using FunctorCreator = typename std::function<PackedFunctor<R(Args...)>()>;
    6. static HashMap<std::string, FunctorCreator> functors;
    7. return &functors;
    8. }
    9. };
    10. template<typename... Fs>
    11.   void add_functor(const std::string& func_name) { ... }
    12. template<typename R, typename... Args>
    13. auto find(const std::string& func_name)
    14. -> Maybe<PackedFunctor<typename PackedFunctorMaker<R(Args...)>::FType>> { ... }
    15. static FunctionLibrary* Global() {
    16. static FunctionLibrary global_function_library;
    17. return &global_function_library;
    18. }
    19. };

    再继续看上面代码中的数据结构部分中用到的PackedFunctor,位于oneflow/core/functional/packed_functor.h,它通过call接口封装了functor的调用:

    1. template<typename R, typename... Args>
    2. class PackedFunctor<R(Args...)> {
    3. public:
    4.   PackedFunctor(const std::string& func_name, const std::function<R(Args...)>& impl) : func_name_(func_name), impl_(impl) {}
    5.   call(Args&&... args) const {
    6.     return impl_(std::forward<Args>(args)...);
    7. }
    8. private:
    9. std::string func_name_;
    10. std::function<R(Args...)> impl_;
    11. };

    前面这部分都是functor的定义和注册部分,它们是提供全局C++接口的基石,下面继续看全局的C++接口functional::Relu是怎么来的,在code base中,有一个oneflow/core/functional/functional_api.yaml的配置文件,与Relu相关的内容如下:

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

    这是一个yaml配置脚本,最终的functional::Relu这个全局C++接口就是通过前面的functor的定义、注册、yaml配置,最后再通过tools/functional/generate_functional_api.py这个python脚本自动生成出来,精简代码如下:

    1. if __name__ == "__main__":
    2. g = Generator("oneflow/core/functional/functional_api.yaml")
    3. g.generate_cpp_header_file(header_fmt, "oneflow/core/functional/functional_api.yaml.h")
    4. g.generate_cpp_source_file(source_fmt, "oneflow/core/functional/functional_api.yaml.h")
    5. ...

    可见具体的接口被生成到了上面指定的文件中,具体的生成过程在generator.py中,内容比较trivial,主要是通过hard code的方式来自动生成全局C++接口,下面是functional::Relu这个全局C++接口的示例:

    1. namespace oneflow {
    2. namespace one {
    3. namespace functional {
    4. ...
    5. Maybe<one::Tensor> Relu(const std::shared_ptr<one::Tensor>& x, bool inplace) {
    6. static thread_local const auto& op = CHECK_JUST(FunctionLibrary::Global()->find<Maybe<one::Tensor>, const std::shared_ptr<one::Tensor>&, bool>("Relu"));
    7. return op->call(x, inplace);
    8. }
    9. ...
    10. } // namespace functional
    11. } // namespace one
    12. } // namespace oneflow

    可以看到上面的Relu接口通过注册类的find接口找到了注册过的ReluFunctor,然后用PackedFunctor中的call接口进行了调用,至此,我们终于知道了functional::Relu这个全局C++接口的前因后果。

    其他人都在看

    欢迎下载体验OneFlow新一代开源深度学习框架:https://github.com/Oneflow-Inc/oneflow/

  • 相关阅读:
    小程序赖加载刷新数据页面数据堆叠问题debug
    【面试题】详解Cookie、localStorage、sessionStorage区别
    vue3响应式原理:Proxy + Reflect
    5、Linux文件系统
    Java SE 16 record 类型说明与使用
    Java版本spring cloud + spring boot企业电子招投标系统源代码
    嵌入式养成计划-33--数据库-sqlite3
    项目管理证书 PMP 的含金量高吗?
    使用Aggregated APIServer扩展你的kubernetes API
    linux部署运维3——centos7.9离线安装部署配置涛思taos2.6时序数据库TDengine以及java项目链接问题处理(二)
  • 原文地址:https://blog.csdn.net/OneFlow_Official/article/details/124013895