Repository navigation
Report OpenMP blocktime in milliseconds #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Aschenbath
wants to merge
1
commit into
joblib:master
Choose a base branch
from
Aschenbath:feat/openmp-blocktime-270
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| """Introspection of OpenMP runtime settings.""" | ||
|
|
||
| import ctypes | ||
| import json | ||
| import os | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| from concurrent.futures import ThreadPoolExecutor | ||
| from types import SimpleNamespace | ||
|
|
||
| import pytest | ||
|
|
||
| from threadpoolctl import ( | ||
| _CDLL_CACHE, | ||
| OpenMPController, | ||
| ThreadpoolController, | ||
| threadpool_info, | ||
| ) | ||
|
|
||
| # Import the OpenMP helpers, if available, to load the runtimes used by CI. | ||
| from . import utils as _ # noqa: F401 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("debugging_info", [False, True]) | ||
| def test_blocktime_missing_symbol(monkeypatch, debugging_info): | ||
| dynlib = SimpleNamespace( | ||
| omp_get_max_threads=lambda: 2, | ||
| omp_set_num_threads=lambda num_threads: None, | ||
| ) | ||
| monkeypatch.setattr(_CDLL_CACHE, "get_cdll", lambda filepath: dynlib) | ||
| controller = OpenMPController(filepath="libgomp.so", prefix="libgomp") | ||
|
|
||
| assert controller.get_blocktime() is None | ||
| assert controller.blocktime_ms is None | ||
| info = controller.info(debugging_info=debugging_info) | ||
| assert info["blocktime_ms"] is None | ||
| assert ("thread_limit_scope" in info) == debugging_info | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def kmp_controller(): | ||
| controllers = ThreadpoolController().select(user_api="openmp").lib_controllers | ||
| for controller in controllers: | ||
| if hasattr(controller.dynlib, "kmp_get_blocktime") and hasattr( | ||
| controller.dynlib, "kmp_set_blocktime" | ||
| ): | ||
| return controller | ||
| pytest.skip("Requires an OpenMP runtime with the kmp blocktime API") | ||
|
|
||
|
|
||
| def test_blocktime_actualized(kmp_controller): | ||
| """A reused controller reflects changes made through the native API.""" | ||
| get_blocktime = kmp_controller.dynlib.kmp_get_blocktime | ||
| get_blocktime.argtypes = [] | ||
| get_blocktime.restype = ctypes.c_int | ||
| set_blocktime = kmp_controller.dynlib.kmp_set_blocktime | ||
| set_blocktime.argtypes = [ctypes.c_int] | ||
| set_blocktime.restype = None | ||
| controller = ThreadpoolController().select(filepath=kmp_controller.filepath) | ||
| original = get_blocktime() | ||
| try: | ||
| for value in (0, 17): | ||
| set_blocktime(value) | ||
| assert get_blocktime() == value | ||
| assert kmp_controller.get_blocktime() == value | ||
| assert kmp_controller.blocktime_ms == value | ||
| assert controller.info()[0]["blocktime_ms"] == value | ||
| assert controller.select(blocktime_ms=value).info() == controller.info() | ||
| info = next( | ||
| lib | ||
| for lib in threadpool_info() | ||
| if lib["filepath"] == kmp_controller.filepath | ||
| ) | ||
| assert info["blocktime_ms"] == value | ||
| finally: | ||
| set_blocktime(original) | ||
|
|
||
|
|
||
| def test_blocktime_current_thread(kmp_controller): | ||
| """Reading a shared controller reports the calling thread's blocktime.""" | ||
| get_blocktime = kmp_controller.dynlib.kmp_get_blocktime | ||
| get_blocktime.argtypes = [] | ||
| get_blocktime.restype = ctypes.c_int | ||
| set_blocktime = kmp_controller.dynlib.kmp_set_blocktime | ||
| set_blocktime.argtypes = [ctypes.c_int] | ||
| set_blocktime.restype = None | ||
| original = get_blocktime() | ||
|
|
||
| def worker(): | ||
| worker_original = get_blocktime() | ||
| try: | ||
| set_blocktime(23) | ||
| return kmp_controller.info()["blocktime_ms"], kmp_controller.blocktime_ms | ||
| finally: | ||
| set_blocktime(worker_original) | ||
|
|
||
| try: | ||
| set_blocktime(7) | ||
| assert kmp_controller.info()["blocktime_ms"] == 7 | ||
| with ThreadPoolExecutor(max_workers=1) as executor: | ||
| assert executor.submit(worker).result() == (23, 23) | ||
| assert get_blocktime() == 7 | ||
| assert kmp_controller.info()["blocktime_ms"] == 7 | ||
| finally: | ||
| set_blocktime(original) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("setting", [None, "37", "37ms", "35001us", "35 us"]) | ||
| def test_blocktime_ms_matches_verbose_runtime_in_subprocess(kmp_controller, setting): | ||
| """Compare introspection with the effective setting printed by the runtime.""" | ||
| env = os.environ.copy() | ||
| env["OMP_DISPLAY_ENV"] = "VERBOSE" | ||
| env["KMP_SETTINGS"] = "1" | ||
| env.pop("KMP_BLOCKTIME", None) | ||
| if setting is not None: | ||
| env["KMP_BLOCKTIME"] = setting | ||
| code = """ | ||
| import ctypes | ||
| import json | ||
| import os | ||
| import sys | ||
| from threadpoolctl import threadpool_info | ||
|
|
||
| runtime = ctypes.CDLL(sys.argv[1]) | ||
| runtime.omp_get_max_threads() | ||
| initial = threadpool_info() | ||
| # The native unit stays fixed when the environment changes after initialization. | ||
| os.environ["KMP_BLOCKTIME"] = "1us" if os.environ.get("KMP_BLOCKTIME", "").endswith("ms") else "1ms" | ||
| runtime.kmp_set_blocktime(17) | ||
| print("THREADPOOLCTL_INFO=" + json.dumps({"initial": initial, "updated": threadpool_info()})) | ||
| """ | ||
| completed = subprocess.run( | ||
| [sys.executable, "-c", code, kmp_controller.filepath], | ||
| env=env, | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| timeout=30, | ||
| ) | ||
| output = completed.stdout + completed.stderr | ||
| # Effective settings follow any user settings in KMP_SETTINGS output. | ||
| settings = re.findall(r"KMP_BLOCKTIME\s*=\s*['\"]?(\d+)\s*(us|ms)?", output) | ||
| assert settings, output | ||
| value, unit = settings[-1] | ||
| if setting is not None and setting.endswith("us") and unit != "us": | ||
| pytest.skip("This runtime does not support microsecond KMP_BLOCKTIME settings") | ||
| expected_ms = int(value) / 1000 if unit == "us" else int(value) | ||
| match = re.search(r"^THREADPOOLCTL_INFO=(.*)$", completed.stdout, re.MULTILINE) | ||
| assert match, output | ||
| libraries = json.loads(match.group(1)) | ||
| info = next( | ||
| lib | ||
| for lib in libraries["initial"] | ||
| if lib["filepath"] == kmp_controller.filepath | ||
| ) | ||
| assert info["blocktime_ms"] == pytest.approx(expected_ms) | ||
| updated = next( | ||
| lib | ||
| for lib in libraries["updated"] | ||
| if lib["filepath"] == kmp_controller.filepath | ||
| ) | ||
| assert updated["blocktime_ms"] == pytest.approx(0.017 if unit == "us" else 17) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should include a test that imports openmp in a Python subprocess and the verbose env variable set and then check that the value reported by
threadpool_infois consistent with the value parsed fromstdout(including when manually set to non-ms units).