mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
609 lines
23 KiB
Python
609 lines
23 KiB
Python
"""Tests with brevitas quantization aware training."""
|
|
|
|
from typing import Optional
|
|
|
|
import brevitas.nn as qnn
|
|
import numpy
|
|
import pytest
|
|
import torch
|
|
import torch.utils
|
|
from brevitas.quant import Int8ActPerTensorFloat, Int8WeightPerTensorFloat
|
|
from brevitas.quant.scaled_int import IntBias
|
|
from sklearn.datasets import load_digits
|
|
from sklearn.model_selection import train_test_split
|
|
from sklearn.preprocessing import StandardScaler
|
|
from torch import nn
|
|
from torch.utils.data import DataLoader, TensorDataset
|
|
|
|
from concrete.ml.common import utils
|
|
from concrete.ml.common.utils import (
|
|
is_classifier_or_partial_classifier,
|
|
is_regressor_or_partial_regressor,
|
|
)
|
|
from concrete.ml.pytest.torch_models import (
|
|
NetWithConstantsFoldedBeforeOps,
|
|
QuantCustomModel,
|
|
TinyQATCNN,
|
|
)
|
|
from concrete.ml.quantization.base_quantized_op import QuantizedMixingOp
|
|
from concrete.ml.quantization.post_training import PowerOfTwoScalingRoundPBSAdapter
|
|
from concrete.ml.quantization.qat_quantizers import Int8ActPerTensorPoT, Int8WeightPerTensorPoT
|
|
from concrete.ml.sklearn import _get_sklearn_neural_net_models
|
|
from concrete.ml.sklearn.qnn_module import SparseQuantNeuralNetwork
|
|
from concrete.ml.torch.compile import compile_brevitas_qat_model
|
|
|
|
|
|
def forward_test_torch(net, test_loader):
|
|
"""Test the network: measure accuracy on the test set.
|
|
|
|
Args:
|
|
test_loader: the test loader
|
|
|
|
Returns:
|
|
res: the number of correctly classified test examples
|
|
|
|
"""
|
|
|
|
# Freeze normalization layers
|
|
net.eval()
|
|
|
|
all_y_pred = numpy.zeros((len(test_loader)), dtype=numpy.int64)
|
|
all_targets = numpy.zeros((len(test_loader)), dtype=numpy.int64)
|
|
|
|
# Iterate over the batches
|
|
idx = 0
|
|
for data, target in test_loader:
|
|
# Accumulate the ground truth labels
|
|
endidx = idx + target.shape[0]
|
|
all_targets[idx:endidx] = target.numpy()
|
|
|
|
# Run forward and get the raw predictions first
|
|
raw_pred = net(data).detach().numpy()
|
|
|
|
# Get the predicted class id, handle NaNs
|
|
if numpy.any(numpy.isnan(raw_pred)):
|
|
output = -1 # pragma: no cover
|
|
else:
|
|
output = raw_pred.argmax(1)
|
|
|
|
all_y_pred[idx:endidx] = output
|
|
|
|
idx += target.shape[0]
|
|
|
|
# Print out the accuracy as a percentage
|
|
n_correct = numpy.sum(all_targets == all_y_pred)
|
|
return n_correct
|
|
|
|
|
|
def train_brevitas_network_tinymnist(is_cnn, qat_bits, signed, narrow, pot_scaling):
|
|
"""Train a QAT network on tiny mnist.
|
|
|
|
Args:
|
|
is_cnn (bool): whether to train a CNN or a FC network
|
|
qat_bits (int): quantization bits
|
|
signed (bool): use signed quantization
|
|
narrow (bool): use brevitas narrow range quantization
|
|
pot_scaling (int): use power of two scaling quantization
|
|
|
|
Returns:
|
|
result (Tuple): the network, the dataset and the test data loader
|
|
"""
|
|
# And some helpers for visualization.
|
|
x_all, y_all = load_digits(return_X_y=True)
|
|
|
|
# The sklearn Digits data-set, though it contains digit images, keeps these images in vectors
|
|
# so we need to reshape them to 2D first. The images are 8x8 px in size and monochrome
|
|
if is_cnn:
|
|
x_all = numpy.expand_dims(x_all.reshape((-1, 8, 8)), 1)
|
|
|
|
x_train, x_test, y_train, y_test = train_test_split(
|
|
x_all, y_all, test_size=0.25, shuffle=True, random_state=numpy.random.randint(0, 2**15)
|
|
)
|
|
|
|
def train_one_epoch(net, optimizer, train_loader):
|
|
# Cross Entropy loss for classification when not using a softmax layer in the network
|
|
loss = nn.CrossEntropyLoss()
|
|
|
|
net.train()
|
|
avg_loss = 0
|
|
for data, target in train_loader:
|
|
optimizer.zero_grad()
|
|
output = net(data)
|
|
loss_net = loss(output, target.long())
|
|
loss_net.backward()
|
|
optimizer.step()
|
|
avg_loss += loss_net.item()
|
|
|
|
return avg_loss / len(train_loader)
|
|
|
|
# Prepare the data:
|
|
# Create a train data loader
|
|
train_dataset = TensorDataset(torch.Tensor(x_train), torch.Tensor(y_train))
|
|
train_dataloader = DataLoader(train_dataset, batch_size=64)
|
|
|
|
# Create a test data loader to supply batches for network evaluation (test)
|
|
test_dataset = TensorDataset(torch.Tensor(x_test), torch.Tensor(y_test))
|
|
test_dataloader = DataLoader(test_dataset)
|
|
|
|
trained_ok = False
|
|
|
|
while not trained_ok:
|
|
# Create the tiny CNN module with 10 output classes
|
|
if is_cnn:
|
|
net = TinyQATCNN(10, qat_bits, 4 if qat_bits <= 3 else 20, signed, narrow, pot_scaling)
|
|
else:
|
|
if pot_scaling:
|
|
act_quant = Int8ActPerTensorPoT
|
|
weight_quant = Int8WeightPerTensorPoT
|
|
bias_quant = IntBias
|
|
else:
|
|
act_quant = Int8ActPerTensorFloat
|
|
weight_quant = Int8WeightPerTensorFloat
|
|
bias_quant = None
|
|
|
|
net = QuantCustomModel(64, 10, 100, qat_bits, act_quant, weight_quant, bias_quant)
|
|
|
|
# Train a single epoch to have a fast test, accuracy should still be the same for both
|
|
# FHE simulation and torch
|
|
# But train 3 epochs for the FHE simulation test to check that training works well
|
|
n_epochs = 1 if qat_bits <= 3 else 3
|
|
|
|
# Train the network with Adam, output the test set accuracy every epoch
|
|
optimizer = torch.optim.Adam(net.parameters())
|
|
for _ in range(n_epochs):
|
|
train_one_epoch(net, optimizer, train_dataloader)
|
|
|
|
# Finally, disable pruning (sets the pruned weights to 0)
|
|
if hasattr(net, "toggle_pruning"):
|
|
net.toggle_pruning(False)
|
|
|
|
torch_correct = forward_test_torch(net, test_dataloader)
|
|
|
|
# If number of correct results was zero, training failed and there were NaNs in the weights
|
|
# Retrain while training is bad
|
|
trained_ok = torch_correct > 0
|
|
|
|
return net, x_all, test_dataloader
|
|
|
|
|
|
# This test is a known flaky
|
|
# FIXME: https://github.com/luxfi/concrete-ml-internal/issues/3933
|
|
@pytest.mark.flaky
|
|
@pytest.mark.parametrize("qat_bits", [3])
|
|
@pytest.mark.parametrize("signed, narrow", [(True, False), (True, True), (False, False)])
|
|
def test_brevitas_tinymnist_cnn(
|
|
qat_bits,
|
|
signed,
|
|
narrow,
|
|
default_configuration,
|
|
check_graph_input_has_no_tlu,
|
|
check_graph_output_has_no_tlu,
|
|
check_is_good_execution_for_cml_vs_circuit,
|
|
): # pylint: disable=too-many-statements, too-many-locals
|
|
"""Train, execute and test a QAT CNN on a small version of MNIST."""
|
|
|
|
net, x_all, test_dataloader = train_brevitas_network_tinymnist(
|
|
True, qat_bits, signed, narrow, False
|
|
)
|
|
|
|
def test_with_concrete(quantized_module, test_loader, use_fhe_simulation):
|
|
"""Test a neural network that is quantized and compiled with Concrete ML."""
|
|
|
|
all_targets = numpy.zeros((len(test_loader)), dtype=numpy.int64)
|
|
|
|
# Iterate over the test batches and accumulate predictions and ground truth
|
|
# labels in a vector
|
|
idx = 0
|
|
for data, target in test_loader:
|
|
data = data.numpy()
|
|
|
|
# Accumulate the ground truth labels
|
|
endidx = idx + target.shape[0]
|
|
all_targets[idx:endidx] = target.numpy()
|
|
|
|
# De-quantize the integer predictions
|
|
check_is_good_execution_for_cml_vs_circuit(
|
|
data, model=quantized_module, simulate=use_fhe_simulation
|
|
)
|
|
|
|
fhe_mode = "simulate" if use_fhe_simulation else "execute"
|
|
|
|
y_pred = quantized_module.forward(data, fhe=fhe_mode)
|
|
|
|
# Take the predicted class from the outputs and store it
|
|
y_pred = numpy.argmax(y_pred, axis=1)
|
|
|
|
# Compute and report results
|
|
n_correct = numpy.sum(all_targets == y_pred)
|
|
return n_correct
|
|
|
|
net.eval()
|
|
|
|
q_module_simulated = compile_brevitas_qat_model(
|
|
net,
|
|
x_all,
|
|
configuration=default_configuration,
|
|
)
|
|
|
|
fhe_s_correct = test_with_concrete(
|
|
q_module_simulated,
|
|
test_dataloader,
|
|
use_fhe_simulation=True,
|
|
)
|
|
|
|
# Accept, at most, 1% examples that are classified differently (currently 5)
|
|
# For now, the correctness test has been disabled as it was too flaky, it should however be put
|
|
# back at one point
|
|
# FIXME: https://github.com/luxfi/concrete-ml-internal/issues/2550
|
|
# assert abs(fhe_simulation_correct - torch_correct) <= numpy.ceil(0.01 * len(y_test))
|
|
|
|
assert fhe_s_correct >= 0
|
|
|
|
check_graph_input_has_no_tlu(q_module_simulated.fhe_circuit.graph)
|
|
check_graph_output_has_no_tlu(q_module_simulated.fhe_circuit.graph)
|
|
|
|
|
|
# Note that this test is currently disabled until the pytorch dtype issue is found
|
|
# and all mismatches between Concrete ML and Brevitas are fixed
|
|
# FIXME: https://github.com/luxfi/concrete-ml-internal/issues/2373
|
|
@pytest.mark.parametrize(
|
|
"n_layers",
|
|
[3],
|
|
)
|
|
@pytest.mark.parametrize("n_bits_w_a", [2, 4, 7])
|
|
@pytest.mark.parametrize("n_accum_bits", [40])
|
|
@pytest.mark.parametrize(
|
|
"activation_function",
|
|
[
|
|
pytest.param(nn.ReLU),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("n_outputs", [5])
|
|
@pytest.mark.parametrize("input_dim", [100])
|
|
@pytest.mark.parametrize("model_class", _get_sklearn_neural_net_models())
|
|
@pytest.mark.parametrize("signed, narrow", [(True, False), (False, False), (True, True)])
|
|
@pytest.mark.skip(reason="Torch dtype setting interferes with parallel test launch, and flaky test")
|
|
def test_brevitas_intermediary_values(
|
|
n_layers,
|
|
n_bits_w_a,
|
|
n_accum_bits,
|
|
activation_function,
|
|
n_outputs,
|
|
input_dim,
|
|
model_class,
|
|
load_data,
|
|
signed,
|
|
narrow,
|
|
): # pylint: disable=too-many-statements, too-many-locals
|
|
"""Test the correctness of the results of quantized NN classifiers through the sklearn
|
|
wrapper.
|
|
|
|
First, we train a Torch classifier, with various quantization options (narrow/signed/bits).
|
|
Then, we wrap the trained model in a debug module. This module will capture the quantized
|
|
integer values that are input to conv/linear layers. We also capture the quantized integer
|
|
weights. For both weights and quantized inputs, we also capture the corresponding floating
|
|
point value that produced the integer value.
|
|
|
|
Next, we convert the Torch model to a QuantizedModule. We use the debug feature to capture
|
|
the integer and floating point values that are inputs to all the conv/linear layers.
|
|
|
|
Finally, we compare the integer values from the Torch/brevitas execution with those captured
|
|
by the QuantizedModule execution and find differences. When a difference in integers is found
|
|
we print the offending raw floating point values, and, when available, quantization options.
|
|
"""
|
|
|
|
# Get the data-set. The data generation is seeded in load_data.
|
|
if is_classifier_or_partial_classifier(model_class):
|
|
x, y = load_data(
|
|
model_class,
|
|
n_samples=1000,
|
|
n_features=input_dim,
|
|
n_redundant=0,
|
|
n_repeated=0,
|
|
n_informative=input_dim,
|
|
n_classes=n_outputs,
|
|
class_sep=2,
|
|
)
|
|
|
|
# Get the data-set. The data generation is seeded in load_data.
|
|
elif is_regressor_or_partial_regressor(model_class):
|
|
x, y, _ = load_data(
|
|
model_class,
|
|
n_samples=1000,
|
|
n_features=input_dim,
|
|
n_informative=input_dim,
|
|
n_targets=n_outputs,
|
|
noise=2,
|
|
coef=True,
|
|
)
|
|
if y.ndim == 1:
|
|
y = numpy.expand_dims(y, 1)
|
|
else:
|
|
raise ValueError(f"Data generator not implemented for {str(model_class)}")
|
|
|
|
# Perform a classic test-train split (deterministic by fixing the seed)
|
|
x_train, x_test, y_train, _ = train_test_split(
|
|
x,
|
|
y,
|
|
test_size=0.25,
|
|
random_state=numpy.random.randint(0, 2**15),
|
|
)
|
|
|
|
params = {
|
|
"module__n_layers": n_layers,
|
|
"module__n_w_bits": n_bits_w_a,
|
|
"module__n_a_bits": n_bits_w_a,
|
|
"module__n_accum_bits": n_accum_bits,
|
|
"module__activation_function": activation_function,
|
|
"module__quant_signed": signed,
|
|
"module__quant_narrow": narrow,
|
|
"max_epochs": 10,
|
|
"verbose": 0,
|
|
}
|
|
|
|
concrete_model = model_class(**params)
|
|
|
|
# Compute mean/stdev on training set and normalize both train and test sets with them
|
|
normalizer = StandardScaler()
|
|
x_train = normalizer.fit_transform(x_train)
|
|
x_test = normalizer.transform(x_test)
|
|
|
|
concrete_model.fit(x_train, y_train)
|
|
|
|
# Wrap the original torch module with a debug module that captures intermediary values
|
|
class DebugQNNModel(SparseQuantNeuralNetwork):
|
|
"""Wrapper class that extracts intermediary values from a Brevitas QAT net."""
|
|
|
|
intermediary_values = []
|
|
intermediary_inp_values_float = []
|
|
quant_weights = []
|
|
raw_weights = []
|
|
narrow_range_inp = []
|
|
narrow_range_weight = []
|
|
|
|
def forward(self, x):
|
|
for mod in self.features:
|
|
if isinstance(mod, qnn.QuantLinear):
|
|
self.intermediary_inp_values_float.append(x.value.detach().numpy())
|
|
x = mod(x)
|
|
if isinstance(mod, qnn.QuantIdentity):
|
|
self.intermediary_values.append(x.int().detach().numpy())
|
|
self.narrow_range_inp.append(mod.act_quant.is_narrow_range)
|
|
elif isinstance(mod, qnn.QuantLinear):
|
|
self.narrow_range_weight.append(mod.weight_quant.is_narrow_range)
|
|
self.quant_weights.append(mod.int_weight().detach().numpy())
|
|
self.raw_weights.append(mod.quant_weight().value.detach().numpy())
|
|
return x
|
|
|
|
params_module = {
|
|
param.replace("module__", ""): value
|
|
for param, value in params.items()
|
|
if "module__" in param
|
|
}
|
|
|
|
# Concrete ML and Concrete Python use float64, so we need to force pytorch to use the same, as
|
|
# it defaults to float32. Note that this change is global and may interfere with
|
|
# threading or multiprocessing. Thus this test can not be launched in parallel with others.
|
|
torch.set_default_dtype(torch.float64)
|
|
|
|
# Wrap the original model, and copy its weights
|
|
dbg_model = DebugQNNModel(**params_module, input_dim=input_dim, n_outputs=n_outputs)
|
|
dbg_model.load_state_dict(concrete_model.base_module.state_dict())
|
|
|
|
# Execute on the test set and capture debug values
|
|
dbg_model(torch.tensor(x_test.astype(numpy.float64)))
|
|
|
|
# Execute the Concrete ML model on the test set and capture debug values
|
|
_, cml_debug_values = concrete_model.quantized_module_.forward(
|
|
x_test, debug=True, fhe="disable"
|
|
)
|
|
|
|
cml_intermediary_values = [
|
|
q_arr[0].qvalues for name, q_arr in cml_debug_values.items() if "Gemm" in name
|
|
]
|
|
cml_input_values = [
|
|
q_arr[0].values for name, q_arr in cml_debug_values.items() if "Gemm" in name
|
|
]
|
|
cml_quantizers = [
|
|
q_arr[0].quantizer for name, q_arr in cml_debug_values.items() if "Gemm" in name
|
|
]
|
|
cml_quant_weights = [
|
|
q_arr[1].qvalues for name, q_arr in cml_debug_values.items() if "Gemm" in name
|
|
]
|
|
cml_raw_weights = [
|
|
q_arr[1].values for name, q_arr in cml_debug_values.items() if "Gemm" in name
|
|
]
|
|
|
|
# Make sure the quantization options were well set by brevitas
|
|
assert len(set(dbg_model.narrow_range_inp)) > 0 and dbg_model.narrow_range_inp[0] == narrow
|
|
assert (
|
|
len(set(dbg_model.narrow_range_weight)) > 0 and dbg_model.narrow_range_weight[0] == narrow
|
|
)
|
|
|
|
# Iterate across conv/linear layers
|
|
|
|
# pylint: disable-next=consider-using-enumerate
|
|
for idx in range(len(cml_intermediary_values)):
|
|
# Check if any activations are different between Brevitas and Concrete ML
|
|
diff_inp = numpy.abs(cml_intermediary_values[idx] - dbg_model.intermediary_values[idx])
|
|
error = ""
|
|
if numpy.any(diff_inp) > 0:
|
|
# If any mismatches, then extract them and print them
|
|
indices = numpy.nonzero(diff_inp)
|
|
error = (
|
|
f"Mismatched values in layer {idx} at input indices: {numpy.transpose(indices)}\n"
|
|
f"Concrete ML Inputs were: {cml_input_values[idx][indices]} \n"
|
|
f"Concrete ML quantized to {cml_intermediary_values[idx][indices]}\n"
|
|
f"Brevitas inputs were {dbg_model.intermediary_inp_values_float[idx][indices]}\n"
|
|
f"Brevitas quantized to {dbg_model.intermediary_values[idx][indices]}\n "
|
|
f"Quant params were {str(cml_quantizers[idx].__dict__)}\n "
|
|
)
|
|
|
|
# Assert if there were any mismatches
|
|
assert numpy.all(diff_inp == 0), error
|
|
|
|
# Check if any weights are different between Brevitas and Concrete ML
|
|
diff_weights = numpy.abs(cml_quant_weights[idx] - dbg_model.quant_weights[idx])
|
|
weights_ok = True
|
|
|
|
if numpy.any(diff_weights) > 0:
|
|
indices = numpy.nonzero(diff_weights)
|
|
diff_raw_weights = numpy.abs(
|
|
dbg_model.raw_weights[idx][indices] - cml_raw_weights[idx][indices]
|
|
)
|
|
|
|
# Here, numpy.all returns Numpy's `bool_` type, which is a different type than `bool`.
|
|
# Since `weights_ok` is initialized using a `bool`, mypy complains and we therefore
|
|
# need to force the type
|
|
weights_ok = bool(numpy.all(diff_raw_weights > 0.0001))
|
|
|
|
error = (
|
|
f"Mismatched weights in layer {idx} at input indices: {numpy.transpose(indices)}\n"
|
|
f"Concrete ML raw weights were: {cml_raw_weights[idx][indices]} \n"
|
|
f"Concrete ML quantized to {cml_quant_weights[idx][indices]}\n"
|
|
f"Brevitas weights were {dbg_model.raw_weights[idx][indices]}\n"
|
|
f"Brevitas quantized to {dbg_model.quant_weights[idx][indices]}\n "
|
|
)
|
|
|
|
assert weights_ok, error
|
|
|
|
torch.set_default_dtype(torch.float32)
|
|
|
|
|
|
def test_brevitas_constant_folding(default_configuration):
|
|
"""Test that a network that does not quantize its inputs raises the right exception.
|
|
|
|
The network tested is not a valid QAT network for Concrete ML as it does not
|
|
quantize its inputs. However, in previous versions of Concrete ML a bug
|
|
in constant folding prevented the correct error being raised.
|
|
"""
|
|
|
|
batch_size = 64
|
|
config = {
|
|
"n_feats": 12,
|
|
"hidden_dim": 32,
|
|
}
|
|
data = torch.randn((batch_size, config["n_feats"]))
|
|
|
|
model = NetWithConstantsFoldedBeforeOps(config, 2)
|
|
|
|
with pytest.raises(ValueError, match=".*Error occurred during quantization aware training.*"):
|
|
compile_brevitas_qat_model(
|
|
model.to("cpu"),
|
|
torch_inputset=data,
|
|
configuration=default_configuration,
|
|
)
|
|
|
|
|
|
# This test is a known flaky
|
|
# FIXME: https://github.com/luxfi/concrete-ml-internal/issues/4356
|
|
@pytest.mark.flaky
|
|
@pytest.mark.parametrize("manual_rounding", [None, 3])
|
|
@pytest.mark.parametrize("power_of_two", [True, False])
|
|
@pytest.mark.parametrize("n_bits", [4])
|
|
@pytest.mark.parametrize("is_cnn", [True, False])
|
|
def test_brevitas_power_of_two(
|
|
default_configuration,
|
|
manual_rounding: Optional[int],
|
|
power_of_two: bool,
|
|
n_bits: int,
|
|
is_cnn: bool,
|
|
check_array_equal,
|
|
):
|
|
"""Test a custom QAT network that uses power-of-two scaling.
|
|
|
|
Test whether a network using power-of-two scaling quantization is imported
|
|
correctly and roundPBS is used. Test that the Concrete ML does not override
|
|
the user's round PBS configuration.
|
|
"""
|
|
|
|
net, x_all, _ = train_brevitas_network_tinymnist(is_cnn, n_bits, True, False, power_of_two)
|
|
|
|
utils.QUANT_ROUND_LIKE_ROUND_PBS = True
|
|
|
|
# If rounding threshold is set -> nothing happens
|
|
# If Quantizer is not setup -> nothing happens
|
|
quantized_module = compile_brevitas_qat_model(
|
|
net.to("cpu"),
|
|
torch_inputset=x_all,
|
|
configuration=default_configuration,
|
|
rounding_threshold_bits=manual_rounding,
|
|
)
|
|
|
|
pot_should_be_applied = not manual_rounding and power_of_two
|
|
# Count the number of patterns that were optimized with roundPBS
|
|
num_round_pbs_layers = 0
|
|
for _, node_op in quantized_module.quant_layers_dict.values():
|
|
if isinstance(node_op, QuantizedMixingOp):
|
|
num_round_pbs_layers += 1 if node_op.rounding_threshold_bits is not None else 0
|
|
if pot_should_be_applied:
|
|
lsbs_to_remove = (
|
|
node_op.lsbs_to_remove["matmul"]
|
|
if (node_op.lsbs_to_remove is not None) and ("matmul" in node_op.lsbs_to_remove)
|
|
else None
|
|
)
|
|
assert node_op.rounding_threshold_bits == lsbs_to_remove
|
|
elif manual_rounding:
|
|
# If manual rounding was set, LSBs_to_remove must be equal
|
|
# to the accumulator size minus the requested rounding_threshold_bits
|
|
assert node_op.rounding_threshold_bits.get("n_bits", None) == manual_rounding
|
|
assert node_op.produces_graph_output or node_op.lsbs_to_remove is not None
|
|
|
|
# The power-of-two optimization will only work
|
|
# when Relu activations are used and scaling factors are forced to be 2**s
|
|
if not pot_should_be_applied:
|
|
return
|
|
|
|
# Apply the PowerOfTwoScalingRoundPBSAdapter again. The second time
|
|
# the adapter will ignore already optimized patterns but report them
|
|
# as ignored.
|
|
adapter = PowerOfTwoScalingRoundPBSAdapter(quantized_module)
|
|
round_pbs_patterns = adapter.process()
|
|
|
|
assert (
|
|
len(round_pbs_patterns) == 0
|
|
), "Expected number of round PBS optimized patterns was not matched"
|
|
# 3 layers
|
|
assert (
|
|
adapter.num_ignored_valid_patterns == 3 - 1
|
|
), "Expected number of ignored round PBS optimizable patterns was not matched"
|
|
|
|
x_test = x_all[numpy.random.choice(len(x_all), 100), ::]
|
|
|
|
x_test_q = quantized_module.quantize_input(x_test)
|
|
|
|
y_pred_clear_round = numpy.argmax(
|
|
quantized_module.quantized_forward(x_test_q, fhe="disable"), axis=1
|
|
)
|
|
|
|
# Compute the results with simulation, which uses the actual
|
|
# lookup tables.
|
|
y_pred_sim_round = numpy.argmax(
|
|
quantized_module.quantized_forward(x_test_q, fhe="simulate"), axis=1
|
|
)
|
|
|
|
# Ensure rounding was compiled in the circuit
|
|
# the number of rounding nodes should be equal
|
|
num_rounding_mlir = quantized_module.fhe_circuit.mlir.count(".round")
|
|
|
|
assert num_rounding_mlir == 2, "Power-of-to adapter: Rounding nodes not found in MLIR"
|
|
|
|
# Remove rounding in the network to perform inference without the optimization.
|
|
# We expect a network that was optimized with the power-of-two adapter
|
|
# to be exactly correct to the non-optimized one
|
|
for _, node_op in quantized_module.quant_layers_dict.values():
|
|
if isinstance(node_op, QuantizedMixingOp):
|
|
node_op.rounding_threshold_bits = None
|
|
node_op.lsbs_to_remove = None
|
|
|
|
# Predict with the unoptimized network
|
|
y_pred_clear_no_round = numpy.argmax(
|
|
quantized_module.quantized_forward(x_test_q, fhe="disable"), axis=1
|
|
)
|
|
|
|
# # Compare the result with the optimized network and without
|
|
# # they should be equal
|
|
|
|
check_array_equal(y_pred_sim_round, y_pred_clear_round)
|
|
check_array_equal(y_pred_clear_round, y_pred_clear_no_round)
|