iT邦幫忙

2023 iThome 鐵人賽

DAY 30
1
Software Development

30 天 CMake 跨平台之旅系列 第 30

[Day 30] Real-world example - PyTorch

  • 分享至 

  • xImage
  •  

本日內容

  • CMakeLists.txt
  • cmake Module Directory
  • torch Library
  • Platform-Specific Build Scripts
  • 完賽感言

今天就是最後一篇啦, 我們來欣賞一下大型 CMake 專案的設計吧
以 PyTorch 為例, 看看該專案用了哪些工具, 是否有前幾天沒有涵蓋的部分

我們會用 PyTorch v2.0.1 來介紹
由於 PyTorch 也支援其他 build system 如 bazel, 我們僅著重在 cmake 的部分

CMakeLists.txt

我們先從專案最上層的 CMakeLists.txt 開始
先來看看有多少行

wc -l CMakeLists.txt
  1228 pytorch/CMakeLists.txt

可以看到總共有 1,228 行, 很大的一個 build script
我們挑重點看

  • cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
    • 最低要求 CMake 3.18 版以上, 否則停止執行並丟出 error
  • cmake_policy() 系列
    • 此系列沒有介紹, 但主要用途是相容性考量
    • 比如 cmake_policy(CMP0010), 是針對 CMake2.6 以前 variable reference 的行為, 見 CMP0010
    • 相關的 cmake policy 都可以在文件找到用途
project(Torch CXX C)

if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
  set(LINUX TRUE)
else()
  set(LINUX FALSE)
endif()
  • 支援 C / C++, 且看起來針對 Linux 會有不同的設定
# check and set CMAKE_CXX_STANDARD
string(FIND "${CMAKE_CXX_FLAGS}" "-std=c++" env_cxx_standard)
if(env_cxx_standard GREATER -1)
  message(
      WARNING "C++ standard version definition detected in environment variable."
      "PyTorch requires -std=c++17. Please remove -std=c++ settings in your environment.")
endif()
set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard whose features are requested to build this target.")
set(CMAKE_C_STANDARD   11 CACHE STRING "The C standard whose features are requested to build this target.")
  • 前面有提過, CMAKE_<LANG>_STANDARD 僅列出此專案 建議 的 standard, 即使不符合仍會向下找到最近的版本去 build, 除非搭配 CMAKE_<LANG>_STANDARD_REQUIREDCMAKE_<LANG>_EXTENSION
  • 不過字串看起來沒有硬性要求 c++17 和 c11, 不過字串看起來是必要的, 下面有解答
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
  include(cmake/CheckAbi.cmake)
  string(APPEND CMAKE_CXX_FLAGS " -D_GLIBCXX_USE_CXX11_ABI=${GLIBCXX_USE_CXX11_ABI}")
  string(APPEND CMAKE_CUDA_FLAGS " -D_GLIBCXX_USE_CXX11_ABI=${GLIBCXX_USE_CXX11_ABI}")
  if(${GLIBCXX_USE_CXX11_ABI} EQUAL 1)
    set(CXX_STANDARD_REQUIRED ON)
  else()
    # Please note this is required in order to ensure compatibility between gcc 9 and gcc 7
    # This could be removed when all Linux PyTorch binary builds are compiled by the same toolchain again
    include(CheckCXXCompilerFlag)
    append_cxx_flag_if_supported("-fabi-version=11" CMAKE_CXX_FLAGS)
  endif()
endif()
  • Linux 的話會額外檢查 abi

中間非 Windows 和 Linux 的設定先跳過
再來就是各種 PyTorch 提供的 build options

然後可以看到支援 ccache

if(USE_CCACHE)
  find_program(CCACHE_PROGRAM ccache)
  if(CCACHE_PROGRAM)
    set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE STRING "C compiler launcher")
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE STRING "CXX compiler launcher")
    set(CMAKE_CUDA_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE STRING "CUDA compiler launcher")
  else()
    message(STATUS "Could not find ccache. Consider installing ccache to speed up compilation.")
  endif()
endif()

中間針對不同 generator 的設定也跳過, 來看安裝 shared library 的部分

# Note(jiayq): when building static libraries, all PRIVATE dependencies
# will also become interface libraries, and as a result if there are any
# dependency libraries that are not exported, the following install export
# script will fail. As a result, we will only provide the targets cmake
# files for shared lib installation. For more info, read:
# https://cmake.org/pipermail/cmake/2016-May/063400.html
if(BUILD_SHARED_LIBS)
  configure_file(
      ${PROJECT_SOURCE_DIR}/cmake/Caffe2Config.cmake.in
      ${PROJECT_BINARY_DIR}/Caffe2Config.cmake
      @ONLY)
  install(FILES
      ${PROJECT_BINARY_DIR}/Caffe2Config.cmake
      DESTINATION share/cmake/Caffe2
      COMPONENT dev)
  install(FILES
      ${PROJECT_SOURCE_DIR}/cmake/public/cuda.cmake
      ${PROJECT_SOURCE_DIR}/cmake/public/glog.cmake
      ${PROJECT_SOURCE_DIR}/cmake/public/gflags.cmake
      ${PROJECT_SOURCE_DIR}/cmake/public/mkl.cmake
      ${PROJECT_SOURCE_DIR}/cmake/public/mkldnn.cmake
      ${PROJECT_SOURCE_DIR}/cmake/public/protobuf.cmake
      ${PROJECT_SOURCE_DIR}/cmake/public/utils.cmake
      ${PROJECT_SOURCE_DIR}/cmake/public/LoadHIP.cmake
      DESTINATION share/cmake/Caffe2/public
      COMPONENT dev)
  install(DIRECTORY
      ${PROJECT_SOURCE_DIR}/cmake/Modules_CUDA_fix
      DESTINATION share/cmake/Caffe2/
      COMPONENT dev)
  install(FILES
      ${PROJECT_SOURCE_DIR}/cmake/Modules/FindCUDAToolkit.cmake
      DESTINATION share/cmake/Caffe2/
      COMPONENT dev)
  install(FILES
      ${PROJECT_SOURCE_DIR}/cmake/Modules/FindCUSPARSELT.cmake
      DESTINATION share/cmake/Caffe2/
      COMPONENT dev)

  install(EXPORT Caffe2Targets DESTINATION share/cmake/Caffe2
      FILE Caffe2Targets.cmake
      COMPONENT dev)
else()
  message(WARNING
      "Generated cmake files are only available when building "
      "shared libs.")
endif()
  • caffe2cuda 的 modules 裝到 share/cmake/Caffe2 底下, 根據 FHS, share 底下是放一些共用的 data
  • 最後安裝 Caffe2Targets.cmake 讓我們可以透過 Caffe2Config.cmake 引進 caffe2 的 targets
  • 透過膝跳反射, 我們知道 Caffe2Config.cmake 是由 Caffe2Config.cmake.in 產生的, 確實在 113 行可以看到 include ("${CMAKE_CURRENT_LIST_DIR}/Caffe2Targets.cmake") 引進這些 targets

最後來看看 Summary

include(cmake/Summary.cmake)
caffe2_print_configuration_summary()
  • PyTorch 在最後會印出各種當前的設定, 讓我們做確認

因為 PyTorch 的 dependencies 相當多, 尤其很多 caffe2 的設定, 蠻奇特的, 好奇去查了一下, 發現 caffe2 在 2018 年就被整合進 PyTorch 了, 見 Caffe2 and PyTorch join forces to create a Research + Production platform PyTorch 1.0, 看起來是 coffe2 也提供了 PyTorch 很核心的功能

總而言之, 頂端的 CMakeLists.txt 會像是 project 的導覽
也就是 developer 會接觸到的部分, 像是各種 options 和設定
其餘的 libraries 和 executables 等等實作都會在各自的路徑底下, 這也符合 CMake3.0 的 target-centric 的風格

cmake Module Directory

再來看看同層的 cmake

.
├── External
├── Modules
├── Modules_CUDA_fix
├── public
├── Allowlist.cmake
├── BuildVariables.cmake
├── Caffe2Config.cmake.in
├── CheckAbi.cmake
├── cmake_uninstall.cmake.in
├── Codegen.cmake
├── DebugHelper.cmake
├── Dependencies.cmake
├── FlatBuffers.cmake
├── GoogleTestPatch.cmake
├── IncludeSource.cpp.in
├── iOS.cmake
├── Metal.cmake
├── MiscCheck.cmake
├── ProtoBuf.cmake
├── ProtoBufPatch.cmake
├── Summary.cmake
├── TorchConfig.cmake.in
├── TorchConfigVersion.cmake.in
├── VulkanCodegen.cmake
└── VulkanDependencies.cmake

先排除 directory, 我們來看看這裡有哪些檔案吧

  • *.cmake
    • 就像 Day 5 介紹的, .cmake 結尾的都是 CMake module
  • *Config.cmake.in, *ConfigVersion.cmake.in
    • *Config.cmake*ConfigVersion.cmakeDay 21 介紹的, 由 configre_package_config_file()write_basic_package_version_file() 產生
    • 用來讓使用 pytorch 的開發者能夠引入 pytorch 專案中的 targets
    • *.cmake.in 則是提供了我們 (developer) 一些 configure 時的選項, 讓我們能控制 pytorch 的 build process
  • Dependencies.cmake
    • 根據 developer 的設定, include() ExternalModules裡對應的 modules 來檢查套件是否能用
  • 其他還有一些針對特定平台和套件的 cmake modules, 這邊先略過

torch Library

接著來看 torch 的 build script
花了一些時間找到底是誰把他加 build tree

和預想的由頂端 CMakeLists.txt 引入不同, torch 是由 caffe2 的 build script 引入的

add_subdirectory(../torch torch)
set(TORCH_PYTHON_COMPILE_OPTIONS ${TORCH_PYTHON_COMPILE_OPTIONS} PARENT_SCOPE)
set(TORCH_PYTHON_LINK_FLAGS ${TORCH_PYTHON_LINK_FLAGS} PARENT_SCOPE)

不過畢竟 torch 會用到 caffe2 的 libraries 和 headers, 先 build caffe2 也算合理

該 script 會產生兩個 shared libraries: torch_pythonnnapi_backend

Target torch_python 會 link 以下 libraries

set(TORCH_PYTHON_LINK_LIBRARIES
    python::python
    pybind::pybind11
    shm
    fmt::fmt-header-only
    ATEN_CPU_FILES_GEN_LIB)

if(USE_ASAN AND TARGET Sanitizer::address)
  list(APPEND TORCH_PYTHON_LINK_LIBRARIES Sanitizer::address)
endif()
if(USE_ASAN AND TARGET Sanitizer::undefined)
  list(APPEND TORCH_PYTHON_LINK_LIBRARIES Sanitizer::undefined)
endif()
if(USE_TSAN AND TARGET Sanitizer::thread)
  list(APPEND TORCH_PYTHON_LINK_LIBRARIES Sanitizer::thread)
endif()

還有其他 libraries 像是 cuda, mpi, nccl 等
不過 ucc 沒有包含在 TORCH_PYTHON_LINK_LIBRARIES, 卻是在 target_link_libraries(torch_python PRIVATE torch_library ${TORCH_PYTHON_LINK_LIBRARIES}) 之後才單獨 link, 有點奇怪

torch_python 包含

set(TORCH_PYTHON_SRCS
    ${GENERATED_THNN_CXX}
    ${GENERATED_CXX_PYTHON}
    )
  • GENERATED_CXX_PYTHON
    • 定義在 caffe2/CMakeLists.txt
    set(GENERATED_CXX_PYTHON
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_functions_0.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_functions_1.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_functions_2.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_functions_3.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_functions_4.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_variable_methods.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_torch_functions_0.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_torch_functions_1.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_torch_functions_2.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_nn_functions.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_fft_functions.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_linalg_functions.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_nested_functions.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_sparse_functions.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_special_functions.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_return_types.cpp"
    "${TORCH_SRC_DIR}/csrc/autograd/generated/python_enum_tag.cpp"
    )
    
  • GENERATED_THNN_CXX_CUDA
    • 看起來沒有用到了@@ 應該可以拔掉才對, 不確定為何留著

Platform-Specific Build Scripts

可以看到 PyTorch 很好心的提供了很~多 build scripts 的範例, 甚至包含 build_ios.shbuild_andriod.sh 讓我們能cross compile iOS 和 Android 版本, 雖然 Apple 系列平台不在這次介紹的範圍, 不過還是可以大概看一下

# Use ios-cmake to build iOS project from CMake.
# This projects sets CMAKE_C_COMPILER to /usr/bin/gcc and
# CMAKE_CXX_COMPILER to /usr/bin/g++. In order to use ccache (if it is available) we
# must override these variables via CMake arguments.
CMAKE_ARGS+=("-DCMAKE_TOOLCHAIN_FILE=$CAFFE2_ROOT/cmake/iOS.cmake")
if [ -n "${CCACHE_WRAPPER_PATH:-}"]; then
  CCACHE_WRAPPER_PATH=/usr/local/opt/ccache/libexec
fi
if [ -d "$CCACHE_WRAPPER_PATH" ]; then
  CMAKE_ARGS+=("-DCMAKE_C_COMPILER=$CCACHE_WRAPPER_PATH/gcc")
  CMAKE_ARGS+=("-DCMAKE_CXX_COMPILER=$CCACHE_WRAPPER_PATH/g++")
fi

可以看到 pytorch 提供了 iOS.cmake toolchain file
並且如果 developer 機器有 ccache 的話, 就改用 ccache 下的 compiler 來加速

完賽感言

30 天終於結束了🎉🎉🎉
第一次參加鐵人賽, 原本想說要先來存個稿, 避免因為上班或平時太忙而沒有時間寫, 結果人算不如天算, 大綱在開賽前的一個星期才開始想, 直到開賽前一天才堪堪寫出來...

對我來說, 這次參加鐵人賽的目的主要是分享學習 CMake 時遇到的一些困難, 和其他相關的知識, 畢竟說到底, CMake 只是一個 build tool (or meta build system), 他僅僅是讓我們免去手刻 build scripts 和呼叫 compiler 的工具, 但對於開發來說, 還是需要理解每個步驟所代表的意義

最開始的計畫其實是想要寫出一個厲害的專案的, 但是思來想去總是想不到要做什麼比較好, 刪刪改改後時間也不多了, 就只能以 "本系列不會做出厲害的專案" 為開頭QQ

開賽後第一週真的是惡夢, 早上 6 點就要起床開始寫, 寫到 7 點去公司
中午午休再繼續寫, 下班 7 點再繼續寫...現在回憶起來都想哭了嗚嗚

幸好後來想到一個解法: 每週的假日花兩天把下一週工作天的稿先擬好, 這大概幫我每天省下了一個小時 (感謝女朋友的建議QQ)
儘管常常到了當天大綱又都改掉...

真的沒時間的時候, 還是逼著自己水一些東西出來, 果然網路小說的作者會水也是有原因的

總之, 雖然過程不盡人意, 但是想表達的東西沒有八成也有六成了, 第一次的鐵人賽就到這了, 希望未來有時間的時候, 還能夠回來改改這些文章, 把偷懶的那幾天補好補滿

最後, 感謝 It 邦幫忙提供這個平台, 讓大家能有一個督促自己的動力, 逼自己寫出一點東西來, 儘管不一定有用, 總是個自我反省的過程, 希望能有越來越多好文章出現~

下台一鞠躬<(_ _)>


上一篇
[Day 29] Windows Installer
系列文
30 天 CMake 跨平台之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言