Skip to content

Commit 1caa0a1

Browse files
authored
[build] Properly implement editable mode (#8722)
* [build] Properly implement editable mode * Add CI * Fix typo * Fix macos job
1 parent c105dda commit 1caa0a1

File tree

6 files changed

+151
-32
lines changed

6 files changed

+151
-32
lines changed

.github/workflows/pull.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ jobs:
5656
# Build and test ExecuTorch with the add model on portable backend.
5757
PYTHON_EXECUTABLE=python bash .ci/scripts/test_model.sh "add" "${BUILD_TOOL}" "portable"
5858
59+
test-pip-install-editable-mode-linux:
60+
name: test-pip-install-editable-mode-linux
61+
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
62+
permissions:
63+
id-token: write
64+
contents: read
65+
strategy:
66+
fail-fast: false
67+
with:
68+
runner: linux.2xlarge
69+
docker-image: executorch-ubuntu-22.04-clang12
70+
submodules: 'true'
71+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
72+
timeout: 90
73+
script: |
74+
# The generic Linux job chooses to use base env, not the one setup by the image
75+
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
76+
conda activate "${CONDA_ENV}"
77+
# Debug
78+
which pip
79+
PYTHON_EXECUTABLE=python bash ./install_executorch.sh --editable --pybind xnnpack --use-pt-pinned-commit
80+
# Try to import extension library
81+
python -c "from executorch.extension.llm.custom_ops import custom_ops"
82+
5983
test-models-linux:
6084
name: test-models-linux
6185
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
@@ -480,7 +504,7 @@ jobs:
480504
481505
# Setup install_requirements for llama
482506
PYTHON_EXECUTABLE=python bash examples/models/llama/install_requirements.sh
483-
507+
484508
# Test static llama weight sharing and accuracy
485509
PYTHON_EXECUTABLE=python bash .ci/scripts/test_qnn_static_llama.sh
486510

.github/workflows/trunk.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,31 @@ jobs:
3636
3737
PYTHONPATH="${PWD}" python .ci/scripts/gather_test_models.py --target-os macos --event "${GITHUB_EVENT_NAME}"
3838
39+
test-pip-install-editable-mode-macos:
40+
name: test-pip-install-editable-mode-macos
41+
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
42+
permissions:
43+
id-token: write
44+
contents: read
45+
strategy:
46+
fail-fast: false
47+
with:
48+
runner: macos-m1-stable
49+
python-version: '3.11'
50+
submodules: 'true'
51+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
52+
timeout: 90
53+
script: |
54+
# The generic Linux job chooses to use base env, not the one setup by the image
55+
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
56+
conda activate "${CONDA_ENV}"
57+
# Debug
58+
which pip
59+
bash .ci/scripts/setup-conda.sh
60+
PYTHON_EXECUTABLE=python ${CONDA_RUN} bash ./install_executorch.sh --editable --pybind xnnpack
61+
# Try to import extension library
62+
python -c "from executorch.extension.llm.custom_ops import custom_ops"
63+
3964
test-models-macos:
4065
name: test-models-macos
4166
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main

extension/llm/custom_ops/custom_ops.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,19 @@
2222
op2 = torch.ops.llama.fast_hadamard_transform.default
2323
assert op2 is not None
2424
except:
25-
import glob
26-
27-
import executorch
28-
2925
# This is needed to ensure that custom ops are registered
3026
from executorch.extension.pybindings import portable_lib # noqa # usort: skip
3127

3228
# Ideally package is installed in only one location but usage of
3329
# PYATHONPATH can result in multiple locations.
3430
# ATM this is mainly used in CI for qnn runner. Will need to revisit this
35-
executorch_package_path = executorch.__path__[-1]
36-
logging.info(f"Looking for libcustom_ops_aot_lib.so in {executorch_package_path}")
37-
libs = list(
38-
glob.glob(
39-
f"{executorch_package_path}/**/libcustom_ops_aot_lib.*", recursive=True
40-
)
41-
)
31+
from pathlib import Path
32+
33+
package_path = Path(__file__).parent.resolve()
34+
logging.info(f"Looking for libcustom_ops_aot_lib.so in {package_path}")
35+
36+
libs = list(package_path.glob("**/libcustom_ops_aot_lib.*"))
37+
4238
assert len(libs) == 1, f"Expected 1 library but got {len(libs)}"
4339
logging.info(f"Loading custom ops library: {libs[0]}")
4440
torch.ops.load_library(libs[0])

install_executorch.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def clean():
6565
"prelude": "BUCK",
6666
"pthreadpool": "CMakeLists.txt",
6767
"pybind11": "CMakeLists.txt",
68+
"shim": "BUCK",
6869
"XNNPACK": "CMakeLists.txt",
6970
}
7071

@@ -138,6 +139,14 @@ def build_args_parser() -> argparse.ArgumentParser:
138139
action="store_true",
139140
help="build from the pinned PyTorch commit instead of nightly",
140141
)
142+
parser.add_argument(
143+
"--editable",
144+
"-e",
145+
action="store_true",
146+
help="build an editable pip wheel, changes to python code will be "
147+
"picked up without rebuilding the wheel. Extension libraries will be "
148+
"installed inside the source tree.",
149+
)
141150
return parser
142151

143152

@@ -226,6 +235,9 @@ def main(args):
226235
"-m",
227236
"pip",
228237
"install",
238+
]
239+
+ (["--editable"] if args.editable else [])
240+
+ [
229241
".",
230242
"--no-build-isolation",
231243
"-v",

pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,25 @@ Changelog = "https://github.com/pytorch/executorch/releases"
8282
[project.scripts]
8383
flatc = "executorch.data.bin:flatc"
8484

85+
# TODO(dbort): Could use py_modules to restrict the set of modules we
86+
# package, and package_data to restrict the set up non-python files we
87+
# include. See also setuptools/discovery.py for custom finders.
88+
[tool.setuptools.package-dir]
89+
"executorch.backends" = "backends"
90+
"executorch.codegen" = "codegen"
91+
# TODO(mnachin T180504136): Do not put examples/models
92+
# into core pip packages. Refactor out the necessary utils
93+
# or core models files into a separate package.
94+
"executorch.examples.models" = "examples/models"
95+
"executorch.exir" = "exir"
96+
"executorch.extension" = "extension"
97+
"executorch.kernels.quantized" = "kernels/quantized"
98+
"executorch.schema" = "schema"
99+
"executorch.devtools" = "devtools"
100+
"executorch.devtools.bundled_program" = "devtools/bundled_program"
101+
"executorch.runtime" = "runtime"
102+
"executorch.util" = "util"
103+
85104
[tool.setuptools.package-data]
86105
# TODO(dbort): Prune /test[s]/ dirs, /third-party/ dirs, yaml files that we
87106
# don't need.

setup.py

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import os
5151
import platform
5252
import re
53+
import site
5354
import sys
5455

5556
# Import this before distutils so that setuptools can intercept the distuils
@@ -239,7 +240,7 @@ def src_path(self, installer: "InstallerBuildExt") -> Path:
239240
srcs = tuple(cmake_cache_dir.glob(self.src))
240241
if len(srcs) != 1:
241242
raise ValueError(
242-
f"""Expected exactly one file matching '{self.src}'; found {repr(srcs)}.
243+
f"""Expected exactly one file matching '{self.src}' in {cmake_cache_dir}; found {repr(srcs)}.
243244
244245
If that file is a CMake-built extension module file, and we are installing in editable mode, please disable the corresponding build option since it's not supported yet.
245246
@@ -372,6 +373,63 @@ def dst_path(self, installer: "InstallerBuildExt") -> Path:
372373
class InstallerBuildExt(build_ext):
373374
"""Installs files that were built by cmake."""
374375

376+
def __init__(self, *args, **kwargs):
377+
self._ran_build = False
378+
super().__init__(*args, **kwargs)
379+
380+
def run(self):
381+
# Run the build command first in editable mode. Since `build` command
382+
# will also trigger `build_ext` command, only run this once.
383+
if self._ran_build:
384+
return
385+
386+
if self.editable_mode:
387+
self._ran_build = True
388+
self.run_command("build")
389+
super().run()
390+
391+
def copy_extensions_to_source(self) -> None:
392+
"""For each extension in `ext_modules`, we need to copy the extension
393+
file from the build directory to the correct location in the local
394+
directory.
395+
396+
This should only be triggered when inplace mode (editable mode) is enabled.
397+
398+
Args:
399+
400+
Returns:
401+
"""
402+
build_py = self.get_finalized_command("build_py")
403+
for ext in self.extensions:
404+
if isinstance(ext, BuiltExtension):
405+
modpath = ext.name.split(".")
406+
package = ".".join(modpath[:-1])
407+
package_dir = os.path.abspath(build_py.get_package_dir(package))
408+
else:
409+
# HACK: get rid of the leading "executorch" in ext.dst.
410+
# This is because we don't have a root level "executorch" module.
411+
package_dir = ext.dst.removeprefix("executorch/")
412+
413+
# Ensure that the destination directory exists.
414+
self.mkpath(os.fspath(package_dir))
415+
416+
regular_file = ext.src_path(self)
417+
inplace_file = os.path.join(
418+
package_dir, os.path.basename(ext.src_path(self))
419+
)
420+
421+
# Always copy, even if source is older than destination, to ensure
422+
# that the right extensions for the current Python/platform are
423+
# used.
424+
if os.path.exists(regular_file) or not ext.optional:
425+
self.copy_file(regular_file, inplace_file, level=self.verbose)
426+
427+
if ext._needs_stub:
428+
inplace_stub = self._get_equivalent_stub(ext, inplace_file)
429+
self._write_stub_file(inplace_stub, ext, compile=True)
430+
# Always compile stub and remove the original (leave the cache behind)
431+
# (this behaviour was observed in previous iterations of the code)
432+
375433
# TODO(dbort): Depend on the "build" command to ensure it runs first
376434

377435
def build_extension(self, ext: _BaseExtension) -> None:
@@ -630,6 +688,10 @@ def run(self):
630688
if not self.dry_run:
631689
# Dry run should log the command but not actually run it.
632690
(Path(cmake_cache_dir) / "CMakeCache.txt").unlink(missing_ok=True)
691+
# Set PYTHONPATH to the location of the pip package.
692+
os.environ["PYTHONPATH"] = (
693+
site.getsitepackages()[0] + ";" + os.environ.get("PYTHONPATH", "")
694+
)
633695
with Buck2EnvironmentFixer():
634696
# The context manager may patch the environment while running this
635697
# cmake command, which happens to run buck2 to get some source
@@ -741,25 +803,6 @@ def get_ext_modules() -> List[Extension]:
741803

742804
setup(
743805
version=Version.string(),
744-
# TODO(dbort): Could use py_modules to restrict the set of modules we
745-
# package, and package_data to restrict the set up non-python files we
746-
# include. See also setuptools/discovery.py for custom finders.
747-
package_dir={
748-
"executorch/backends": "backends",
749-
"executorch/codegen": "codegen",
750-
# TODO(mnachin T180504136): Do not put examples/models
751-
# into core pip packages. Refactor out the necessary utils
752-
# or core models files into a separate package.
753-
"executorch/examples/models": "examples/models",
754-
"executorch/exir": "exir",
755-
"executorch/extension": "extension",
756-
"executorch/kernels/quantized": "kernels/quantized",
757-
"executorch/schema": "schema",
758-
"executorch/devtools": "devtools",
759-
"executorch/devtools/bundled_program": "devtools/bundled_program",
760-
"executorch/runtime": "runtime",
761-
"executorch/util": "util",
762-
},
763806
cmdclass={
764807
"build": CustomBuild,
765808
"build_ext": InstallerBuildExt,

0 commit comments

Comments
 (0)