最近比較有時間,就打算好好研究一下 TorchDynamo。中間無意間看到 PEP-523 這個 TorchDynamo 實現 JIT compile 的主要 CPython API ,好奇心驅使下就開始跟 CPython 玩了一下。過程中用了 CMake 來管理實驗用的 extension module,但由於我習慣用 pyenv
來管理我的 python 環境,而在這個情況下透過 find_package
總是連結到系統用的 python。經過一番波折之後,總算整理出一個解法可以更精準地控制 cmake
中 python 相關 header 與 object files 的設定,就想說記錄一下這個過程。
我的老朋友:python-config
最早開始實驗的時候,其實我很偷懶地用 make
來管理而已,只是想說可以少打一些指令 XD
當時管理的方式思路也很單純,就是想用 python-config
這個指令幫我找到針對目前使用的 python 編譯時所需要的 include paths 跟 linking flags。我自己跟一些身邊的朋友聊,發現好像不是太多人知道這個指令,那這邊是它的 help message:
上網找了一下,這邊有一些簡單的說明:https://www.commandlinux.com/man-page/man1/python-config.1.html
如果用 editor 把這個指令打開來看,就會發現它其實就是個 python script:
它是在安裝 python 時通常會ㄧ併被生成的,也就是說可以透過這個 script 去得到當時編譯時所需要的 flags:
所以當時最簡單的方式就是透過 Makefile 跟 shell script interpolation 去控制相關編譯的 flags:
my_module: my_module.c
clang $$(python-config --includes --libs --embed) -shared $< -o $@$$(python-config --extension-suffix)
但以前這樣做碰到過些問題,換一個 platform 可能就不能 compile 之類的,思來想去還是想說用上 cmake
好了,結果就發現 find_package(Python REQUIRED)
沒辦法很正確的跟 pyenv
整合,總是用到系統的 python header 還有 share objects。
其實想想也正常,畢竟 pyenv
背後幫我做的,不過也是把 python 的原始碼下載下來進行 configure 跟 compile,而 CPython 本身也不是用 cmake
管理的,兩邊搭不起來實屬正常,那又該怎麼辦呢?
這時候我就把腦筋動到 python-config
身上了。
CMakeLists.txt Example
基本思路也簡單,大概步驟是這樣:
- 透過
execute_process
這個cmake
指令去執行python-config
,並且輸出用變數存起來。 - 定義一個 interface target (譬如說 python),然後讓我的 module 去 link 這個 interface target
大概就這樣。
首先第一步,先執行 python-config
並把結果存在變數中 (譬如下面範例的 PYTHON_INCLUDES
, PYTHON_LIBS
跟 PYTHON_LIB_SUFFIX
)
# python3 configurations
execute_process(
COMMAND python3-config --includes --embed
OUTPUT_VARIABLE PYTHON_INCLUDES
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(
COMMAND python3-config --libs --embed
OUTPUT_VARIABLE PYTHON_LIBS
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND python3-config --extension-suffix
OUTPUT_VARIABLE PYTHON_LIB_SUFFIX
OUTPUT_STRIP_TRAILING_WHITESPACE)
separate_arguments(PYTHON_INCLUDES)
separate_arguments(PYTHON_LIBS)
裡面一個比較特別的地方在於透過 separate_arguments
把這些變數從一個 string 改變成一個 list,這在後續定義 interface target 時可以避免一些 bug。
再來定義一個 python 的 interface target 方便後續我們拿其他 target 跟它進行 link:
# Python3 interface
add_library(python3 INTERFACE)
target_compile_options(python3 INTERFACE ${PYTHON_INCLUDES})
target_link_options(python3 INTERFACE ${PYTHON_LIBS})
最後就簡單的設定好我們想要跟 python 進行 link 的 target:
# my_module
add_library(my_module SHARED my_module.c)
set_target_properties(
my_module PROPERTIES
PREFIX ""
SUFFIX ${PYTHON_LIB_SUFFIX}
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}
)
target_link_libraries(my_module PRIVATE python3)
這樣進行編譯之後就可以得到一個 my_module.cpython-311-darwin.so
可以用了🎉🎉🎉
有沒有感覺很簡單?😛
結語
總而言之,算是在看 TorchDynamo 原始碼的過程中發生的一個小插曲,想說應該也有人跟我有一項的需求吧(?) 寫個小記錄,紀念我某一天查 cmake
文件一個上午的時間。
順道一提,我嘗試過問 ChatGPT 這些問題,但可能我問的方式不對,最後沒問出來還是摸摸鼻子去查文件,還好我之前 cmake
還算熟,沒花多久時間
就找到我需要的東西,拼湊出上面的解法,所以歡迎大家跟我分享用 LLM 問出來的方法,我自己也很好奇 🤓
Happy Python Programming!