Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
af9b710
Fix Keras v3 model conversion in numerical profiling
Abubakar-rashid Jan 20, 2026
d87185e
Merge branch 'main' into fix-keras-v3-profiling
jmitrevs Jan 21, 2026
e02aa9f
Address review feedback: remove unnecessary else clause and add test …
Abubakar-rashid Jan 22, 2026
4b165c8
Resolve merge conflict: add test_keras_v3_profiling to KERAS3_LIST
Abubakar-rashid Jan 22, 2026
50d117b
Fix line length issue in generate_ci_yaml.py
Abubakar-rashid Jan 22, 2026
e6294bc
Merge branch 'main' into fix-keras-v3-profiling
Abubakar-rashid Jan 22, 2026
335def9
Merge branch 'main' into fix-keras-v3-profiling
Abubakar-rashid Jan 26, 2026
9e4994a
Einsum and EinsumDense for oneAPI
laurilaatu Jan 26, 2026
558170b
Add tests
laurilaatu Jan 26, 2026
474a4fd
Add tests
laurilaatu Jan 26, 2026
2d4944e
Remove hgq2 from testing-keras3 dependencies and test_hgq2_mha from K…
Abubakar-rashid Jan 27, 2026
2aa04c3
Merge branch 'main' into fix-keras-v3-profiling
Abubakar-rashid Jan 27, 2026
abab3e9
[pre-commit.ci] pre-commit autoupdate (#1425)
pre-commit-ci[bot] Jan 30, 2026
288a27f
Add LHC trigger use case context to README (#1418)
siddardhadesu Jan 30, 2026
81d23ac
[pre-commit.ci] pre-commit autoupdate (#1430)
pre-commit-ci[bot] Feb 4, 2026
11c3a3a
Fix Keras v3 profiling tests: build models and correct bar count expe…
Abubakar-rashid Feb 4, 2026
cf113fa
Merge branch 'main' into fix-keras-v3-profiling
Abubakar-rashid Feb 4, 2026
77792e0
Merge branch 'main' into fix-keras-v3-profiling
Abubakar-rashid Feb 5, 2026
5aeeb21
Fix qkeras import to avoid namespace conflict with hgq2
Abubakar-rashid Feb 5, 2026
ffbc6a4
Fix activation and batch norm tests
Abubakar-rashid Feb 6, 2026
bcebc44
Add new arguments to convert_from_keras_model call
Abubakar-rashid Feb 6, 2026
acf9c89
Fix Keras v3 profiling tests - 4/5 passing
Abubakar-rashid Feb 6, 2026
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
12 changes: 10 additions & 2 deletions hls4ml/converters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,16 @@ def convert_from_config(config):
model = onnx_to_hls(yamlConfig)
elif 'PytorchModel' in yamlConfig:
model = pytorch_to_hls(yamlConfig)
else:
model = keras_v2_to_hls(yamlConfig)
elif 'KerasModel' in yamlConfig:
import keras

if keras.__version__ >= '3.0':
# Get fallback flags from config or use defaults
allow_da_fallback = yamlConfig.get('HLSConfig', {}).get('Model', {}).get('AllowDAFallback', True)
allow_v2_fallback = yamlConfig.get('HLSConfig', {}).get('Model', {}).get('AllowV2Fallback', True)
model = keras_v3_to_hls(yamlConfig, allow_da_fallback, allow_v2_fallback)
else:
model = keras_v2_to_hls(yamlConfig)

return model

Expand Down
4 changes: 2 additions & 2 deletions hls4ml/model/profiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
__torch_profiling_enabled__ = False

try:
import qkeras
from qkeras import QActivation

__qkeras_profiling_enabled__ = True
except ImportError:
Expand All @@ -37,7 +37,7 @@
if __keras_profiling_enabled__:
__keras_activations.append(keras.layers.Activation)
if __qkeras_profiling_enabled__:
__keras_activations.append(qkeras.QActivation)
__keras_activations.append(QActivation)


def get_unoptimized_hlsmodel(model):
Expand Down
9 changes: 8 additions & 1 deletion test/pytest/generate_ci_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@

# Long-running tests will not be bundled with other tests
LONGLIST = {'test_hgq_layers', 'test_hgq_players', 'test_qkeras', 'test_pytorch_api'}
KERAS3_LIST = {'test_keras_v3_api', 'test_hgq2_mha', 'test_einsum_dense', 'test_qeinsum', 'test_multiout_onnx'}
KERAS3_LIST = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is test_hg2_mha removed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had asked for it since installing hgq2 brings with it a broken qkeras version that breaks the profiling code. As these tests are skipped anyway internally, I figured we'd remove them for now until this is properly resolved. Maybe @calad0i can also have a look?

'test_keras_v3_api',
'test_hgq2_mha',
'test_einsum_dense',
'test_qeinsum',
'test_multiout_onnx',
'test_keras_v3_profiling',
}

# Test files to split by individual test cases
# Value = chunk size per CI job
Expand Down
141 changes: 141 additions & 0 deletions test/pytest/test_keras_v3_profiling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Test numerical profiling with Keras v3 models."""

import numpy as np
import pytest

try:
import keras

__keras_profiling_enabled__ = keras.__version__ >= '3.0'
except ImportError:
__keras_profiling_enabled__ = False

if __keras_profiling_enabled__:
from hls4ml.model.profiling import numerical


def count_bars_in_figure(fig):
"""Count the number of bars in all axes of a figure."""
count = 0
for ax in fig.get_axes():
count += len(ax.patches)
return count


@pytest.mark.skipif(not __keras_profiling_enabled__, reason='Keras 3.0 or higher is required')
def test_keras_v3_numerical_profiling_simple_model():
"""Test numerical profiling with a simple Keras v3 Dense model."""
model = keras.Sequential(
[
keras.layers.Dense(20, input_shape=(10,), activation='relu'),
keras.layers.Dense(5, activation='softmax'),
]
)
model.compile(optimizer='adam', loss='categorical_crossentropy')
# Build the model so weights are initialized
model.build((None, 10))

# Test profiling weights only
wp, _, _, _ = numerical(model)
assert wp is not None
# Should have 2 bars (one per layer, each showing weights and biases combined)
assert count_bars_in_figure(wp) == 2


@pytest.mark.skipif(not __keras_profiling_enabled__, reason='Keras 3.0 or higher is required')
def test_keras_v3_numerical_profiling_with_activations():
"""Test numerical profiling with Keras v3 model including activations."""
# Use functional API instead of Sequential to ensure input layer is properly defined
inputs = keras.Input(shape=(10,))
x = keras.layers.Dense(20, activation='relu')(inputs)
outputs = keras.layers.Dense(5)(x)
model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer='adam', loss='mse')

# Generate test data
X_test = np.random.rand(100, 10).astype(np.float32)

# Test profiling with activations
wp, _, ap, _ = numerical(model, X=X_test)
assert wp is not None
assert ap is not None


@pytest.mark.skipif(not __keras_profiling_enabled__, reason='Keras 3.0 or higher is required')
def test_keras_v3_numerical_profiling_conv_model():
"""Test numerical profiling with a Keras v3 Conv model."""
model = keras.Sequential(
[
keras.layers.Conv2D(16, (3, 3), activation='relu', input_shape=(28, 28, 1)),
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Flatten(),
keras.layers.Dense(10, activation='softmax'),
]
)
model.compile(optimizer='adam', loss='categorical_crossentropy')
# Build the model so weights are initialized
model.build((None, 28, 28, 1))

# Test profiling weights
wp, _, _, _ = numerical(model)
assert wp is not None
# Conv layer has 1 bar, Dense layer has 1 bar = 2 bars total
assert count_bars_in_figure(wp) == 2


@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 numerical profiling with both Keras v3 model and hls4ml model."""
import hls4ml

# Use functional API to ensure input layer is properly defined
inputs = keras.Input(shape=(8,))
x = keras.layers.Dense(16, activation='relu')(inputs)
outputs = keras.layers.Dense(4, activation='softmax')(x)
model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer='adam', loss='categorical_crossentropy')

# Generate test data
X_test = np.random.rand(100, 8).astype(np.float32)

# Create hls4ml model
config = hls4ml.utils.config_from_keras_model(model, granularity='name')
hls_model = hls4ml.converters.convert_from_keras_model(
model,
hls_config=config,
output_dir='/tmp/test_keras_v3_profiling_hls',
backend='Vivado',
allow_da_fallback=True,
allow_v2_fallback=True,
)

# Test profiling with both models
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."""
model = keras.Sequential(
[
keras.layers.Dense(20, input_shape=(10,)),
keras.layers.BatchNormalization(),
keras.layers.Activation('relu'),
keras.layers.Dense(5, activation='softmax'),
]
)
model.compile(optimizer='adam', loss='categorical_crossentropy')
# Build the model so weights are initialized
model.build((None, 10))

# Test profiling weights
wp, _, _, _ = numerical(model)
assert wp is not None
# Dense has 1 bar, BatchNorm has 1 bar, second Dense has 1 bar = 3 bars
assert count_bars_in_figure(wp) == 3
Loading