因為工作業務需求的關係,需編譯onnxruntime引入專案中使用,主專案exe是使用的vs2017+qt5.12。
onnxruntime就不用介紹是啥了撒,在最佳化和加速AI機器學習推理和訓練這塊赫赫有名就是了。
有現成的別人編譯好的只有dll動態庫,當然我們顯然是不可能使用的,因為BOSS首先就提出一定要讓釋出出去的程式體積儘量變少,我肯定是無法精細的拆分哪一些用到了的,哪一些程式碼是沒用到的,還多次強調同時執行效率當然也要槓槓滴。
所以下面就開始描述這幾天一系列坎坷之路,留個記錄,希望過久了自己不會忘記吧,如果能幫助到某些同行少走些彎路也最好:
1. Clone repo
詫一聽你可能會覺得一個大名鼎鼎的Microsoft開源專案,又是在自家的Windows上編譯應該很簡單很容易吧?
的確我一開始也是這樣認為的,犯了太藐視的心態來對待他。
一開始就使用git clone下來程式碼,分支先鎖定在rel-1.13.1發行版上。
簡單cmake跑了下,這些就省略,發現少了一些external裡面的子專案的引用,
這當然難不倒我,簡單,git --recursive 重新下載所有的submodules。
礙於網路情況,等待許久時間下載到一半就斷了,後來給git設定了代理,下載完成。
2. CMake Configure
後來等待cmake跑先編譯純cpu的版本(不帶gpu並行運算加速)。
具體表現在cmake上是設定 onnxruntime_USE_CUDA 、onnxruntime_USE_TENSORRT、onnxruntime_USE_ROCM 等等一系列環境變數設定 False。
現在都忘記中間的過程了,反正自己鼓弄後來發現這步驟,最好是使用他所提供的一個python指令碼,內部去呼叫cmake生成專案。這個步驟在它onnxruntime的官方文件上有說。
大概就是用這個指令碼專案根目錄上的build.bat 去執行。
內部會呼叫 onnxruntime\tools\ci_build\build.py 去跑cmake,內部會用cmake Configure和Generate版本。
前提是很多引數需要提前設定好,比如說我是預先執行這一串命令:
set ZLIB_INCLUDE_DIR=D:\extlibs\zlib\build-msvc-static\install\include/
set ZLIB_LIBRARY=D:/extlibs/zlib/build-msvc-static/install/lib/
set ZLIB_LIBRARY_DEBUG=D:/extlibs/zlib/build-msvc-static/install/lib/zlibstaticd.lib
set ZLIB_LIBRARY_RELEASE=D:/extlibs/zlib/build-msvc-static/install/lib/zlibstatic.lib
set CUDA_HOME=F:\NVIDIA\CUDA\v11.6
set CUDNN_HOME=F:\NVIDIA\CUDNN\v8.5
set TENSORRT_HOME=F:\NVIDIA\TensorRT\v8.4.3.1
set HTTP_PROXY=http://127.0.0.1:10809 rem 你懂的
set HTTPS_PROXY=http://127.0.0.1:10809 rem 你懂的
然後執行:
build.bat --config RelWithDebInfo --skip_tests --parallel --cmake_generator "Visual Studio 15 2017"
中間肯定是不會一次成功的,相信我,後來再跑到cmake成功後,我用cmake-gui重新開啟剛剛generate好的project,發現有不少環境變數需要調整,比如一些test不需要,一些unit_test不需要,所有shared_lib都改成static_lib,還有absl需要使用自己編譯的,AVX2和AVX等CPU指令加速都開啟來。當然這些只是小試牛刀,後面還有更多隱晦的引數需要改動。
3. 開始用vs2017編譯
一切都生成sln解決方案檔案之後,開始編譯,大概等待了30分鐘之後,發現編譯錯誤:
看了下報錯這個地方程式碼,大概我判斷是因為專案使用了一些 c++17 的新特性,但是編譯器不認識,所以就報奇怪的{ }大括號錯了,又有使用了nodiscard關鍵字,遂返回去CMake將這兩個變數設定成如圖所示:
查了下vs2017是支援c++17的,滿懷欣喜的喝了一口茶,回來看還是一樣錯誤,後來就在折騰cppreference,和模擬onnxruntime專案上那種寫法去(c++17的noexcept用法)測試,為什麼vs2017上那樣使用noexcept就會報錯,用線上的gcc和clang都可以,在vs2017上就是不行,擺脫喂,c++17既然支援不完整,那就別說支援呀,最後直接放棄vs2017了。
後來不想折騰了,重新下載安裝了vs2022,等待許久,還好公司網路還好,線上安裝下載vs速度都是10MB/s左右跑滿百兆,這裡充分體現了網速對工作效率的重要性XD。
4. 改用vs2022編譯cpu版本
vs2022安裝好後,直接修改了下build.bat引數
build.bat --config RelWithDebInfo --skip_tests --parallel --cmake_generator "Visual Studio 17 2022"
然後重新走上面的流程,直接就很順利的編譯完了所有lib檔案,竟很訝異,隱隱覺得事情絕對沒有這麼簡單。
體積也還很小哦。
5. 加入CUDA + cuDNN + TensorRT環境
這裡有一個坑,必須必須要按照文件所說的版本號去對照,不能什麼都去直接裝最新版!
打個比方如果你打算編譯的是onnxruntime 1.13.1
那麼能夠與之搭配的TensorRT版本就是8.4.*.*,
TensorRT又依賴的cuDNN版本是8.5.0.96 和 CUDA 11.4,如果你安裝的是CUDA10或者CUDA12,那麼將會在CUDA編譯的時候報錯各種函式找不到!
ONNX Runtime TensorRT CUDA版本對應表:
https://onnxruntime.ai/docs/execution-providers/TensorRT-ExecutionProvider.html
ONNX Runtime CUDA cuDNN版本對應表:
https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements
TensorRT components版本對應表:
https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-843/install-guide/index.html
這裡放出我當初記錄的下載地址:
CUDA各版本下載地址:
https://developer.nvidia.com/cuda-toolkit-archive
cuDNN各版本下載地址:
https://developer.nvidia.com/rdp/cudnn-archive
TensortRT各版本下載地址:
https://developer.nvidia.com/nvidia-tensorrt-download
TensorRT依賴庫版本對應表:
https://docs.nvidia.com/deeplearning/tensorrt/release-notes/tensorrt-8.html#rel-8-4-3
TensortRT文件手冊:
https://docs.nvidia.com/deeplearning/tensorrt/archives/index.html#trt_8
cuDNN安裝教程:
https://docs.nvidia.com/deeplearning/cudnn/install-guide/index.html#installdriver-windows
TensortRT安裝教程:
https://docs.nvidia.com/deeplearning/tensorrt/install-guide/index.html#installing-zip
下載之後 最後分別按照順序(先CUDA、再CUDNN,最後TensorRT)安裝解壓到磁碟上。
最後設定環境變數
CUDA_HOME=F:\NVIDIA\CUDA\v11.6
CUDA_PATH=F:\NVIDIA\CUDA\v11.6
CUDA_PATH_V11_6=F:\NVIDIA\CUDA\v11.6
CUDNN_HOME=F:\NVIDIA\CUDNN\v8.5
TENSORRT_HOME=F:\NVIDIA\TensorRT\v8.4.3.1
PATH+=F:\NVIDIA\CUDA\v11.6\bin
PATH+=F:\NVIDIA\CUDA\v11.6\libnvvp
PATH+=F:\NVIDIA\CUDNN\v8.5\bin
PATH+=F:\NVIDIA\TensorRT\v8.4.3.1\lib
然後勾上CMake上的這三個變數,再重新generate
onnxruntime_USE_CUDA
onnxruntime_USE_TENSORRT
onnxruntime_USE_TENSORRT_BUILTIN_PARSER
其中ROCm和MiGraphX、oneDNN和OpenVINO應該也類似,我沒去試。
另外放出所有onnxruntime C++它支援的硬體加速庫有這些:
CoreML(Apple Machine Learning framework)
cuDNN(NVIDIA CUDA(Compute Unified Device Architecture) Deep Neural Network Library)
Direct ML(Microsoft DirectX 12 library for machine learning)
NNAPI(Android Neural Networks API)
oneDNN(Intel oneAPI Deep Neural Network Library)
OpenVINO(Open Visual Inference and Neural network Optimization)
SNPE(Qualcomm Snapdragon Neural Processing Engine SDK)
TensorRT(NVIDIA Machine Learning framework)
ACL(Arm Compute Library)
ArmNN(machine learning inference engine for Android and Linux)
CANN(HuaWei Compute Architecture for Neural Networks)
MIGraphX(AMD's graph inference engine that accelerates machine learning model inference)
ROCm(AMD's Open Software Platform for GPU Compute)
6. 測試專案中去連結靜態庫
將lib檔案複製過去qt寫的demo,pro上引入lib檔案和include檔案。
pro加入c++17的支援:CONFIG += c++17
加入:
#Dbghelp.lib
LIBS += -lDbghelp
#Advapi32.lib
LIBS += -lAdvapi32
這些全部配好之後,以編譯demo專案,報LNK2001連結錯誤,如圖所示:
Google查了下這個錯誤是因為 Modi Mo 這傢伙乾的,從2019年之後有一個vcrt新特性,c++的exception在x64 runtime下的libraries檔案結構進行了調整,可以節約了exe大量體積。
詳見:https://devblogs.microsoft.com/cppblog/making-cpp-exception-handling-smaller-x64/
所以我使用vs2022編譯的lib檔案,在vs2017的exe中去引用,會報 __CxxFrameHandler4 不相容的錯誤。
在這篇blog下,還不乏各種因為失去相容性而帶來的抱怨之聲,更多的是問如果關閉這個特性。
按照他的方法,我在CMake中將CMAKE_CXX_FLAGS 末尾加上 -d2FH4-
以及在CMake的 LINKER_FLAGS相關的幾個末尾加上 -d2:-FH4-
設定了這個標誌之後再重新編譯,再重新copy到demo專案中去引用,就沒報__CxxFrameHandler4找不到符號的連結錯誤了。
只能庫比程式vcrt版本低(向前相容),但是不能反過來程式比庫版本還低
但是又變成報另外一種奇奇怪怪的連結錯誤,如下圖所示:
7. 改用vs2019編譯
查了下上面的 __imp__std_init_once_complete的錯誤原因,網上基本上說的就是因為vcrt版本不一致所導致的錯誤。
解決辦法就只有統一使用同一種編譯器進行編譯,我簡直要醉了。
我裝個vs2019再改這兩個編譯flag試試吧,畢竟vs2022和vs2017間隔太遠了,誰知道期間除了__CxxFrameHandler4還有別的什麼二進位制不相容的問題。
漫長的下載安裝等待中,結束之後,裡面去使用vs2019重新走一遍之前的操作,居然一樣,還是報跟2022一樣的錯誤,看來vcrt2017到2019發生了重大更新呀。
那麼我嘗試將有報__imp__std_init_once_complete錯誤的專案更改他們的編譯器(平臺工具集),部分專案降級修改成Visual Studio 2017(v141),
再嘗試編譯。
錯誤:LINK : error LNK1218: 警告被視為錯誤;未生成輸出檔案
警告:warning LNK4221: 此物件檔案未定義任何之前未定義的公共符號,因此任何耗用此庫的連結操作都不會使用此檔案
查了原因是因為,官方解釋:
是因為vs2017比較嚴格一些?這個會報警告出來,查了下有兩種解決方法,
一是將這個警告忽略,即在【配置屬性】 【C/C++】 【高階設定】中將這個警告禁用掉:
第二種是去【配置屬性】 【C/C++】 【常規】,有一個選項叫’將警告視為錯誤‘,將其設定成【否】。
但是我按照這樣操作一波,仍然不行,重啟了vs也不行,難道這是遇到vc的bug了?仔細看錯誤資訊發現就是那個cpuid_uarch.cc和string_view.cc檔案有問題,閱讀了下程式碼,發現是有一個宏,只在arm環境下會編譯,在win下應該不會產生任何函式和類的實現體,相當於是一個空的namespace而已。
我修改它這個檔案,把它的#ifdef 從namespace裡面移到上一級namespace外面來,就解決這個問題了。
這樣就還剩下只有protobuf的庫的靜態連結沒有解決。onnx_proto.lib(onnx-ml.pb.obj) : error LNK2001: 無法解析的外部符號 "public: virtual class google::protobuf::MessageLite * __cdecl google::protobuf::MessageLite::New(class google::protobuf::Arena *)const " (?New@MessageLite@protobuf@google@@UEBAPEAV123@PEAVArena@23@@Z)
onnx_proto.lib(onnx-data.pb.obj) : error LNK2001: 無法解析的外部符號 "public: virtual class google::protobuf::MessageLite * __cdecl google::protobuf::MessageLite::New(class google::protobuf::Arena *)const " (?New@MessageLite@protobuf@google@@UEBAPEAV123@PEAVArena@23@@Z)
onnx_proto.lib(onnx-ml.pb.obj) : error LNK2001: 無法解析的外部符號 "public: virtual void __cdecl google::protobuf::Message::DiscardUnknownFields(void)" (?DiscardUnknownFields@Message@protobuf@google@@UEAAXXZ)
onnx_proto.lib(onnx-data.pb.obj) : error LNK2001: 無法解析的外部符號 "public: virtual void __cdecl google::protobuf::Message::DiscardUnknownFields(void)" (?DiscardUnknownFields@Message@protobuf@google@@UEAAXXZ)
debug\videoonnxdemo.exe : fatal error LNK1120: 2 個無法解析的外部命令
看了下專案的CMakeLists裡面似乎有個註釋
還有一段話:The onnxruntime_PREFER_SYSTEM_LIB is mainly designed for package managers like apt/yum/vcpkg.
Please note, by default Protobuf_USE_STATIC_LIBS is OFF but it's recommended to turn it ON on Windows. You should set it properly when onnxruntime_PREFER_SYSTEM_LIB is ON otherwise you'll hit linkage errors.
If you have already installed protobuf(or the others) in your system at the default system paths(like /usr/include), then it's better to set onnxruntime_PREFER_SYSTEM_LIB ON. Otherwise onnxruntime may see two different protobuf versions and we won't know which one will be used, the worst case could be onnxruntime picked up header files from one of them but the binaries from the other one.
機翻譯過來就是onnxruntime_PREFER_SYSTEM_LIB 主要是為像 apt/yum/vcpkg 這樣的包管理器設計的。
請注意,預設情況下 Protobuf_USE_STATIC_LIBS 是關閉的,但建議在 Windows 上將其開啟。 當 onnxruntime_PREFER_SYSTEM_LIB 為 ON 時,您應該正確設定它,否則您會遇到連結錯誤。
如果您已經在系統中的預設系統路徑(如 /usr/include)安裝了 protobuf(或其他),那麼最好將 onnxruntime_PREFER_SYSTEM_LIB 設定為 ON。 否則 onnxruntime 可能會看到兩個不同的 protobuf 版本,我們不知道將使用哪個版本,最壞的情況可能是 onnxruntime 從其中一個獲取標頭檔案,但從另一個獲取二進位制檔案。
發現雖然沒有開啟onnxruntime的ONNX_USE_LITE_PROTO選項,
(實測,如果開啟這個選項是開啟不了的,每次Configure之後又會被改成False)
但是連結的libprotobuf仍然會引用到幾個lite相關的函式,所以有此連結錯誤。
因此手動開啟\external\protobuf\cmake\protobuf.sln,進行手動編譯。
很順利就編譯成功,最終生成lite相關的lib檔案:
但是加入編譯lite之後,libprotobuf和libprotobuf-lite和同時都引用到專案中來,會報很多很多函式符號重複定義的問題,如下圖所示:
顯然不能兩個同時使用,但是現在問題來了
如果單獨libprotobuf使用,會報告缺少一些lite裡面的符號(報4個錯誤)。
如果libprotobuf、libprotobuf-lite兩個同時使用,會報告很多函式符號重複定義。
如果單獨引入libprotobuf-lite的話,報告函式符號重複定義的更多。
兩個都不引用的話,會跟只引用libprotobuf一樣,缺少幾個lite相關的符號(報4個錯誤)。
所以,專心來解決第一種情況,畢竟錯誤最少XD。。。
搜尋全工程專案中,找到 DiscardUnknownFields() 定義的地方。
是在libprotoc的 cpp_message.cc 和 message.cc 有提及到。
其中 cpp_message.cc 有一段話:
DiscardUnknownFields() is implemented in message.cc using reflections. (DiscardUnknownFields() 在 message.cc 中使用反射實現。)
We need to implement this function in generated code for messages. (我們需要在生成的訊息程式碼中實現此功能。)
靠靠,你用反射是爽,有沒有考慮c++靜態連結的問題啊?
難怪連onnxruntime的開發者都沒解決protobuf在windows下的靜態編譯問題。(指的上文有一張CMakeLists裡面有個註釋)
目前卡在這個地方。。。在想要不要考慮改成protobuf的shared_lib方式來妥協?
看Github上有一個Issue關於這個protobuf static libraries or shared libraries的問題:
https://github.com/microsoft/onnxruntime/issues/12867
最後大功告成,demo連結錯誤完全不報了。
這時候看到 error 0, warning 0 有一種異樣到無以言表的感覺。
全篇完結。