mirror of
https://github.com/luxfhe/ml-sdk.git
synced 2026-10-30 18:13:04 +00:00
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.
345 lines
12 KiB
Python
345 lines
12 KiB
Python
import argparse
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import concrete.compiler
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
import torch
|
|
from concrete.fhe import Configuration
|
|
from resnet import ResNet18_Weights, resnet18_custom
|
|
from utils_resnet import ImageNetProcessor
|
|
|
|
from concrete.ml.torch.compile import build_quantized_module, compile_torch_model
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
CALIBRATION_SAMPLES = 10
|
|
NUM_TEST_SAMPLES = 100
|
|
|
|
|
|
def load_model():
|
|
# Load the ResNet18 model with pre-trained weights
|
|
return resnet18_custom(weights=ResNet18_Weights.IMAGENET1K_V1)
|
|
|
|
|
|
def load_data():
|
|
processor = ImageNetProcessor()
|
|
calib_images = processor.get_calibration_data(CALIBRATION_SAMPLES)
|
|
return processor, calib_images
|
|
|
|
|
|
def evaluate_model(model, processor):
|
|
with torch.no_grad():
|
|
device = "cpu" # torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
model.to(device)
|
|
|
|
outputs = []
|
|
all_labels = []
|
|
for batch in processor.dataloader:
|
|
batch_images = batch["pixel_values"].to(device)
|
|
batch_outputs = model(batch_images)
|
|
outputs.append(batch_outputs)
|
|
all_labels.append(batch["labels"])
|
|
|
|
outputs = torch.cat(outputs)
|
|
outputs = outputs.cpu()
|
|
all_labels = torch.cat(all_labels)
|
|
all_labels = all_labels.cpu()
|
|
|
|
accuracy = processor.accuracy(outputs, all_labels)
|
|
topk_accuracy = processor.accuracy_top5(outputs, all_labels)
|
|
print(f"Accuracy of the ResNet18 model on the images: {accuracy:.2f}%")
|
|
print(f"Top-5 Accuracy of the ResNet18 model on the images: {topk_accuracy:.2f}%")
|
|
|
|
|
|
def compile_model(
|
|
model,
|
|
images,
|
|
n_bits,
|
|
rounding_threshold_bits=None,
|
|
fhe_mode="disable",
|
|
compilation_device="cpu",
|
|
):
|
|
"""
|
|
Compile the model using either build_quantized_module or compile_torch_model.
|
|
|
|
Args:
|
|
model: The PyTorch model to compile.
|
|
images: The calibration images.
|
|
n_bits: The number of bits for quantization (int). Can be a dictionary:
|
|
{
|
|
"model_inputs": 8,
|
|
"op_inputs": 6,
|
|
"op_weights": 6,
|
|
"model_outputs": 8
|
|
}
|
|
rounding_threshold_bits: The rounding threshold bits.
|
|
fhe_mode: The FHE mode ('disable' or 'simulate').
|
|
compilation_device: Whether to use GPU or CPU for compilation.
|
|
|
|
Returns:
|
|
The compiled quantized module.
|
|
"""
|
|
compile_config = {
|
|
"n_bits": n_bits,
|
|
"rounding_threshold_bits": (
|
|
{"n_bits": rounding_threshold_bits, "method": "APPROXIMATE"}
|
|
if rounding_threshold_bits is not None
|
|
else None
|
|
),
|
|
}
|
|
|
|
if fhe_mode != "disable":
|
|
config = Configuration(enable_tlu_fusing=True, print_tlu_fusing=False)
|
|
compile_config.update(
|
|
{
|
|
"p_error": 0.05,
|
|
"configuration": config,
|
|
}
|
|
)
|
|
compile_func = compile_torch_model
|
|
else:
|
|
compile_func = build_quantized_module
|
|
|
|
print(f"Compiling the model with {compile_func.__name__}...")
|
|
return compile_func(model, torch_inputset=images, **compile_config, device=compilation_device)
|
|
|
|
|
|
def export_statistics(q_module):
|
|
open("resnet.graph", "w").write(q_module.fhe_circuit.graph.format(show_locations=True))
|
|
open("resnet.mlir", "w").write(q_module.fhe_circuit.mlir)
|
|
|
|
def make_serializable(obj):
|
|
if isinstance(obj, dict):
|
|
return {str(key): make_serializable(value) for key, value in obj.items()}
|
|
elif isinstance(obj, list):
|
|
return [make_serializable(element) for element in obj]
|
|
elif isinstance(obj, tuple):
|
|
return tuple(make_serializable(element) for element in obj)
|
|
elif isinstance(obj, (str, int, float, bool, type(None))):
|
|
return obj
|
|
else:
|
|
return str(obj)
|
|
|
|
statistics = make_serializable(q_module.fhe_circuit.statistics)
|
|
|
|
with open("resnet_statistics.json", "w") as f:
|
|
json.dump(statistics, f)
|
|
|
|
|
|
def evaluate_model_cml(q_module, processor, fhe):
|
|
assert fhe in ["disable", "simulate"], "fhe must be either 'disable' or 'simulate'"
|
|
|
|
with torch.no_grad():
|
|
outputs = []
|
|
all_labels = []
|
|
for batch in processor.dataloader:
|
|
batch_images = batch["pixel_values"].detach().numpy()
|
|
batch_outputs = q_module.forward(batch_images, fhe=fhe)
|
|
outputs.append(batch_outputs)
|
|
all_labels.append(batch["labels"].detach().numpy())
|
|
|
|
outputs = torch.from_numpy(np.concatenate(outputs))
|
|
all_labels = torch.from_numpy(np.concatenate(all_labels))
|
|
|
|
accuracy = processor.accuracy(outputs, all_labels)
|
|
topk_accuracy = processor.accuracy_top5(outputs, all_labels)
|
|
if fhe == "simulate":
|
|
print("FHE simulation Accuracy of the FHEResNet18 on the images: " f"{accuracy:.2f}%")
|
|
print(
|
|
"FHE simulation Top-5 Accuracy of the FHEResNet18 on the images: "
|
|
f"{topk_accuracy:.2f}%"
|
|
)
|
|
else:
|
|
print("Quantized Model Accuracy of the FHEResNet18 on the images: " f"{accuracy:.2f}%")
|
|
print(
|
|
"Quantized Model Top-5 Accuracy of the FHEResNet18 on the images: "
|
|
f"{topk_accuracy:.2f}%"
|
|
)
|
|
|
|
|
|
def run_fhe_execution(q_module, images, num_images=1):
|
|
images = images[:num_images].detach().numpy()
|
|
n_features = images.shape[1:] # Get the shape of features (channels, height, width)
|
|
|
|
def get_top5_labels(output):
|
|
return np.argsort(output.flatten())[-5:][::-1]
|
|
|
|
print(f"Processing {num_images} image(s)...")
|
|
total_fhe_time = 0
|
|
|
|
for i in range(num_images):
|
|
print(f"\nImage {i+1}:")
|
|
img = images[i].reshape(1, *n_features) # Reshape to (1, *n_features)
|
|
|
|
print(" Running FHE execution...")
|
|
start = time.time()
|
|
output_fhe = q_module.forward(img, fhe="execute")
|
|
fhe_end = time.time()
|
|
fhe_time = fhe_end - start
|
|
total_fhe_time += fhe_time
|
|
print(f" FHE execution completed in {fhe_time:.4f} seconds")
|
|
|
|
fhe_top5 = get_top5_labels(output_fhe)
|
|
print(" FHE top 5 labels:", ", ".join(map(str, fhe_top5)))
|
|
|
|
print(" Running simulation...")
|
|
output_simulate = q_module.forward(img, fhe="simulate")
|
|
|
|
sim_top5 = get_top5_labels(output_simulate)
|
|
print(" Simulation top 5 labels:", ", ".join(map(str, sim_top5)))
|
|
|
|
print(f"\nTotal FHE execution time for {num_images} image(s): {total_fhe_time:.4f} seconds")
|
|
|
|
|
|
def run_experiment(resnet18, calib_images, processor, fhe_mode="disable"):
|
|
|
|
# Define ranges for n_bits and rounding_threshold_bits
|
|
n_bits_range = range(2, 16)
|
|
rounding_threshold_bits_range = list(range(2, 9)) + [None] # 2 to 8 and None
|
|
|
|
# Initialize a dictionary to store accuracies for each combination
|
|
accuracies = {}
|
|
|
|
total_combinations = len(n_bits_range) * len(rounding_threshold_bits_range)
|
|
current_combination = 0
|
|
|
|
# Loop over the ranges of n_bits and rounding_threshold_bits
|
|
for n_bits in n_bits_range:
|
|
for rounding_threshold_bits in rounding_threshold_bits_range:
|
|
current_combination += 1
|
|
print(f"\nProcessing combination {current_combination}/{total_combinations}")
|
|
print(f"n_bits: {n_bits}, rounding_threshold_bits: {rounding_threshold_bits}")
|
|
|
|
q_module = compile_model(
|
|
resnet18, calib_images, n_bits, rounding_threshold_bits, fhe_mode
|
|
)
|
|
|
|
outputs = []
|
|
all_labels = []
|
|
|
|
print("Evaluating model...")
|
|
for batch in processor.dataloader:
|
|
batch_images = batch["pixel_values"].detach().numpy()
|
|
batch_outputs = q_module.forward(batch_images, fhe=fhe_mode)
|
|
outputs.append(batch_outputs)
|
|
all_labels.append(batch["labels"].detach().numpy())
|
|
|
|
outputs = torch.from_numpy(np.concatenate(outputs))
|
|
all_labels = torch.from_numpy(np.concatenate(all_labels))
|
|
|
|
# Calculate and store accuracy
|
|
fhe_accuracy = processor.accuracy(outputs, all_labels)
|
|
accuracies[(n_bits, rounding_threshold_bits)] = fhe_accuracy
|
|
print(f"Accuracy: {fhe_accuracy:.4f}")
|
|
|
|
# Convert accuracies to a 2D array for plotting
|
|
accuracy_matrix = np.zeros((len(n_bits_range), len(rounding_threshold_bits_range)))
|
|
for i, n_bits in enumerate(n_bits_range):
|
|
for j, rounding_threshold_bits in enumerate(rounding_threshold_bits_range):
|
|
accuracy_matrix[i, j] = accuracies[(n_bits, rounding_threshold_bits)]
|
|
|
|
# Save the accuracy matrix to disk
|
|
np.save("accuracy_matrix.npy", accuracy_matrix)
|
|
|
|
print("\nGenerating plot...")
|
|
|
|
# Plotting
|
|
fig, ax = plt.subplots(figsize=(10, 8))
|
|
cax = ax.matshow(accuracy_matrix, cmap="viridis")
|
|
fig.colorbar(cax)
|
|
|
|
# Set ticks and labels
|
|
ax.set_xticks(range(len(rounding_threshold_bits_range)))
|
|
ax.set_xticklabels([str(x) for x in rounding_threshold_bits_range], rotation=45)
|
|
ax.set_yticks(range(len(n_bits_range)))
|
|
ax.set_yticklabels([str(x) for x in n_bits_range])
|
|
ax.set_xlabel("Rounding Threshold Bits")
|
|
ax.set_ylabel("N Bits")
|
|
ax.set_title(f"Accuracy of FHE ({fhe_mode})")
|
|
|
|
# Annotate each cell with the accuracy percentage
|
|
for i in range(len(n_bits_range)):
|
|
for j in range(len(rounding_threshold_bits_range)):
|
|
ax.text(j, i, f"{accuracy_matrix[i, j]:.2f}", va="center", ha="center", color="white")
|
|
|
|
plt.tight_layout()
|
|
plt.savefig("accuracy_matrix.png", dpi=300)
|
|
print("Plot saved as accuracy_matrix.png")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Run ResNet18 model with FHE execution.")
|
|
parser.add_argument("--run_fhe", action="store_true", help="Run the actual FHE execution.")
|
|
parser.add_argument(
|
|
"--export_statistics", action="store_true", help="Export the circuit statistics."
|
|
)
|
|
parser.add_argument(
|
|
"--use_gpu", action="store_true", help="Use the available GPU at FHE runtime."
|
|
)
|
|
parser.add_argument(
|
|
"--run_experiment",
|
|
action="store_true",
|
|
help="Run the experiment with different n_bits and rounding_threshold_bits.",
|
|
)
|
|
parser.add_argument(
|
|
"--dataset_cache_dir",
|
|
type=str,
|
|
default=None,
|
|
help="Path to the directory where the dataset is cached.",
|
|
)
|
|
parser.add_argument(
|
|
"--num_images",
|
|
type=int,
|
|
default=1,
|
|
help="Number of images to process in FHE execution (default: 1)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if args.use_gpu and not concrete.compiler.check_gpu_available():
|
|
print("Follow the GPU setup guide to install the GPU-enabled TorusML compiler.")
|
|
print("GPU Enabled:", concrete.compiler.check_gpu_enabled())
|
|
print("GPU Available:", concrete.compiler.check_gpu_available())
|
|
|
|
resnet18 = load_model()
|
|
processor = ImageNetProcessor(
|
|
NUM_TEST_SAMPLES, CALIBRATION_SAMPLES, cache_dir=args.dataset_cache_dir
|
|
)
|
|
calib_images = processor.get_calibration_tensor()
|
|
|
|
evaluate_model(resnet18, processor)
|
|
|
|
if args.run_experiment:
|
|
# Get the test images and labels
|
|
run_experiment(resnet18, calib_images, processor)
|
|
else:
|
|
q_module = compile_model(
|
|
resnet18,
|
|
calib_images,
|
|
n_bits={"model_inputs": 8, "op_inputs": 7, "op_weights": 7, "model_outputs": 9},
|
|
rounding_threshold_bits=7,
|
|
fhe_mode="simulate",
|
|
compilation_device=(
|
|
"cuda" if args.use_gpu and concrete.compiler.check_gpu_available() else "cpu"
|
|
),
|
|
)
|
|
|
|
if args.export_statistics:
|
|
export_statistics(q_module)
|
|
|
|
print("Model compiled successfully.")
|
|
|
|
evaluate_model_cml(q_module, processor, fhe="disable")
|
|
evaluate_model_cml(q_module, processor, fhe="simulate")
|
|
|
|
if args.run_fhe:
|
|
num_images = args.num_images
|
|
run_fhe_execution(q_module, calib_images, num_images)
|
|
else:
|
|
print("FHE execution was not run. Use --run_fhe to enable it.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|