_hypothesis_ftz_detector.py 6.0 KB

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