关于深度学习:MegEngine-Windows-Python-wheel-包减肥之路

7次阅读

共计 9038 个字符,预计需要花费 23 分钟才能阅读完成。

作者:张浩龙 | 旷视科技 MegEngine 架构师

写在之前

本文的目标

  • 通过讲述在反对 MegEngine Windows Python wheel 过程中遇到的问题以及解决问题的流程,此文最初的解决办法可能不是最优,欢送留言斧正。
  • 过程中顺便科普一些对于 MegEngine 的构建以及构建时用到的根底货色,当然这些基础知识我置信是工程之道常常会用到的,包含但不限于:

    • CMake
    • 编译、链接、符号暗藏,符号 export 等。此处先举荐一本“老书”《程序员的自我涵养》,天然它没有 xxx 四库全书让人搜索枯肠,然而它外面的基础知识仍然是目前咱们和计算机“交换”中常常遇到的。
    • Python wheel 包构建

MegEngine 各平台反对状况

  • cpp 推理反对状况:

TEE:https://en.wikipedia.org/wiki…

  • 训练:

    • Python 侧:
    • 目前官网公布的 wheel 包,只有 Windows-X64-CPU-CUDA,many Linux 64bit -X64-CPU-CUDA,MacOS-X64-CPU,其余的可本人编译,或者社区提单索取。
    • cpp 侧训练反对状况和下面的 cpp 推理状况统一。

从下面的状况,可看见 MegEngine 无论训练还是推理,还是各种硬件,还是各种 OS 都反对的十分全面,如有需要,无妨试用!!!!

遇到的问题

为了全面的反对下面提到的 MegEngine 各个平台,各个 OS,期间或多或少会遇到一些问题,比方 Windows 平台上 Python wheel 包体积过大。

先看一下目前 MegEngine wheel 包体积大小,摘自 1.7 版本 pypi

其中因为 Linux 和 Windows 反对了 CUDA,所以包体积在 900MB 左右,这是一个失常的 size。

在之前 Windows CUDA 包体积在 1.7G 左右:这就是前面尝试剖析和修复的问题

先对问题 MECE 一下

MECE 是 Mutually Exclusive Collectively Exhaustive 缩写,中文意思是“互相独立,齐全穷尽”。也就是对于一个问题的议题,可能做到不重叠、不脱漏的分类,而且可能藉此无效把握问题的外围,并解决问题的办法。强调两点:

  • 各局部之间互相独立(MutuallyExclusive)

    • 化简后,感觉就是剖析问题时,可能的办法要尽可能的独立,尽量不要有交际
  • 所有局部齐全穷尽(CollectivelyExhaustive)

    • 化简后,感觉就是剖析问题时,尽可能的要把办法想全,尽量不要有脱漏

为啥会抉择一条鱼呢:鱼头左近个别都比拟大(胖),而越往鱼尾走,会越来越小(瘦),从而心愿通过这个流程“Windows wheel 减肥”之路,达到减肥的目标。

问题的影响

这样的体积会有什么问题呢(毕竟人太胖也会有些副作用 xxx etc.)

  • 首先就是体积超过了咱们申请的 pypi 上单个文件最大体积限度
  • 给用户体验不好,为什么雷同的版本,Window 比 Linux 大这么多呢
  • Windows 上显存比 Linux 占用大很多(预计提到这点,大家曾经猜测到问题所在)

解决这个问题可能须要的相干常识

问题给人的第一印象是:

  • 编译构建相干的
  • Python wheel 打包相干的
  • Windows OS 独有的

先就 MegEngine 如下基础知识做一些根底补充(减肥前总得有一些科普吧,到底是吃药还是锤炼,或者具体到吃什么药吧)

MegEngine CMake 构建流程

  • MegEngine 构建依赖 CMake 和 Ninja
  • 其中 CMake 形容次要在:

    • 顶层 CMakeList

      • 此文件蕴含很多的 option, 次要用于管制是否编译一些模块,比方是否关上 MGE_BUILD_WITH_ASAN 用于调试内存问题
      • 此文件蕴含对各种 ARCH 的适配管制,比方编译 X86_64 还是 AARCH64 等
      • 此文件蕴含对各种 OS 的适配,比方是编译 Linux,Android 还是 Windows 等
      • 以及一些杂项配置,比方有优化等级,比方 CUDA SM 的配置等
    • src CMakelist

      • 此文件蕴含了 MegEngine 外围代码 MegBrain 层所有源代码的编译治理

        • dnn CMakelist
      • 此文件蕴含了 MegEngine 外围代码 dnn (次要实现各种 backends)层所有源代码的编译治理

        • 一些杂项 CMakelist
      • 各种 example,比方 lite example
      • 各种 test,比方 megbrain test
      • 各种 helper,见 helper module
  • 其中 Ninja 提供丰盛的可视化调试性能,上面列举如何通过 Ninja debug server 来看 MegEngine 局部模块的构建依赖

    • 执行 host_build.sh 来进行 host 编译,同时它会在 build dir 生成整个构建依赖形容文件 build.ninja

      • 更多的编译反对请参考 BUILD_README.md
      • build.ninja 文件性能相似 GNU Makefile
    • 有了 build.ninja 后,便可进行一些调试

      $ ninja -t list
      ninja subtools:
          browse  browse dependency graph in a web browser
           clean  clean built files
        commands  list all commands required to rebuild given targets
            deps  show dependencies stored in the deps log
           graph  output graphviz dot file for targets
           query  show inputs/outputs for a path
         targets  list targets by their rule or depth in the DAG
          compdb  dump JSON compilation database to stdout
       recompact  recompacts ninja-internal data structures
          restat  restats all outputs in the build log
           rules  list all rules
       cleandead  clean built files that are no longer produced by the manifest
      
      • 可通过如下命令来启动一个可视化 server (当然相熟后,也可通过其余命令行参数来调试,天然也能够间接看 CMakeLists.txt 来找关系)

        ninja -t browse --port you_port --hostname you_ip --no-browser
      • 而后通过在浏览器输出: you_ip:you_port 进行可视化浏览了
      • 还能够通过 dot 生成 png 来查看

        ninja -t graph > 1.dot
        dot -Tpng 1.dot > output.png
      • 比方 megenginelite 一个最上层的指标:liblite_shared_whl.so 它的依赖图如下
      • 依赖一些 lite/CMakeFiles 上面的 .o
      • 依赖一些第三方的.a,比方 cpuinfo.a
      • 依赖 libmegbrain_shared.so 此库蕴含了 megbrain/dnn 所有的编译输入,当然还能够鼠标在 ninja 起的 server 任意点击开展任一一个指标来查看它的依赖状况

MegEngine wheel 构建流程

  • 有了下面 Ninja 编译进去的各种库后,咱们就能够将它们和 MegEngine 中的 py src 一起进行打包,最初生成可装置,可散发的 Python wheel 包了
  • 构建流程次要阐明在 BUILD_PYTHON_WHL_README

    • 形容了目前 MegEngine python wheel 的反对状态
    • 本人本地构建须要的一些 env 筹备
    • 一些应用阐明
    • MegEngine wheel 包听从 pep-0571

      • 包 setup 入口在 python wheel setup
      • 调用 setup 前各个 OS 的筹备差异化在 wheel scripts
      • 有人可能会问,为什么不应用 auditwheel 来主动治理 wheel 包中的 so 依赖,有两个起因

        • auditwheel 不反对所有的操作系统,比方 Windows
        • auditwheel 不反对依赖库应用 dlopen 的状况
        • auditwheel 不反对 subpackage 的 wheel 包

          • 当你执行 python3 -m pip install megengine -f https://megengine.org.cn/whl/… 后,能够 import megengine,也能够 import megenginelite,是因为 megengine 和 megenginelite 均会存在装置的包中,且他们会复用 megengine_shared 这一体积超大的库

MegEngine 构建上如何适配 Windows

下面介绍了 MegEngine 基于 CMake 的构建根底和应用 Ninja 自带的调试性能以及帮咱们从宏观理解了一下 project 的编译依赖和进行一些惯例调试,上面再介绍一下 MegEngine 是如何适配 Windows 平台的。

  • 首先 MegEngine 大部分的源代码都是 c++,且 cpp 推理要求是c++14,编译 Python 训练要求是 c++17

    • 各家编译器其实对这些规范实现不是完全一致的,抛开和零碎相干的,比方 POSIX 外,其实还有比拟多根本的下层用法各家编译器其实时不太兼容的,特地是显著的是 gcc 和 clang 能编译过的代码,Windows cl.exe 其实是编译不过的。
  • 为了解决下面提到的两个问题

    • 尽可能的摈弃 cl.exe,Windows 上应用 llvm-clang-cl 进行构建

      cmake ...
      -DCMAKE_C_COMPILER=clang-cl.exe
      -DCMAKE_CXX_COMPILER=clang-cl.exe
      ...
    • 当然因为 CUDA 的起因,目前不可能齐全摈弃 cl.exe,在编译 CUDA host 代码时,仍然应用 cl.exe
    • 辨别开零碎相干的函数实现,所以你会在 MegEngine 代码中看到不少如下相似的代码

      #if WIN32
      ....
      #else
      ....
      #endif
  • 再加上,下面提到的 CMake, Ninja 自身是跨平台的,这样一组合,MegEngine 便原生反对了 Windows,留神不是基于 WSL 的哦

问题简略的剖析

在下面“MegEngine CMake 构建流程”大节中,咱们提到了 Ninja debug server 可能帮忙可视化整个构建组件的依赖关系,上面咱们补充一下在问题修复前 Windows 和 Linux 下 imperative 依赖的可视化后果。

Linux 下

Windows 下

Windows 和 Linux 下最大的差异化如下:

  • Linux 下 imperative 是依赖的 libmegengine_shared.so
  • Windows 下 imperative 是依赖的 megbrain 和 megdnn,又因为 megbrain 和 dnn 在 CMake 这边其实一个 OBJECT,所以相当于间接依赖他们的 .obj 了

初步论断:

  • MegEngine wheel 包,有两个 Python module 接口

    • MegEngine 用于 python 侧训练的根底接口

      • imperative: 当你 装置 实现 MegEngine 时,在 Python 中输出 import megengine 时。加载的就是它,咱们提供了一些入门的教程供您疾速上手 MegEngine
    • MegEngineLite:易用的 cpp,Python 推理接口

      • 当你应用 MegEngine 实现训练模型后,可参考 部署 文档应用 MegEngineLite,疾速将你的模型部署落地。
  • 因为有这两个顶层构建指标的存在,且他们在 Windows 和其余 OS 上,依赖底层的指标不同,导致了问题的产生
  • 为什么不同的依赖关系,会产生这么大的体积区别呢,先看一张 MegEngine 的架构图

    • 从下往上顺次是:

      • MegEngine 不同 backends 的差异化实现被封装到了 dnn(对应到上图的“硬件层”,对应到 CMakeList 中的 megdnn 模块),而其中 CUDA backends 因为有大量的 kernel 以及对较多的 SM 反对,会对整个库或者可执行程序体积产生大量的体积奉献
      • 图中“硬件形象层”,局部“外围组件层”,对应 CMakeList 中的 megbrain 模块
      • 在往上“接口层”,对应 CMakeList 中的 imperative 和 MegEngineLite 模块。
    • 因为上述的起因,在 Windows 平台,imperative 模块和 MegEngineLite 模块会同时动态依赖 dnn 和 megbrain 代码,导致体积简直翻倍。问题修复前的依赖图:

可能的解决方案

通过下面的剖析,问题起因曾经找到,再来猜测一下

  • 为什么 Windows 平台上和其余平台指标依赖有差别

    • Windows class member 不能隐式的被 export,须要显式的应用 dllexport 和 dllimport,具体见 Microsoft Specific
  • dnn,megbrain 层有大量的 data 数据拜访并没有形象成函数,而是须要间接拜访数据成员
    上面举一个栗子来阐明跨 dll 动静库拜访数据成员形式的差别,次要蕴含三支文件
    api.h 和 api.c 实现函数 func_a 和 定义一个变量 a,被编译成动静库 dll
    client.c 会调用下面 api.c 实现办法和拜访变量 a

    • 跨 dll 动静库间接拜访数据成员形式

      ////api.h
      #ifdef DLL_EXPORT
      #define DECLSPEC_FUC __declspec(dllexport)
      #define DECLSPEC_DATA __declspec(dllexport)
      #else
      #define DECLSPEC_FUC
      #define DECLSPEC_DATA __declspec(dllimport)
      #endif
      DECLSPEC_DATA extern int a;
      DECLSPEC_FUC void func_a();
      /////api.c
      #include "api.h"
      int a = 0;
      void func_a() {}
      # build api.c with define DLL_EXPORT
      
      ////client.c
      #include "api.h"
      int main(){func_a();a = 1;}
      # build client.c without define DLL_EXPORT
    • 跨 dll 动静库通过函数拜访数据成员形式

      革新下面的 example 代码,把其中变量 a 的拜访封装到一个函数

      ////api.h
      #ifdef DLL_EXPORT
      #define DECLSPEC_FUC __declspec(dllexport)
      #else
      #define DECLSPEC_FUC
      #endif
      extern int a;
      DECLSPEC_FUC void func_a();
      DECLSPEC_FUC int * get_a();
      /////api.c
      #include "api.h"
      int a = 0;
      void func_a() {}
      int * get_a() {return &a;}
      # build api.c with define DLL_EXPORT
      
      ////client.c
      #include "api.h"
      int main(){func_a();int *a = get_a(); *a = 1; return 0;}
      # build client.c without define DLL_EXPORT
    • 能够看见在 Windows 上函数符号和数据成员符号 export 是等价的,然而 import data 要求要严格的多
    • 在 Linux, MacOS 下,函数符号和数据成员符号 export 属性是等价的

因为下面提到种种限度,导致在最后反对 Windows 平台时,所有的下层指标(MegEngine,MegEngineLite)都必须动态依赖 megbrain 和 dnn。

既然问题起因曾经找到,须要修复这个问题的指标就变的十分清晰了:让 megengine_shared 动静库 (dll) 在 Windows 平台上可用。

列举一下可能的计划:

  • 计划一:CMake 自带的 WINDOWS_EXPORT_ALL_SYMBOLS

    • 论断:不太实用 MegEngine 这类“大”工程
    • 起因:MegEngine 符号太多,超过了 link.exe max symbols 65536 的限度(使能 CUDA 时,大概有 1.7W 个符号)
    • 剖析 CMake WINDOWS_EXPORT_ALL_SYMBOLS 的原理,是否两头加一些 hook 来过滤不须要 export 的符号,以达到相似 gcc/clang -Wl,–version-script 的成果,cmake 对他的解决逻辑:

      • (stage a): 生成 CMakeFiles/megengine_export.dir/exports.def.objs,实质是 obj 的汇合
      • (stage b): 插入 PRE_LINK stage 生成 CMakeFiles/megengine_export.dir/exports.def(此文件相似 gcc/clang -Wl,–version-script)
      • (stage c): LINK_FLAG 主动插入 /DEF exports.def
      • CMake 提供了对 stage a output 的 hook,意思是能够批改 exports.def.objs,然而没有机会批改 exports.def

        • 退出 hook command,把 exports.def.objs 中所有 DNN 的 obj 删除,设想中应该能够了
        • 然而 imperive 和 megenginelite,不仅仅是和 megbrain 打交到,很多间接应用 dnn 的接口和数据成员
  • 计划二:“优化”版本的 WINDOWS_EXPORT_ALL_SYMBOLS

    • 如下面剖析 WINDOWS_EXPORT_ALL_SYMBOLS 有肯定的缺点,会把所有的 obj 的符号全副 export,那能不能手动批改 WINDOWS_EXPORT_ALL_SYMBOLS 生成的 exports.def

      • 保留必要的 symbols
      • CMakeList 中指标依赖批改过后的 exports.def(让其符号不超过 65536)
      • 论断:不可行

        • Windows cl linker.exe 不反对 * 通配符,不反对寄存一个不存在的符号,导致一旦放了固定的 exports.def,略微更改一个编译参数,或者加点代码,都会编译不过
  • 计划三:到最初发现没有一个“偷懒的”形式来解决这个问题,回退到最 naive 的形式

    • 把 megbrain、dnn、megenginelite 对外裸露的 API 依赖的成员符号全副显式的加上 declspec(dllexport) 和 declspec(dllimport) 属性形容

    修复示例,残缺批改见 PR

diff --git a/dnn/include/megdnn/basic_types.h b/dnn/include/megdnn/basic_types.h
index 53f22c9af..44831f6d7 100644
--- a/dnn/include/megdnn/basic_types.h
+++ b/dnn/include/megdnn/basic_types.h
@@ -104,22 +104,22 @@ struct TensorShape {
 #if MEGDNN_CC_HOST
     TensorShape() = default;
     TensorShape(const TensorShape& rhs) = default;
-    TensorShape(const SmallVector<size_t>& init_shape);
-    TensorShape(std::initializer_list<size_t> init_shape);
-    std::string to_string() const;
+    MGE_WIN_DECLSPEC_FUC TensorShape(const SmallVector<size_t>& init_shape);
+    MGE_WIN_DECLSPEC_FUC TensorShape(std::initializer_list<size_t> init_shape);
+    MGE_WIN_DECLSPEC_FUC std::string to_string() const;
 #endif
 
     //! total number of elements
-    size_t total_nr_elems() const;
+    MGE_WIN_DECLSPEC_FUC size_t total_nr_elems() const;
 
     //! check whether two shapes are equal
-    bool eq_shape(const TensorShape& rhs) const;
+    MGE_WIN_DECLSPEC_FUC bool eq_shape(const TensorShape& rhs) const;
 
     //! check whether the shape can be treated as a scalar
     bool is_scalar() const { return ndim == 1 && shape[0] == 1; }
 
     //! check whether ndim != 0 and at least one shape is 0
-    bool is_empty() const;
+    MGE_WIN_DECLSPEC_FUC bool is_empty() const;
 
     //! access single element, without boundary check
     size_t& operator[](size_t i) {return shape[i]; }
@@ -168,8 +168,8 @@ struct TensorLayout : public TensorShape {
         class ImplBase;
 
 #if MEGDNN_CC_HOST
-        Format();
-        Format(DType dtype);
+        MGE_WIN_DECLSPEC_FUC Format();
+        MGE_WIN_DECLSPEC_FUC Format(DType dtype);

设想将来更好的解决办法:

  • 批改 CMake 自身源代码,让 flag WINDOWS_EXPORT_ALL_SYMBOLS 反对用户自定义 filter,让其生成的 exports.def 自身就是带用户过滤参数的

    • 当然因为 Windows 数据成员在 import 局部处还必须显式的加上 dllimport,对这块仿佛 CMake 也无能为力

      • 能够思考工程一开始设计时,API 尽可能的不要存在隐式的数据成员之间的拜访,尽可能的将其转换成一个函数 API

GitHub:MegEngine 天元

官网:MegEngine- 深度学习,简略开发

欢送退出 MegEngine 技术交换 QQ 群:1029741705

正文完
 0