Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions hls4ml/model/profiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,10 @@ def activations_hlsmodel(model, X, fmt='summary', plot='boxplot'):
elif fmt == 'summary':
data = []

_, trace = model.trace(np.ascontiguousarray(X))
if isinstance(X, (list, tuple)):
_, trace = model.trace([np.ascontiguousarray(x) for x in X])
else:
_, trace = model.trace(np.ascontiguousarray(X))

if len(trace) == 0:
raise RuntimeError('ModelGraph must have tracing on for at least 1 layer (this can be set in its config)')
Expand Down Expand Up @@ -457,8 +460,11 @@ def numerical(model=None, hls_model=None, X=None, plot='boxplot'):
Args:
model (optional): Keras of PyTorch model. Defaults to None.
hls_model (ModelGraph, optional): The ModelGraph to profile. Defaults to None.
X (ndarray, optional): Test data on which to evaluate the model to profile activations.
Must be formatted suitably for the ``model.predict(X)``. Defaults to None.
X (ndarray or list of ndarray, optional): Test data on which to evaluate the model to profile
activations. Must be formatted suitably for the ``model.predict(X)``. For models with
multiple inputs, pass a list holding one array per model input, ordered as
``hls_model.get_input_variables()`` (i.e. the order of the inputs of the original model).
Defaults to None.
plot (str, optional): The type of plot to produce. Options are: 'boxplot' (default), 'violinplot', 'histogram',
'FacetGrid'. Defaults to 'boxplot'.

Expand Down Expand Up @@ -682,7 +688,9 @@ def compare(keras_model, hls_model, X, plot_type='dist_diff'):
Args:
keras_model: Original keras model.
hls_model (ModelGraph): Converted ModelGraph, with "Trace:True" in the configuration file.
X (ndarray): Input tensor for the model.
X (ndarray or list of ndarray): Input tensor for the model. For models with multiple inputs,
pass a list holding one array per model input, ordered as
``hls_model.get_input_variables()`` (i.e. the order of the inputs of the original model).
plot_type (str, optional): Different methods to visualize the y_model and y_sim differences.
Possible options include:
- 'norm_diff':: square root of the sum of the squares of the differences between each output vectors.
Expand All @@ -696,7 +704,10 @@ def compare(keras_model, hls_model, X, plot_type='dist_diff'):
# Take in output from both models
# Note that each y is a dictionary with structure {"layer_name": flattened ouput array}
ymodel = get_ymodel_keras(keras_model, X)
_, ysim = hls_model.trace(X)
if isinstance(X, (list, tuple)):
_, ysim = hls_model.trace([np.ascontiguousarray(x) for x in X])
else:
_, ysim = hls_model.trace(np.ascontiguousarray(X))

print('Plotting difference...')
f = plt.figure()
Expand Down
42 changes: 40 additions & 2 deletions test/pytest/test_keras_v3_profiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ def test_keras_v3_numerical_profiling_conv_model():


@pytest.mark.skipif(not __keras_profiling_enabled__, reason='Keras 3.0 or higher is required')
@pytest.mark.skip(reason='convert_from_config needs update for Keras v3 model serialization format')
def test_keras_v3_numerical_profiling_with_hls_model(test_case_id):
"""Test numerical profiling with both Keras v3 model and hls4ml model."""
import hls4ml
Expand All @@ -101,8 +100,10 @@ def test_keras_v3_numerical_profiling_with_hls_model(test_case_id):
# Generate test data
X_test = np.random.rand(100, 8).astype(np.float32)

# Create hls4ml model
# Create hls4ml model, tracing every layer so that activations can be profiled
config = hls4ml.utils.config_from_keras_model(model, granularity='name')
for layer in config['LayerName'].keys():
config['LayerName'][layer]['Trace'] = True
hls_model = hls4ml.converters.convert_from_keras_model(
model,
hls_config=config,
Expand All @@ -121,6 +122,43 @@ def test_keras_v3_numerical_profiling_with_hls_model(test_case_id):
assert aph is not None # HLS model activations (after optimization)


@pytest.mark.skipif(not __keras_profiling_enabled__, reason='Keras 3.0 or higher is required')
def test_keras_v3_numerical_profiling_multiple_inputs(test_case_id):
"""Test numerical profiling of a model with more than one input, of differing shapes."""
import hls4ml

input_1 = keras.Input(shape=(16, 21), name='basic_input')
input_2 = keras.Input(shape=(1,), name='jet_pt')
x = keras.layers.Flatten()(input_1)
x = keras.layers.Dense(8, activation='relu')(x)
x = keras.layers.Concatenate()([x, input_2])
outputs = keras.layers.Dense(4, activation='softmax')(x)
model = keras.Model(inputs=[input_1, input_2], outputs=outputs)
model.compile(optimizer='adam', loss='categorical_crossentropy')

# One array per model input, with different shapes
X_test = [np.random.rand(10, 16, 21).astype(np.float32), np.random.rand(10, 1).astype(np.float32)]

config = hls4ml.utils.config_from_keras_model(model, granularity='name', backend='Vivado')
for layer in config['LayerName'].keys():
config['LayerName'][layer]['Trace'] = True

hls_model = hls4ml.converters.convert_from_keras_model(
model,
hls_config=config,
output_dir=str(Path(__file__).parent / test_case_id),
backend='Vivado',
)
hls_model.compile()

wp, wph, ap, aph = numerical(model, hls_model=hls_model, X=X_test)

assert wp is not None # Keras model weights (before optimization)
assert wph is not None # HLS model weights (after optimization)
assert ap is not None # Keras model activations (before optimization)
assert aph is not None # HLS model activations (after optimization)


@pytest.mark.skipif(not __keras_profiling_enabled__, reason='Keras 3.0 or higher is required')
def test_keras_v3_numerical_profiling_batch_norm():
"""Test numerical profiling with Keras v3 model containing BatchNormalization."""
Expand Down
Loading