diff --git a/test/pytest/baselines/VivadoAccelerator/2020.1/test_keras_api_vivadoacc_test_dense_io_parallel-VivadoAccelerator.json b/test/pytest/baselines/VivadoAccelerator/2020.1/test_keras_api_vivadoacc_test_dense_io_parallel-VivadoAccelerator.json new file mode 100644 index 0000000000..5b7cb5c46a --- /dev/null +++ b/test/pytest/baselines/VivadoAccelerator/2020.1/test_keras_api_vivadoacc_test_dense_io_parallel-VivadoAccelerator.json @@ -0,0 +1,34 @@ +{ + "CSynthesisReport": { + "TargetClockPeriod": "5.00", + "EstimatedClockPeriod": "4.349", + "BestLatency": "11", + "WorstLatency": "11", + "IntervalMin": "12", + "IntervalMax": "12", + "BRAM_18K": "1", + "DSP": "2", + "FF": "622", + "LUT": "1947", + "URAM": "0", + "AvailableBRAM_18K": "1824", + "AvailableDSP": "2520", + "AvailableFF": "548160", + "AvailableLUT": "274080", + "AvailableURAM": "0" + }, + "VivadoSynthReport": { + "LUT": "47", + "FF": "35", + "BRAM_18K": "0.5", + "DSP48E": "2" + }, + "TimingReport": { + "WNS": 3.988, + "TNS": 0.0, + "WHS": 0.01, + "THS": 0.0, + "WPWS": 3.5, + "TPWS": 0.0 + } +} diff --git a/test/pytest/ci-template.yml b/test/pytest/ci-template.yml index ebbcd8a21e..379860ef2b 100644 --- a/test/pytest/ci-template.yml +++ b/test/pytest/ci-template.yml @@ -14,11 +14,14 @@ - if [ $EXAMPLEMODEL == 1 ]; then git submodule update --init example-models; fi - pip install .${EXTRA_DEPS} - # set up vivado_hls command + # set up Vivado commands - mkdir -p cmd_vivado_${VIVADO_VERSION} - echo '#!/bin/bash' > cmd_vivado_${VIVADO_VERSION}/vivado_hls - echo "apptainer exec --cleanenv --env LANG=C,LC_ALL=C /cvmfs/projects.cern.ch/hls4ml/vivado/${VIVADO_VERSION} vivado_hls \"\$@\"" >> cmd_vivado_${VIVADO_VERSION}/vivado_hls + - echo '#!/bin/bash' > cmd_vivado_${VIVADO_VERSION}/vivado + - echo "apptainer exec --cleanenv --env LANG=C,LC_ALL=C /cvmfs/projects.cern.ch/hls4ml/vivado/${VIVADO_VERSION} vivado \"\$@\"" >> cmd_vivado_${VIVADO_VERSION}/vivado - chmod +x cmd_vivado_${VIVADO_VERSION}/vivado_hls + - chmod +x cmd_vivado_${VIVADO_VERSION}/vivado - export PATH=$PWD/cmd_vivado_${VIVADO_VERSION}:$PATH # set up vitis-run command diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py index 7841a1a989..0e1d9d1fae 100644 --- a/test/pytest/conftest.py +++ b/test/pytest/conftest.py @@ -56,12 +56,23 @@ def synthesis_config(): 'run_synthesis': str_to_bool(os.getenv('RUN_SYNTHESIS', 'false')), 'tools_version': { 'Vivado': os.getenv('VIVADO_VERSION', '2020.1'), + 'VivadoAccelerator': os.getenv('VIVADO_VERSION', '2020.1'), 'Vitis': os.getenv('VITIS_VERSION', '2024.1'), 'Quartus': os.getenv('QUARTUS_VERSION', 'latest'), 'oneAPI': os.getenv('ONEAPI_VERSION', '2025.0.1'), }, 'build_args': { 'Vivado': {'csim': False, 'synth': True, 'export': False}, + # Full accelerator flow: run C/RTL synthesis, downstream Vivado synthesis, and board project/bitfile. + 'VivadoAccelerator': { + 'csim': False, + 'synth': True, + 'cosim': False, + 'validation': False, + 'export': True, + 'vsynth': True, + 'bitfile': True, + }, 'Vitis': {'csim': False, 'synth': True, 'export': False}, 'Quartus': {'synth': True, 'fpgasynth': False}, 'oneAPI': {'build_type': 'report', 'run': False}, diff --git a/test/pytest/generate_ci_yaml.py b/test/pytest/generate_ci_yaml.py index 684abc0511..80b86768c1 100644 --- a/test/pytest/generate_ci_yaml.py +++ b/test/pytest/generate_ci_yaml.py @@ -41,6 +41,7 @@ # Value = chunk size per CI job SPLIT_BY_TEST_CASE = { 'test_keras_api': 1, + 'test_keras_api_vivadoacc': 1, } diff --git a/test/pytest/synthesis_helpers.py b/test/pytest/synthesis_helpers.py index 27d953b101..7a71700b2c 100644 --- a/test/pytest/synthesis_helpers.py +++ b/test/pytest/synthesis_helpers.py @@ -119,6 +119,7 @@ def compare_oneapi_backend(data, baseline): COMPARE_FUNCS = { 'Vivado': compare_vitis_backend, + 'VivadoAccelerator': compare_vitis_backend, 'Vitis': compare_vitis_backend, 'oneAPI': compare_oneapi_backend, } @@ -126,6 +127,7 @@ def compare_oneapi_backend(data, baseline): EXPECTED_REPORT_KEYS = { 'Vivado': {'CSynthesisReport'}, + 'VivadoAccelerator': {'CSynthesisReport'}, 'Vitis': {'CSynthesisReport'}, 'oneAPI': {'report'}, } diff --git a/test/pytest/test_keras_api_vivadoacc.py b/test/pytest/test_keras_api_vivadoacc.py new file mode 100644 index 0000000000..b259c9fc7a --- /dev/null +++ b/test/pytest/test_keras_api_vivadoacc.py @@ -0,0 +1,80 @@ +from pathlib import Path + +import numpy as np +import pytest +import tensorflow as tf +from synthesis_helpers import run_synthesis_test +from tensorflow.keras.layers import ( + Activation, + Dense, +) + +import hls4ml + +test_root_path = Path(__file__).parent +VIVADOACC_BOARD = 'zcu102' +VIVADOACC_PART = 'xczu9eg-ffvb1156-2-e' + + +@pytest.mark.parametrize('backend', ['VivadoAccelerator']) +@pytest.mark.parametrize('io_type', ['io_parallel']) +def test_dense(test_case_id, backend, io_type, synthesis_config): + model = tf.keras.models.Sequential() + model.add( + Dense( + 2, + input_shape=(1,), + name='Dense', + use_bias=True, + kernel_initializer=tf.keras.initializers.RandomUniform(minval=1, maxval=10), + bias_initializer='zeros', + kernel_regularizer=None, + bias_regularizer=None, + activity_regularizer=None, + kernel_constraint=None, + bias_constraint=None, + ) + ) + model.add(Activation(activation='elu', name='Activation')) + model.compile(optimizer='adam', loss='mse') + + X_input = np.random.rand(100, 1) + + keras_prediction = model.predict(X_input) + + config = hls4ml.utils.config_from_keras_model(model) + output_dir = str(test_root_path / test_case_id) + baseline_file_name = f'{test_case_id}.json' + + hls_model = hls4ml.converters.convert_from_keras_model( + model, + hls_config=config, + output_dir=output_dir, + backend=backend, + io_type=io_type, + board=VIVADOACC_BOARD, + part=VIVADOACC_PART, + ) + + hls_model.compile() + + hls_prediction = hls_model.predict(X_input) + + np.testing.assert_allclose(hls_prediction, keras_prediction, rtol=1e-2, atol=0.01) + + assert len(model.layers) + 1 == len(hls_model.get_layers()) + assert list(hls_model.get_layers())[0].attributes['class_name'] == 'InputLayer' + assert list(hls_model.get_layers())[1].attributes['class_name'] == model.layers[0]._name + assert list(hls_model.get_layers())[2].attributes['class_name'] == 'ELU' + assert list(hls_model.get_layers())[0].attributes['input_shape'] == list(model.layers[0].input_shape[1:]) + assert list(hls_model.get_layers())[1].attributes['n_in'] == model.layers[0].input_shape[1:][0] + assert list(hls_model.get_layers())[1].attributes['n_out'] == model.layers[0].output_shape[1:][0] + assert list(hls_model.get_layers())[2].attributes['activation'] == str(model.layers[1].activation).split()[1] + assert list(hls_model.get_layers())[1].attributes['activation'] == str(model.layers[0].activation).split()[1] + + run_synthesis_test( + config=synthesis_config, + hls_model=hls_model, + baseline_file_name=baseline_file_name, + backend=backend, + )