Skip to content

Commit d93d371

Browse files
[libshortfin] Python build ergonomics. (#177)
* Move bindings/python -> python * Rework setup.py so that it can be the main driver for Python based dev: * Add env vars SHORTFIN_IREE_SOURCE_DIR, SHORTFIN_ENABLE_ASAN, SHORTFIN_DEV_MODE, SHORTFIN_RUN_CTESTS * Update README with python based dev flow commands * Change build dir to build/cmake (from build/b) * Removes a redundant build of IREE in the main CI (was not doing anything in bundled mode). With this, I can change my dev process to be entirely based on `pip install` with a fully editable build and the ability to manipulate cmake post build. This better aligns with how users will consume it.
1 parent e52b086 commit d93d371

18 files changed

+128
-118
lines changed

.github/workflows/ci_linux_x64-libshortfin.yml

+2-21
Original file line numberDiff line numberDiff line change
@@ -57,25 +57,6 @@ jobs:
5757
git submodule update --init --depth 1 -- third_party/googletest
5858
git submodule update --init --depth 1 -- third_party/hip-build-deps/
5959
60-
- name: Build IREE runtime
61-
run: |
62-
mkdir ${{ env.IREE_REPO_DIR }}/build
63-
cd ${{ env.IREE_REPO_DIR }}/build
64-
cmake -GNinja \
65-
-DCMAKE_C_COMPILER=clang-18 \
66-
-DCMAKE_CXX_COMPILER=clang++-18 \
67-
-DIREE_ENABLE_LLD=ON \
68-
-DIREE_ERROR_ON_MISSING_SUBMODULES=OFF \
69-
-DIREE_HAL_DRIVER_DEFAULTS=OFF \
70-
-DIREE_HAL_DRIVER_LOCAL_SYNC=ON \
71-
-DIREE_HAL_DRIVER_LOCAL_TASK=ON \
72-
-DIREE_HAL_DRIVER_HIP=ON \
73-
-DIREE_BUILD_COMPILER=OFF \
74-
-DIREE_BUILD_SAMPLES=OFF \
75-
-DIREE_BUILD_TESTS=OFF \
76-
..
77-
cmake --build . --target all
78-
7960
- name: Setup Python
8061
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
8162
with:
@@ -97,7 +78,7 @@ jobs:
9778
-DCMAKE_CXX_COMPILER=clang++-18 \
9879
-DCMAKE_LINKER_TYPE=LLD \
9980
-DSHORTFIN_BUNDLE_DEPS=ON \
100-
-DCMAKE_PREFIX_PATH=${{ env.IREE_REPO_DIR }}/build/lib/cmake/IREE \
81+
-DSHORTFIN_IREE_SOURCE_DIR=${{ env.IREE_REPO_DIR }} \
10182
-DSHORTFIN_BUILD_PYTHON_BINDINGS=ON \
10283
..
10384
cmake --build . --target all
@@ -120,7 +101,7 @@ jobs:
120101
-DCMAKE_C_COMPILER=clang-18 \
121102
-DCMAKE_CXX_COMPILER=clang++-18 \
122103
-DCMAKE_LINKER_TYPE=LLD \
123-
-DCMAKE_PREFIX_PATH=${{ env.IREE_REPO_DIR }}/build/lib/cmake/IREE \
104+
-DSHORTFIN_IREE_SOURCE_DIR=${{ env.IREE_REPO_DIR }} \
124105
-DSHORTFIN_BUILD_PYTHON_BINDINGS=ON \
125106
-DSHORTFIN_HAVE_AMDGPU=OFF \
126107
-DSHORTFIN_BUILD_STATIC=ON \

.github/workflows/ci_linux_x64_asan-libshortfin.yml

+5-19
Original file line numberDiff line numberDiff line change
@@ -147,27 +147,13 @@ jobs:
147147
- name: Build libshortfin
148148
run: |
149149
eval "$(pyenv init -)"
150-
mkdir ${{ env.LIBSHORTFIN_DIR }}/build
151-
cd ${{ env.LIBSHORTFIN_DIR }}/build
152-
cmake -GNinja \
153-
-DCMAKE_BUILD_TYPE=Debug \
154-
-DCMAKE_C_COMPILER=clang-18 \
155-
-DCMAKE_CXX_COMPILER=clang++-18 \
156-
-DCMAKE_LINKER_TYPE=LLD \
157-
-DSHORTFIN_BUNDLE_DEPS=ON \
158-
-DSHORTFIN_IREE_SOURCE_DIR=${{ env.IREE_SOURCE_DIR }} \
159-
-DSHORTFIN_BUILD_PYTHON_BINDINGS=ON \
160-
-DSHORTFIN_ENABLE_ASAN=ON \
161-
..
162-
cmake --build . --target all
150+
cd ${{ env.LIBSHORTFIN_DIR }}
151+
SHORTFIN_IREE_SOURCE_DIR="${{ env.IREE_SOURCE_DIR }}" \
152+
SHORTFIN_ENABLE_ASAN=ON \
153+
SHORTFIN_DEV_MODE=ON \
154+
SHORTFIN_RUN_CTESTS=ON \
163155
pip install -v -e .
164156
165-
- name: Run ctest
166-
if: ${{ !cancelled() }}
167-
run: |
168-
cd ${{ env.LIBSHORTFIN_DIR }}/build
169-
ctest --timeout 30 --output-on-failure
170-
171157
- name: Run pytest
172158
if: ${{ !cancelled() }}
173159
env:

libshortfin/CMakeLists.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# https://llvm.org/LICENSE.txt for license information. SPDX-License-Identifier:
55
# Apache-2.0 WITH LLVM-exception
66

7-
cmake_minimum_required(VERSION 3.28)
7+
cmake_minimum_required(VERSION 3.29)
88

99
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
1010
message(
@@ -138,7 +138,7 @@ if (NOT SHORTFIN_IREE_SOURCE_DIR AND SHORTFIN_BUNDLE_DEPS)
138138
)
139139
FetchContent_GetProperties(iree)
140140
if(NOT iree_POPULATED)
141-
FetchContent_Populate(iree)
141+
FetchContent_MakeAvailable(iree)
142142
endif()
143143
set(SHORTFIN_IREE_SOURCE_DIR ${iree_SOURCE_DIR})
144144
endif()
@@ -184,7 +184,7 @@ add_subdirectory(src)
184184

185185
if(SHORTFIN_BUILD_PYTHON_BINDINGS)
186186
find_package(Python 3.8 COMPONENTS Interpreter Development.Module REQUIRED)
187-
add_subdirectory(bindings/python)
187+
add_subdirectory(python)
188188
set(SHORTFIN_PYTHON_CPP_PREBUILT "TRUE") # See setup.py.
189189
configure_file(setup.py setup.py @ONLY)
190190
configure_file(pyproject.toml pyproject.toml COPYONLY)

libshortfin/README.md

+33-29
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,19 @@
11
# libshortfin - SHARK C++ inference library
22

3-
## Dev Builds
4-
5-
Library dependencies:
6-
7-
* [spdlog](https://github.com/gabime/spdlog)
8-
* [xtensor](https://github.com/xtensor-stack/xtensor)
9-
* [iree runtime](https://github.com/iree-org/iree)
10-
11-
On recent Ubuntu, the primary dependencies can be satisfied via:
12-
13-
```
14-
apt install libspdlog-dev libxtensor-dev
15-
```
16-
17-
CMake must be told how to find the IREE runtime, either from a distribution
18-
tarball, or local build/install dir. For a local build directory, pass:
19-
20-
```
21-
# Assumes that the iree-build directory is adjacent to this repo.
22-
-DCMAKE_PREFIX_PATH=$(pwd)/../../iree-build/lib/cmake/IREE
23-
```
24-
25-
One liner recommended CMake command (note that CMAKE_LINKER_TYPE requires
26-
cmake>=3.29):
3+
## Native Dev Builds
274

285
```
296
cmake -GNinja -S. -Bbuild \
307
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
31-
-DCMAKE_LINKER_TYPE=LLD \
32-
-DCMAKE_PREFIX_PATH=$(pwd)/../../iree-build/lib/cmake/IREE
8+
-DCMAKE_LINKER_TYPE=LLD
339
```
3410

35-
## Building Python Bindings
11+
## Python Dev Builds
3612

3713
If using a Python based development flow, there are two options:
3814

39-
1. `pip install -v .` to build and install the library (TODO: Not yet implemented).
40-
2. Build with cmake and `-DSHORTFIN_BUILD_PYTHON_BINDINGS=ON` and then
15+
1. `pip install -e` based.
16+
2. Build with cmake as above and `-DSHORTFIN_BUILD_PYTHON_BINDINGS=ON` and then
4117
from the `build/` directory, run `pip install -v -e .` to create an
4218
editable install that will update as you build the C++ project.
4319

@@ -46,6 +22,34 @@ recommended. Your python install should track any source file changes or
4622
builds without further interaction. Re-installing will be necessary if package
4723
structure changes significantly.
4824

25+
For pure Python based dev, everything can be done from pip:
26+
27+
```
28+
# Install build system pre-reqs (since we are building in dev mode, this
29+
# is not done for us). See source of truth in pyproject.toml:
30+
pip install setuptools wheel
31+
32+
# Optionally install cmake and ninja if you don't have them or need a newer
33+
# version. If doing heavy development in Python, it is strongly recommended
34+
# to install these natively on your system as it will make it easier to
35+
# switch Python interpreters and build options (and the launcher in debug/asan
36+
# builds of Python is much slower). Note CMakeLists.txt for minimum CMake
37+
# version, which is usually quite recent.
38+
pip install cmake ninja
39+
40+
# Optional env vars:
41+
# SHORTFIN_IREE_SOURCE_DIR=$(pwd)/../../iree
42+
# Note that the `--no-build-isolation` flag is useful in development setups
43+
# because it does not create an intermediate venv that will keep later
44+
# invocations of cmake/ninja from working at the command line. If just doing
45+
# a one-shot build, it can be ommitted.
46+
SHORTFIN_DEV_MODE=ON pip install --no-build-isolation -v -e .
47+
48+
Once built the first time, `cmake`, `ninja`, and `ctest` commands can be run
49+
directly from `build/cmake` and changes will apply directly to the next
50+
process launch.
51+
```
52+
4953
## Running Tests
5054

5155
The project uses a combination of ctest for native C++ tests and pytest. Much
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

libshortfin/requirements-tests.txt

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ requests
33
fastapi
44
onnx
55
uvicorn
6+
7+
# Libraries needed to build in dev setups.
8+
setuptools
9+
wheel

libshortfin/setup.py

+81-46
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,35 @@
1515
from setuptools.command.build_py import build_py as _build_py
1616

1717

18+
def get_env_boolean(name: str, default_value: bool = False) -> bool:
19+
svalue = os.getenv(name)
20+
if svalue is None:
21+
return default_value
22+
svalue = svalue.upper()
23+
if svalue in ["1", "ON", "TRUE"]:
24+
return True
25+
elif svalue in ["0", "OFF", "FALSE"]:
26+
return False
27+
else:
28+
print(f"WARNING: {name} env var cannot be interpreted as a boolean value")
29+
return default_value
30+
31+
32+
def get_env_cmake_option(name: str, default_value: bool = False) -> str:
33+
svalue = os.getenv(name)
34+
if not svalue:
35+
svalue = "ON" if default_value else "OFF"
36+
return f"-D{name}={svalue}"
37+
38+
39+
def add_env_cmake_setting(args, env_name: str, cmake_name=None) -> str:
40+
svalue = os.getenv(env_name)
41+
if svalue is not None:
42+
if not cmake_name:
43+
cmake_name = env_name
44+
args.append(f"-D{cmake_name}={svalue}")
45+
46+
1847
# This file can be generated into the build directory to allow an arbitrary
1948
# CMake built version of the project to be installed into a venv for development.
2049
# This can be detected if the CPP_PREBUILT global contains the string
@@ -30,23 +59,33 @@ def is_cpp_prebuilt():
3059
return CPP_PREBUILT == "TRUE"
3160

3261

62+
DEV_MODE = False
63+
3364
if is_cpp_prebuilt():
34-
print("setup.py running in pre-built mode:", file=sys.stderr)
65+
print("setup.py running in pre-built mode:")
3566
SOURCE_DIR = Path(CPP_PREBUILT_SOURCE_DIR)
3667
BINARY_DIR = Path(CPP_PREBUILT_BINARY_DIR)
68+
CMAKE_BUILD_DIR = BINARY_DIR
3769
else:
38-
print("setup.py running in cmake build mode:", file=sys.stderr)
70+
print("setup.py running in cmake build mode:")
3971
# setup.py is in the source directory.
4072
SOURCE_DIR = Path(SETUPPY_DIR)
41-
BINARY_DIR = Path(os.path.join(SETUPPY_DIR, "build", "b"))
73+
BINARY_DIR = Path(os.path.join(SETUPPY_DIR, "build"))
74+
# TODO: Should build default and tracing version to different dirs.
75+
CMAKE_BUILD_DIR = BINARY_DIR / "cmake"
76+
DEV_MODE = get_env_boolean("SHORTFIN_DEV_MODE")
4277

43-
print(f" SOURCE_DIR = {SOURCE_DIR}", file=sys.stderr)
44-
print(f" BINARY_DIR = {BINARY_DIR}", file=sys.stderr)
78+
print(f" SOURCE_DIR = {SOURCE_DIR}")
79+
print(f" BINARY_DIR = {BINARY_DIR}")
80+
81+
if DEV_MODE:
82+
print(f" DEV MODE ENABLED: Building debug with clang/lld and other dev settings")
4583

4684
# Due to a quirk of setuptools, that package_dir map must only contain
4785
# paths relative to the directory containing setup.py. Why? No one knows.
4886
REL_SOURCE_DIR = SOURCE_DIR.relative_to(SETUPPY_DIR, walk_up=True)
4987
REL_BINARY_DIR = BINARY_DIR.relative_to(SETUPPY_DIR, walk_up=True)
88+
REL_CMAKE_BUILD_DIR = CMAKE_BUILD_DIR.relative_to(SETUPPY_DIR, walk_up=True)
5089

5190

5291
class CMakeExtension(Extension):
@@ -98,7 +137,7 @@ def maybe_nuke_cmake_cache(cmake_build_dir):
98137
# Mismatch or not found. Clean it.
99138
cmake_cache_file = os.path.join(cmake_build_dir, "CMakeCache.txt")
100139
if os.path.exists(cmake_cache_file):
101-
print("Removing CMakeCache.txt because Python version changed", file=sys.stderr)
140+
print("Removing CMakeCache.txt because Python version changed")
102141
os.remove(cmake_cache_file)
103142

104143
# And write.
@@ -115,67 +154,63 @@ def run(self):
115154
if not is_cpp_prebuilt():
116155

117156
# Build extension using cmake.
118-
print("*****************************", file=sys.stderr)
119-
print("* Building libshortfin *", file=sys.stderr)
120-
print("*****************************", file=sys.stderr)
121-
122-
cfg = os.getenv("SHORTFIN_CMAKE_BUILD_TYPE", "Release")
123-
124-
CMAKE_BUILD_DIR = BINARY_DIR
157+
print("Building libshortfin")
158+
cfg = os.getenv(
159+
"SHORTFIN_CMAKE_BUILD_TYPE", "Debug" if DEV_MODE else "Release"
160+
)
125161

126162
# Configure CMake.
127-
os.makedirs(BINARY_DIR, exist_ok=True)
128-
maybe_nuke_cmake_cache(CMAKE_BUILD_DIR)
129-
print(f"CMake build dir: {CMAKE_BUILD_DIR}", file=sys.stderr)
163+
os.makedirs(CMAKE_BUILD_DIR, exist_ok=True)
164+
if not DEV_MODE:
165+
maybe_nuke_cmake_cache(CMAKE_BUILD_DIR)
166+
print(f"CMake build dir: {CMAKE_BUILD_DIR}")
130167
cmake_args = [
131168
"-GNinja",
132169
"--log-level=VERBOSE",
133170
"-DSHORTFIN_BUNDLE_DEPS=ON",
134171
f"-DCMAKE_BUILD_TYPE={cfg}",
135172
"-DSHORTFIN_BUILD_PYTHON_BINDINGS=ON",
136-
# TODO: This shouldn't be hardcoded... but shortfin doesn't
137-
# compile without it.
138-
"-DCMAKE_C_COMPILER=clang",
139-
"-DCMAKE_CXX_COMPILER=clang++",
173+
f"-DPython3_EXECUTABLE={sys.executable}",
140174
]
141175

176+
if DEV_MODE:
177+
cmake_args.extend(
178+
[
179+
"-DCMAKE_C_COMPILER=clang",
180+
"-DCMAKE_CXX_COMPILER=clang++",
181+
"-DCMAKE_LINKER_TYPE=LLD",
182+
]
183+
)
184+
185+
add_env_cmake_setting(cmake_args, "SHORTFIN_IREE_SOURCE_DIR")
186+
add_env_cmake_setting(cmake_args, "SHORTFIN_ENABLE_ASAN")
187+
142188
# Only do a from-scratch configure if not already configured.
143189
cmake_cache_file = os.path.join(CMAKE_BUILD_DIR, "CMakeCache.txt")
144190
if not os.path.exists(cmake_cache_file):
145-
print(f"Configuring with: {cmake_args}", file=sys.stderr)
191+
print(f"Configuring with: {cmake_args}")
146192
subprocess.check_call(
147193
["cmake", SOURCE_DIR] + cmake_args, cwd=CMAKE_BUILD_DIR
148194
)
149195
else:
150-
print(f"Not re-configing (already configured)", file=sys.stderr)
196+
print(f"Not re-configing (already configured)")
151197

152198
# Build.
153199
subprocess.check_call(["cmake", "--build", "."], cwd=CMAKE_BUILD_DIR)
154-
print("Build complete.", file=sys.stderr)
200+
print("Build complete.")
155201

156-
# We only take _shortfin_default from the build.
157-
target_dir = os.path.join(
158-
os.path.abspath(self.build_lib), "_shortfin_default"
159-
)
160-
print(f"Building in target: {target_dir}", file=sys.stderr)
161-
os.makedirs(target_dir, exist_ok=True)
162-
print("Copying build to target.", file=sys.stderr)
163-
if os.path.exists(target_dir):
164-
shutil.rmtree(target_dir)
165-
shutil.copytree(
166-
os.path.join(
167-
CMAKE_BUILD_DIR,
168-
"bindings",
169-
"python",
170-
"_shortfin_default",
171-
),
172-
target_dir,
173-
symlinks=False,
174-
)
202+
# Optionally run CTests.
203+
if get_env_boolean("SHORTFIN_RUN_CTESTS", False):
204+
print("Running ctests...")
205+
subprocess.check_call(
206+
["ctest", "--timeout", "30", "--output-on-failure"],
207+
cwd=CMAKE_BUILD_DIR,
208+
)
175209

176210

177-
PYTHON_SOURCE_DIR = REL_SOURCE_DIR / "bindings" / "python"
178-
PYTHON_BINARY_DIR = REL_BINARY_DIR / "bindings" / "python"
211+
PYTHON_SOURCE_DIR = REL_SOURCE_DIR / "python"
212+
# TODO: Need multiple binary dirs for different build variants.
213+
PYTHON_DEFAULT_BINARY_DIR = REL_CMAKE_BUILD_DIR / "python"
179214

180215
# We need some directories to exist before setup.
181216
def populate_built_package(abs_dir):
@@ -190,7 +225,7 @@ def populate_built_package(abs_dir):
190225
pass
191226

192227

193-
populate_built_package(os.path.join(PYTHON_BINARY_DIR / "_shortfin_default"))
228+
populate_built_package(os.path.join(PYTHON_DEFAULT_BINARY_DIR / "_shortfin_default"))
194229

195230
setup(
196231
name="shortfin",
@@ -206,7 +241,7 @@ def populate_built_package(abs_dir):
206241
zip_safe=False,
207242
package_dir={
208243
"_shortfin": str(PYTHON_SOURCE_DIR / "_shortfin"),
209-
"_shortfin_default": str(PYTHON_BINARY_DIR / "_shortfin_default"),
244+
"_shortfin_default": str(PYTHON_DEFAULT_BINARY_DIR / "_shortfin_default"),
210245
# TODO: Conditionally map additional native library variants.
211246
"shortfin": str(PYTHON_SOURCE_DIR / "shortfin"),
212247
},

0 commit comments

Comments
 (0)