You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
No protection: import numpy in two different threads can lead to race-condition #91238
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=Nonecreated_at=<Date2022-03-21.17:03:05.346>labels= ['interpreter-core', 'type-bug', '3.11']
title='No protection: `import numpy` in two different threads can lead to race-condition'updated_at=<Date2022-03-21.23:04:38.800>user='https://github.com/jhgoebbert'
While using Xpra we came across a bug which might be a Python or a NumPy issue.
Perhaps some of you can help us understanding some internals.
Calling import numpy at the same time in two different threads of the Python program can lead to a race-condition.
This happens for example with Xpra when loading the encoder nvjpeg:
2022-03-20 12:54:59,298 cannot load enc_nvjpeg (nvjpeg encoder)
Traceback (most recent call last):
File "<pythondir>/lib/python3.9/site-packages/xpra/codecs/loader.py", line 52, in codec_import_check
ic = __import__(class_module, {}, {}, classnames)
File "xpra/codecs/nvjpeg/encoder.pyx", line 8, in init xpra.codecs.nvjpeg.encoder
File "<pythondir>/lib/python3.9/site-packages/numpy/__init__.py", line 150, in <module>
from . import core
File "<pythondir>/lib/python3.9/site-packages/numpy/core/__init__.py", line 51, in <module>
del os.environ[envkey]
File "<pythondir>/lib/python3.9/os.py", line 695, in __delitem__
raise KeyError(key) from None
KeyError: 'OPENBLAS_MAIN_FREE'
So, my current hypothesis (and I have briefly checked the Python code) is that Python does not do manual locking. But it effectively locks due to this going into C and thus holding the GIL. But somewhere during the import of NumPy, NumPy probably releases the GIL briefly and that could allow the next thread to go into the import machinery.
[..]
NumPy may be doing some worse than typical stuff here, but right now it seems to me that Python should be protecting us.
To add to this: it would seem to me that the side-effects of importing should be guaranteed to only be called once?
However, IO or other operations could be part of the import side-effects and release the GIL. So even a simple, pure-Python, package could run into this same issue and probably won't even realize that they can run into it.
(Assuming I understand how this is happening correctly.)
So it would seem to me that either:
Python should lock on the thread level or maybe the sys.modules dictionary?
The threading module could somehow ensure safety by hooking into
the import machinery?
Packages that may release the GIL or have side-effects that must
only be run once have to lock (i.e. NumPy).
(But it seems to me that many packages will not even be aware of
possible issues.)
Python used to have a global import lock. The global import lock prevented recursive imports from other threads while the current thread was importing. The import lock was source of issues and dead locks. Antoine replaced it with a fine grained import lock in bpo-9260.
Reacted by Gregory P. Smith
seberg commented on Mar 21, 2022
sebergmannequin
Mannequin
More actions
Thanks, so there should already be a lock in place (sorry, I missed that). But somehow we seem to get around it?
Do you know what may cause the locking logic to fail in this case? Recursive imports in NumPy itself? Or Cython using low-level C-API?
I.e. can you think of something to investigate that may help NumPy/Cython to make sure that locking is successful?
/* Cythons Import code (slightly cleaned up for Python 3 only): */staticPyObject*__Pyx_Import(PyObject*name, PyObject*from_list, intlevel) {
PyObject*empty_list=0;
PyObject*module=0;
PyObject*global_dict=0;
PyObject*empty_dict=0;
PyObject*list;
if (from_list)
list=from_list;
else {
empty_list=PyList_New(0);
if (!empty_list)
goto bad;
list=empty_list;
}
global_dict=PyModule_GetDict(__pyx_m);
if (!global_dict)
goto bad;
empty_dict=PyDict_New();
if (!empty_dict)
goto bad;
{
if (level==-1) {
if ((1) && (strchr(__Pyx_MODULE_NAME, '.'))) {
module=PyImport_ImportModuleLevelObject(
name, global_dict, empty_dict, list, 1);
if (!module) {
if (!PyErr_ExceptionMatches(PyExc_ImportError))
goto bad;
PyErr_Clear();
}
}
level=0;
}
if (!module) {
module=PyImport_ImportModuleLevelObject(
name, global_dict, empty_dict, list, level);
}
}
bad:
Py_XDECREF(empty_list);
Py_XDECREF(empty_dict);
returnmodule;
}
Unfortunately not, it was @jhgoebbert who experienced it, but maybe I can try tomorrow or so. My hypothesis was cython got involved. But of course it could be that there is a deeper threading issue elsewhere. Another thing may be that you need a fairly new NumPy versions to run into the environment variable issue, because older versions probably do not mess with OPENBLAS_MAIN_FREE.
E 04-10 22:26:00 sdk.py:1486] Traceback (most recent call last):
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/server/requests/executor.py", line 361, in _request_execution_wrapper
E 04-10 22:26:00 sdk.py:1486] return_value = func(**request_body.to_kwargs())
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/__init__.py", line 103, in list_accelerator_counts
E 04-10 22:26:00 sdk.py:1486] results = _map_clouds_catalog(clouds,
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/__init__.py", line 51, in _map_clouds_catalog
E 04-10 22:26:00 sdk.py:1486] results = subprocess_utils.run_in_parallel(_execute_catalog_method,
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/utils/subprocess_utils.py", line 138, in run_in_parallel
E 04-10 22:26:00 sdk.py:1486] return list(ordered_iterators)
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/multiprocessing/pool.py", line 868, in next
E 04-10 22:26:00 sdk.py:1486] raise value
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/multiprocessing/pool.py", line 125, in worker
E 04-10 22:26:00 sdk.py:1486] result = (True, func(*args, **kwds))
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/__init__.py", line 49, in _execute_catalog_method
E 04-10 22:26:00 sdk.py:1486] return method(*args, **kwargs)
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/vast_catalog.py", line 102, in list_accelerators
E 04-10 22:26:00 sdk.py:1486] return common.list_accelerators_impl('Vast', _df, gpus_only, name_filter,
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 578, in list_accelerators_impl
E 04-10 22:26:00 sdk.py:1486] df = df[~df['GpuInfo'].isna()]
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 153, in __getitem__
E 04-10 22:26:00 sdk.py:1486] return self._load_df()[key]
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 145, in _load_df
E 04-10 22:26:00 sdk.py:1486] raise e
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 138, in _load_df
E 04-10 22:26:00 sdk.py:1486] self._update_func()
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 209, in _update_catalog
E 04-10 22:26:00 sdk.py:1486] r = requests.get(url=url,
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/test-env/lib/python3.8/site-packages/requests/api.py", line 73, in get
E 04-10 22:26:00 sdk.py:1486] return request("get", url, params=params, **kwargs)
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/test-env/lib/python3.8/site-packages/requests/api.py", line 59, in request
E 04-10 22:26:00 sdk.py:1486] return session.request(method=method, url=url, **kwargs)
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/test-env/lib/python3.8/site-packages/requests/sessions.py", line 579, in request
E 04-10 22:26:00 sdk.py:1486] settings = self.merge_environment_settings(
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/test-env/lib/python3.8/site-packages/requests/sessions.py", line 760, in merge_environment_settings
E 04-10 22:26:00 sdk.py:1486] env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/test-env/lib/python3.8/site-packages/requests/utils.py", line 835, in get_environ_proxies
E 04-10 22:26:00 sdk.py:1486] return getproxies()
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/urllib/request.py", line 2516, in getproxies_environment
E 04-10 22:26:00 sdk.py:1486] for name, value in os.environ.items():
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/_collections_abc.py", line 744, in __iter__
E 04-10 22:26:00 sdk.py:1486] yield (key, self._mapping[key])
E 04-10 22:26:00 sdk.py:1486] File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/os.py", line 675, in __getitem__
E 04-10 22:26:00 sdk.py:1486] raise KeyError(key) from None
E 04-10 22:26:00 sdk.py:1486] KeyError: 'OPENBLAS_MAIN_FREE'
E 04-10 22:26:00 sdk.py:1486]
We have some custom code to lazily import numpy as needed, and I guess it is running in one thread. It causes a crash in another thread - which is not doing an import, but is just trying to read from os.environ. In fact, you can see that the bottom three frames of the stacktrace are in CPython, and they all look very innocuous.
Activity
jhgoebbert commentedon Mar 21, 2022
While using Xpra we came across a bug which might be a Python or a NumPy issue.
Perhaps some of you can help us understanding some internals.
Calling
import numpyat the same time in two different threads of the Python program can lead to a race-condition.This happens for example with Xpra when loading the encoder nvjpeg:
Here the environment variable OPENBLAS_MAIN_FREE is set in the
numpycode:https://github.com/numpy/numpy/blob/maintenance/1.21.x/numpy/core/__init__.py#L18
and short after that it is deleted
https://github.com/numpy/numpy/blob/maintenance/1.21.x/numpy/core/__init__.py#L51
But this deletion fails ... perhaps because the initialization runs at the same time in two threads 🤔
Shouldn't Python protect us by design?
@seberg comments HERE:
Can anyone comment on this?
seberg commentedon Mar 21, 2022
To add to this: it would seem to me that the side-effects of importing should be guaranteed to only be called once?
However, IO or other operations could be part of the import side-effects and release the GIL. So even a simple, pure-Python, package could run into this same issue and probably won't even realize that they can run into it.
(Assuming I understand how this is happening correctly.)
So it would seem to me that either:
sys.modulesdictionary?threadingmodule could somehow ensure safety by hooking intothe import machinery?
only be run once have to lock (i.e. NumPy).
(But it seems to me that many packages will not even be aware of
possible issues.)
tiran commentedon Mar 21, 2022
Python used to have a global import lock. The global import lock prevented recursive imports from other threads while the current thread was importing. The import lock was source of issues and dead locks. Antoine replaced it with a fine grained import lock in bpo-9260.
seberg commentedon Mar 21, 2022
Thanks, so there should already be a lock in place (sorry, I missed that). But somehow we seem to get around it?
Do you know what may cause the locking logic to fail in this case? Recursive imports in NumPy itself? Or Cython using low-level C-API?
I.e. can you think of something to investigate that may help NumPy/Cython to make sure that locking is successful?
fbrunodr commentedon Apr 15, 2022
@seberg can you provide a working example of that bug? I tried to simulate this race condition and could not reproduce the bug.
seberg commentedon Apr 15, 2022
Unfortunately not, it was @jhgoebbert who experienced it, but maybe I can try tomorrow or so. My hypothesis was cython got involved. But of course it could be that there is a deeper threading issue elsewhere. Another thing may be that you need a fairly new NumPy versions to run into the environment variable issue, because older versions probably do not mess with
OPENBLAS_MAIN_FREE.cg505 commentedon Apr 11, 2025
Hi! We have an extremely fragile reproduction case in the CI for SkyPilot here: https://github.com/skypilot-org/skypilot/actions/runs/14391450370/job/40359190320?pr=5095
Relevant stacktrace:
We have some custom code to lazily import
numpyas needed, and I guess it is running in one thread. It causes a crash in another thread - which is not doing an import, but is just trying to read fromos.environ. In fact, you can see that the bottom three frames of the stacktrace are in CPython, and they all look very innocuous.It seems that the numpy import is somehow changing os.environ out from under the thread that crashes. That's easy enough to understand, but what I can't understand is how this is not protected by GIL, since numpy's
os.environmodification is happening in Python:https://github.com/numpy/numpy/blob/v1.24.4/numpy/core/__init__.py#L14-L20
https://github.com/numpy/numpy/blob/v1.24.4/numpy/core/__init__.py#L50-L52
This is python 3.8.20 (I know EOL, but sharing anyway since it might help pin down the actual issue) with numpy 1.24.4. The threads are all part of a
multiprocessing.pool.ThreadPool, which you can see in the stacktrace. It's here in the SkyPilot source:https://github.com/skypilot-org/skypilot/blob/d01d496e66615656a2ddf57e4695151392b61c63/sky/utils/subprocess_utils.py#L136-L138
Unsurprisingly, it unfortunately seems very difficult to reproduce this in other environments.
gpshead commentedon Jan 11, 2026
I believe the root cause of this is #143650 - your numpy environment error matches the scenario that led me to debug this.
fix(startup): remove background preloader daemon to stop import race