Skip to content

Commit 14781ae

Browse files
authored
Merge pull request #143 from crusaderky/tests-overhaul
Tests overhaul
2 parents d207299 + 555d35f commit 14781ae

4 files changed

Lines changed: 106 additions & 122 deletions

File tree

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,7 @@ addopts = "--strict-markers --strict-config -v -r sxfE --color=yes --durations=1
6363
python_files = ["test_*.py"]
6464
testpaths = ["tests"]
6565
filterwarnings = ["error"]
66+
markers = [
67+
# Needed when pytest-run-parallel is not installed
68+
"thread_unsafe: Run test in single thread in pytest-run-parallel"
69+
]

tests/blis_tests_common.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
# Copyright ExplosionAI GmbH, released under BSD.
2-
import numpy as np
3-
4-
np.random.seed(0)
5-
6-
from hypothesis.strategies import tuples, integers, floats
2+
from concurrent.futures import ThreadPoolExecutor
3+
from hypothesis import settings
4+
from hypothesis import strategies as st
75
from hypothesis.extra.numpy import arrays
86

9-
10-
def lengths(lo=1, hi=10):
11-
return integers(min_value=lo, max_value=hi)
12-
13-
14-
def ndarrays_of_shape(shape, lo=-1000.0, hi=1000.0, dtype="float64"):
15-
width = 64 if dtype == "float64" else 32
16-
return arrays(
17-
dtype,
18-
shape=shape,
19-
elements=floats(
20-
min_value=lo,
21-
max_value=hi,
22-
width=width,
23-
),
24-
)
25-
26-
27-
def ndarrays(
28-
min_len=0, max_len=10, min_val=-10000000.0, max_val=1000000.0, dtype="float64"
29-
):
30-
return lengths(lo=min_len, hi=max_len).flatmap(
31-
lambda n: ndarrays_of_shape(n, lo=min_val, hi=max_val, dtype=dtype)
32-
)
7+
# Increase this to run more thorough tests
8+
hypothesis_default_profile = settings.register_profile("default", max_examples=200)
9+
10+
# (dtype, atol, rtol)
11+
dtypes_tols = st.sampled_from(
12+
[
13+
# FIXME huge tolerance needed for float32:
14+
# https://github.com/explosion/cython-blis/issues/142
15+
("float32", 1e-2, 1e-4),
16+
("float64", 1e-9, 1e-9),
17+
]
18+
)
19+
20+
21+
def ndarrays(shape, lo, hi, dtype):
22+
"""Draw ND NumPy arrays of floats"""
23+
assert isinstance(dtype, str) and dtype.startswith("float")
24+
width = int(dtype[5:])
25+
elements = st.floats(min_value=lo, max_value=hi, width=width)
26+
return arrays(dtype, shape=shape, elements=elements)
27+
28+
29+
def run_threaded(func, max_workers=8, outer_iterations=2):
30+
"""Runs a function many times in parallel"""
31+
with ThreadPoolExecutor(max_workers=max_workers) as tpe:
32+
for _ in range(outer_iterations):
33+
futures = [tpe.submit(func) for _ in range(max_workers)]
34+
for f in futures:
35+
f.result()

tests/test_dotv.py

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,45 @@
11
# Copyright ExplosionAI GmbH, released under BSD.
2-
from hypothesis import given, assume
3-
2+
import numpy as np
3+
import pytest
4+
from hypothesis import given
5+
from hypothesis import strategies as st
46
from numpy.testing import assert_allclose
5-
from blis_tests_common import ndarrays
7+
8+
from blis_tests_common import dtypes_tols, ndarrays, run_threaded
69
from blis.py import dotv
710

811

9-
@given(
10-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float64"),
11-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float64"),
12-
)
13-
def test_memoryview_double_noconj(A, B):
14-
if len(A) < len(B):
15-
B = B[: len(A)]
16-
else:
17-
A = A[: len(B)]
18-
assume(A is not None)
19-
assume(B is not None)
12+
@st.composite
13+
def dotv_arrays(draw):
14+
"""Draw two 1D NumPy arrays of the same size and dtype"""
15+
size = draw(st.integers(min_value=1, max_value=100))
16+
dtype, atol, rtol = draw(dtypes_tols)
17+
arr_st = ndarrays((size,), lo=-100.0, hi=100.0, dtype=dtype)
18+
return draw(arr_st), draw(arr_st), atol, rtol
19+
20+
21+
def test_incompatible_shape():
22+
with pytest.raises(ValueError):
23+
dotv(np.zeros(2), np.zeros(3))
24+
25+
26+
@given(dotv_arrays())
27+
def test_memoryview_noconj(arrays):
28+
A, B, atol, rtol = arrays
2029
numpy_result = A.dot(B)
2130
result = dotv(A, B)
22-
assert_allclose([numpy_result], result, atol=1e-3, rtol=1e-3)
23-
24-
25-
@given(
26-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float32"),
27-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float32"),
28-
)
29-
def test_memoryview_float_noconj(A, B):
30-
if len(A) < len(B):
31-
B = B[: len(A)]
32-
else:
33-
A = A[: len(B)]
34-
assume(A is not None)
35-
assume(B is not None)
31+
assert_allclose(result, numpy_result, atol=atol, rtol=rtol)
32+
33+
34+
@given(dotv_arrays())
35+
@pytest.mark.thread_unsafe(reason="Uses run_threaded")
36+
def test_threads_share_input(arrays):
37+
"""Test when multiple threads share the same input arrays."""
38+
A, B, atol, rtol = arrays
3639
numpy_result = A.dot(B)
37-
result = dotv(A, B)
38-
assert_allclose([numpy_result], result, atol=1e-4, rtol=1e-3)
40+
41+
def f():
42+
result = dotv(A, B)
43+
assert_allclose(result, numpy_result, atol=atol, rtol=rtol)
44+
45+
run_threaded(f)

tests/test_gemm.py

Lines changed: 33 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,25 @@
11
# Copyright ExplosionAI GmbH, released under BSD.
2-
from math import sqrt, floor
3-
4-
from hypothesis import given, assume
5-
from hypothesis.strategies import integers
6-
72
import numpy as np
8-
from numpy.testing import assert_allclose
93
import pytest
4+
from hypothesis import given
5+
from hypothesis import strategies as st
6+
from numpy.testing import assert_allclose
107

11-
from blis_tests_common import ndarrays
8+
from blis_tests_common import dtypes_tols, ndarrays, run_threaded
129
from blis.py import gemm
1310

1411

15-
def _stretch_matrix(data, m, n):
16-
ratio = sqrt(len(data) / (m * n))
17-
m = int(floor(m * ratio))
18-
n = int(floor(n * ratio))
19-
data = np.ascontiguousarray(data[: m * n], dtype=data.dtype)
20-
return data.reshape((m, n)), m, n
21-
22-
23-
def _reshape_for_gemm(
24-
A, B, a_rows, a_cols, out_cols, dtype, trans_a=False, trans_b=False
25-
):
26-
A, a_rows, a_cols = _stretch_matrix(A, a_rows, a_cols)
27-
if len(B) < a_cols or a_cols < 1:
28-
return (None, None, None)
29-
b_cols = int(floor(len(B) / a_cols))
30-
B = np.ascontiguousarray(B.flatten()[: a_cols * b_cols], dtype=dtype)
31-
B = B.reshape((a_cols, b_cols))
32-
out_cols = B.shape[1]
33-
C = np.zeros(shape=(A.shape[0], B.shape[1]), dtype=dtype)
34-
if trans_a:
35-
A = np.ascontiguousarray(A.T, dtype=dtype)
36-
return A, B, C
12+
@st.composite
13+
def gemm_arrays(draw):
14+
"""Draw two NumPy arrays with shapes (l, m) and (m, n) of the same dtype"""
15+
max_size = 100
16+
l = draw(st.integers(min_value=1, max_value=max_size)) # noqa: E741
17+
n = draw(st.integers(min_value=1, max_value=max_size))
18+
m = draw(st.integers(min_value=1, max_value=max_size // max(l, n)))
19+
dtype, atol, rtol = draw(dtypes_tols)
20+
A = draw(ndarrays((l, m), lo=-100.0, hi=100.0, dtype=dtype))
21+
B = draw(ndarrays((m, n), lo=-100.0, hi=100.0, dtype=dtype))
22+
return A, B, atol, rtol
3723

3824

3925
def test_incompatible_shape():
@@ -47,41 +33,25 @@ def test_incompatible_shape():
4733
gemm(np.zeros((3, 2)), np.zeros((3, 2)), trans1=True, trans2=True)
4834

4935

50-
@given(
51-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float64"),
52-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float64"),
53-
integers(min_value=2, max_value=1000),
54-
integers(min_value=2, max_value=1000),
55-
integers(min_value=2, max_value=1000),
56-
)
57-
def test_memoryview_double_notrans(A, B, a_rows, a_cols, out_cols):
58-
A, B, C = _reshape_for_gemm(A, B, a_rows, a_cols, out_cols, "float64")
59-
assume(A is not None)
60-
assume(B is not None)
61-
assume(C is not None)
62-
assume(A.size >= 1)
63-
assume(B.size >= 1)
64-
assume(C.size >= 1)
65-
gemm(A, B, out=C)
36+
@given(gemm_arrays())
37+
def test_memoryview_notrans(arrays):
38+
A, B, atol, rtol = arrays
6639
numpy_result = A.dot(B)
67-
assert_allclose(numpy_result, C, atol=1e-3, rtol=1e-3)
40+
C = np.zeros_like(numpy_result) # (l, n)
41+
gemm(A, B, out=C)
42+
assert_allclose(C, numpy_result, atol=atol, rtol=rtol)
6843

6944

70-
@given(
71-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float32"),
72-
ndarrays(min_len=10, max_len=100, min_val=-100.0, max_val=100.0, dtype="float32"),
73-
integers(min_value=2, max_value=1000),
74-
integers(min_value=2, max_value=1000),
75-
integers(min_value=2, max_value=1000),
76-
)
77-
def test_memoryview_float_notrans(A, B, a_rows, a_cols, out_cols):
78-
A, B, C = _reshape_for_gemm(A, B, a_rows, a_cols, out_cols, dtype="float32")
79-
assume(A is not None)
80-
assume(B is not None)
81-
assume(C is not None)
82-
assume(A.size >= 1)
83-
assume(B.size >= 1)
84-
assume(C.size >= 1)
85-
gemm(A, B, out=C)
45+
@given(gemm_arrays())
46+
@pytest.mark.thread_unsafe(reason="Uses run_threaded")
47+
def test_threads_share_input(arrays):
48+
"""Test when multiple threads share the same input arrays."""
49+
A, B, atol, rtol = arrays
8650
numpy_result = A.dot(B)
87-
assert_allclose(numpy_result, C, atol=1e-3, rtol=1e-3)
51+
52+
def f():
53+
C = np.zeros_like(numpy_result) # (l, n)
54+
gemm(A, B, out=C)
55+
assert_allclose(C, numpy_result, atol=atol, rtol=rtol)
56+
57+
run_threaded(f)

0 commit comments

Comments
 (0)