hook-torch.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. # ------------------------------------------------------------------
  2. # Copyright (c) 2020 PyInstaller Development Team.
  3. #
  4. # This file is distributed under the terms of the GNU General Public
  5. # License (version 2.0 or later).
  6. #
  7. # The full license is available in LICENSE, distributed with
  8. # this software.
  9. #
  10. # SPDX-License-Identifier: GPL-2.0-or-later
  11. # ------------------------------------------------------------------
  12. import os
  13. from PyInstaller.utils.hooks import (
  14. logger,
  15. collect_data_files,
  16. is_module_satisfies,
  17. collect_dynamic_libs,
  18. collect_submodules,
  19. get_package_paths,
  20. )
  21. if is_module_satisfies("PyInstaller >= 6.0"):
  22. from PyInstaller import compat
  23. from PyInstaller.utils.hooks import PY_DYLIB_PATTERNS
  24. module_collection_mode = "pyz+py"
  25. warn_on_missing_hiddenimports = False
  26. datas = collect_data_files(
  27. "torch",
  28. excludes=[
  29. "**/*.h",
  30. "**/*.hpp",
  31. "**/*.cuh",
  32. "**/*.lib",
  33. "**/*.cpp",
  34. "**/*.pyi",
  35. "**/*.cmake",
  36. ],
  37. )
  38. hiddenimports = collect_submodules("torch")
  39. binaries = collect_dynamic_libs(
  40. "torch",
  41. # Ensure we pick up fully-versioned .so files as well
  42. search_patterns=PY_DYLIB_PATTERNS + ['*.so.*'],
  43. )
  44. # On Linux, torch wheels built with non-default CUDA version bundle CUDA libraries themselves (and should be handled
  45. # by the above `collect_dynamic_libs`). Wheels built with default CUDA version (which are available on PyPI), on the
  46. # other hand, use CUDA libraries provided by nvidia-* packages. Due to all possible combinations (CUDA libs from
  47. # nvidia-* packages, torch-bundled CUDA libs, CPU-only CUDA libs) we do not add hidden imports directly, but instead
  48. # attempt to infer them from requirements listed in the `torch` metadata.
  49. if compat.is_linux:
  50. def _infer_nvidia_hiddenimports():
  51. import packaging.requirements
  52. from _pyinstaller_hooks_contrib.compat import importlib_metadata
  53. from _pyinstaller_hooks_contrib.utils import nvidia_cuda as cudautils
  54. dist = importlib_metadata.distribution("torch")
  55. requirements = [packaging.requirements.Requirement(req) for req in dist.requires or []]
  56. requirements = [req.name for req in requirements if req.marker is None or req.marker.evaluate()]
  57. return cudautils.infer_hiddenimports_from_requirements(requirements)
  58. try:
  59. nvidia_hiddenimports = _infer_nvidia_hiddenimports()
  60. except Exception:
  61. # Log the exception, but make it non-fatal
  62. logger.warning("hook-torch: failed to infer NVIDIA CUDA hidden imports!", exc_info=True)
  63. nvidia_hiddenimports = []
  64. logger.info("hook-torch: inferred hidden imports for CUDA libraries: %r", nvidia_hiddenimports)
  65. hiddenimports += nvidia_hiddenimports
  66. # On Linux, prevent binary dependency analysis from generating symbolic links for libraries from `torch/lib` to
  67. # the top-level application directory. These symbolic links seem to confuse `torch` about location of its shared
  68. # libraries (likely because code in one of the libraries looks up the library file's location, but does not
  69. # fully resolve it), and prevent it from finding dynamically-loaded libraries in `torch/lib` directory, such as
  70. # `torch/lib/libtorch_cuda_linalg.so`. The issue was observed with earlier versions of `torch` builds provided
  71. # by https://download.pytorch.org/whl/torch, specifically 1.13.1+cu117, 2.0.1+cu117, and 2.1.2+cu118; later
  72. # versions do not seem to be affected. The wheels provided on PyPI do not seem to be affected, either, even
  73. # for torch 1.13.1, 2.01, and 2.1.2. However, these symlinks should be not necessary on linux in general, so
  74. # there should be no harm in suppressing them for all versions.
  75. #
  76. # The `bindepend_symlink_suppression` hook attribute requires PyInstaller >= 6.11, and is no-op in earlier
  77. # versions.
  78. bindepend_symlink_suppression = ['**/torch/lib/*.so*']
  79. # The Windows nightly build for torch 2.3.0 added dependency on MKL. The `mkl` distribution does not provide an
  80. # importable package, but rather installs the DLLs in <env>/Library/bin directory. Therefore, we cannot write a
  81. # separate hook for it, and must collect the DLLs here. (Most of these DLLs are missed by PyInstaller's binary
  82. # dependency analysis due to being dynamically loaded at run-time).
  83. if compat.is_win:
  84. def _collect_mkl_dlls():
  85. # Determine if torch is packaged by Anaconda or not. Ideally, we would use our `get_installer()` hook
  86. # utility function to check if installer is `conda`. However, it seems that some builds (e.g., those from
  87. # `pytorch` and `nvidia` channels) provide legacy metadata in form of .egg-info directory, which does not
  88. # include an INSTALLER file. So instead, search the conda metadata for a conda distribution/package that
  89. # provides a `torch` importable package, if any.
  90. conda_torch_dist = None
  91. if compat.is_conda:
  92. from PyInstaller.utils.hooks import conda_support
  93. try:
  94. conda_torch_dist = conda_support.package_distribution('torch')
  95. except ModuleNotFoundError:
  96. conda_torch_dist = None
  97. if conda_torch_dist:
  98. # Anaconda-packaged torch
  99. if 'mkl' not in conda_torch_dist.dependencies:
  100. logger.info('hook-torch: this torch build (Anaconda package) does not depend on MKL...')
  101. return []
  102. logger.info('hook-torch: collecting DLLs from MKL and its dependencies (Anaconda packages)')
  103. mkl_binaries = conda_support.collect_dynamic_libs('mkl', dependencies=True)
  104. else:
  105. # Non-Anaconda torch (e.g., PyPI wheel)
  106. import packaging.requirements
  107. from _pyinstaller_hooks_contrib.compat import importlib_metadata
  108. # Check if torch depends on `mkl`
  109. dist = importlib_metadata.distribution("torch")
  110. requirements = [packaging.requirements.Requirement(req) for req in dist.requires or []]
  111. requirements = [req.name for req in requirements if req.marker is None or req.marker.evaluate()]
  112. if 'mkl' not in requirements:
  113. logger.info('hook-torch: this torch build does not depend on MKL...')
  114. return []
  115. # Find requirements of mkl - this should yield `intel-openmp` and `tbb`, which install DLLs in the same
  116. # way as `mkl`.
  117. try:
  118. dist = importlib_metadata.distribution("mkl")
  119. except importlib_metadata.PackageNotFoundError:
  120. return [] # For some reason, `mkl` distribution is unavailable.
  121. requirements = [packaging.requirements.Requirement(req) for req in dist.requires or []]
  122. requirements = [req.name for req in requirements if req.marker is None or req.marker.evaluate()]
  123. requirements = ['mkl'] + requirements
  124. mkl_binaries = []
  125. logger.info('hook-torch: collecting DLLs from MKL and its dependencies: %r', requirements)
  126. for requirement in requirements:
  127. try:
  128. dist = importlib_metadata.distribution(requirement)
  129. except importlib_metadata.PackageNotFoundError:
  130. continue
  131. # Go over files, and match DLLs in <env>/Library/bin directory
  132. for dist_file in (dist.files or []):
  133. # NOTE: `importlib_metadata.PackagePath.match()` does not seem to properly normalize the
  134. # separator, and on Windows, RECORD can apparently end up with entries that use either Windows
  135. # or POSIX-style separators (see pyinstaller/pyinstaller-hooks-contrib#879). This is why we
  136. # first resolve the file's location (which yields a `pathlib.Path` instance), and perform
  137. # matching on resolved path.
  138. dll_file = dist.locate_file(dist_file).resolve()
  139. if not dll_file.match('**/Library/bin/*.dll'):
  140. continue
  141. mkl_binaries.append((str(dll_file), '.'))
  142. if mkl_binaries:
  143. logger.info(
  144. 'hook-torch: found MKL DLLs: %r',
  145. sorted([os.path.basename(src_name) for src_name, dest_name in mkl_binaries])
  146. )
  147. else:
  148. logger.info('hook-torch: no MKL DLLs found.')
  149. return mkl_binaries
  150. try:
  151. mkl_binaries = _collect_mkl_dlls()
  152. except Exception:
  153. # Log the exception, but make it non-fatal
  154. logger.warning("hook-torch: failed to collect MKL DLLs!", exc_info=True)
  155. mkl_binaries = []
  156. binaries += mkl_binaries
  157. else:
  158. datas = [(get_package_paths("torch")[1], "torch")]