Files
zeekay 531fc57003 chore(brand): purge Zama refs across docs, README, configs — phase 1
Brand-only sweep. Touches READMEs, docs, package metadata, comments,
notebook markdown, requirements.txt files; does NOT yet touch Python
`concrete.*` imports (those are module-level dependencies requiring the
phase-2 Python module port from `concrete` → `torus`).

Substitutions applied:

  Concrete ML                  → TorusML
  concrete-ml                  → torus-ml
  Concrete-ML                  → Torus-ML
  concrete_ml                  → torus_ml
  zama-ai/concrete-ml          → luxfhe/ml-sdk
  zama-ai/concrete             → luxfhe/torus
  github.com/zama-ai           → github.com/luxfhe
  docs.zama.ai/concrete-ml     → docs.luxfhe.com/ml-sdk
  docs.zama.ai/concrete        → docs.luxfhe.com/torus
  Zama Concrete ML             → Lux TorusML
  Zama Concrete                → Lux Torus
  Zama tfhe-rs / Zama TFHE-rs  → Lux-FHE
  tfhe-rs / TFHE-rs            → Lux-FHE
  Zama fhEVM / fhEVM           → TorusEVM
  Concrete framework           → Torus framework
  Concrete compiler            → Torus compiler
  Concrete by Zama             → Torus by Lux
  the Zama team                → the Lux Industries team
  Zama's                       → Lux Industries'
  (Zama)                       → (Lux)
  CoFHE / Fhenix               → (removed)

LICENSE: original Zama copyright RETAINED per BSD-3-Clause-Clear §1-§2
requirements; Lux Industries modifications copyright added. Per §3 no
name of ZAMA used to endorse the derived work — product marketed
exclusively as TorusML.

README image URLs migrated to github.com/luxfhe/ml-sdk/assets to break
the dependency on Zama's CDN.

Hit count: 350 → 295. Residual hits are Python `from concrete.fhe import`
imports referencing the upstream Zama `concrete` PyPI package; these
require module-level port (phase 2):

  Phase 2 plan:
    1. Fork zama-ai/concrete (the Python compiler) to luxfhe/torus
    2. Rename Python module `concrete` → `torus`; PyPI publish as `torus-fhe`
    3. Update ml-sdk imports from `concrete.fhe` → `torus.fhe`, etc.
    4. Wire `torus.compiler` GPU dispatch through `luxcpp/gpu` vtbl
       (lux-private/gpu-kernels for CUDA/Metal/WebGPU/FPGA targets)
    5. Port concrete-cuda + concrete-mlir kernels to
       lux-private/gpu-kernels/{cuda,metal,wgsl}/ckks/ (matrix-vector
       mul, rescale, rotate, bootstrap for TorusML inference)

  Phase 3 plan (kernels):
    - CKKS/BGV/BFV GPU kernels in lux-private/gpu-kernels/
      (currently only TFHE PBS / keyswitch / blind-rotate exist)
    - FPGA bitstreams in lux-private/fpga/ (currently scaffold only)

Phase 1 = docs/branding sweep. PR not opened; user reviews and merges.
2026-05-29 13:53:42 -07:00

934 lines
31 KiB
Python

import argparse
import json
import random
import time
from pathlib import Path
from typing import Any, Optional, Tuple, Union
import numpy as np
import py_progress_tracker as progress
import torch
import torch.utils
from common import (
benchmark_generator,
benchmark_name_generator,
run_and_report_classification_metrics,
seed_everything,
)
from concrete.fhe.compilation.circuit import Circuit
from sklearn.datasets import load_digits
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from torch import nn
from torch.nn.utils import prune
from torch.utils.data import DataLoader, TensorDataset
from tqdm import tqdm
from concrete.ml.common.utils import get_model_name
from concrete.ml.quantization.quantized_module import QuantizedModule
from concrete.ml.torch.compile import compile_torch_model
# To define the length of train sample for compilation
N_MAX_COMPILE_FHE = 200
class _CustomCNN(nn.Module):
"""A CNN to classify images.
This class also allows pruning, which should help with keeping the accumulator bit-width low.
This is done by defining a maximum number of active neurons (i.e., weight != 0) allowed
as inputs to other neurons.
"""
def __init__(
self,
n_classes,
hidden_size,
activation_function,
n_active_neurons,
n_deep_conv=None,
):
"""Construct the CNN with a configurable number of classes."""
super().__init__()
self.conv_layers = nn.Sequential()
self.conv_layers.add_module(
"conv1", nn.Conv2d(in_channels=1, out_channels=2, kernel_size=3, stride=1, padding=0)
)
self.conv_layers.add_module(
"conv2",
nn.Conv2d(
in_channels=2,
out_channels=hidden_size,
kernel_size=3,
stride=2,
padding=0,
),
)
if n_deep_conv is not None:
for i in range(n_deep_conv):
self.conv_layers.add_module(
f"deep_conv{i}",
nn.Conv2d(
in_channels=hidden_size,
out_channels=hidden_size,
kernel_size=1,
stride=1,
padding=0,
),
)
self.conv_layers.add_module(
"conv3",
nn.Conv2d(in_channels=hidden_size, out_channels=16, kernel_size=2, stride=1, padding=0),
)
self.fc1 = nn.Linear(16, n_classes)
self.act = activation_function
self.n_active_neurons = n_active_neurons
# Enable pruning for training
self.toggle_pruning(True)
def toggle_pruning(self, enable):
"""Enable or remove pruning."""
# Iterate over all the convolution layers
for layer in self.conv_layers:
layer_shape = layer.weight.shape
# Compute the fan-in, the number of inputs to a neuron, and the fan-out, the number of
# neurons in the current layer.
# The fan-in is the product of the kernel width x height x in_channels while the fan-out
# is out_channels
fan_in = np.prod(layer_shape[1:])
fan_out = layer_shape[0]
# If there are more inputs than the maximum amount allowed, prune the layer
if fan_in > self.n_active_neurons:
# If pruning is enabled, which is generally the case during training, a
# forward hook is added in order to create a mask tensor (made of 0 or 1) that will
# be multiplied with the weights during the forward pass.
if enable:
prune.l1_unstructured(
layer, "weight", (fan_in - self.n_active_neurons) * fan_out
)
# Else, the mask is multiplied with the weights and the result is stored in the
# weight member.
# This is mostly done before saving or loading pruned networks into pre-trained
# files as these features is not properly handled in Torch
# https://discuss.pytorch.org/t/proper-way-to-load-a-pruned-network/77694
else:
prune.remove(layer, "weight")
def forward(self, x):
"""Run the inference.
The decision layer (the linear layer) is applied on a reshaped output of the last
convolutional layer.
"""
# Run the convolutional layers
for layer in self.conv_layers:
x = layer(x)
x = self.act(x)
# Squeeze size (batch, n, 1, 1) to size (batch, n) and run the final linear layer
x = x.flatten(start_dim=1)
x = self.fc1(x)
return x
class ShallowNarrowCNN(_CustomCNN):
"""A shallow and narrow CNN to classify images."""
def __init__(self, n_classes):
super().__init__(
n_classes=n_classes,
hidden_size=3,
activation_function=torch.relu,
n_active_neurons=10,
)
class ShallowWideCNN(_CustomCNN):
"""A shallow and wide CNN to classify images.
This CNN considers 92 filters in the middle hidden layer in order to maximize CPU usage
with parallelized computations.
"""
def __init__(self, n_classes):
super().__init__(
n_classes=n_classes,
hidden_size=92,
activation_function=torch.relu,
n_active_neurons=10,
)
class Deep20CNN(_CustomCNN):
"""A deep CNN using 20 hidden layers to classify images."""
def __init__(self, n_classes):
super().__init__(
n_classes=n_classes,
hidden_size=3,
activation_function=torch.relu,
n_active_neurons=10,
n_deep_conv=20,
)
class Deep50CNN(_CustomCNN):
"""A deep CNN using 50 hidden layers to classify images."""
def __init__(self, n_classes):
super().__init__(
n_classes=n_classes,
hidden_size=3,
activation_function=torch.relu,
n_active_neurons=10,
n_deep_conv=50,
)
class Deep100CNN(_CustomCNN):
"""A deep CNN using 100 hidden layers to classify images."""
def __init__(self, n_classes):
super().__init__(
n_classes=n_classes,
hidden_size=3,
activation_function=torch.relu,
n_active_neurons=10,
n_deep_conv=100,
)
CNN_CLASSES = [ShallowNarrowCNN, Deep20CNN, Deep50CNN, Deep100CNN, ShallowWideCNN]
CNN_STRING_TO_CLASS = {cnn_model.__name__: cnn_model for cnn_model in CNN_CLASSES}
CNN_DATASETS = ["MNIST"]
def load_data(dataset: str = "mnist") -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""Load the data.
Args:
dataset (str): the data-set to use.
Returns:
x_train, x_test, y_train, y_test (Tuple): The input and target values to
use for training and evaluating.
Raise:
ValueError: If dataset is not currently handled.
"""
if dataset == "MNIST":
# Load the MNIST input and target values
X, y = load_digits(return_X_y=True)
# Standardize the data
scaler = StandardScaler()
X = scaler.fit_transform(X)
# The scikit-learn MNIST data-set keeps the images flattened, we therefore need to reshape
# them to 2D array of 8x8 (grayscale) in order to be able to apply convolutions in the
# first layer.
X = np.expand_dims(X.reshape((-1, 8, 8)), 1)
# Split the data into train and test subsets
x_train, x_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, shuffle=True, random_state=42
)
else:
raise ValueError(f"Wrong data-set. Expected one of {CNN_DATASETS}, got {dataset}.")
return x_train, x_test, y_train, y_test
def get_data_loader(
x: np.ndarray, y: np.ndarray, batch_size: int = 1, samples: Optional[int] = None
):
"""Build a data loader.
Args:
x (np.ndarray): The input values.
y (np.ndarray): The target values.
batch_size (int): The batch size to consider. Default to 1.
samples (Optional[int]). The number of samples to consider. Default to None, indicating that
the whole data-set should be taken.
Returns:
data_loader (DataLoader): The batched data loader.
"""
# Create a TensorDataset object from the data
if samples is None:
dataset = TensorDataset(torch.Tensor(x), torch.Tensor(y))
else:
dataset = TensorDataset(torch.Tensor(x[0:samples, ::]), torch.Tensor(y[0:samples]))
# Create a DataLoader object with data batches
data_loader = DataLoader(dataset, batch_size=batch_size)
return data_loader
def get_pt_file(model_name: str, dataset: str) -> Path:
"""Retrieve the model's pre-trained file associated to the given data-set.
Args:
model_name (str): The model's name.
dataset (str): the data-set's name.
Returns:
Path: The model's pre-trained file.
"""
return Path(__file__).resolve().parent / f"pre_trained_models/{dataset}_{model_name}.pt"
def load_pre_trained_cnn_model(cnn_model: Any, dataset: str) -> Any:
"""Load the model's pre-trained weights.
Args:
cnn_model (Any): The instantiated model.
dataset (str): the data-set's name.
Returns:
Any: The pre-trained model.
"""
model_name = get_model_name(cnn_model)
# Retrieve the pre-trained file path
pt_file = get_pt_file(model_name, dataset)
if not pt_file.is_file():
raise ValueError(
f"Pre-trained file for model {model_name} cannot be found. "
f"Expected path {pt_file.resolve()}"
)
print(f"Loading {pt_file.name}\n")
# Disable pruning as Torch doesn't allow to easily load and save pruned networks
# https://discuss.pytorch.org/t/proper-way-to-load-a-pruned-network/77694
cnn_model.toggle_pruning(False)
# Load the weights and update the model
state_dict = torch.load(pt_file)
cnn_model.load_state_dict(state_dict)
return cnn_model
def train_one_epoch(
cnn_model: Any,
train_loader: DataLoader,
optimizer: torch.optim.Optimizer,
loss: nn.Module,
):
"""Train the model for a single epoch.
Args:
cnn_model (Any): The instantiated model to train.
train_loader (DataLoader): The batched train data to consider.
optimizer (torch.optim.Optimizer): The optimizer to use for computing the gradients.
loss (nn.Module): The loss function to use.
"""
# Set the network to training mode (enable gradient computation on normalization layers)
cnn_model.train()
accumulated_loss = 0
for data, target in train_loader:
# Clear the gradients
optimizer.zero_grad()
# Run the forward pass
output = cnn_model(data)
# Compute the loss
loss_value = loss(output, target.long())
# Compute the gradients with backpropagation
loss_value.backward()
# Update the weights using the gradients
optimizer.step()
# Store the computed loss
accumulated_loss += loss_value.item()
print(f"Loss: {accumulated_loss / len(train_loader):.2f}")
def train_cnn_model(cnn_class: Any, dataset: str, epochs: int, batch_size: int):
"""Train the model on the data-set for several epochs and save the weights in a file.
Args:
cnn_model (Any): The instantiated model to train.
dataset (str): the data-set to train on.
epochs (int): The number of epochs to consider.
batch_size (int): The training batches' size.
Return:
Any: The trained model.
"""
# Retrieve the model's name
model_name = get_model_name(cnn_class)
print(f"Training {model_name} on {dataset} for {epochs} epochs\n")
# Load the data and split it in train and test subsets
x_train, x_test, y_train, y_test = load_data(dataset)
# Extract the number of classes in the data set
n_classes = len(np.unique(y_train))
# Create the model with n_classes output classes
cnn_model = cnn_class(n_classes)
# Create the train and test data loaders
train_loader = get_data_loader(x_train, y_train, batch_size=batch_size)
test_loader = get_data_loader(x_test, y_test, batch_size=batch_size)
# Use Adam for training
optimizer = torch.optim.Adam(cnn_model.parameters())
# Use the cross Entropy loss for classification when not using a softmax layer in the CNN
loss = nn.CrossEntropyLoss()
# Train the model for several epochs and evaluate its test accuracy at each step
for epoch in range(epochs):
print(f"Training epoch {epoch+1}:")
train_one_epoch(cnn_model, train_loader, optimizer, loss)
evaluate_module(
framework="torch",
module=cnn_model,
test_loader=test_loader,
n_classes=n_classes,
metric_id_prefix=None,
metric_label_prefix=None,
train=True,
)
# Disable pruning as Torch doesn't allow to easily load and save pruned networks
# https://discuss.pytorch.org/t/proper-way-to-load-a-pruned-network/77694
cnn_model.toggle_pruning(False)
# Retrieve the pre-trained file path
pt_file = get_pt_file(model_name, dataset)
# Save the pre-trained model's weights in a pt file
torch.save(cnn_model.state_dict(), pt_file)
return cnn_model
def report_compiler_feedback(fhe_circuit: Circuit):
"""Report all compiler feedback values.
Args:
fhe_circuit (Circuit): The FHE circuit to consider.
"""
compiler_feedback_names = [
"complexity",
"size_of_secret_keys",
"size_of_bootstrap_keys",
"size_of_keyswitch_keys",
"size_of_inputs",
"size_of_outputs",
"p_error",
"global_p_error",
]
for compiler_feedback_name in compiler_feedback_names:
compiler_feedback_value = getattr(fhe_circuit, compiler_feedback_name)
progress.measure(
id=f"compiler-{compiler_feedback_name}",
label=f"Compiler {compiler_feedback_name}",
value=compiler_feedback_value,
)
def concrete_inference(quantized_module: QuantizedModule, x: np.ndarray, in_fhe: bool):
"""Execute the model's inference using TorusML (quantized clear or FHE).
Args:
quantized_module (QuantizedModule): The quantized module representing the model.
x (np.ndarray): The input data.
in_fhe (bool): Indicate if the inference should be executed in FHE.
Returns:
y_preds (np.ndarray): The model's predictions.
q_y_pred_proba (np.ndarray): The model's predicted quantized probabilities.
"""
# Quantize the inputs
q_x = quantized_module.quantize_input(x)
fhe_mode = "execute" if in_fhe else "disable"
# Execute the forward pass, in FHE or in the clear
q_y_pred_proba = quantized_module.quantized_forward(q_x, fhe=fhe_mode)
# De-quantize the output probabilities
y_pred_proba = quantized_module.dequantize_output(q_y_pred_proba)
# Apply the argmax in the clear
y_pred = np.argmax(y_pred_proba, 1)
return y_pred, q_y_pred_proba
def torch_inference(cnn_model: nn.Module, x: torch.Tensor):
"""Execute the model's inference using Torch (in float, in the clear).
Args:
cnn_model (nn.Module): The torch model.
x (torch.Tensor): The input data.
Returns:
y_preds (np.ndarray): The model's predictions.
y_pred_proba (np.ndarray): The model's predicted probabilities.
"""
# Freeze normalization layers
cnn_model.eval()
# Execute the inference in the clear
y_pred_proba = cnn_model(x)
# Apply the argmax in the clear
y_pred = y_pred_proba.argmax(1)
return y_pred.detach().numpy(), y_pred_proba.detach().numpy()
def evaluate_module(
framework: str,
module: Union[nn.Module, QuantizedModule],
test_loader: DataLoader,
n_classes: int,
metric_id_prefix: Optional[str] = None,
metric_label_prefix: Optional[str] = None,
train: bool = False,
in_fhe: bool = False,
):
"""Evaluate several metrics using a Torch or TorusML module.
Args:
framework (str): The framework to evaluate, either 'concrete' or 'torch'.
module (Union[nn.Module, QuantizedModule]): The Torch or TorusML module representing
the model to evaluate.
test_loader (DataLoader): The test data loader.
n_classes (int): The number of classes to target.
metric_id_prefix (Optional[str]): The id's prefix to consider when tracking the metrics.
Default to None.
metric_label_prefix (Optional[str]): The label's prefix to consider when tracking the
metrics. Default to None.
train (bool): Indicate if the evaluation is done during training. If so, the test accuracy
is printed but not tracked. This parameter cannot be set while using the TorusML
framework. Default to False.
in_fhe (bool): Indicate if the inference should be executed in FHE. This parameter cannot
be set while using the torch framework. Default to False.
Returns:
y_preds_proba (np.ndarray): The model's predicted probabilities (quantized form for the
'concrete' framework).
"""
assert framework in [
"concrete",
"torch",
], f"Wrong framework. Expected one of 'torch' or 'concrete', got {framework}."
if framework == "torch":
assert (
not in_fhe
), "Torch models can't be executed in FHE. Either use 'concrete' or set 'in_fhe' to False."
else:
assert not train, "Training can only be done using Torch models."
# If the module is a QuantizedModule whose inference will be executed in FHE, generated the
# keys and track their generation time
if isinstance(module, QuantizedModule) and in_fhe:
keygen_start = time.time()
module.fhe_circuit.keygen()
keygen_time = time.time() - keygen_start
progress.measure(id="fhe-keygen-time", label="FHE Key Generation Time", value=keygen_time)
# Retrieve the batches' size as well as the total amount of values in the test data loader
batch_size = test_loader.batch_size if test_loader.batch_size is not None else 1
total_size = len(test_loader) * batch_size
# Initialize the arrays for storing the predicted values, probabilities and ground truth
# target labels
y_preds = np.zeros((total_size), dtype=np.float64)
y_preds_proba = np.zeros((total_size, n_classes), dtype=np.float64)
targets = np.zeros((total_size), dtype=np.float64)
inference_start = time.time()
with tqdm(total=total_size) as progress_bar:
# Iterate over the test batches and store the predicted values as well as the ground
# truth labels
for batch_i, (data, target) in enumerate(test_loader):
# Execute TorusML's inference
if framework == "concrete":
y_pred, y_pred_proba = concrete_inference(module, data.numpy(), in_fhe)
# Else, execute torch's inference
else:
y_pred, y_pred_proba = torch_inference(module, data)
# Store the predicted values and the ground truth target labels at the right indexes
start_index = batch_i * batch_size
batch_slice = slice(start_index, start_index + min(batch_size, target.shape[0]))
y_preds[batch_slice] = y_pred
y_preds_proba[batch_slice] = y_pred_proba
targets[batch_slice] = target.numpy()
progress_bar.update(batch_size)
inference_time = time.time() - inference_start
# If the evaluation is done during training, print the test accuracy as a percentage
if train:
total_correct = np.sum(targets == y_preds)
print(f"Test accuracy: {total_correct / total_size * 100:.2f}%\n")
# Else, track the different metrics
else:
assert (
metric_id_prefix is not None and metric_label_prefix is not None
), "Please prove metric prefixes when executing the inference."
# If we evaluate a TorusML module in FHE, the inference execution time is also tracked
if framework == "concrete" and in_fhe:
progress.measure(
id=metric_id_prefix + "-execution-time-per-sample",
label="Execution Time per sample for " + metric_label_prefix,
value=inference_time / total_size,
)
# Compute and report the metrics
run_and_report_classification_metrics(
targets, y_preds, metric_id_prefix, metric_label_prefix, use_f1=False
)
return y_preds_proba
def evaluate_pre_trained_cnn_model(dataset: str, cnn_class: type, config: dict, cli_args):
"""Evaluate the pre-trained CNN model on the data-set.
It first evaluates both the Torch and TorusML models in the clear bu computing their
accuracy score on the full data-set. Then, the TorusML model's inference is executed on a
sub-sample in the clear as well as in FHE in order to compute a MSE score between them.
Args:
dataset (str): the data-set to consider.
cnn_class (type): The model's class to train.
config (dict): The configuration parameters to consider for the model, such as the number
of bits of quantization to consider during compilation.
cli_args (): The parsed arguments from the command line to consider.
"""
# Load the data and split it in train and test subsets
x_train, x_test, y_train, y_test = load_data(dataset)
# Extract the number of classes in the data set
n_classes = len(np.unique(y_train))
# Create the model with n_classes output classes
cnn_model = cnn_class(n_classes)
# Load the pre-trained weights into the model
cnn_model = load_pre_trained_cnn_model(cnn_model, dataset)
p_error = config["p_error"]
n_bits = config["n_bits"]
if cli_args.verbose:
print(
f"Converting and compiling to a quantized module, using n_bits: {n_bits}, "
f"p_error: {p_error}"
)
progress.measure(id="p-error", label="Using p-error", value=p_error)
# Compile the model for FHE computations, using N_MAX_COMPILE_FHE samples at most in order
# to avoid a long compilation time
compilation_start = time.time()
fhe_module = compile_torch_model(
cnn_model,
x_train[0:N_MAX_COMPILE_FHE, ::],
n_bits=n_bits,
p_error=p_error,
verbose=cli_args.verbose,
)
compilation_time = time.time() - compilation_start
progress.measure(id="fhe-compile-time", label="FHE Compile Time", value=compilation_time)
assert fhe_module.fhe_circuit is not None, "Please compile the FHE module."
if cli_args.mlir_only:
print("MLIR:", fhe_module.fhe_circuit.mlir)
return
report_compiler_feedback(fhe_module.fhe_circuit)
# Create a test data loader to supply batches for evaluating the model's different metrics
test_loader = get_data_loader(x_test, y_test, batch_size=50)
if cli_args.verbose:
print("Evaluating the Torch model's inference in the clear:")
# Evaluate the float model using Torch i order to better compare results
evaluate_module(
framework="torch",
module=cnn_model,
test_loader=test_loader,
n_classes=n_classes,
metric_id_prefix="torch-fp32",
metric_label_prefix="Torch fp32",
)
circuit_bitwidth = fhe_module.fhe_circuit.graph.maximum_integer_bit_width()
progress.measure(
id="maximum-circuit-bit-width",
label="Circuit bit width",
value=circuit_bitwidth,
)
if cli_args.verbose:
print("\nMax numbers of bits reached during the inference:", circuit_bitwidth)
print("\nEvaluating the TorusML model's quantized clear inference an all test samples:")
# Evaluate the quantized clear inference using the full data-set
evaluate_module(
framework="concrete",
module=fhe_module,
test_loader=test_loader,
n_classes=n_classes,
metric_id_prefix="quantized-clear",
metric_label_prefix="Quantized Clear",
)
# Build a subset for FHE tests
fhe_test_loader = get_data_loader(x_test, y_test, samples=cli_args.fhe_samples)
if cli_args.verbose:
print("\nEvaluating the TorusML model's quantized clear inference on FHE samples:")
# Evaluate the quantized clear inference using a specific number of FHE samples
q_y_preds_proba_clear = evaluate_module(
framework="concrete",
module=fhe_module,
test_loader=fhe_test_loader,
n_classes=n_classes,
metric_id_prefix="quant-clear-fhe-set",
metric_label_prefix="Quantized Clear (FHE set)",
)
if not cli_args.dont_execute_in_fhe:
if cli_args.verbose:
print("\nEvaluating the TorusML model's inference in FHE on FHE samples:")
# Evaluate the FHE inference using a specific number of FHE samples
q_y_preds_proba_fhe = evaluate_module(
framework="concrete",
module=fhe_module,
test_loader=fhe_test_loader,
n_classes=n_classes,
metric_id_prefix="fhe",
metric_label_prefix="FHE",
in_fhe=True,
)
# Compute the MAE between predicted quantized probabilities from the quantized clear model
# and the FHE one
mae_score = mean_absolute_error(q_y_preds_proba_clear, q_y_preds_proba_fhe)
# Track this MAE metric as well
progress.measure(
id="clear-vs-fhe-mae",
label="MAE score between clear and FHE quantized probabilities",
value=mae_score,
)
else:
print("\nExecution of the inference in FHE skipped.")
def argument_manager():
"""Parse the command line arguments."""
parser = argparse.ArgumentParser()
parser.add_argument(
"--mlir_only", type=int, help="Dump the MLIR graph and stop (no inference executed)."
)
parser.add_argument(
"--verbose", action="store_true", help="Print more information, such as execution timings."
)
parser.add_argument(
"--seed",
type=int,
default=random.randint(0, 2**32 - 1),
help="Set the seed for reproducibility. A new seed is randomly generated by default.",
)
parser.add_argument(
"--datasets",
choices=CNN_DATASETS,
type=str,
nargs="+",
default=None,
help="Dataset(s) to use. All data-sets are chosen by default.",
)
parser.add_argument(
"--models",
type=str,
nargs="+",
default=None,
choices=CNN_STRING_TO_CLASS.keys(),
help="Chose a CNN to benchmark. All models are chosen by default.",
)
parser.add_argument(
"--configs",
nargs="+",
type=json.loads,
default=None,
help="Config(s) to use, such as the n_bits parameter.",
)
parser.add_argument(
"--train",
action="store_true",
help="Indicate if the model(s) need(s) to be trained again. Else, it uses stored .pt files",
)
parser.add_argument(
"--epochs",
type=int,
default=10,
help="The number of epochs to use during training.",
)
parser.add_argument(
"--batch_size",
type=int,
default=16,
help="The batch_size to use during training.",
)
parser.add_argument(
"--fhe_samples",
type=int,
default=1,
help="The number of FHE samples on which to predict.",
)
parser.add_argument(
"--model_samples",
type=int,
default=1,
help="Number of times each tests should be executed (overwrites PROGRESS_SAMPLES).",
)
parser.add_argument(
"--long_list",
action="store_true",
help="List all tasks and stop.",
)
parser.add_argument(
"--short_list",
action="store_true",
help="List a task per each models and stop.",
)
parser.add_argument(
"--dont_execute_in_fhe",
action="store_true",
help="Don't execute the FHE inference (default is to use should_test_config_in_fhe)",
)
args = parser.parse_args()
if args.datasets is None:
args.datasets = CNN_DATASETS
if args.models is None:
args.models = CNN_CLASSES
else:
args.models = [CNN_STRING_TO_CLASS[cnn_name] for cnn_name in args.models]
return args
def main():
"""Main function to execute."""
# Retrieve the parameters from the command line
args = argument_manager()
# Seed everything using a see
seed_everything(args.seed)
print(f"Using --seed {args.seed}")
# Generate all the tasks to execute in this benchmark file
all_tasks = list(benchmark_generator(args))
if args.long_list or args.short_list:
# Print the short or long lists if asked and stop
printed_models = set()
for dataset, cnn_class, config in all_tasks:
configs = json.dumps(config).replace("'", '"')
cnn_name = cnn_class.__name__
# Only print one config per model if printing the short_list
if not (args.short_list and cnn_name in printed_models):
print(
f"--models {cnn_name} --dataset {dataset} --configs '{configs}'"
+ f" --train --epochs {args.epochs}" * args.train
+ f" --fhe_samples {args.fhe_samples}" * (not args.train)
)
printed_models.add(cnn_name)
return
if args.train:
# Train each models on each data-sets and stop
for dataset in args.datasets:
for cnn_class in args.models:
train_cnn_model(cnn_class, dataset, args.epochs, args.batch_size)
return
# Pylint does not seem to properly understand the following loop and needs to be disabled
# pylint: disable=undefined-loop-variable
@progress.track(
[
{
"id": benchmark_name_generator(dataset, cnn_class, config, "_"),
"name": benchmark_name_generator(dataset, cnn_class, config, " on "),
"samples": args.model_samples,
"parameters": {"cnn_class": cnn_class, "dataset": dataset, "config": config},
}
for (dataset, cnn_class, config) in all_tasks
]
)
def perform_deep_learning_benchmark(dataset, cnn_class, config):
"""
This is the test function called by the py-progress module. It just calls the
benchmark function with the right parameter combination
"""
evaluate_pre_trained_cnn_model(dataset, cnn_class, config, args)
if __name__ == "__main__":
main()