| "Manages CMake." |
| |
| |
| import multiprocessing |
| import os |
| import platform |
| import re |
| from subprocess import check_call, check_output, CalledProcessError |
| import sys |
| import sysconfig |
| from distutils.version import LooseVersion |
| from typing import IO, Any, Dict, List, Optional, Union, cast |
| |
| from . import which |
| from .env import BUILD_DIR, IS_64BIT, IS_DARWIN, IS_WINDOWS, check_negative_env_flag |
| from .numpy_ import USE_NUMPY, NUMPY_INCLUDE_DIR |
| |
| |
| def _mkdir_p(d: str) -> None: |
| try: |
| os.makedirs(d, exist_ok=True) |
| except OSError as e: |
| raise RuntimeError( |
| f"Failed to create folder {os.path.abspath(d)}: {e.strerror}" |
| ) from e |
| |
| |
| # Ninja |
| # Use ninja if it is on the PATH. Previous version of PyTorch required the |
| # ninja python package, but we no longer use it, so we do not have to import it |
| USE_NINJA = not check_negative_env_flag("USE_NINJA") and which("ninja") is not None |
| |
| |
| CMakeValue = Optional[Union[bool, str]] |
| |
| |
| def convert_cmake_value_to_python_value( |
| cmake_value: str, cmake_type: str |
| ) -> CMakeValue: |
| r"""Convert a CMake value in a string form to a Python value. |
| |
| Args: |
| cmake_value (string): The CMake value in a string form (e.g., "ON", "OFF", "1"). |
| cmake_type (string): The CMake type of :attr:`cmake_value`. |
| |
| Returns: |
| A Python value corresponding to :attr:`cmake_value` with type :attr:`cmake_type`. |
| """ |
| |
| cmake_type = cmake_type.upper() |
| up_val = cmake_value.upper() |
| if cmake_type == "BOOL": |
| # https://gitlab.kitware.com/cmake/community/wikis/doc/cmake/VariablesListsStrings#boolean-values-in-cmake |
| return not ( |
| up_val in ("FALSE", "OFF", "N", "NO", "0", "", "NOTFOUND") |
| or up_val.endswith("-NOTFOUND") |
| ) |
| elif cmake_type == "FILEPATH": |
| if up_val.endswith("-NOTFOUND"): |
| return None |
| else: |
| return cmake_value |
| else: # Directly return the cmake_value. |
| return cmake_value |
| |
| |
| def get_cmake_cache_variables_from_file( |
| cmake_cache_file: IO[str], |
| ) -> Dict[str, CMakeValue]: |
| r"""Gets values in CMakeCache.txt into a dictionary. |
| |
| Args: |
| cmake_cache_file: A CMakeCache.txt file object. |
| Returns: |
| dict: A ``dict`` containing the value of cached CMake variables. |
| """ |
| |
| results = dict() |
| for i, line in enumerate(cmake_cache_file, 1): |
| line = line.strip() |
| if not line or line.startswith(("#", "//")): |
| # Blank or comment line, skip |
| continue |
| |
| # Almost any character can be part of variable name and value. As a practical matter, we assume the type must be |
| # valid if it were a C variable name. It should match the following kinds of strings: |
| # |
| # USE_CUDA:BOOL=ON |
| # "USE_CUDA":BOOL=ON |
| # USE_CUDA=ON |
| # USE_CUDA:=ON |
| # Intel(R) MKL-DNN_SOURCE_DIR:STATIC=/path/to/pytorch/third_party/ideep/mkl-dnn |
| # "OpenMP_COMPILE_RESULT_CXX_openmp:experimental":INTERNAL=FALSE |
| matched = re.match( |
| r'("?)(.+?)\1(?::\s*([a-zA-Z_-][a-zA-Z0-9_-]*)?)?\s*=\s*(.*)', line |
| ) |
| if matched is None: # Illegal line |
| raise ValueError( |
| "Unexpected line {} in {}: {}".format(i, repr(cmake_cache_file), line) |
| ) |
| _, variable, type_, value = matched.groups() |
| if type_ is None: |
| type_ = "" |
| if type_.upper() in ("INTERNAL", "STATIC"): |
| # CMake internal variable, do not touch |
| continue |
| results[variable] = convert_cmake_value_to_python_value(value, type_) |
| |
| return results |
| |
| |
| class CMake: |
| "Manages cmake." |
| |
| def __init__(self, build_dir: str = BUILD_DIR) -> None: |
| self._cmake_command = CMake._get_cmake_command() |
| self.build_dir = build_dir |
| |
| @property |
| def _cmake_cache_file(self) -> str: |
| r"""Returns the path to CMakeCache.txt. |
| |
| Returns: |
| string: The path to CMakeCache.txt. |
| """ |
| return os.path.join(self.build_dir, "CMakeCache.txt") |
| |
| @staticmethod |
| def _get_cmake_command() -> str: |
| "Returns cmake command." |
| |
| cmake_command = "cmake" |
| if IS_WINDOWS: |
| return cmake_command |
| cmake3_version = CMake._get_version(which("cmake3")) |
| cmake_version = CMake._get_version(which("cmake")) |
| |
| _cmake_min_version = LooseVersion("3.10.0") |
| if all( |
| ( |
| ver is None or ver < _cmake_min_version |
| for ver in [cmake_version, cmake3_version] |
| ) |
| ): |
| raise RuntimeError("no cmake or cmake3 with version >= 3.10.0 found") |
| |
| if cmake3_version is None: |
| cmake_command = "cmake" |
| elif cmake_version is None: |
| cmake_command = "cmake3" |
| else: |
| if cmake3_version >= cmake_version: |
| cmake_command = "cmake3" |
| else: |
| cmake_command = "cmake" |
| return cmake_command |
| |
| @staticmethod |
| def _get_version(cmd: Optional[str]) -> Any: |
| "Returns cmake version." |
| |
| if cmd is None: |
| return None |
| for line in check_output([cmd, "--version"]).decode("utf-8").split("\n"): |
| if "version" in line: |
| return LooseVersion(line.strip().split(" ")[2]) |
| raise RuntimeError("no version found") |
| |
| def run(self, args: List[str], env: Dict[str, str]) -> None: |
| "Executes cmake with arguments and an environment." |
| |
| command = [self._cmake_command] + args |
| print(" ".join(command)) |
| try: |
| check_call(command, cwd=self.build_dir, env=env) |
| except (CalledProcessError, KeyboardInterrupt) as e: |
| # This error indicates that there was a problem with cmake, the |
| # Python backtrace adds no signal here so skip over it by catching |
| # the error and exiting manually |
| sys.exit(1) |
| |
| @staticmethod |
| def defines(args: List[str], **kwargs: CMakeValue) -> None: |
| "Adds definitions to a cmake argument list." |
| for key, value in sorted(kwargs.items()): |
| if value is not None: |
| args.append("-D{}={}".format(key, value)) |
| |
| def get_cmake_cache_variables(self) -> Dict[str, CMakeValue]: |
| r"""Gets values in CMakeCache.txt into a dictionary. |
| Returns: |
| dict: A ``dict`` containing the value of cached CMake variables. |
| """ |
| with open(self._cmake_cache_file) as f: |
| return get_cmake_cache_variables_from_file(f) |
| |
| def generate( |
| self, |
| version: Optional[str], |
| cmake_python_library: Optional[str], |
| build_python: bool, |
| build_test: bool, |
| my_env: Dict[str, str], |
| rerun: bool, |
| ) -> None: |
| "Runs cmake to generate native build files." |
| |
| if rerun and os.path.isfile(self._cmake_cache_file): |
| os.remove(self._cmake_cache_file) |
| |
| ninja_build_file = os.path.join(self.build_dir, "build.ninja") |
| if os.path.exists(self._cmake_cache_file) and not ( |
| USE_NINJA and not os.path.exists(ninja_build_file) |
| ): |
| # Everything's in place. Do not rerun. |
| return |
| |
| args = [] |
| if USE_NINJA: |
| # Avoid conflicts in '-G' and the `CMAKE_GENERATOR` |
| os.environ["CMAKE_GENERATOR"] = "Ninja" |
| args.append("-GNinja") |
| elif IS_WINDOWS: |
| generator = os.getenv("CMAKE_GENERATOR", "Visual Studio 15 2017") |
| supported = ["Visual Studio 15 2017", "Visual Studio 16 2019"] |
| if generator not in supported: |
| print("Unsupported `CMAKE_GENERATOR`: " + generator) |
| print("Please set it to one of the following values: ") |
| print("\n".join(supported)) |
| sys.exit(1) |
| args.append("-G" + generator) |
| toolset_dict = {} |
| toolset_version = os.getenv("CMAKE_GENERATOR_TOOLSET_VERSION") |
| if toolset_version is not None: |
| toolset_dict["version"] = toolset_version |
| curr_toolset = os.getenv("VCToolsVersion") |
| if curr_toolset is None: |
| print( |
| "When you specify `CMAKE_GENERATOR_TOOLSET_VERSION`, you must also " |
| "activate the vs environment of this version. Please read the notes " |
| "in the build steps carefully." |
| ) |
| sys.exit(1) |
| if IS_64BIT: |
| if platform.machine() == "ARM64": |
| args.append("-A ARM64") |
| else: |
| args.append("-Ax64") |
| toolset_dict["host"] = "x64" |
| if toolset_dict: |
| toolset_expr = ",".join( |
| ["{}={}".format(k, v) for k, v in toolset_dict.items()] |
| ) |
| args.append("-T" + toolset_expr) |
| |
| base_dir = os.path.dirname( |
| os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| ) |
| install_dir = os.path.join(base_dir, "torch") |
| |
| _mkdir_p(install_dir) |
| _mkdir_p(self.build_dir) |
| |
| # Store build options that are directly stored in environment variables |
| build_options: Dict[str, CMakeValue] = {} |
| |
| # Build options that do not start with "BUILD_", "USE_", or "CMAKE_" and are directly controlled by env vars. |
| # This is a dict that maps environment variables to the corresponding variable name in CMake. |
| additional_options = { |
| # Key: environment variable name. Value: Corresponding variable name to be passed to CMake. If you are |
| # adding a new build option to this block: Consider making these two names identical and adding this option |
| # in the block below. |
| "_GLIBCXX_USE_CXX11_ABI": "GLIBCXX_USE_CXX11_ABI", |
| "CUDNN_LIB_DIR": "CUDNN_LIBRARY", |
| "USE_CUDA_STATIC_LINK": "CAFFE2_STATIC_LINK_CUDA", |
| } |
| additional_options.update( |
| { |
| # Build options that have the same environment variable name and CMake variable name and that do not start |
| # with "BUILD_", "USE_", or "CMAKE_". If you are adding a new build option, also make sure you add it to |
| # CMakeLists.txt. |
| var: var |
| for var in ( |
| "BLAS", |
| "BUILDING_WITH_TORCH_LIBS", |
| "CUDA_HOST_COMILER", |
| "CUDA_NVCC_EXECUTABLE", |
| "CUDA_SEPARABLE_COMPILATION", |
| "CUDNN_LIBRARY", |
| "CUDNN_INCLUDE_DIR", |
| "CUDNN_ROOT", |
| "EXPERIMENTAL_SINGLE_THREAD_POOL", |
| "INSTALL_TEST", |
| "JAVA_HOME", |
| "INTEL_MKL_DIR", |
| "INTEL_OMP_DIR", |
| "MKL_THREADING", |
| "MKLDNN_CPU_RUNTIME", |
| "MSVC_Z7_OVERRIDE", |
| "CAFFE2_USE_MSVC_STATIC_RUNTIME", |
| "Numa_INCLUDE_DIR", |
| "Numa_LIBRARIES", |
| "ONNX_ML", |
| "ONNX_NAMESPACE", |
| "ATEN_THREADING", |
| "WERROR", |
| "OPENSSL_ROOT_DIR", |
| "STATIC_DISPATCH_BACKEND", |
| ) |
| } |
| ) |
| |
| # Aliases which are lower priority than their canonical option |
| low_priority_aliases = { |
| "CUDA_HOST_COMPILER": "CMAKE_CUDA_HOST_COMPILER", |
| "CUDAHOSTCXX": "CUDA_HOST_COMPILER", |
| "CMAKE_CUDA_HOST_COMPILER": "CUDA_HOST_COMPILER", |
| "CMAKE_CUDA_COMPILER": "CUDA_NVCC_EXECUTABLE", |
| "CUDACXX": "CUDA_NVCC_EXECUTABLE", |
| } |
| for var, val in my_env.items(): |
| # We currently pass over all environment variables that start with "BUILD_", "USE_", and "CMAKE_". This is |
| # because we currently have no reliable way to get the list of all build options we have specified in |
| # CMakeLists.txt. (`cmake -L` won't print dependent options when the dependency condition is not met.) We |
| # will possibly change this in the future by parsing CMakeLists.txt ourselves (then additional_options would |
| # also not be needed to be specified here). |
| true_var = additional_options.get(var) |
| if true_var is not None: |
| build_options[true_var] = val |
| elif var.startswith(("BUILD_", "USE_", "CMAKE_")) or var.endswith( |
| ("EXITCODE", "EXITCODE__TRYRUN_OUTPUT") |
| ): |
| build_options[var] = val |
| |
| if var in low_priority_aliases: |
| key = low_priority_aliases[var] |
| if key not in build_options: |
| build_options[key] = val |
| |
| # The default value cannot be easily obtained in CMakeLists.txt. We set it here. |
| py_lib_path = sysconfig.get_path("purelib") |
| cmake_prefix_path = build_options.get("CMAKE_PREFIX_PATH", None) |
| if cmake_prefix_path: |
| build_options["CMAKE_PREFIX_PATH"] = ( |
| cast(str, py_lib_path) + ";" + cast(str, cmake_prefix_path) |
| ) |
| else: |
| build_options["CMAKE_PREFIX_PATH"] = py_lib_path |
| |
| # Some options must be post-processed. Ideally, this list will be shrunk to only one or two options in the |
| # future, as CMake can detect many of these libraries pretty comfortably. We have them here for now before CMake |
| # integration is completed. They appear here not in the CMake.defines call below because they start with either |
| # "BUILD_" or "USE_" and must be overwritten here. |
| build_options.update( |
| { |
| # Note: Do not add new build options to this dict if it is directly read from environment variable -- you |
| # only need to add one in `CMakeLists.txt`. All build options that start with "BUILD_", "USE_", or "CMAKE_" |
| # are automatically passed to CMake; For other options you can add to additional_options above. |
| "BUILD_PYTHON": build_python, |
| "BUILD_TEST": build_test, |
| # Most library detection should go to CMake script, except this one, which Python can do a much better job |
| # due to NumPy's inherent Pythonic nature. |
| "USE_NUMPY": USE_NUMPY, |
| } |
| ) |
| |
| # Options starting with CMAKE_ |
| cmake__options = { |
| "CMAKE_INSTALL_PREFIX": install_dir, |
| } |
| |
| # We set some CMAKE_* options in our Python build code instead of relying on the user's direct settings. Emit an |
| # error if the user also attempts to set these CMAKE options directly. |
| specified_cmake__options = set(build_options).intersection(cmake__options) |
| if len(specified_cmake__options) > 0: |
| print( |
| ", ".join(specified_cmake__options) |
| + " should not be specified in the environment variable. They are directly set by PyTorch build script." |
| ) |
| sys.exit(1) |
| build_options.update(cmake__options) |
| |
| CMake.defines( |
| args, |
| PYTHON_EXECUTABLE=sys.executable, |
| PYTHON_LIBRARY=cmake_python_library, |
| PYTHON_INCLUDE_DIR=sysconfig.get_path("include"), |
| TORCH_BUILD_VERSION=version, |
| NUMPY_INCLUDE_DIR=NUMPY_INCLUDE_DIR, |
| **build_options, |
| ) |
| |
| expected_wrapper = "/usr/local/opt/ccache/libexec" |
| if IS_DARWIN and os.path.exists(expected_wrapper): |
| if "CMAKE_C_COMPILER" not in build_options and "CC" not in os.environ: |
| CMake.defines(args, CMAKE_C_COMPILER="{}/gcc".format(expected_wrapper)) |
| if "CMAKE_CXX_COMPILER" not in build_options and "CXX" not in os.environ: |
| CMake.defines( |
| args, CMAKE_CXX_COMPILER="{}/g++".format(expected_wrapper) |
| ) |
| |
| for env_var_name in my_env: |
| if env_var_name.startswith("gh"): |
| # github env vars use utf-8, on windows, non-ascii code may |
| # cause problem, so encode first |
| try: |
| my_env[env_var_name] = str(my_env[env_var_name].encode("utf-8")) |
| except UnicodeDecodeError as e: |
| shex = ":".join( |
| "{:02x}".format(ord(c)) for c in my_env[env_var_name] |
| ) |
| print( |
| "Invalid ENV[{}] = {}".format(env_var_name, shex), |
| file=sys.stderr, |
| ) |
| print(e, file=sys.stderr) |
| # According to the CMake manual, we should pass the arguments first, |
| # and put the directory as the last element. Otherwise, these flags |
| # may not be passed correctly. |
| # Reference: |
| # 1. https://cmake.org/cmake/help/latest/manual/cmake.1.html#synopsis |
| # 2. https://stackoverflow.com/a/27169347 |
| args.append(base_dir) |
| self.run(args, env=my_env) |
| |
| def build(self, my_env: Dict[str, str]) -> None: |
| "Runs cmake to build binaries." |
| |
| from .env import build_type |
| |
| build_args = [ |
| "--build", |
| ".", |
| "--target", |
| "install", |
| "--config", |
| build_type.build_type_string, |
| ] |
| |
| # Determine the parallelism according to the following |
| # priorities: |
| # 1) MAX_JOBS environment variable |
| # 2) If using the Ninja build system, delegate decision to it. |
| # 3) Otherwise, fall back to the number of processors. |
| |
| # Allow the user to set parallelism explicitly. If unset, |
| # we'll try to figure it out. |
| max_jobs = os.getenv("MAX_JOBS") |
| |
| if max_jobs is not None or not USE_NINJA: |
| # Ninja is capable of figuring out the parallelism on its |
| # own: only specify it explicitly if we are not using |
| # Ninja. |
| |
| # This lists the number of processors available on the |
| # machine. This may be an overestimate of the usable |
| # processors if CPU scheduling affinity limits it |
| # further. In the future, we should check for that with |
| # os.sched_getaffinity(0) on platforms that support it. |
| max_jobs = max_jobs or str(multiprocessing.cpu_count()) |
| |
| # This ``if-else'' clause would be unnecessary when cmake |
| # 3.12 becomes minimum, which provides a '-j' option: |
| # build_args += ['-j', max_jobs] would be sufficient by |
| # then. Until then, we use "--" to pass parameters to the |
| # underlying build system. |
| build_args += ["--"] |
| if IS_WINDOWS and not USE_NINJA: |
| # We are likely using msbuild here |
| build_args += ["/p:CL_MPCount={}".format(max_jobs)] |
| else: |
| build_args += ["-j", max_jobs] |
| self.run(build_args, my_env) |