-
Notifications
You must be signed in to change notification settings - Fork 478
PQuant🌶️ integration #1362
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
base: main
Are you sure you want to change the base?
PQuant🌶️ integration #1362
Changes from all commits
36d1740
3e300a3
0b25401
c63c7ca
e73f4f7
857cbe8
450b503
f51faca
34327ec
50d4d69
e3440b2
8271847
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,10 @@ def handle( | |
'n_out': n_out, | ||
'n_in': n_in, | ||
} | ||
|
||
if hasattr(layer, 'quantization_parameters'): | ||
config['quantization_parameters'] = layer.quantization_parameters | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not part of standard keras 3 property, should be in your pquant handler (maybe a base class for all pquant layers?) |
||
|
||
return config | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import typing | ||
from collections.abc import Sequence | ||
|
||
import numpy as np | ||
|
||
from hls4ml.model.types import FixedPrecisionType | ||
|
||
from ._base import KerasV3LayerHandler, register | ||
from .conv import gen_conv_config | ||
|
||
if typing.TYPE_CHECKING: | ||
import pquant | ||
from keras import KerasTensor | ||
|
||
|
||
@register | ||
class PQuantReLUHandler(KerasV3LayerHandler): | ||
handles = ('pquant.core.activations_quantizer.QuantizedReLU',) | ||
|
||
def handle( | ||
self, | ||
layer: 'pquant.core.activations_quantizer.QuantizedReLU', | ||
in_tensors: Sequence['KerasTensor'], | ||
out_tensors: Sequence['KerasTensor'], | ||
): | ||
config = {} | ||
config.update(self.default_config) | ||
config['quantization_parameters'] = layer.quantization_parameters | ||
|
||
if ( | ||
not config['quantization_parameters']['use_high_granularity_quantization'] | ||
and layer.config['quantization_parameters']['use_relu_multiplier'] | ||
): | ||
config['class_name'] = 'MultiplierReLU' | ||
config['param_data'] = np.array(layer.multiplier) | ||
config['activation'] = 'multiplier_relu' | ||
|
||
else: | ||
config['class_name'] = 'QActivation' | ||
config['activation'] = 'relu' | ||
|
||
return (config,) | ||
|
||
|
||
@register | ||
class PQuantTanhHandler(KerasV3LayerHandler): | ||
handles = ('pquant.core.activations_quantizer.QuantizedTanh',) | ||
|
||
def handle( | ||
self, | ||
layer: 'pquant.core.activations_quantizer.QuantizedTanh', | ||
in_tensors: Sequence['KerasTensor'], | ||
out_tensors: Sequence['KerasTensor'], | ||
): | ||
config = {} | ||
config.update(self.default_config) | ||
config['quantization_parameters'] = layer.quantization_parameters | ||
|
||
if not layer.config['quantization_parameters']['use_real_tanh']: | ||
config['class_name'] = 'HardActivation' | ||
config['slope'] = 0.5 # the default values in QKeras | ||
config['shift'] = 0.5 | ||
# Quartus seems to have trouble if the width is 1. | ||
config['slope_prec'] = FixedPrecisionType(width=2, integer=0, signed=False) | ||
config['shift_prec'] = FixedPrecisionType(width=2, integer=0, signed=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. width should be 1 here |
||
config['activation'] = 'hard_tanh' | ||
|
||
else: | ||
config['class_name'] = 'QActivation' | ||
config['activation'] = 'tanh' | ||
|
||
return (config,) | ||
|
||
|
||
@register | ||
class PQuantPoolingHandler(KerasV3LayerHandler): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks mostly the same as the original pooling handler. Can reuse some code there I think? |
||
handles = ('pquant.core.tf_impl.compressed_layers_tf.QuantizedPooling',) | ||
|
||
def handle( | ||
self, | ||
layer: 'pquant.core.tf_impl.compressed_layers_tf.QuantizedPooling', | ||
in_tensors: Sequence['KerasTensor'], | ||
out_tensors: Sequence['KerasTensor'], | ||
): | ||
assert len(in_tensors) == 1, f'Layer {layer.name} has more than one input' | ||
assert len(out_tensors) == 1, f'Layer {layer.name} has more than one output' | ||
|
||
in_shape: tuple[int, ...] = in_tensors[0].shape[1:] # type: ignore | ||
out_shape: tuple[int, ...] = out_tensors[0].shape[1:] # type: ignore | ||
assert all(isinstance(x, int) for x in in_shape), f'Layer {layer.name} has non-fixed size input: {in_shape}' | ||
assert all(isinstance(x, int) for x in out_shape), f'Layer {layer.name} has non-fixed size output: {out_shape}' | ||
|
||
data_format = layer.data_format | ||
|
||
if data_format == 'channels_last': | ||
*px_in_shape, _ = in_shape | ||
else: | ||
_, *px_in_shape = in_shape | ||
|
||
pool_size: tuple[int, ...] = layer.pool_size | ||
|
||
strides = layer.strides | ||
padding = layer.padding | ||
pooling_config = gen_conv_config( | ||
in_shape=in_shape, | ||
out_shape=out_shape, | ||
ker_px_shape=pool_size, | ||
strides=strides, | ||
data_format=data_format, | ||
padding=padding, | ||
name=layer.name, | ||
) | ||
|
||
pooling_config['pool_width'] = pooling_config.pop('filt_width') | ||
if 'filt_height' in pooling_config: | ||
pooling_config['pool_height'] = pooling_config.pop('filt_height') | ||
if len(px_in_shape) == 1: | ||
# inconsistent pooling1d config key name... | ||
pooling_config['n_in'] = pooling_config['in_width'] | ||
pooling_config['n_out'] = pooling_config['out_width'] | ||
|
||
config = {} | ||
config.update(self.default_config) | ||
config.update(pooling_config) | ||
config['class_name'] = f'AveragePooling{layer.dimensions}D' | ||
config['quantization_parameters'] = layer.quantization_parameters | ||
return (config,) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,10 @@ def parse_conv1d_layer(operation, layer_name, input_names, input_shapes, node, c | |
|
||
output_shape = [input_shapes[0][0], layer['n_filt'], layer['out_width']] # Channel first as default | ||
|
||
# Quantization parameter for PQuant integration | ||
if hasattr(class_object, "quantization_parameters"): | ||
layer['quantization_parameters'] = class_object.quantization_parameters | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the handler is functional you can't really subclassing it so fine. Though, all quantization parameters saved here can be derived from the values themselves so not strictly necessary.... |
||
|
||
return layer, output_shape | ||
|
||
|
||
|
@@ -94,4 +98,8 @@ def parse_conv2d_layer(operation, layer_name, input_names, input_shapes, node, c | |
|
||
output_shape = [input_shapes[0][0], layer['n_filt'], layer['out_height'], layer['out_width']] | ||
|
||
# Quantization parameter for PQuant integration | ||
if hasattr(class_object, "quantization_parameters"): | ||
layer['quantization_parameters'] = class_object.quantization_parameters | ||
|
||
return layer, output_shape |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
from hls4ml.converters.pytorch.core import parse_activation_layer | ||
from hls4ml.converters.pytorch.pooling import parse_pooling_layer | ||
from hls4ml.converters.pytorch_to_hls import pytorch_handler | ||
from hls4ml.model.types import FixedPrecisionType | ||
|
||
|
||
@pytorch_handler('QuantizedActivationTorchWrapper') | ||
def parse_pquant_activation_layer(operation, layer_name, input_names, input_shapes, node, class_object, data_reader, config): | ||
|
||
layer, output_shape = parse_activation_layer( | ||
class_object.activation.__class__.__name__, | ||
layer_name, | ||
input_names, | ||
input_shapes, | ||
node, | ||
class_object.activation, | ||
data_reader, | ||
config, | ||
) | ||
layer['quantization_parameters'] = class_object.activation.quantization_parameters | ||
|
||
if ( | ||
layer['activation'] == 'quantizedtanh' | ||
and not class_object.activation.config['quantization_parameters']['use_real_tanh'] | ||
): | ||
layer['class_name'] = 'HardActivation' | ||
layer['slope'] = 0.5 # the default values in QKeras | ||
layer['shift'] = 0.5 | ||
# Quartus seems to have trouble if the width is 1. | ||
layer['slope_prec'] = FixedPrecisionType(width=2, integer=0, signed=False) | ||
layer['shift_prec'] = FixedPrecisionType(width=2, integer=0, signed=False) | ||
layer['activation'] = 'hard_tanh' | ||
|
||
elif ( | ||
layer['activation'] == 'quantizedrelu' | ||
and not layer['quantization_parameters']["use_high_granularity_quantization"] | ||
and class_object.activation.config['quantization_parameters']['use_relu_multiplier'] | ||
): | ||
layer['class_name'] = 'MultiplierReLU' | ||
layer['param_data'] = class_object.activation.multiplier.numpy() | ||
layer['activation'] = 'multiplier_relu' | ||
|
||
else: | ||
layer['class_name'] = 'QActivation' | ||
activation_map = { | ||
'quantizedrelu': 'relu', | ||
'quantizedtanh': 'tanh', | ||
} | ||
layer['activation'] = activation_map.get(layer['activation'], layer['activation']) | ||
|
||
return layer, output_shape | ||
|
||
|
||
@pytorch_handler('QuantizedPooling') | ||
def parse_pquant_pooling_layer(operation, layer_name, input_names, input_shapes, node, class_object, data_reader, config): | ||
|
||
layer, output_shape = parse_pooling_layer( | ||
class_object.pooling.__class__.__name__, | ||
layer_name, | ||
input_names, | ||
input_shapes, | ||
node, | ||
class_object.pooling, | ||
data_reader, | ||
config, | ||
) | ||
layer['quantization_parameters'] = class_object.quantization_parameters | ||
|
||
return layer, output_shape |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -352,7 +352,10 @@ def parse_pytorch_model(config, verbose=True): | |
if '.' not in node.target: | ||
obj = getattr(model, node.name) | ||
else: | ||
obj = getattr(children[node.target.split('.')[0], node.name]) | ||
if '_' not in node.name: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment explaining why this is needed |
||
obj = getattr(children[node.target.split('.')[0]], node.name) | ||
else: | ||
obj = getattr(children[node.target.split('.')[0]], node.name.split('_')[1]) | ||
|
||
input_layer = {} | ||
input_layer['name'] = node.name | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -785,6 +785,27 @@ void prelu(data_T data[CONFIG_T::n_in], param_T alpha[CONFIG_T::n_in], res_T res | |
} | ||
} | ||
|
||
// ************************************************* | ||
// MultiplierReLU Activation | ||
// ************************************************* | ||
template <class data_T, class multiplier_T, class res_T, typename CONFIG_T> | ||
void multiplier_relu(data_T data[CONFIG_T::n_in], multiplier_T mul[1], res_T res[CONFIG_T::n_in]) { | ||
#pragma HLS PIPELINE | ||
|
||
data_T datareg; | ||
for (int ii = 0; ii < CONFIG_T::n_in; ii++) { | ||
datareg = data[ii]; | ||
if (datareg > 0) { | ||
|
||
if (mul[0] >= 0) | ||
res[ii] = datareg << mul[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
else | ||
res[ii] = datareg >> (-mul[0]); | ||
} else | ||
res[ii] = 0; | ||
} | ||
} | ||
|
||
// ************************************************* | ||
// Binary TanH Activation | ||
// ************************************************* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same, should be in pquant base class