Skip to content

Add support for parsing simple brevitas layers as part of pytorch models #1019

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 71 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
e077161
playing with brevitas
JanFSchulte Nov 28, 2023
c946a03
add brevitas quantizer
JanFSchulte Dec 15, 2023
52bb225
latest brevitas developments
JanFSchulte Feb 9, 2024
b212f8e
Avoid Y2K22 Xilinx bug
fgias Mar 1, 2024
037c751
Merge pull request #2 from fgias/fgiasemi/y2k22-bug
JanFSchulte Mar 1, 2024
bf37179
state of the art brevitas parsing and add pytest
JanFSchulte May 20, 2024
df452f0
Merge branch 'brevitas' of https://github.com/JanFSchulte/hls4ml into…
JanFSchulte May 20, 2024
5316a48
fix some compilation errors
JanFSchulte May 21, 2024
6e47e9a
fix another trivial error in pytests
JanFSchulte May 21, 2024
f2201b0
Delete test_brevitas.py
simon71701 May 22, 2024
f64fab6
Delete test_brevitas_conv.py
simon71701 May 22, 2024
45954f9
fix dimensions in Conv2D pytest for brevitas parsing
JanFSchulte Jun 7, 2024
3eb759c
Merge pull request #3 from simon71701/brevitas
JanFSchulte Jun 7, 2024
73af4c1
trigger pre-commit
JanFSchulte Jun 7, 2024
44a6927
Merge branch 'brevitas' of https://github.com/JanFSchulte/hls4ml into…
JanFSchulte Jun 7, 2024
272c418
Merge branch 'main' into brevitas
JanFSchulte Jun 7, 2024
4f401ed
move quantizer to new file
JanFSchulte Jun 7, 2024
9c1740f
reduce diff and update access to tensors to latest version
JanFSchulte Jun 7, 2024
c769fef
[pre-commit.ci] auto fixes from pre-commit hooks
pre-commit-ci[bot] Jun 7, 2024
0bb09f0
add brevitas to the requirements for tests
JanFSchulte Jul 9, 2024
e23b2ed
Merge branch 'brevitas' of https://github.com/JanFSchulte/hls4ml into…
JanFSchulte Jul 9, 2024
cda36b6
adjust required precision in brevitas pytests
JanFSchulte Jul 22, 2024
ef380ad
Add conv1d tests, fix output dir and tolerances
Jul 22, 2024
528b659
Merge branch 'main' into brevitas
JanFSchulte Jul 22, 2024
dffa379
[pre-commit.ci] auto fixes from pre-commit hooks
pre-commit-ci[bot] Jul 22, 2024
399613e
Test QuantMaxPool and ignore QuantDropout
Jul 25, 2024
73590b7
Merge branch 'main' into brevitas
JanFSchulte Jan 10, 2025
d13bf52
[pre-commit.ci] auto fixes from pre-commit hooks
pre-commit-ci[bot] Jan 10, 2025
22dd2cb
merge with master
JanFSchulte Jan 10, 2025
1f17845
restore accidental change
JanFSchulte Jan 10, 2025
787c4e1
Merge branch 'main' into brevitas
JanFSchulte Jan 10, 2025
7e2fdf7
[pre-commit.ci] auto fixes from pre-commit hooks
pre-commit-ci[bot] Jan 10, 2025
10d77b6
update pytests for interface changes and fix merge errors
JanFSchulte Jan 10, 2025
0cce3ef
update to work with brevitas 0.11.0
JanFSchulte Jan 17, 2025
6802c94
Merge branch 'main' into brevitas
JanFSchulte Jan 17, 2025
7543fab
add handling of input/output quantization for layers. Non Pow-2 weigh…
JanFSchulte Feb 19, 2025
bd79ad1
Merge branch 'brevitas' of https://github.com/JanFSchulte/hls4ml into…
JanFSchulte Feb 19, 2025
c7ac19f
merge with main
JanFSchulte Feb 19, 2025
9fa82db
Merge branch 'main' into brevitas
JanFSchulte Feb 20, 2025
be87dd4
hack around lazy imports
JanFSchulte Feb 20, 2025
c9d2415
tidy up and support only power of 2 scaling for now
JanFSchulte Feb 21, 2025
3fb5d69
Merge branch 'main' into brevitas
JanFSchulte Feb 21, 2025
8910fa6
add missing file
JanFSchulte Feb 21, 2025
680728f
move tracer import
JanFSchulte Feb 21, 2025
1319791
add brevitas to testing environment
JanFSchulte Feb 21, 2025
1d08c8a
fix import path in pytests
JanFSchulte Feb 21, 2025
d064b0c
fix import path in pytests 2
JanFSchulte Feb 21, 2025
fb30b35
fix revert accidental changes
JanFSchulte Feb 22, 2025
bb36f71
remove unnecessary attributes from Quant class
JanFSchulte Feb 24, 2025
ff4b2e2
add support for QuantIdentity layers
JanFSchulte Feb 24, 2025
d91bdf0
fix output names
JanFSchulte Feb 24, 2025
4f03dff
Merge branch 'main' into brevitas
JanFSchulte Feb 24, 2025
2f1ce04
add support for QuantCat layer - which seems broken :/
JanFSchulte Feb 25, 2025
27f14b1
Merge branch 'main' into brevitas
JanFSchulte Feb 26, 2025
7bf57f1
remove QuantCat, it's deprecated, add support for QuantUpsample in io…
JanFSchulte Feb 26, 2025
6840b7a
Merge branch 'brevitas' of https://github.com/JanFSchulte/hls4ml into…
JanFSchulte Feb 26, 2025
c5f96c6
add support for QuantEltWiseAdd
JanFSchulte Feb 27, 2025
05d99fa
first try at RNN layers
JanFSchulte Mar 6, 2025
7ad2709
remove leftover print statement
JanFSchulte Mar 6, 2025
53b82df
Merge branch 'main' into brevitas
JanFSchulte Mar 6, 2025
d87566f
Merge branch 'main' into brevitas
JanFSchulte Mar 21, 2025
c3912bc
add tests for QuantRNN and QuantLSTM layers. Accuracy still quite bad
JanFSchulte Mar 21, 2025
111b83b
don't break RNNs in non-brevitas workflows
JanFSchulte Mar 26, 2025
82c6c0f
using custom tracer file from extension API
JanFSchulte Apr 2, 2025
a421aaa
Merge branch 'main' into brevitas
JanFSchulte Apr 2, 2025
b4dd15e
remove QuantLSTM for now
JanFSchulte Apr 9, 2025
6448e10
Merge branch 'brevitas' of https://github.com/JanFSchulte/hls4ml into…
JanFSchulte Apr 9, 2025
ed1c2c8
clean diff
JanFSchulte Apr 9, 2025
1d749db
Merge branch 'main' into brevitas
JanFSchulte Apr 9, 2025
6f1c24f
fix failing pytests
JanFSchulte Apr 14, 2025
19218e0
Merge branch 'brevitas' of https://github.com/JanFSchulte/hls4ml into…
JanFSchulte Apr 14, 2025
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
107 changes: 95 additions & 12 deletions hls4ml/converters/pytorch/convolution.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from hls4ml.converters.pytorch_to_hls import pytorch_handler
import numpy as np

from hls4ml.converters.pytorch_to_hls import addQuantizationParameters, convert_uaq_to_apfixed, pytorch_handler
from hls4ml.converters.utils import compute_padding_1d_pytorch, compute_padding_2d_pytorch, parse_data_format
from hls4ml.model.quantizers import BrevitasQuantizer
from hls4ml.model.types import FixedPrecisionType


@pytorch_handler('Conv1d')
@pytorch_handler('Conv1d', 'QuantConv1d')
def parse_conv1d_layer(operation, layer_name, input_names, input_shapes, node, class_object, data_reader, config):
assert 'Conv1d' in operation

Expand All @@ -13,12 +17,50 @@ def parse_conv1d_layer(operation, layer_name, input_names, input_shapes, node, c
layer['class_name'] = 'Conv1D'
layer['data_format'] = 'channels_first' # Pytorch default (can't change)

layer['weight_data'] = class_object.weight.data.numpy()
if class_object.bias is not None:
layer['bias_data'] = class_object.bias.data.numpy()
else:
layer['bias_data'] = None
if "Quant" in operation:
if class_object.weight_quant.is_quant_enabled:
width = int(class_object.quant_weight().bit_width)
scale = class_object.quant_weight().scale.detach().numpy()
mantissa, _ = np.frexp(scale)
# if scale is power of 2 we can simply use hls4ml FixedPrecisionType and directly
# use the already quantized tensor from brevitas
if mantissa == 0.5:
ap_fixed_params = convert_uaq_to_apfixed(width, float(class_object.quant_weight().scale))
layer['weight_data'] = class_object.quant_weight().detach().value.numpy()
layer['weight_quantizer'] = BrevitasQuantizer(
width, FixedPrecisionType(width=width, integer=int(ap_fixed_params[1]), signed=True)
)
else:
raise Exception(
'''Non-power of 2 quantization of weights not supported when injecting brevitas models.
Please used QONNX instead.'''
)
else:
layer['weight_data'] = class_object.weight.data.numpy()

if class_object.bias_quant.is_quant_enabled:
width = int(class_object.quant_bias().bit_width)
ap_fixed_params = convert_uaq_to_apfixed(width, float(class_object.quant_bias().scale))
layer['bias_data'] = class_object.quant_bias().detach().value.numpy()
layer['bias_quantizer'] = BrevitasQuantizer(
width, FixedPrecisionType(width=width, integer=int(ap_fixed_params[1]), signed=True)
)
else:
if class_object.bias is not None:
layer['bias_data'] = class_object.bias.data.numpy()
else:
layer['bias_data'] = None
if class_object.input_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'input', act=True)
if class_object.output_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'output', act=True)

else:
layer['weight_data'] = class_object.weight.data.numpy()
if class_object.bias is not None:
layer['bias_data'] = class_object.bias.data.numpy()
else:
layer['bias_data'] = None
# Input info
(*_, layer['in_width'], layer['n_chan']) = parse_data_format(
input_shapes[0], 'channels_first'
Expand Down Expand Up @@ -47,7 +89,7 @@ def parse_conv1d_layer(operation, layer_name, input_names, input_shapes, node, c
return layer, output_shape


@pytorch_handler('Conv2d')
@pytorch_handler('Conv2d', 'QuantConv2d')
def parse_conv2d_layer(operation, layer_name, input_names, input_shapes, node, class_object, data_reader, config):
assert 'Conv2d' in operation

Expand All @@ -58,11 +100,52 @@ def parse_conv2d_layer(operation, layer_name, input_names, input_shapes, node, c
layer['class_name'] = 'Conv2D'
layer['data_format'] = 'channels_first' # Pytorch default (can't change)

layer['weight_data'] = class_object.weight.data.numpy()
if class_object.bias is not None:
layer['bias_data'] = class_object.bias.data.numpy()
if "Quant" in operation:
if class_object.weight_quant.is_quant_enabled:
width = int(class_object.quant_weight().bit_width)
scale = class_object.quant_weight().scale.detach().numpy()
mantissa, _ = np.frexp(scale)
# if scale is power of 2 we can simply use hls4ml FixedPrecisionType and directly
# use the already quantized tensor from brevitas
if mantissa == 0.5:
ap_fixed_params = convert_uaq_to_apfixed(width, float(class_object.quant_weight().scale))
layer['weight_data'] = class_object.quant_weight().detach().value.numpy()
layer['weight_quantizer'] = BrevitasQuantizer(
width, FixedPrecisionType(width=width, integer=int(ap_fixed_params[1]), signed=True)
)
else:
raise Exception(
'''Non-power of 2 quantization of weights not supported when injecting brevitas models.
Please used QONNX instead.'''
)
# layer = addQuantizationParameters(layer, class_object.quant_weight(), 'weight')
# layer['weight_data'] = class_object.quant_weight().detach().value.numpy()
else:
layer['weight_data'] = class_object.weight.data.numpy()

if class_object.bias_quant.is_quant_enabled:
width = int(class_object.quant_bias().bit_width)
ap_fixed_params = convert_uaq_to_apfixed(width, float(class_object.quant_bias().scale))
layer['bias_data'] = class_object.quant_bias().detach().value.numpy()
layer['bias_quantizer'] = BrevitasQuantizer(
width, FixedPrecisionType(width=width, integer=int(ap_fixed_params[1]), signed=True)
)
else:
if class_object.bias is not None:
layer['bias_data'] = class_object.bias.data.numpy()
else:
layer['bias_data'] = None
if class_object.input_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'input', act=True)
if class_object.output_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'output', act=True)

else:
layer['bias_data'] = None
layer['weight_data'] = class_object.weight.data.numpy()
if class_object.bias is not None:
layer['bias_data'] = class_object.bias.data.numpy()
else:
layer['bias_data'] = None

# Input info
(*_, layer['in_height'], layer['in_width'], layer['n_chan']) = parse_data_format(
Expand Down
90 changes: 87 additions & 3 deletions hls4ml/converters/pytorch/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import numpy as np

from hls4ml.converters.pytorch_to_hls import pytorch_handler
from hls4ml.converters.pytorch_to_hls import addQuantizationParameters, convert_uaq_to_apfixed, pytorch_handler
from hls4ml.model.quantizers import BrevitasQuantizer
from hls4ml.model.types import FixedPrecisionType


@pytorch_handler('Constant')
Expand All @@ -20,7 +22,33 @@ def parse_constant_layer(operation, layer_name, node):
return layer, output_shape


@pytorch_handler('Linear')
# A QuantIdentity layer does nothing but quantize its inputs. Insert `Quant` node to be processed by QONNX optimizers
@pytorch_handler('QuantIdentity')
def parse_quantidentity_layer(operation, layer_name, input_names, input_shapes, node, class_object, data_reader, config):
assert 'QuantIdentity' in operation

layer = {}
layer['inputs'] = input_names

layer['class_name'] = 'Quant'
layer['name'] = layer_name

if class_object.act_quant.is_quant_enabled:
layer['bitwidth'] = int(class_object.act_quant.bit_width())
layer['signed'] = class_object.act_quant.is_signed
layer['scale'] = np.full(np.array(input_shapes[0][1:]), class_object.act_quant.scale())
layer['zeropt'] = float(class_object.act_quant.zero_point())
layer['narrow'] = class_object.act_quant.is_narrow_range
layer['rounding_mode'] = class_object.act_quant.rounding_mode

else:
raise Exception('''QuantIdentify layer without act quant does nothing, please remove from model.''')
output_shape = input_shapes[0]

return layer, output_shape


@pytorch_handler('Linear', 'QuantLinear')
def parse_linear_layer(operation, layer_name, input_names, input_shapes, node, class_object, data_reader, config):
assert 'Linear' in operation

Expand All @@ -36,6 +64,44 @@ def parse_linear_layer(operation, layer_name, input_names, input_shapes, node, c
else:
layer['bias_data'] = None

if "Quant" in operation:
if class_object.weight_quant.is_quant_enabled:
width = int(class_object.quant_weight().bit_width)
scale = class_object.quant_weight().scale.detach().numpy()
mantissa, _ = np.frexp(scale)
# if scale is power of 2 we can simply use hls4ml FixedPrecisionType and directly
# use the already quantized tensor from brevitas
if mantissa == 0.5:
ap_fixed_params = convert_uaq_to_apfixed(width, float(class_object.quant_weight().scale))
layer['weight_data'] = class_object.quant_weight().detach().value.numpy()
layer['weight_quantizer'] = BrevitasQuantizer(
width, FixedPrecisionType(width=width, integer=int(ap_fixed_params[1]), signed=True)
)
else:
raise Exception(
'''Non-power of 2 quantization of weights not supported when injecting brevitas models.
Please used QONNX instead.'''
)
else:
layer['weight_data'] = class_object.weight.data.numpy()

if class_object.bias_quant.is_quant_enabled:
width = int(class_object.quant_bias().bit_width)
ap_fixed_params = convert_uaq_to_apfixed(width, float(class_object.quant_bias().scale))
layer['bias_data'] = class_object.quant_bias().detach().value.numpy()
layer['bias_quantizer'] = BrevitasQuantizer(
width, FixedPrecisionType(width=width, integer=int(ap_fixed_params[1]), signed=True)
)
else:
if class_object.bias is not None:
layer['bias_data'] = class_object.bias.data.numpy()
else:
layer['bias_data'] = None
if class_object.input_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'input', act=True)
if class_object.output_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'output', act=True)

if class_object is not None:
layer['n_in'] = class_object.in_features
layer['n_out'] = class_object.out_features
Expand All @@ -54,7 +120,19 @@ def parse_linear_layer(operation, layer_name, input_names, input_shapes, node, c
return layer, output_shape


activation_layers = ['Softmax', 'ReLU', 'LeakyReLU', 'Threshold', 'ELU', 'PReLU', 'Sigmoid', 'Tanh']
activation_layers = [
'Softmax',
'ReLU',
'LeakyReLU',
'Threshold',
'ELU',
'PReLU',
'Sigmoid',
'Tanh',
'QuantReLU',
'QuantSigmoid',
'QuantTanh',
]


@pytorch_handler(*activation_layers)
Expand All @@ -66,6 +144,12 @@ def parse_activation_layer(operation, layer_name, input_names, input_shapes, nod
layer['name'] = layer_name
layer['inputs'] = input_names

if "Quant" in operation:
layer['class_name'] = operation.split('Quant')[-1]
layer['activation'] = layer['class_name']
if class_object.act_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.act_quant, 'output', act=True)

if node.op == 'call_module':
if layer['class_name'] in ['ReLU', 'Sigmoid', 'Tanh']:
layer['class_name'] = 'Activation'
Expand Down
10 changes: 8 additions & 2 deletions hls4ml/converters/pytorch/merge.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from hls4ml.converters.pytorch_to_hls import pytorch_handler
from hls4ml.converters.pytorch_to_hls import addQuantizationParameters, pytorch_handler

concat_layers = ['cat', 'concat', 'concatenate']

Expand Down Expand Up @@ -28,7 +28,7 @@ def parse_concat_layer(operation, layer_name, input_names, input_shapes, node, c
return layer, output_shape


add_layers = ['add']
add_layers = ['add', 'QuantEltwiseAdd']
multiply_layers = ['mul', 'multiply']
subtract_layers = ['sub', 'subtract']
min_layers = ['fmin', 'minimum']
Expand Down Expand Up @@ -56,6 +56,12 @@ def parse_merge_layer(operation, layer_name, input_names, input_shapes, node, cl

layer['inputs'] = input_names

if 'Quant' in operation:
if class_object.input_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'input', act=True)
if class_object.output_quant.is_quant_enabled:
layer = addQuantizationParameters(layer, class_object.input_quant, 'output', act=True, scale_up=True)

output_shape = input_shapes[0][:]

return layer, output_shape
11 changes: 8 additions & 3 deletions hls4ml/converters/pytorch/pooling.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from hls4ml.converters.pytorch_to_hls import pytorch_handler
from hls4ml.converters.utils import compute_padding_1d_pytorch, compute_padding_2d_pytorch, parse_data_format

pooling_layers = ['MaxPool1d', 'MaxPool2d', 'AvgPool1d', 'AvgPool2d']
pooling_layers = [
'MaxPool1d',
'MaxPool2d',
'AvgPool1d',
'AvgPool2d',
] # TODO add support for special quantized average pool layers


@pytorch_handler(*pooling_layers)
Expand All @@ -10,9 +15,9 @@ def parse_pooling_layer(operation, layer_name, input_names, input_shapes, node,

layer = {}

if operation == 'MaxPool1d':
if 'MaxPool1d' in operation:
layer['class_name'] = 'MaxPooling1D'
if operation == 'MaxPool2d':
if 'MaxPool2d' in operation:
layer['class_name'] = 'MaxPooling2D'
if operation == 'AvgPool1d':
layer['class_name'] = 'AveragePooling1D'
Expand Down
Loading
Loading