_hypothesis_ftz_detector.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. # This file is part of Hypothesis, which may be found at
  2. # https://github.com/HypothesisWorks/hypothesis/
  3. #
  4. # Copyright the Hypothesis Authors.
  5. # Individual contributors are listed in AUTHORS.rst and the git log.
  6. #
  7. # This Source Code Form is subject to the terms of the Mozilla Public License,
  8. # v. 2.0. If a copy of the MPL was not distributed with this file, You can
  9. # obtain one at https://mozilla.org/MPL/2.0/.
  10. """
  11. This is a toolkit for determining which module set the "flush to zero" flag.
  12. For details, see the docstring and comments in `identify_ftz_culprit()`. This module
  13. is defined outside the main Hypothesis namespace so that we can avoid triggering
  14. import of Hypothesis itself from each subprocess which must import the worker function.
  15. """
  16. import importlib
  17. import sys
  18. from typing import TYPE_CHECKING, Callable, Optional, Set, Tuple
  19. if TYPE_CHECKING:
  20. from multiprocessing import Queue
  21. from typing import TypeAlias
  22. FTZCulprits: "TypeAlias" = Tuple[Optional[bool], Set[str]]
  23. KNOWN_EVER_CULPRITS = (
  24. # https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html
  25. # fmt: off
  26. "archive-pdf-tools", "bgfx-python", "bicleaner-ai-glove", "BTrees", "cadbiom",
  27. "ctranslate2", "dyNET", "dyNET38", "gevent", "glove-python-binary", "higra",
  28. "hybridq", "ikomia", "ioh", "jij-cimod", "lavavu", "lavavu-osmesa", "MulticoreTSNE",
  29. "neural-compressor", "nwhy", "openjij", "openturns", "perfmetrics", "pHashPy",
  30. "pyace-lite", "pyapr", "pycompadre", "pycompadre-serial", "PyKEP", "pykep",
  31. "pylimer-tools", "pyqubo", "pyscf", "PyTAT", "python-prtree", "qiskit-aer",
  32. "qiskit-aer-gpu", "RelStorage", "sail-ml", "segmentation", "sente", "sinr",
  33. "snapml", "superman", "symengine", "systran-align", "texture-tool", "tsne-mp",
  34. "xcsf",
  35. # fmt: on
  36. )
  37. def flush_to_zero() -> bool:
  38. # If this subnormal number compares equal to zero we have a problem
  39. return 2.0**-1073 == 0
  40. def run_in_process(fn: Callable[..., FTZCulprits], *args: object) -> FTZCulprits:
  41. import multiprocessing as mp
  42. mp.set_start_method("spawn", force=True)
  43. q: "Queue[FTZCulprits]" = mp.Queue()
  44. p = mp.Process(target=target, args=(q, fn, *args))
  45. p.start()
  46. retval = q.get()
  47. p.join()
  48. return retval
  49. def target(
  50. q: "Queue[FTZCulprits]", fn: Callable[..., FTZCulprits], *args: object
  51. ) -> None:
  52. q.put(fn(*args))
  53. def always_imported_modules() -> FTZCulprits:
  54. return flush_to_zero(), set(sys.modules)
  55. def modules_imported_by(mod: str) -> FTZCulprits:
  56. """Return the set of modules imported transitively by mod."""
  57. before = set(sys.modules)
  58. try:
  59. importlib.import_module(mod)
  60. except Exception:
  61. return None, set()
  62. imports = set(sys.modules) - before
  63. return flush_to_zero(), imports
  64. # We don't want to redo all the expensive process-spawning checks when we've already
  65. # done them, so we cache known-good packages and a known-FTZ result if we have one.
  66. KNOWN_FTZ = None
  67. CHECKED_CACHE = set()
  68. def identify_ftz_culprits() -> str:
  69. """Find the modules in sys.modules which cause "mod" to be imported."""
  70. # If we've run this function before, return the same result.
  71. global KNOWN_FTZ
  72. if KNOWN_FTZ:
  73. return KNOWN_FTZ
  74. # Start by determining our baseline: the FTZ and sys.modules state in a fresh
  75. # process which has only imported this module and nothing else.
  76. always_enables_ftz, always_imports = run_in_process(always_imported_modules)
  77. if always_enables_ftz:
  78. raise RuntimeError("Python is always in FTZ mode, even without imports!")
  79. CHECKED_CACHE.update(always_imports)
  80. # Next, we'll search through sys.modules looking for a package (or packages) such
  81. # that importing them in a new process sets the FTZ state. As a heuristic, we'll
  82. # start with packages known to have ever enabled FTZ, then top-level packages as
  83. # a way to eliminate large fractions of the search space relatively quickly.
  84. def key(name: str) -> Tuple[bool, int, str]:
  85. """Prefer known-FTZ modules, then top-level packages, then alphabetical."""
  86. return (name not in KNOWN_EVER_CULPRITS, name.count("."), name)
  87. # We'll track the set of modules to be checked, and those which do trigger FTZ.
  88. candidates = set(sys.modules) - CHECKED_CACHE
  89. triggering_modules = {}
  90. while candidates:
  91. mod = min(candidates, key=key)
  92. candidates.discard(mod)
  93. enables_ftz, imports = run_in_process(modules_imported_by, mod)
  94. imports -= CHECKED_CACHE
  95. if enables_ftz:
  96. triggering_modules[mod] = imports
  97. candidates &= imports
  98. else:
  99. candidates -= imports
  100. CHECKED_CACHE.update(imports)
  101. # We only want to report the 'top level' packages which enable FTZ - for example,
  102. # if the enabling code is in `a.b`, and `a` in turn imports `a.b`, we prefer to
  103. # report `a`. On the other hand, if `a` does _not_ import `a.b`, as is the case
  104. # for `hypothesis.extra.*` modules, then `a` will not be in `triggering_modules`
  105. # and we'll report `a.b` here instead.
  106. prefixes = tuple(n + "." for n in triggering_modules)
  107. result = {k for k in triggering_modules if not k.startswith(prefixes)}
  108. # Suppose that `bar` enables FTZ, and `foo` imports `bar`. At this point we're
  109. # tracking both, but only want to report the latter.
  110. for a in sorted(result):
  111. for b in sorted(result):
  112. if a in triggering_modules[b] and b not in triggering_modules[a]:
  113. result.discard(b)
  114. # There may be a cyclic dependency which that didn't handle, or simply two
  115. # separate modules which both enable FTZ. We already gave up comprehensive
  116. # reporting for speed above (`candidates &= imports`), so we'll also buy
  117. # simpler reporting by arbitrarily selecting the alphabetically first package.
  118. KNOWN_FTZ = min(result) # Cache the result - it's likely this will trigger again!
  119. return KNOWN_FTZ
  120. if __name__ == "__main__":
  121. # This would be really really annoying to write automated tests for, so I've
  122. # done some manual exploratory testing: `pip install grequests gevent==21.12.0`,
  123. # and call print() as desired to observe behavior.
  124. import grequests # noqa
  125. # To test without skipping to a known answer, uncomment the following line and
  126. # change the last element of key from `name` to `-len(name)` so that we check
  127. # grequests before gevent.
  128. # KNOWN_EVER_CULPRITS = [c for c in KNOWN_EVER_CULPRITS if c != "gevent"]
  129. print(identify_ftz_culprits())