@@ -89,6 +89,69 @@ def test_eigvalsh_thread_safety():
8989 pass_count = True )
9090
9191
92+ def _detected_blas ():
93+ blas = np .show_config ('dicts' ).get ('Build Dependencies' , {}).get ('blas' , {})
94+ return blas .get ('name' , 'unknown' ), blas .get ('version' , 'unknown' )
95+
96+
97+ def _openblas_predates_gemm_fix (name , version ):
98+ if 'openblas' not in name :
99+ return False
100+ try :
101+ parsed = tuple (int (p ) for p in version .split ('.' ))
102+ except ValueError :
103+ return False
104+ return parsed < (0 , 3 , 33 , 112 )
105+
106+
107+ def test_blas_gemm_thread_safety ():
108+ # gh-31618: concurrently run transpose and no-transpose GEMM variants to
109+ # exercise possible thread safety issues due to lock sharding between
110+ # kernels, see OpenBLAS issue #5836.
111+ num_threads = 8
112+ num_iters = 10
113+ M = 512 * 512
114+
115+ rng = np .random .default_rng (0x9e3779b9 )
116+ no_trans = rng .random ((M , 4 )) # C-contiguous -> NoTrans GEMM
117+ no_trans_w = rng .random ((4 , 2 ))
118+ trans = rng .random ((2 , M )).T # F-contiguous -> Trans GEMM
119+ trans_w = rng .random ((2 , 2 ))
120+ expected_no_trans = no_trans @ no_trans_w
121+ expected_trans = trans @ trans_w
122+
123+ mismatches = 0
124+ lock = threading .Lock ()
125+
126+ def closure (i , b ):
127+ nonlocal mismatches
128+ count = 0
129+ for _ in range (num_iters ):
130+ b .wait ()
131+ if i % 2 :
132+ ok = np .array_equal (no_trans @ no_trans_w , expected_no_trans )
133+ else :
134+ ok = np .array_equal (trans @ trans_w , expected_trans )
135+ if not ok :
136+ count += 1
137+ with lock :
138+ mismatches += count
139+
140+ run_threaded (closure , num_threads , pass_count = True , pass_barrier = True )
141+
142+ blas_name , blas_version = _detected_blas ()
143+ if mismatches and _openblas_predates_gemm_fix (blas_name , blas_version ):
144+ pytest .xfail (
145+ f"OpenBLAS version ({ blas_version } ) predates first OpenBLAS "
146+ "version with a fix (0.3.33.112)"
147+ )
148+
149+ assert mismatches == 0 , (
150+ f"{ mismatches } concurrent matmul results were corrupted "
151+ f"({ blas_name } { blas_version } )"
152+ )
153+
154+
92155def test_printoptions_thread_safety ():
93156 # until NumPy 2.1 the printoptions state was stored in globals
94157 # this verifies that they are now stored in a context variable
0 commit comments