Skip to content

Commit 3c63e27

Browse files
authored
Merge pull request #1153 from JanFSchulte/split_pytests
Split hgq tests and isolate qkeras tests to make tests run in under 1h
2 parents 0dd372a + fb12040 commit 3c63e27

File tree

3 files changed

+174
-88
lines changed

3 files changed

+174
-88
lines changed

test/pytest/generate_ci_yaml.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
EXAMPLEMODEL: {}
1919
"""
2020

21+
2122
n_test_files_per_yml = int(os.environ.get('N_TESTS_PER_YAML', 4))
2223

2324
# Blacklisted tests will be skipped
2425
BLACKLIST = {'test_reduction'}
2526

2627
# Long-running tests will not be bundled with other tests
27-
LONGLIST = {'test_hgq_layers'}
28+
LONGLIST = {'test_hgq_layers', 'test_hgq_players', 'test_qkeras', 'test_pytorch_api'}
2829

2930

3031
def path_to_name(test_path):
@@ -71,7 +72,7 @@ def generate_test_yaml(test_root='.'):
7172
name = path.stem.replace('test_', '')
7273
test_file = str(path.relative_to(test_root))
7374
needs_examples = uses_example_model(path)
74-
diff_yml = yaml.safe_load(template.format(name, test_file, needs_examples))
75+
diff_yml = yaml.safe_load(template.format(name, test_file, int(needs_examples)))
7576
yml.update(diff_yml)
7677

7778
return yml

test/pytest/test_hgq_layers.py

Lines changed: 0 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
Signature,
2020
)
2121
from HGQ.proxy import to_proxy_model
22-
from HGQ.proxy.fixed_point_quantizer import gfixed
2322
from tensorflow import keras
2423

2524
from hls4ml.converters import convert_from_keras_model
@@ -79,51 +78,6 @@ def run_model_test(
7978
_run_synth_match_test(proxy, data, io_type, backend, dir, cond=cond)
8079

8180

82-
def create_player_model(layer: str, rnd_strategy: str, io_type: str):
83-
pa_config = get_default_paq_conf()
84-
pa_config['rnd_strategy'] = rnd_strategy
85-
pa_config['skip_dims'] = 'all' if io_type == 'io_stream' else 'batch'
86-
set_default_paq_conf(pa_config)
87-
88-
inp = keras.Input(shape=(15))
89-
if 'PConcatenate' in layer:
90-
_inp = [HQuantize()(inp)] * 2
91-
out = eval(layer)(_inp)
92-
out = HDense(15)(out)
93-
return keras.Model(inp, out)
94-
elif 'Signature' in layer:
95-
_inp = eval(layer)(inp)
96-
out = HDense(15)(_inp)
97-
return keras.Model(inp, out)
98-
elif 'Pool2D' in layer:
99-
_inp = PReshape((3, 5, 1))(HQuantize()(inp))
100-
elif 'Pool1D' in layer:
101-
_inp = PReshape((5, 3))(HQuantize()(inp))
102-
elif 'Dense' in layer or 'Activation' in layer:
103-
_inp = HQuantize()(inp)
104-
elif 'Flatten' in layer:
105-
out = HQuantize()(inp)
106-
out = PReshape((3, 5))(out)
107-
out = HConv1D(2, 2)(out)
108-
out = eval(layer)(out)
109-
out = HDense(15)(out)
110-
return keras.Model(inp, out)
111-
else:
112-
raise Exception(f'Please add test for {layer}')
113-
114-
out = eval(layer)(_inp)
115-
model = keras.Model(inp, out)
116-
117-
for layer in model.layers:
118-
# No weight bitwidths to randomize
119-
# And activation bitwidths
120-
if hasattr(layer, 'paq'):
121-
fbw: tf.Variable = layer.paq.fbw
122-
fbw.assign(tf.constant(np.random.uniform(4, 6, fbw.shape).astype(np.float32)))
123-
124-
return model
125-
126-
12781
def create_hlayer_model(layer: str, rnd_strategy: str, io_type: str):
12882
pa_config = get_default_paq_conf()
12983
pa_config['rnd_strategy'] = rnd_strategy
@@ -222,43 +176,3 @@ def test_syn_hlayers(layer, N: int, rnd_strategy: str, io_type: str, cover_facto
222176
path = test_path / f'hls4mlprj_hgq_{layer}_{rnd_strategy}_{io_type}_{aggressive}_{backend}'
223177

224178
run_model_test(model, cover_factor, data, io_type, backend, str(path), aggressive, cond=cond)
225-
226-
227-
@pytest.mark.parametrize(
228-
'layer',
229-
[
230-
"PConcatenate()",
231-
"PMaxPool1D(2, padding='same')",
232-
"PMaxPool1D(4, padding='same')",
233-
"PMaxPool2D((5,3), padding='same')",
234-
"PMaxPool1D(2, padding='valid')",
235-
"PMaxPool2D((2,3), padding='valid')",
236-
"Signature(1,6,3)",
237-
"PAvgPool1D(2, padding='same')",
238-
"PAvgPool2D((1,2), padding='same')",
239-
"PAvgPool2D((2,2), padding='same')",
240-
"PAvgPool1D(2, padding='valid')",
241-
"PAvgPool2D((1,2), padding='valid')",
242-
"PAvgPool2D((2,2), padding='valid')",
243-
"PFlatten()",
244-
],
245-
)
246-
@pytest.mark.parametrize("N", [1000])
247-
@pytest.mark.parametrize("rnd_strategy", ['floor', 'standard_round'])
248-
@pytest.mark.parametrize("io_type", ['io_parallel', 'io_stream'])
249-
@pytest.mark.parametrize("cover_factor", [1.0])
250-
@pytest.mark.parametrize("aggressive", [True, False])
251-
@pytest.mark.parametrize("backend", ['vivado', 'vitis'])
252-
def test_syn_players(layer, N: int, rnd_strategy: str, io_type: str, cover_factor: float, aggressive: bool, backend: str):
253-
model = create_player_model(layer=layer, rnd_strategy=rnd_strategy, io_type=io_type)
254-
data = get_data((N, 15), 7, 1)
255-
256-
path = test_path / f'hls4mlprj_hgq_{layer}_{rnd_strategy}_{io_type}_{aggressive}_{backend}'
257-
258-
if 'Signature' in layer:
259-
q = gfixed(1, 6, 3)
260-
data = q(data).numpy()
261-
if "padding='same'" in layer and io_type == 'io_stream':
262-
pytest.skip("io_stream does not support padding='same' for pools at the moment")
263-
264-
run_model_test(model, cover_factor, data, io_type, backend, str(path), aggressive)

test/pytest/test_hgq_players.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
from pathlib import Path
2+
3+
import HGQ # noqa: F401
4+
import numpy as np
5+
import pytest
6+
import tensorflow as tf
7+
from HGQ import get_default_paq_conf, set_default_paq_conf, trace_minmax
8+
from HGQ.layers import ( # noqa: F401
9+
HConv1D,
10+
HDense,
11+
HQuantize,
12+
PAvgPool1D,
13+
PAvgPool2D,
14+
PConcatenate,
15+
PFlatten,
16+
PMaxPool1D,
17+
PMaxPool2D,
18+
PReshape,
19+
Signature,
20+
)
21+
from HGQ.proxy import to_proxy_model
22+
from HGQ.proxy.fixed_point_quantizer import gfixed
23+
from tensorflow import keras
24+
25+
from hls4ml.converters import convert_from_keras_model
26+
27+
# tf.config.experimental_run_functions_eagerly(True) # noqa
28+
29+
30+
test_path = Path(__file__).parent
31+
32+
33+
def _run_synth_match_test(proxy: keras.Model, data, io_type: str, backend: str, dir: str, cond=None):
34+
35+
output_dir = dir + '/hls4ml_prj'
36+
hls_model = convert_from_keras_model(
37+
proxy,
38+
io_type=io_type,
39+
output_dir=output_dir,
40+
backend=backend,
41+
hls_config={'Model': {'Precision': 'fixed<1,0>', 'ReuseFactor': 1}},
42+
)
43+
hls_model.compile()
44+
45+
data_len = data.shape[0] if isinstance(data, np.ndarray) else data[0].shape[0]
46+
# Multiple output case. Check each output separately
47+
if len(proxy.outputs) > 1: # type: ignore
48+
r_proxy: list[np.ndarray] = [x.numpy() for x in proxy(data)] # type: ignore
49+
r_hls: list[np.ndarray] = hls_model.predict(data) # type: ignore
50+
r_hls = [x.reshape(r_proxy[i].shape) for i, x in enumerate(r_hls)]
51+
else:
52+
r_proxy: list[np.ndarray] = [proxy(data).numpy()] # type: ignore
53+
r_hls: list[np.ndarray] = [hls_model.predict(data).reshape(r_proxy[0].shape)] # type: ignore
54+
55+
errors = []
56+
for i, (p, h) in enumerate(zip(r_proxy, r_hls)):
57+
try:
58+
if cond is None:
59+
mismatch_ph = p != h
60+
assert (
61+
np.sum(mismatch_ph) == 0
62+
), f"Proxy-HLS4ML mismatch for out {i}: {np.sum(np.any(mismatch_ph, axis=1))} out of {data_len} samples are different. Sample: {p[mismatch_ph].ravel()[:5]} vs {h[mismatch_ph].ravel()[:5]}" # noqa: E501
63+
else:
64+
cond(p, h)
65+
except AssertionError as e:
66+
errors.append(e)
67+
if len(errors) > 0:
68+
msgs = [str(e) for e in errors]
69+
raise AssertionError('\n'.join(msgs))
70+
71+
72+
def run_model_test(
73+
model: keras.Model, cover_factor: float | None, data, io_type: str, backend: str, dir: str, aggressive: bool, cond=None
74+
):
75+
data_len = data.shape[0] if isinstance(data, np.ndarray) else data[0].shape[0]
76+
if cover_factor is not None:
77+
trace_minmax(model, data, cover_factor=cover_factor, bsz=data_len)
78+
proxy = to_proxy_model(model, aggressive=aggressive, unary_lut_max_table_size=4096)
79+
_run_synth_match_test(proxy, data, io_type, backend, dir, cond=cond)
80+
81+
82+
def create_player_model(layer: str, rnd_strategy: str, io_type: str):
83+
pa_config = get_default_paq_conf()
84+
pa_config['rnd_strategy'] = rnd_strategy
85+
pa_config['skip_dims'] = 'all' if io_type == 'io_stream' else 'batch'
86+
set_default_paq_conf(pa_config)
87+
88+
inp = keras.Input(shape=(15))
89+
if 'PConcatenate' in layer:
90+
_inp = [HQuantize()(inp)] * 2
91+
out = eval(layer)(_inp)
92+
out = HDense(15)(out)
93+
return keras.Model(inp, out)
94+
elif 'Signature' in layer:
95+
_inp = eval(layer)(inp)
96+
out = HDense(15)(_inp)
97+
return keras.Model(inp, out)
98+
elif 'Pool2D' in layer:
99+
_inp = PReshape((3, 5, 1))(HQuantize()(inp))
100+
elif 'Pool1D' in layer:
101+
_inp = PReshape((5, 3))(HQuantize()(inp))
102+
elif 'Dense' in layer or 'Activation' in layer:
103+
_inp = HQuantize()(inp)
104+
elif 'Flatten' in layer:
105+
out = HQuantize()(inp)
106+
out = PReshape((3, 5))(out)
107+
out = HConv1D(2, 2)(out)
108+
out = eval(layer)(out)
109+
out = HDense(15)(out)
110+
return keras.Model(inp, out)
111+
else:
112+
raise Exception(f'Please add test for {layer}')
113+
114+
out = eval(layer)(_inp)
115+
model = keras.Model(inp, out)
116+
117+
for layer in model.layers:
118+
# No weight bitwidths to randomize
119+
# And activation bitwidths
120+
if hasattr(layer, 'paq'):
121+
fbw: tf.Variable = layer.paq.fbw
122+
fbw.assign(tf.constant(np.random.uniform(4, 6, fbw.shape).astype(np.float32)))
123+
124+
return model
125+
126+
127+
def get_data(shape: tuple[int, ...], v: float, max_scale: float):
128+
rng = np.random.default_rng()
129+
a1 = rng.uniform(-v, v, shape).astype(np.float32)
130+
a2 = rng.uniform(0, max_scale, (1, shape[1])).astype(np.float32)
131+
return (a1 * a2).astype(np.float32)
132+
133+
134+
@pytest.mark.parametrize(
135+
'layer',
136+
[
137+
"PConcatenate()",
138+
"PMaxPool1D(2, padding='same')",
139+
"PMaxPool1D(4, padding='same')",
140+
"PMaxPool2D((5,3), padding='same')",
141+
"PMaxPool1D(2, padding='valid')",
142+
"PMaxPool2D((2,3), padding='valid')",
143+
"Signature(1,6,3)",
144+
"PAvgPool1D(2, padding='same')",
145+
"PAvgPool2D((1,2), padding='same')",
146+
"PAvgPool2D((2,2), padding='same')",
147+
"PAvgPool1D(2, padding='valid')",
148+
"PAvgPool2D((1,2), padding='valid')",
149+
"PAvgPool2D((2,2), padding='valid')",
150+
"PFlatten()",
151+
],
152+
)
153+
@pytest.mark.parametrize("N", [1000])
154+
@pytest.mark.parametrize("rnd_strategy", ['floor', 'standard_round'])
155+
@pytest.mark.parametrize("io_type", ['io_parallel', 'io_stream'])
156+
@pytest.mark.parametrize("cover_factor", [1.0])
157+
@pytest.mark.parametrize("aggressive", [True, False])
158+
@pytest.mark.parametrize("backend", ['vivado', 'vitis'])
159+
def test_syn_players(layer, N: int, rnd_strategy: str, io_type: str, cover_factor: float, aggressive: bool, backend: str):
160+
model = create_player_model(layer=layer, rnd_strategy=rnd_strategy, io_type=io_type)
161+
data = get_data((N, 15), 7, 1)
162+
163+
path = test_path / f'hls4mlprj_hgq_{layer}_{rnd_strategy}_{io_type}_{aggressive}_{backend}'
164+
165+
if 'Signature' in layer:
166+
q = gfixed(1, 6, 3)
167+
data = q(data).numpy()
168+
if "padding='same'" in layer and io_type == 'io_stream':
169+
pytest.skip("io_stream does not support padding='same' for pools at the moment")
170+
171+
run_model_test(model, cover_factor, data, io_type, backend, str(path), aggressive)

0 commit comments

Comments
 (0)