MegEngine Windows Python wheel 包減肥之路
作者:張浩龍 | 曠視科技 MegEngine 架構(gòu)師
寫在之前
本文的目的
通過(guò)講述在支持?MegEngine Windows Python wheel (https://github.com/MegEngine/MegEngine/blob/master/scripts/whl/windows/windows_build_whl.sh)過(guò)程中遇到的問(wèn)題以及解決問(wèn)題的流程,此文最后的解決方法可能不是最優(yōu),歡迎留言指正。
過(guò)程中順便科普一些關(guān)于 MegEngine 的構(gòu)建以及構(gòu)建時(shí)用到的基礎(chǔ)東西,當(dāng)然這些基礎(chǔ)知識(shí)我相信是工程之道經(jīng)常會(huì)用到的,包括但不限于:
CMake(https://cmake.org/)
編譯、鏈接、符號(hào)隱藏,符號(hào) export 等。此處先推薦一本 “老書”《程序員的自我修養(yǎng)》,自然它沒(méi)有 xxx 四庫(kù)全書讓人絞盡腦汁,但是它里面的基礎(chǔ)知識(shí)依然是目前我們和計(jì)算機(jī)“交流”中經(jīng)常遇到的。
Python wheel 包構(gòu)建
MegEngine?各平臺(tái)支持情況
cpp 推理支持情況:

TEE:https://en.wikipedia.org/wiki/Trusted_execution_environment
訓(xùn)練:
Python側(cè):

目前官方發(fā)布的 wheel 包,只有 Windows-X64-CPU-CUDA,many Linux 64bit -X64-CPU-CUDA,MacOS-X64-CPU,其他的可自己編譯,或者社區(qū)提單索取。
cpp 側(cè)訓(xùn)練支持情況和上面的 cpp 推理情況一致。
從上面的情況,可看見(jiàn) MegEngine 無(wú)論訓(xùn)練還是推理,還是各種硬件,還是各種 OS 都支持的非常全面,如有需求,不妨試用?。。?!
遇到的問(wèn)題
為了全面的支持上面提到的 MegEngine 各個(gè)平臺(tái),各個(gè) OS,期間或多或少會(huì)遇到一些問(wèn)題,比如 Windows 平臺(tái)上 Python wheel 包體積過(guò)大。
先看一下目前 MegEngine wheel 包體積大小,摘自 1.7 版本?pypi(https://pypi.org/project/MegEngine/1.7.0/#files)

其中因?yàn)?Linux 和 Windows 支持了 CUDA,所以包體積在 900MB 左右,這是一個(gè)正常的 size。
在之前 Windows CUDA 包體積在 1.7G 左右:這就是后面嘗試分析和修復(fù)的問(wèn)題。
先對(duì)問(wèn)題?MECE?一下
MECE 是 Mutually Exclusive Collectively Exhaustive 縮寫,中文意思是“相互獨(dú)立,完全窮盡”。也就是對(duì)于一個(gè)問(wèn)題的議題,能夠做到不重疊、不遺漏的分類,而且能夠藉此有效把握問(wèn)題的核心,并解決問(wèn)題的方法。強(qiáng)調(diào)兩點(diǎn):
各部分之間相互獨(dú)立(MutuallyExclusive)
化簡(jiǎn)后, 感覺(jué)就是分析問(wèn)題時(shí),可能的方法要盡可能的獨(dú)立,盡量不要有交集
所有部分完全窮盡(CollectivelyExhaustive)
化簡(jiǎn)后, 感覺(jué)就是分析問(wèn)題時(shí),盡可能的要把方法想全,盡量不要有遺漏
為啥會(huì)選擇一條魚呢:魚頭附近一般都比較大(胖),而越往魚尾走,會(huì)越來(lái)越小(瘦),從而希望通過(guò)這個(gè)流程“Windows wheel 減肥”之路,達(dá)到減肥的目的。

問(wèn)題的影響
這樣的體積會(huì)有什么問(wèn)題呢(畢竟人太胖也會(huì)有些副作用 xxx etc.)
首先就是體積超過(guò)了我們申請(qǐng)的?pypi?上單個(gè)文件最大體積限制
給用戶體驗(yàn)不好, 為什么相同的版本, Window 比 Linux大這么多呢
Windows 上顯存比 Linux 占用大很多(估計(jì)提到這點(diǎn),大家已經(jīng)猜測(cè)到問(wèn)題所在)
解決這個(gè)問(wèn)題可能需要的相關(guān)知識(shí)
問(wèn)題給人的第一印象是:
編譯構(gòu)建相關(guān)的
Python wheel 打包相關(guān)的
Windows OS 獨(dú)有的
先就 MegEngine 如下基礎(chǔ)知識(shí)做一些基礎(chǔ)補(bǔ)充(減肥前總得有一些科普吧,到底是吃藥還是鍛煉,或者具體到吃什么藥吧)
MegEngine CMake 構(gòu)建流程
MegEngine 構(gòu)建依賴?CMake(https://cmake.org/)?和?Ninja(https://ninja-build.org/)
其中 CMake 描述主要在:

頂層 CMakeList(https://github.com/MegEngine/MegEngine/blob/master/CMakeLists.txt)
此文件包含很多的 option,主要用于控制是否編譯一些模塊,比如是否打開(kāi) MGE_BUILD_WITH_ASAN 用于調(diào)試內(nèi)存問(wèn)題
此文件包含對(duì)各種 ARCH 的適配控制, 比如編譯 X86_64 還是 AARCH64 等
此文件包含對(duì)各種 OS 的適配, 比如是編譯 Linux,Android 還是 Windows 等
以及一些雜項(xiàng)配置,比如有優(yōu)化等級(jí),比如 CUDA SM 的配置等
src CMakelist(https://github.com/MegEngine/MegEngine/blob/master/src/CMakeLists.txt)
此文件包含了 MegEngine 核心代碼 MegBrain 層所有源代碼的編譯管理
dnn CMakelist(https://github.com/MegEngine/MegEngine/blob/master/dnn/src/CMakeLists.txt)
此文件包含了 MegEngine 核心代碼 dnn (主要實(shí)現(xiàn)各種 backends)層所有源代碼的編譯管理
一些雜項(xiàng) CMakelist
各種 example,比如?lite example(https://github.com/MegEngine/MegEngine/blob/master/lite/example/CMakeLists.txt)
各種 test,比如?megbrain test(https://github.com/MegEngine/MegEngine/blob/master/test/CMakeLists.txt)
各種 helper,見(jiàn)?helper module(https://github.com/MegEngine/MegEngine/tree/master/cmake)
執(zhí)行?host_build.sh?來(lái)進(jìn)行 host 編譯,同時(shí)它會(huì)在 build dir 生成整個(gè)構(gòu)建依賴描述文件 build.ninja
更多的編譯支持請(qǐng)參考?BUILD_README.md
build.ninja 文件功能類似?GNU Makefile
有了 build.ninja 后,便可進(jìn)行一些調(diào)試
其中 Ninja 提供豐富的可視化調(diào)試功能,下面列舉如何通過(guò) Ninja debug server 來(lái)看 MegEngine 部分模塊的構(gòu)建依賴
可通過(guò)如下命令來(lái)啟動(dòng)一個(gè)可視化 server (當(dāng)然熟悉后,也可通過(guò)其他命令行參數(shù)來(lái)調(diào)試,自然也可以直接看 CMakeLists.txt 來(lái)找關(guān)系)
然后通過(guò)在瀏覽器輸入: you_ip:you_port 進(jìn)行可視化瀏覽了
還可以通過(guò) dot 生成 png 來(lái)查看
比如 megenginelite 一個(gè)最上層的目標(biāo):
liblite_shared_whl.so
?它的依賴圖如下

MegEngine wheel 構(gòu)建流程
有了上面 Ninja 編譯出來(lái)的各種庫(kù)后,我們就可以將它們和 MegEngine 中的 py src 一起進(jìn)行打包,最后生成可安裝,可分發(fā)的 Python wheel 包了

構(gòu)建流程主要說(shuō)明在?BUILD_PYTHON_WHL_README(https://github.com/MegEngine/MegEngine/blob/master/scripts/whl/BUILD_PYTHON_WHL_README.md)
包 setup 入口在?python wheel setup(https://www.python.org/dev/peps/pep-0571/)
調(diào)用 setup 前各個(gè) OS 的準(zhǔn)備差異化在?wheel scripts(https://github.com/MegEngine/MegEngine/tree/master/scripts/whl)
有人可能會(huì)問(wèn),為什么不使用?auditwheel?來(lái)自動(dòng)管理 wheel 包中的 so 依賴,有兩個(gè)原因
當(dāng)你執(zhí)行 python3 -m pip install megengine -f?https://megengine.org.cn/whl/mge.html?后,可以import megengine,也可以import megenginelite,是因?yàn)?megengine 和 megenginelite 均會(huì)存在安裝的包中,且他們會(huì)復(fù)用 megengine_shared 這一體積超大的庫(kù)
auditwheel 不支持所有的操作系統(tǒng),比如 Windows
auditwheel 不支持依賴庫(kù)使用 dlopen 的情況
auditwheel 不支持 subpackage 的 wheel 包
描述了目前 MegEngine python wheel 的支持狀態(tài)
自己本地構(gòu)建需要的一些 env 準(zhǔn)備
一些使用說(shuō)明
MegEngine wheel 包遵從?pep-0571
MegEngine 構(gòu)建上如何適配 Windows
上面介紹了 MegEngine 基于 CMake 的構(gòu)建基礎(chǔ)和使用 Ninja 自帶的調(diào)試功能以及幫我們從宏觀了解了一下 project 的編譯依賴和進(jìn)行一些常規(guī)調(diào)試,下面再介紹一下 MegEngine 是如何適配 Windows 平臺(tái)的。
首先 MegEngine 大部分的源代碼都是 c++,且 cpp 推理要求是
c++14
,編譯 Python 訓(xùn)練要求是?c++17
各家編譯器其實(shí)對(duì)這些標(biāo)準(zhǔn)實(shí)現(xiàn)不是完全一致的,拋開(kāi)和系統(tǒng)相關(guān)的,比如?POSIX?外,其實(shí)還有比較多基本的上層用法各家編譯器其實(shí)時(shí)不太兼容的,特別是明顯的是 gcc 和 clang 能編譯過(guò)的代碼,Windows cl.exe 其實(shí)是編譯不過(guò)的。
為了解決上面提到的兩個(gè)問(wèn)題
盡可能的拋棄 cl.exe,Windows 上使用?llvm-clang-cl?進(jìn)行構(gòu)建
當(dāng)然因?yàn)?CUDA 的原因,目前不可能完全拋棄 cl.exe,在編譯 CUDA host 代碼時(shí),依然使用 cl.exe
區(qū)分開(kāi)系統(tǒng)相關(guān)的函數(shù)實(shí)現(xiàn), 所以你會(huì)在 MegEngine 代碼中看到不少如下類似的代碼
再加上,上面提到的 CMake, Ninja 本身是跨平臺(tái)的,這樣一組合,MegEngine 便原生支持了Windows,注意不是基于?WSL?的哦
問(wèn)題簡(jiǎn)單的分析
在上面“MegEngine CMake 構(gòu)建流程”小節(jié)中,我們提到了 Ninja debug server 能夠幫忙可視化整個(gè)構(gòu)建組件的依賴關(guān)系,下面我們補(bǔ)充一下在問(wèn)題修復(fù)前 Windows 和Linux 下?imperative?依賴的可視化結(jié)果。
Linux 下

Windows 下

Windows 和 Linux 下最大的差異化如下:
Linux 下 imperative 是依賴的 libmegengine_shared.so
Windows 下 imperative 是依賴的 megbrain 和 megdnn,又因?yàn)?megbrain 和 dnn 在 CMake 這邊其實(shí)一個(gè) OBJECT,所以相當(dāng)于直接依賴他們的 .obj 了
初步結(jié)論:
MegEngine wheel 包,有兩個(gè) Python module 接口
當(dāng)你使用 MegEngine 完成訓(xùn)練模型后,可參考?部署?文檔使用 MegEngineLite,快速將你的模型部署落地。
imperative: 當(dāng)你?安裝?完成 MegEngine 時(shí),在 Python 中輸入 import megengine 時(shí)。加載的就是它,我們提供了一些入門的教程供您快速上手 MegEngine(https://megengine.org.cn/doc/stable/zh/getting-started/quick-start.html)
MegEngine 用于 python 側(cè)訓(xùn)練的基礎(chǔ)接口
MegEngineLite:易用的 cpp,Python 推理接口
當(dāng)你使用 MegEngine 完成訓(xùn)練模型后,可參考?部署?文檔使用 MegEngineLite,快速將你的模型部署落地。(https://megengine.org.cn/doc/stable/zh/user-guide/deployment/lite/index.html)
因?yàn)橛羞@兩個(gè)頂層構(gòu)建目標(biāo)的存在,且他們?cè)?Windows 和其他 OS 上,依賴底層的目標(biāo)不同,導(dǎo)致了問(wèn)題的產(chǎn)生
為什么不同的依賴關(guān)系,會(huì)產(chǎn)生這么大的體積區(qū)別呢,先看一張 MegEngine 的架構(gòu)圖

從下往上依次是:
MegEngine 不同 backends 的差異化實(shí)現(xiàn)被封裝到了 dnn(對(duì)應(yīng)到上圖的“硬件層”,對(duì)應(yīng)到 CMakeList 中的 megdnn 模塊), 而其中 CUDA backends 因?yàn)橛写罅康?kernel以及對(duì)較多的 SM 支持,會(huì)對(duì)整個(gè)庫(kù)或者可執(zhí)行程序體積產(chǎn)生大量的體積貢獻(xiàn)
圖中“硬件抽象層”,部分“核心組件層”,對(duì)應(yīng) CMakeList 中的 megbrain 模塊
在往上“接口層”,對(duì)應(yīng) CMakeList 中的 imperative 和 MegEngineLite 模塊。
從下往上依次是:
由于上述的原因,在 Windows 平臺(tái), imperative 模塊和 MegEngineLite 模塊會(huì)同時(shí)靜態(tài)依賴 dnn 和 megbrain 代碼,導(dǎo)致體積幾乎翻倍。問(wèn)題修復(fù)前的依賴圖:

可能的解決方案
通過(guò)上面的分析,問(wèn)題原因已經(jīng)找到,再來(lái)猜想一下
為什么 Windows 平臺(tái)上和其他平臺(tái)目標(biāo)依賴有差異
Windows class member 不能隱式的被export,需要顯式的使用 dllexport 和 dllimport,詳細(xì)見(jiàn)?Microsoft Specific
dnn,megbrain 層有大量的 data 數(shù)據(jù)訪問(wèn)并沒(méi)有抽象成函數(shù),而是需要直接訪問(wèn)數(shù)據(jù)成員
下面舉一個(gè)栗子來(lái)說(shuō)明跨 dll 動(dòng)態(tài)庫(kù)訪問(wèn)數(shù)據(jù)成員方式的差異,主要包含三支文件
api.h 和 api.c 實(shí)現(xiàn)函數(shù) func_a 和 定義一個(gè)變量 a,被編譯成動(dòng)態(tài)庫(kù) dll
client.c 會(huì)調(diào)用上面 api.c 實(shí)現(xiàn)方法和訪問(wèn)變量 a跨 dll 動(dòng)態(tài)庫(kù)直接訪問(wèn)數(shù)據(jù)成員方式
跨 dll 動(dòng)態(tài)庫(kù)通過(guò)函數(shù)訪問(wèn)數(shù)據(jù)成員方式
改造上面的 example 代碼, 把其中變量 a 的訪問(wèn)封裝到一個(gè)函數(shù)
可以看見(jiàn)在 Windows 上函數(shù)符號(hào)和數(shù)據(jù)成員符號(hào) export 是等價(jià)的,但是 import data要求要嚴(yán)格的多
在 Linux, MacOS下, 函數(shù)符號(hào)和數(shù)據(jù)成員符號(hào) export 屬性是等價(jià)的
由于上面提到種種限制,導(dǎo)致在最初支持 Windows 平臺(tái)時(shí),所有的上層目標(biāo)(MegEngine,MegEngineLite)都必須靜態(tài)依賴 megbrain 和 dnn。
既然問(wèn)題原因已經(jīng)找到,需要修復(fù)這個(gè)問(wèn)題的目標(biāo)就變的非常清晰了:讓 megengine_shared 動(dòng)態(tài)庫(kù) (dll) 在 Windows 平臺(tái)上可用。
列舉一下可能的方案:
方案一:CMake 自帶的 WINDOWS_EXPORT_ALL_SYMBOLS
(stage a): 生成 CMakeFiles/megengine_export.dir/exports.def.objs,本質(zhì)是 obj 的集合
(stage b): 插入 PRE_LINK stage 生成 CMakeFiles/megengine_export.dir/exports.def (此文件類似 gcc/clang -Wl,--version-script)
(stage c): LINK_FLAG 自動(dòng)插入 /DEF exports.def
CMake 提供了對(duì) stage a output的 hook,意思是可以修改exports.def.objs,但是沒(méi)有機(jī)會(huì)修改 exports.def
加入 hook command,把 exports.def.objs 中所有DNN 的obj 刪除,想象中應(yīng)該可以了
但是 imperive和megenginelite,不僅僅是和 megbrain 打交到,很多直接使用 dnn 的接口和數(shù)據(jù)成員
結(jié)論:不太適用 MegEngine 這類“大”工程
原因:MegEngine 符號(hào)太多,超過(guò)了 link.exe max symbols 65536 的限制 (使能 CUDA 時(shí),大約有 1.7W 個(gè)符號(hào))
分析 CMake WINDOWS_EXPORT_ALL_SYMBOLS 的原理,能否中間加一些 hook 來(lái)過(guò)濾不需要 export 的符號(hào),以達(dá)到類似gcc/clang -Wl,--version-script 的效果,cmake 對(duì)他的處理邏輯:
方案二:“優(yōu)化”版本的 WINDOWS_EXPORT_ALL_SYMBOLS
保留必要的 symbols
CMakeList 中目標(biāo)依賴修改過(guò)后的exports.def (讓其符號(hào)不超過(guò) 65536)
結(jié)論:不可行
Windows cl linker.exe 不支持 * 通配符,不支持存放一個(gè)不存在的符號(hào),導(dǎo)致一旦放了固定的 exports.def,稍微更改一個(gè)編譯參數(shù),或者加點(diǎn)代碼,都會(huì)編譯不過(guò)
如上面分析 WINDOWS_EXPORT_ALL_SYMBOLS 有一定的缺陷,會(huì)把所有的 obj 的符號(hào)全部export,那能不能手動(dòng)修改 WINDOWS_EXPORT_ALL_SYMBOLS 生成的 exports.def
方案三:到最后發(fā)現(xiàn)沒(méi)有一個(gè)“偷懶的”方式來(lái)解決這個(gè)問(wèn)題,回退到最 naive 的方式
修復(fù)示例, 完整修改見(jiàn)?PR(https://github.com/MegEngine/MegEngine/commit/25ec2530bab350b0809cfe22449451f591d5ffb8)
把 megbrain、dnn、megenginelite 對(duì)外暴露的 API 依賴的成員符號(hào)全部顯式的加上declspec(dllexport) 和 declspec(dllimport) 屬性描述
想象未來(lái)更好的解決方法:
修改 CMake 本身源代碼,讓 flag WINDOWS_EXPORT_ALL_SYMBOLS 支持用戶自定義filter,讓其生成的exports.def 本身就是帶用戶過(guò)濾參數(shù)的
可以考慮工程一開(kāi)始設(shè)計(jì)時(shí),API 盡可能的不要存在隱式的數(shù)據(jù)成員之間的訪問(wèn),盡可能的將其轉(zhuǎn)換成一個(gè)函數(shù) API
當(dāng)然因?yàn)?Windows 數(shù)據(jù)成員在import 部分處還必須顯式的加上 dllimport,對(duì)這塊似乎 CMake 也無(wú)能為力
更多 MegEngine 信息獲取,您可以查看:
文檔:https://www.megengine.org.cn/doc/stable/zh/?
GitHub 項(xiàng)目:https://github.com/MegEngine
加入 MegEngine 用戶交流 QQ 群:1029741705
歡迎參與 MegEngine 社區(qū)貢獻(xiàn),成為 Awesome MegEngineer: https://www.megengine.org.cn/community-AMGE,榮譽(yù)證書、定制禮品享不停。