Skip to content

Commit 5c90b69

Browse files
committed
Overaul hypothesis strategies
Improve readability. Given infinite hypothesis examples, this commit does not introduce any functional changes. However, given a fixed and relatively low max_examples setting, it substantially increases test coverage: 1. It prevents examples that were previously skipped by assume(). According to pytest --hypothesis-show-statistics, these were ~10% for dotv and ~20% for gemm. 2. It prevents examples that ended up being duplicates due to trimming. For example, in dotv, hypothesis could previously generate examples e.g. A=[1,2,3,4], B=[5,6] and A=[1,2], B=[5,6,7,8]; both would get trimmed to A=[1,2], B=[5,6]. 3. In gemm, it removes an entire degree of freedom by removing unused variable out_col, which again would result in duplicate examples.
1 parent 0fe793e commit 5c90b69

3 files changed

Lines changed: 57 additions & 125 deletions

File tree

tests/blis_tests_common.py

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,25 @@
11
# Copyright ExplosionAI GmbH, released under BSD.
2-
import numpy as np
3-
4-
np.random.seed(0)
5-
62
from hypothesis import settings
7-
from hypothesis.strategies import integers, floats
3+
from hypothesis import strategies as st
84
from hypothesis.extra.numpy import arrays
95

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

tests/test_dotv.py

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,31 @@
11
# Copyright ExplosionAI GmbH, released under BSD.
22
import numpy as np
33
import pytest
4-
from hypothesis import given, assume
5-
4+
from hypothesis import given
5+
from hypothesis import strategies as st
66
from numpy.testing import assert_allclose
7-
from blis_tests_common import ndarrays
7+
8+
from blis_tests_common import dtypes_tols, ndarrays
89
from blis.py import dotv
910

1011

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+
1121
def test_incompatible_shape():
1222
with pytest.raises(ValueError):
1323
dotv(np.zeros(2), np.zeros(3))
1424

1525

16-
@given(
17-
ndarrays(min_len=1, max_len=100, min_val=-100.0, max_val=100.0, dtype="float64"),
18-
ndarrays(min_len=1, max_len=100, min_val=-100.0, max_val=100.0, dtype="float64"),
19-
)
20-
def test_memoryview_double_noconj(A, B):
21-
if len(A) < len(B):
22-
B = B[: len(A)]
23-
else:
24-
A = A[: len(B)]
25-
assume(A is not None)
26-
assume(B is not None)
27-
numpy_result = A.dot(B)
28-
result = dotv(A, B)
29-
assert_allclose(result, numpy_result, atol=1e-9, rtol=1e-9)
30-
31-
32-
@given(
33-
ndarrays(min_len=1, max_len=100, min_val=-100.0, max_val=100.0, dtype="float32"),
34-
ndarrays(min_len=1, max_len=100, min_val=-100.0, max_val=100.0, dtype="float32"),
35-
)
36-
def test_memoryview_float_noconj(A, B):
37-
if len(A) < len(B):
38-
B = B[: len(A)]
39-
else:
40-
A = A[: len(B)]
41-
assume(A is not None)
42-
assume(B is not None)
26+
@given(dotv_arrays())
27+
def test_memoryview_noconj(arrays):
28+
A, B, atol, rtol = arrays
4329
numpy_result = A.dot(B)
4430
result = dotv(A, B)
45-
assert_allclose(result, numpy_result, atol=1e-2, rtol=1e-4)
31+
assert_allclose(result, numpy_result, atol=atol, rtol=rtol)

tests/test_gemm.py

Lines changed: 20 additions & 65 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
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,10 @@ 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=1, max_len=100, min_val=-100.0, max_val=100.0, dtype="float64"),
52-
ndarrays(min_len=1, 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(C, numpy_result, atol=1e-9, rtol=1e-9)
68-
69-
70-
@given(
71-
ndarrays(min_len=1, max_len=100, min_val=-100.0, max_val=100.0, dtype="float32"),
72-
ndarrays(min_len=1, 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)
40+
C = np.zeros_like(numpy_result) # (l, n)
8541
gemm(A, B, out=C)
86-
numpy_result = A.dot(B)
87-
assert_allclose(C, numpy_result, atol=1e-2, rtol=1e-4)
42+
assert_allclose(C, numpy_result, atol=atol, rtol=rtol)

0 commit comments

Comments
 (0)