Skip to content

Commit 9b41e87

Browse files
Add free-threading test for mkl_random
1 parent dd3c9c9 commit 9b41e87

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright (c) 2026, Intel Corporation
2+
#
3+
# Redistribution and use in source and binary forms, with or without
4+
# modification, are permitted provided that the following conditions are met:
5+
#
6+
# * Redistributions of source code must retain the above copyright notice,
7+
# this list of conditions and the following disclaimer.
8+
# * Redistributions in binary form must reproduce the above copyright
9+
# notice, this list of conditions and the following disclaimer in the
10+
# documentation and/or other materials provided with the distribution.
11+
# * Neither the name of Intel Corporation nor the names of its contributors
12+
# may be used to endorse or promote products derived from this software
13+
# without specific prior written permission.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
19+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
26+
import os
27+
import sys
28+
import sysconfig
29+
import threading
30+
31+
import numpy as np
32+
import pytest
33+
34+
# Oversubscription: MKL spawns its own thread pool per calling thread, so
35+
# generating from many Python threads concurrently can spawn far more OS
36+
# threads than cores. Cap it before mkl_random/MKL initialize.
37+
os.environ.setdefault("MKL_NUM_THREADS", "1")
38+
39+
import mkl_random # noqa: E402
40+
41+
FREE_THREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
42+
43+
44+
def test_concurrent_sampling_per_instance():
45+
# Each thread owns a private MKLRandomState seeded identically, so the
46+
# per-instance lock + `nogil` sampling must reproduce the single-threaded
47+
# result exactly regardless of concurrency.
48+
n_threads = 4
49+
size = 10**5 + 1 # large enough that per-thread nogil sampling overlaps
50+
seed = 1234
51+
52+
expected = mkl_random.MKLRandomState(seed).normal(size=size)
53+
54+
results = [None] * n_threads
55+
errors = []
56+
57+
def worker(i):
58+
try:
59+
rs = mkl_random.MKLRandomState(seed)
60+
results[i] = rs.normal(size=size)
61+
except Exception as exc: # pylint: disable=broad-except
62+
errors.append(exc)
63+
64+
threads = [
65+
threading.Thread(target=worker, args=(i,)) for i in range(n_threads)
66+
]
67+
for t in threads:
68+
t.start()
69+
for t in threads:
70+
t.join()
71+
72+
assert not errors
73+
74+
for i in range(n_threads):
75+
np.testing.assert_array_equal(results[i], expected)
76+
77+
78+
def test_concurrent_shared_singleton():
79+
# Module-level functions share a single lock-guarded RandomState. Hammering
80+
# it from many threads must not corrupt state, crash, or return garbage.
81+
n_threads = 8
82+
size = 10**5 + 1
83+
results = [None] * n_threads
84+
errors = []
85+
86+
def worker(i):
87+
try:
88+
results[i] = mkl_random.uniform(size=size)
89+
except Exception as exc: # pylint: disable=broad-except
90+
errors.append(exc)
91+
92+
threads = [
93+
threading.Thread(target=worker, args=(i,)) for i in range(n_threads)
94+
]
95+
for t in threads:
96+
t.start()
97+
for t in threads:
98+
t.join()
99+
100+
assert not errors
101+
102+
for i in range(n_threads):
103+
assert results[i].shape == (size,)
104+
assert np.all(np.isfinite(results[i]))
105+
106+
107+
def test_concurrent_patch_restore():
108+
n_threads = 8
109+
n_iters = 20
110+
111+
def worker():
112+
for _ in range(n_iters):
113+
mkl_random.patch_numpy_random()
114+
mkl_random.restore_numpy_random()
115+
116+
threads = [threading.Thread(target=worker) for _ in range(n_threads)]
117+
for t in threads:
118+
t.start()
119+
for t in threads:
120+
t.join()
121+
122+
assert not mkl_random.is_patched()
123+
124+
125+
@pytest.mark.skipif(
126+
not FREE_THREADED, reason="requires a free-threaded CPython build"
127+
)
128+
def test_gil_not_reenabled_on_import():
129+
assert not sys._is_gil_enabled() # pylint: disable=no-member

0 commit comments

Comments
 (0)