Skip to content

Commit d35db53

Browse files
zklauspytorchmergebot
authored andcommitted
Build torch._C extension module and version.py via CMake (pytorch#180243)
Move the build of the `torch._C` Python extension and the install of `torch/version.py` from `setup.py` into `torch/CMakeLists.txt`, so both the existing setuptools backend and the upcoming scikit-build-core backend (pytorch#180247) go through the same code path. The new `Python_add_library(_C MODULE WITH_SOABI ...)` target builds the same `torch/csrc/stub.c` and installs at the wheel root (mapped to `torch/` by `wheel.install-dir`); `setup.py`'s `Extension("torch._C", ...)` block and its companion `extra_compile_args` / `extra_link_args` / `make_relative_rpath_args` plumbing are removed. `version.py` is gitignored, so it is installed explicitly via `install(FILES ... DESTINATION ".")` rather than relying on Python package collection. Adds a `BinaryDistribution` class in `setup.py` that forces `has_ext_modules=True` so `bdist_wheel` uses the binary layout (package_data + data_files only) instead of the purelib layout, which would otherwise dump CMake's `build/lib/` tree -- including test artifacts -- into the wheel root. `package_data` collects the CMake-built extension via the interpreter's exact module name (`f"_C{sysconfig.get_config_var('EXT_SUFFIX')}"`) rather than a `_C*` glob, so a `build/` dir reused across Python versions cannot leak other ABIs' `_C` modules into the wheel. Windows handling: the manual `setup.py` copy of `_C.lib` from `build_temp` to `build_lib` is replaced by the `_C` target's `ARCHIVE DESTINATION "lib"`. For MSVC, the linker is told where to find `pythonXX.lib` via `/LIBPATH` (Python headers use `#pragma comment(lib, "pythonXX")` which expects a bare-name lookup); for Windows venvs, FindPython's library search path is extended with `sys.base_prefix/libs` since the import library lives in the base install, not the venv. `nnapi_backend` gains `TORCH_PYTHON_LINK_FLAGS` so it picks up the same MSVC linker options as `torch_python`. ## Test plan Verification is via artifact-content comparison against the parent (pytorch#180242). Expectation: the wheel must be content-equivalent modulo the one file the PR rebuilds (`torch/_C.cpython-*.so`), which must remain functionally identical (same exports, same imports, same rpath). - [x] **No regression in artifact sizes** across every manywheel / libtorch artifact (sub-0.001 % to 0.001 % delta, zip-timestamp noise consistent across artifacts this PR can't directly affect): | artifact | parent (pytorch#180242) | this PR (pytorch#180243) | delta | | --- | ---: | ---: | ---: | | manywheel-py3_12-rocm7_2 | 6,153,280,701 | 6,153,275,144 | -5,557 (-0.000%) | | manywheel-py3_12-rocm7_1 | 5,788,503,789 | 5,788,499,957 | -3,832 (-0.000%) | | manywheel-py3_12-cuda12_6 | 841,797,521 | 841,797,813 | +292 (+0.000%) | | manywheel-py3_12-cuda13_2 | 624,213,880 | 624,212,127 | -1,753 (-0.000%) | | manywheel-py3_12-cpu | 189,248,452 | 189,248,117 | -335 (-0.000%) | | manywheel-py3_12-xpu | 778,712,445 | 778,711,285 | -1,160 (-0.000%) | | libtorch-rocm7_2-shared-with-deps-release | 12,117,315,856 | 12,117,314,539 | -1,317 (-0.000%) | | libtorch-cpu-shared-with-deps-release | 257,626,615 | 257,624,612 | -2,003 (-0.001%) | - [x] **Wheel manifest equivalence vs parent** (`manywheel-py3_12-rocm7_2`): both wheels contain **26,779 entries**; filename diff is empty -- no files added or removed. Confirms the `BinaryDistribution` layout fix worked (no `build/lib/` spillage into the wheel root). - [x] **Per-file size diff: exactly one file changed.** ``` torch/_C.cpython-312-x86_64-linux-gnu.so 22,960 -> 15,720 (-7,240) ``` Every other one of 26,778 files is byte-identical in size. That is precisely the file the PR rebuilds via CMake. - [x] **`torch._C` is functionally equivalent** between parent and PR. Extracted both `.so` files and compared: - **Defined symbols**: zero-line diff. - **Undefined symbols** (imports from `libtorch_python`): zero-line diff. - **RPATH**: identical (`$ORIGIN:$ORIGIN/lib`). - **NEEDED**: 180242's `.so` includes a redundant `libpthread.so.0` (merged into `libc.so.6` since glibc 2.34); the CMake build drops it. The size shrinkage matches the cleaner NEEDED + tighter section layout from the CMake toolchain. - [x] **`torch/version.py`** is present in both wheels at 339 bytes, byte-identical except for the expected `git_version` SHA (each commit has its own). `__version__`, `debug`, `cuda`, `hip`, `rocm`, `xpu` fields all match -- the CMake-driven generation produces the same output as the setuptools path. - [x] **Exactly one `_C` ABI per wheel.** On a fresh multi-ABI `manywheel-cpu-build` (build_all.sh builds all CPython ABIs in one checkout), each wheel ships only its own `torch/_C.cpython-XY.so`. An earlier greedy `_C*.so` `package_data` glob leaked stale per-ABI stubs (a cp312 wheel carried cp310/cp311/cp312); pinning to the exact `EXT_SUFFIX` fixes it. ``` unzip -l manywheel-py3_13-cpu.whl | grep 'torch/_C\.cpython' # -> only torch/_C.cpython-313-x86_64-linux-gnu.so ``` ### Coverage notes - **NNAPI MSVC link-flag fix** and **Windows venv `pythonXX.lib` discovery fix** are not exercised by the ROCm-Linux artifacts above. They are exercised by the Windows pull-CI (where NNAPI builds run and venv-based discovery is relevant); the green Windows CI is the evidence for those. - ROCm wheels are Linux-only by build matrix (`.github/scripts/generate_binary_build_matrix.py:130-135`, `supported_platforms=["Linux"]`). Co-Generated with [Claude Code](https://claude.com/claude-code) Pull Request resolved: pytorch#180243 Approved by: https://github.com/malfet
1 parent 21d63ff commit d35db53

3 files changed

Lines changed: 81 additions & 117 deletions

File tree

cmake/Dependencies.cmake

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,21 @@ include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR})
814814

815815

816816
if(BUILD_PYTHON)
817+
# On Windows venvs, the Python import library (pythonXX.lib) lives in the
818+
# base installation's libs/ directory, not in the venv. Help FindPython
819+
# locate it by adding sys.base_prefix/libs to the library search path.
820+
if(WIN32 AND Python_EXECUTABLE)
821+
execute_process(
822+
COMMAND "${Python_EXECUTABLE}" -c "import sys; print(sys.base_prefix)"
823+
OUTPUT_VARIABLE _py_base_prefix
824+
OUTPUT_STRIP_TRAILING_WHITESPACE
825+
ERROR_QUIET
826+
)
827+
if(_py_base_prefix AND IS_DIRECTORY "${_py_base_prefix}/libs")
828+
list(APPEND CMAKE_LIBRARY_PATH "${_py_base_prefix}/libs")
829+
endif()
830+
endif()
831+
817832
set(PYTHON_COMPONENTS Development.Module)
818833
if(USE_NUMPY)
819834
list(APPEND PYTHON_COMPONENTS NumPy)

setup.py

Lines changed: 20 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@
313313
from tools.clean import clean as _clean
314314
from tools.generate_torch_version import get_torch_version
315315
from tools.setup_helpers.cmake import CMake, CMakeValue
316-
from tools.setup_helpers.env import build_type, IS_DARWIN, IS_LINUX, IS_WINDOWS
316+
from tools.setup_helpers.env import IS_DARWIN, IS_LINUX, IS_WINDOWS
317317

318318

319319
def str2bool(value: str | None) -> bool:
@@ -973,23 +973,6 @@ def run(self) -> None:
973973

974974
super().run()
975975

976-
# Copy the essential export library to compile C++ extensions.
977-
if IS_WINDOWS:
978-
build_temp = Path(self.build_temp)
979-
build_lib = Path(self.build_lib)
980-
981-
ext_filename = self.get_ext_filename("_C")
982-
lib_filename = ".".join(ext_filename.split(".")[:-1]) + ".lib"
983-
984-
export_lib = build_temp / "torch" / "csrc" / lib_filename
985-
target_lib = build_lib / "torch" / "lib" / "_C.lib"
986-
987-
# Create "torch/lib" directory if not exists.
988-
# (It is not created yet in "develop" mode.)
989-
target_dir = target_lib.parent
990-
target_dir.mkdir(parents=True, exist_ok=True)
991-
self.copy_file(export_lib, target_lib)
992-
993976
def get_outputs(self) -> list[str]:
994977
outputs = super().get_outputs()
995978
outputs.append(os.path.join(self.build_lib, "caffe2"))
@@ -1031,6 +1014,17 @@ def run(self) -> None:
10311014
_clean()
10321015

10331016

1017+
class BinaryDistribution(Distribution):
1018+
# torch._C is built by CMake (torch/CMakeLists.txt), so setuptools has
1019+
# no ext_modules. Force the wheel to be tagged as a binary distribution
1020+
# so bdist_wheel uses the binary layout (only files listed via
1021+
# package_data / data_files are packaged) rather than the purelib
1022+
# layout, which would dump the entire build/lib/ tree -- including
1023+
# CMake's test artifacts -- into the wheel at site-packages root.
1024+
def has_ext_modules(self) -> bool:
1025+
return True
1026+
1027+
10341028
def get_cmake_cache_vars() -> defaultdict[str, CMakeValue]:
10351029
try:
10361030
return defaultdict(lambda: False, cmake.get_cmake_cache_variables())
@@ -1059,58 +1053,8 @@ def configure_extension_build() -> tuple[
10591053
# Configure compile flags
10601054
################################################################################
10611055

1062-
library_dirs: list[str] = [str(TORCH_LIB_DIR)]
10631056
extra_install_requires: list[str] = []
10641057

1065-
if IS_WINDOWS:
1066-
# /NODEFAULTLIB makes sure we only link to DLL runtime
1067-
# and matches the flags set for protobuf and ONNX
1068-
extra_link_args: list[str] = ["/NODEFAULTLIB:LIBCMT.LIB"]
1069-
# /MD links against DLL runtime
1070-
# and matches the flags set for protobuf and ONNX
1071-
# /EHsc is about standard C++ exception handling
1072-
extra_compile_args: list[str] = ["/MD", "/FS", "/EHsc"]
1073-
else:
1074-
extra_link_args = []
1075-
extra_compile_args = [
1076-
"-Wall",
1077-
"-Wextra",
1078-
"-Wno-strict-overflow",
1079-
"-Wno-unused-parameter",
1080-
"-Wno-missing-field-initializers",
1081-
"-Wno-unknown-pragmas",
1082-
# Python 2.6 requires -fno-strict-aliasing, see
1083-
# http://legacy.python.org/dev/peps/pep-3123/
1084-
# We also depend on it in our code (even Python 3).
1085-
"-fno-strict-aliasing",
1086-
]
1087-
1088-
main_compile_args: list[str] = []
1089-
main_libraries: list[str] = ["torch_python"]
1090-
1091-
main_link_args: list[str] = []
1092-
main_sources: list[str] = ["torch/csrc/stub.c"]
1093-
1094-
if BUILD_LIBTORCH_WHL:
1095-
main_libraries = ["torch"]
1096-
main_sources = []
1097-
1098-
if build_type.is_debug():
1099-
if IS_WINDOWS:
1100-
extra_compile_args += ["/Z7"]
1101-
extra_link_args += ["/DEBUG:FULL"]
1102-
else:
1103-
extra_compile_args += ["-O0", "-g"]
1104-
extra_link_args += ["-O0", "-g"]
1105-
1106-
if build_type.is_rel_with_deb_info():
1107-
if IS_WINDOWS:
1108-
extra_compile_args += ["/Z7"]
1109-
extra_link_args += ["/DEBUG:FULL"]
1110-
else:
1111-
extra_compile_args += ["-g"]
1112-
extra_link_args += ["-g"]
1113-
11141058
# pypi cuda package that requires installation of cuda runtime, cudnn and cublas
11151059
# should be included in all wheels uploaded to pypi
11161060
pytorch_extra_install_requires = os.getenv("PYTORCH_EXTRA_INSTALL_REQUIREMENTS")
@@ -1120,39 +1064,12 @@ def configure_extension_build() -> tuple[
11201064
map(str.strip, pytorch_extra_install_requires.split("|"))
11211065
)
11221066

1123-
# Cross-compile for M1
1124-
if IS_DARWIN:
1125-
macos_target_arch = os.getenv("CMAKE_OSX_ARCHITECTURES", "")
1126-
if macos_target_arch in ["arm64", "x86_64"]:
1127-
macos_sysroot_path = os.getenv("CMAKE_OSX_SYSROOT")
1128-
if macos_sysroot_path is None:
1129-
macos_sysroot_path = (
1130-
subprocess.check_output(
1131-
["xcrun", "--show-sdk-path", "--sdk", "macosx"]
1132-
)
1133-
.decode("utf-8")
1134-
.strip()
1135-
)
1136-
extra_compile_args += [
1137-
"-arch",
1138-
macos_target_arch,
1139-
"-isysroot",
1140-
macos_sysroot_path,
1141-
]
1142-
extra_link_args += ["-arch", macos_target_arch]
1143-
1144-
def make_relative_rpath_args(path: str) -> list[str]:
1145-
if IS_DARWIN:
1146-
return ["-Wl,-rpath,@loader_path/" + path]
1147-
elif IS_WINDOWS:
1148-
return []
1149-
else:
1150-
return ["-Wl,-rpath,$ORIGIN/" + path]
1151-
11521067
################################################################################
11531068
# Declare extensions and package
11541069
################################################################################
11551070

1071+
# torch._C is now built by CMake (torch/CMakeLists.txt) and installed
1072+
# into the cmake install prefix, which maps to torch/ in the source tree.
11561073
ext_modules: list[Extension] = []
11571074
# packages that we want to install into site-packages and include them in wheels
11581075
includes = ["torch", "torch.*", "torchgen", "torchgen.*"]
@@ -1163,24 +1080,6 @@ def make_relative_rpath_args(path: str) -> list[str]:
11631080
else:
11641081
excludes.extend(["functorch", "functorch.*"])
11651082
packages = find_packages(include=includes, exclude=excludes)
1166-
C = Extension(
1167-
"torch._C",
1168-
libraries=main_libraries,
1169-
sources=main_sources,
1170-
language="c",
1171-
extra_compile_args=[
1172-
*main_compile_args,
1173-
*extra_compile_args,
1174-
],
1175-
include_dirs=[],
1176-
library_dirs=library_dirs,
1177-
extra_link_args=[
1178-
*extra_link_args,
1179-
*main_link_args,
1180-
*make_relative_rpath_args("lib"),
1181-
],
1182-
)
1183-
ext_modules.append(C)
11841083

11851084
cmdclass = {
11861085
"bdist_wheel": bdist_wheel,
@@ -1272,6 +1171,11 @@ def main() -> None:
12721171

12731172
torch_package_data = [
12741173
"py.typed",
1174+
# torch._C is built by CMake (torch/CMakeLists.txt) and installed into
1175+
# torch/. Match this interpreter's exact extension suffix, not a glob:
1176+
# build_all.sh reuses one build/ dir across Python versions, so a glob
1177+
# would also pick up other ABIs' _C modules left behind in the tree.
1178+
f"_C{sysconfig.get_config_var('EXT_SUFFIX')}",
12751179
"bin/*",
12761180
"bin/**/*",
12771181
"test/*",
@@ -1378,6 +1282,7 @@ def main() -> None:
13781282
setup(
13791283
name=TORCH_PACKAGE_NAME,
13801284
version=TORCH_VERSION,
1285+
distclass=BinaryDistribution,
13811286
ext_modules=ext_modules,
13821287
cmdclass=cmdclass,
13831288
packages=packages,

torch/CMakeLists.txt

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,16 @@ set(TORCH_PYTHON_LINK_FLAGS "")
107107

108108
if(MSVC)
109109
string(APPEND TORCH_PYTHON_LINK_FLAGS " /NODEFAULTLIB:LIBCMT.LIB")
110-
list(APPEND TORCH_PYTHON_LINK_LIBRARIES ${PYTHON_LIBRARIES} onnx_library)
110+
# Python headers use #pragma comment(lib, "pythonXX") which makes MSVC search
111+
# for the library by bare name. Add the directory so the linker can find it.
112+
foreach(_pylib IN LISTS Python_LIBRARIES)
113+
if(_pylib MATCHES "python[0-9].*\\.lib$")
114+
get_filename_component(_pylib_dir "${_pylib}" DIRECTORY)
115+
string(APPEND TORCH_PYTHON_LINK_FLAGS " /LIBPATH:\"${_pylib_dir}\"")
116+
break()
117+
endif()
118+
endforeach()
119+
list(APPEND TORCH_PYTHON_LINK_LIBRARIES ${Python_LIBRARIES} onnx_library)
111120
if(NOT CMAKE_BUILD_TYPE MATCHES "Release")
112121
string(APPEND TORCH_PYTHON_LINK_FLAGS " /DEBUG:FULL")
113122
endif()
@@ -424,11 +433,40 @@ if(${CMAKE_BUILD_TYPE} STREQUAL "RelWithAssert")
424433
endif()
425434

426435
if(NOT TORCH_PYTHON_LINK_FLAGS STREQUAL "")
427-
set_target_properties(torch_python PROPERTIES LINK_FLAGS ${TORCH_PYTHON_LINK_FLAGS})
436+
set_target_properties(torch_python PROPERTIES LINK_FLAGS "${TORCH_PYTHON_LINK_FLAGS}")
428437
endif()
429438

430439
install(TARGETS torch_python DESTINATION "${TORCH_INSTALL_LIB_DIR}")
431440

441+
# Build the torch._C Python extension module from the thin stub that
442+
# forwards to torch_python. This was historically built by setuptools;
443+
# building it here lets both setuptools and scikit-build-core use the
444+
# same code path.
445+
if(BUILD_PYTHON AND NOT BUILD_LIBTORCH_WHL)
446+
Python_add_library(_C MODULE WITH_SOABI "${TORCH_SRC_DIR}/csrc/stub.c")
447+
target_link_libraries(_C PRIVATE torch_python)
448+
set_target_properties(_C PROPERTIES
449+
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/torch"
450+
# libtorch_python.so is installed to lib/ relative to _C, so use
451+
# $ORIGIN/lib as the install RPATH (the global default is just $ORIGIN).
452+
INSTALL_RPATH "${_rpath_portable_origin}/lib"
453+
)
454+
if(NOT WIN32)
455+
set_target_properties(_C PROPERTIES
456+
C_VISIBILITY_PRESET "default"
457+
)
458+
endif()
459+
# Install at the install prefix root, which maps to torch/ in the wheel
460+
# via wheel.install-dir = "torch". The import library (Windows _C.lib) goes
461+
# alongside the other import libs in torch/lib/, matching the layout that
462+
# setup.py used before _C moved to CMake.
463+
install(TARGETS _C
464+
RUNTIME DESTINATION "."
465+
LIBRARY DESTINATION "."
466+
ARCHIVE DESTINATION "lib"
467+
)
468+
endif()
469+
432470
# Generate torch/version.py from the appropriate CMake cache variables.
433471
if(${CMAKE_BUILD_TYPE} STREQUAL "Debug")
434472
set(TORCH_VERSION_DEBUG 1)
@@ -454,6 +492,9 @@ add_custom_target(
454492
WORKING_DIRECTORY ${TORCH_ROOT}
455493
)
456494
add_dependencies(torch_python gen_torch_version)
495+
# version.py is gitignored (generated), so scikit-build-core won't include it
496+
# via Python package collection. Install it explicitly via cmake instead.
497+
install(FILES "${TORCH_SRC_DIR}/version.py" DESTINATION ".")
457498

458499
# Skip building this library under MacOS, since it is currently failing to build on Mac
459500
# Github issue #61930
@@ -470,6 +511,9 @@ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
470511
target_link_libraries(nnapi_backend PRIVATE torch)
471512
endif()
472513
target_link_libraries(nnapi_backend PRIVATE torch_python pybind::pybind11 fmt::fmt-header-only)
514+
if(MSVC AND TORCH_PYTHON_LINK_FLAGS)
515+
set_target_properties(nnapi_backend PROPERTIES LINK_FLAGS "${TORCH_PYTHON_LINK_FLAGS}")
516+
endif()
473517
endif()
474518

475519
set(TORCH_PYTHON_COMPILE_OPTIONS ${TORCH_PYTHON_COMPILE_OPTIONS} PARENT_SCOPE)

0 commit comments

Comments
 (0)