diff --git a/hls4ml/model/profiling.py b/hls4ml/model/profiling.py index 81052a589e..6309c35c5e 100644 --- a/hls4ml/model/profiling.py +++ b/hls4ml/model/profiling.py @@ -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)') @@ -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'. @@ -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. @@ -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() diff --git a/test/pytest/test_keras_v3_profiling.py b/test/pytest/test_keras_v3_profiling.py index c3ed98c3cc..0b1d03b9c7 100644 --- a/test/pytest/test_keras_v3_profiling.py +++ b/test/pytest/test_keras_v3_profiling.py @@ -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 @@ -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, @@ -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."""