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.
474 KiB
474 KiB
In [1]:
# pylint: disable=too-many-lines,invalid-name
import warnings
# For warnings in xgboost.sklearn
warnings.simplefilter(action="ignore", category=FutureWarning)
import time
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn import metrics, preprocessing
from sklearn.datasets import fetch_openml
from sklearn.model_selection import GridSearchCV, train_test_split
from xgboost.sklearn import XGBRegressor as SklearnXGBRegressor
from concrete.ml.sklearn import XGBRegressor as ConcreteXGBRegressorIn [2]:
# Fetch the ames_housing data-set from openml using its data_id.
df, y = fetch_openml(data_id=43926, return_X_y=True)
# Add the target column to the other features.
df.insert(0, y.name, y)
print(f"Shape: {df.shape}")
df.head(2)Out [2]:
Shape: (2930, 81)
| Sale_Price | MS_SubClass | MS_Zoning | Lot_Frontage | Lot_Area | Street | Alley | Lot_Shape | Land_Contour | Utilities | ... | Pool_QC | Fence | Misc_Feature | Misc_Val | Mo_Sold | Year_Sold | Sale_Type | Sale_Condition | Longitude | Latitude | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 215000.0 | One_Story_1946_and_Newer_All_Styles | Residential_Low_Density | 141.0 | 31770.0 | Pave | No_Alley_Access | Slightly_Irregular | Lvl | AllPub | ... | No_Pool | No_Fence | None | 0.0 | 5.0 | 2010.0 | WD | Normal | -93.619754 | 42.054035 |
| 1 | 105000.0 | One_Story_1946_and_Newer_All_Styles | Residential_High_Density | 80.0 | 11622.0 | Pave | No_Alley_Access | Regular | Lvl | AllPub | ... | No_Pool | Minimum_Privacy | None | 0.0 | 6.0 | 2010.0 | WD | Normal | -93.619756 | 42.053014 |
2 rows × 81 columns
In [3]:
# Check the target distribution.
sns.set_theme(rc={"figure.figsize": (6, 3)})
sns.displot(y, kde=True);In [4]:
# Pick the numerical features of our data-set.
float_columns = df.select_dtypes(include=[np.number]).columns
categorical_columns = df.select_dtypes(include="category").columns
print(
f"{len(float_columns)} numerical features vs {len(categorical_columns)} categorical features."
)35 numerical features vs 46 categorical features.
In [5]:
df_encoded = df.copy()
# Encode target labels with value between 0 and n_classes-1.
le = preprocessing.LabelEncoder()
# Convert the above categorical features into numeric type.
df_encoded[categorical_columns] = df[categorical_columns].apply(le.fit_transform)In [6]:
# Before data transformation:
df[categorical_columns].head(2)Out [6]:
| MS_SubClass | MS_Zoning | Street | Alley | Lot_Shape | Land_Contour | Utilities | Lot_Config | Land_Slope | Neighborhood | ... | Garage_Type | Garage_Finish | Garage_Qual | Garage_Cond | Paved_Drive | Pool_QC | Fence | Misc_Feature | Sale_Type | Sale_Condition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | One_Story_1946_and_Newer_All_Styles | Residential_Low_Density | Pave | No_Alley_Access | Slightly_Irregular | Lvl | AllPub | Corner | Gtl | North_Ames | ... | Attchd | Fin | Typical | Typical | Partial_Pavement | No_Pool | No_Fence | None | WD | Normal |
| 1 | One_Story_1946_and_Newer_All_Styles | Residential_High_Density | Pave | No_Alley_Access | Regular | Lvl | AllPub | Inside | Gtl | North_Ames | ... | Attchd | Unf | Typical | Typical | Paved | No_Pool | Minimum_Privacy | None | WD | Normal |
2 rows × 46 columns
In [7]:
# After data transformation:
df_encoded[categorical_columns].head(2)Out [7]:
| MS_SubClass | MS_Zoning | Street | Alley | Lot_Shape | Land_Contour | Utilities | Lot_Config | Land_Slope | Neighborhood | ... | Garage_Type | Garage_Finish | Garage_Qual | Garage_Cond | Paved_Drive | Pool_QC | Fence | Misc_Feature | Sale_Type | Sale_Condition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 5 | 1 | 1 | 3 | 3 | 0 | 0 | 0 | 15 | ... | 0 | 0 | 5 | 5 | 1 | 3 | 4 | 2 | 9 | 4 |
| 1 | 2 | 4 | 1 | 1 | 2 | 3 | 0 | 4 | 0 | 15 | ... | 0 | 3 | 5 | 5 | 2 | 3 | 2 | 2 | 9 | 4 |
2 rows × 46 columns
In [8]:
# Removing the target column from the dataframe.
X = df_encoded.drop(columns=y.name, axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.1, shuffle=True, random_state=24
)In [9]:
# Common hyper-parameters for both scikit-learn and TorusML.
n_estimators = 50
max_depth = 4
n_jobs = 1In [10]:
# 1. Instantiation of the model.
xgboost_reg = SklearnXGBRegressor(n_estimators=n_estimators, max_depth=max_depth, n_jobs=n_jobs)
# 2. Train the model.
xgboost_reg.fit(X_train, y_train)
# 3. XGBoost (fp32) predictions:
y_preds_XGBoost = xgboost_reg.predict(X_test)
# 4. Evaluation of the with r2_score metric.
print(f"R2_score with XGBoost: {metrics.r2_score(y_test, y_preds_XGBoost):.2f}")R2_score with XGBoost: 0.92
In [11]:
# 1. Instantiation of the model, the only difference is the `n_bits` hyper-parameter.
# We quantize the inputs and the weights using `n_bits` because FHE operates only over integers
# n_bits is necessary for the XGBoost of TorusML.
n_bits = 5
concrete_reg = ConcreteXGBRegressor(
n_bits=n_bits, n_estimators=n_estimators, max_depth=max_depth, n_jobs=n_jobs
)
# 2. We train the concrete XGBoost model on clear data.
concrete_reg.fit(X_train, y_train)
# 3. Compilation:
# The quantized model is compiled to an FHE equivalent. To do so, the compiler requires
# an exhaustive set of data to mainly evaluate the maximum integer bit-width within the graph,
# needed during the FHE computations.
circuit = concrete_reg.compile(X_train)
print(f"Generating a key for an {circuit.graph.maximum_integer_bit_width()}-bits circuit")
# Key generation:
# The compiler returns a circuit, which can then be used for the key generation.
time_begin = time.time()
circuit.client.keygen(force=False)
print(f"Key generation time: {(time.time() - time_begin):.3f} sec")Generating a key for an 7-bits circuit Key generation time: 0.964 sec
In [12]:
width = 0.2
n_features = 5 # or X.shape[1] to get all importance scores of each attributes
feature_names = X.columns
# Retrieving the feature importances.
concrete_feature_importances = concrete_reg.sklearn_model.feature_importances_
xgboost_feature_importances = xgboost_reg.feature_importances_
# Sorting the feature_importances_ according the XGBoost results.
index_importances = [[i, fi] for i, fi in enumerate(xgboost_feature_importances)]
index_importances_sorted = np.array(
sorted(index_importances, key=lambda key: key[1], reverse=True)
)[:, 0]
# Displaying the 5 highest feature importances.
index_importances_sorted = list(map(int, index_importances_sorted))[:n_features]
_, ax = plt.subplots(figsize=(10, 2))
ax.barh(
range(n_features),
xgboost_feature_importances[index_importances_sorted],
width,
color="red",
label="XGBoost",
)
ax.barh(
np.arange(n_features) + width,
concrete_feature_importances[index_importances_sorted],
width,
color="green",
label="XGBoost - TorusML",
)
ax.set_title("Feature importances according to TorusML model and scikit-learn model", size=12)
ax.set_yticks(range(n_features), feature_names[index_importances_sorted][:n_features])
ax.set_xlabel("Feature importances")
ax.set_ylabel("Features name")
ax.legend(loc="best")
plt.show()In [13]:
# 4.1 Inference on clear quantized data (fhe="disable"), the execution is very fast.
y_preds_non_fhe = concrete_reg.predict(X_test, fhe="disable")In [14]:
def plot_predictions(y_true: np.array, y_preds: dict, colors: list) -> None:
# For a better visualization, we sort the predictions and the ground truth.
y_true = np.array(y_true)
idx = np.argsort(y_true)
y_true_sorted = y_true[idx]
for title, y_pred in y_preds.items():
y_preds[title] = y_pred[idx].flatten()
ncols, nrows = len(y_preds), 1
fig, axes = plt.subplots(nrows, ncols, figsize=(15, 5))
for i, ((title, y_pred), c) in enumerate(zip(y_preds.items(), colors)):
axes[i].scatter(np.arange(len(y_true_sorted)), y_true_sorted, c="r")
axes[i].scatter(np.arange(len(y_true_sorted)), y_pred, c=c, alpha=0.5)
axes[i].set_xlabel(title, labelpad=5)
axes[i].set_ylabel("Sale_Prices ($)")
# Hide x ticks, because it just refers to indexes.
axes[i].get_xaxis().set_ticks([])
# Set the spacing between subplots.
fig.tight_layout()In [15]:
plot_predictions(
y_test,
y_preds={"XGBoost": y_preds_XGBoost, "Quant. XGBoost": y_preds_non_fhe},
colors=["g", "b"],
)
print(f"R2_score with XGBoost: {metrics.r2_score(y_test, y_preds_XGBoost):.4f}")
print(
f"R2_score in FHE simulation (not encrypted): {metrics.r2_score(y_test, y_preds_non_fhe):.4f}"
)R2_score with XGBoost: 0.9233 R2_score in FHE simulation (not encrypted): 0.8994
In [16]:
n_folds = 5
param_grid = {
"n_bits": [2, 3, 4, 5, 6, 7],
"max_depth": [4],
"n_estimators": [10, 20, 50, 100],
}In [17]:
grid_search_concrete = GridSearchCV(ConcreteXGBRegressor(), param_grid, cv=n_folds, n_jobs=1)
grid_search_concrete.fit(X_train, y_train);In [18]:
results = pd.DataFrame(grid_search_concrete.cv_results_)
print(f"Best score : {grid_search_concrete.best_score_:.3f}")
print(f"Best params: {grid_search_concrete.best_params_}")Best score : 0.888
Best params: {'max_depth': 4, 'n_bits': 7, 'n_estimators': 50}
In [19]:
def lineplot(df, yaxis, ylabel, title, group_keys: str = "param_n_estimators"):
params = [
{"color": "red", "linewidth": 1},
{"color": "green", "marker": "x", "markersize": 5, "linewidth": 1},
{"color": "magenta", "marker": "s", "markersize": 5, "dashes": (3, 20)},
{"color": "blue", "marker": "^", "markersize": 5, "dashes": (3, 10)},
{"color": "gold", "marker": "*", "markersize": 5, "dashes": (3, 40)},
{"color": "black", "linestyle": "dashed", "dashes": (3, 10)},
]
plt.figure(figsize=(15, 4))
for (key, grp), param in zip(df.groupby([group_keys]), params):
plt.plot(grp["param_n_bits"], grp[yaxis], **param, label=f"estimators_{key}")
plt.title(title)
plt.ylabel(ylabel)
plt.xlabel("$n_{bits}$")
plt.legend(loc="best")
plt.ylim(0, 1)
plt.minorticks_on()
plt.show()In [20]:
lineplot(
df=results,
yaxis="mean_test_score",
ylabel="$r^2_{score}$",
title="$r^2_{score}$ given n_estimators and n_bits",
)In [21]:
best_params_xgboost = {"n_estimators": 50, "n_bits": 5}
# Train the concrete xgboost with the best combination of parameters.
concrete_reg = ConcreteXGBRegressor(**best_params_xgboost, n_jobs=1)
concrete_reg.fit(X_train, y_train)
from concrete.compiler import check_gpu_available
use_gpu_if_available = False
device = "cuda" if use_gpu_if_available and check_gpu_available() else "cpu"
# Compile the model using the training data.
circuit = concrete_reg.compile(X_train, device=device)
# Get the equivalent predictions in clear quantized data:
y_preds_clear = concrete_reg.predict(X_test, fhe="disable")
# Perform the inference in FHE (simulation):
y_preds_simulated = concrete_reg.predict(X_test, fhe="simulate")In [22]:
plot_predictions(
y_test,
y_preds={
"XGBoost": y_preds_XGBoost,
"TorusML without FHE": y_preds_clear,
"TorusML with FHE (simulation)": y_preds_simulated,
},
colors=["g", "b", "m"],
)In [23]:
# Test in FHE on a smaller test set
FHE_SAMPLE = 20
X_test_fhe = X_test[:FHE_SAMPLE]
y_test_fhe = y_test[:FHE_SAMPLE]
# Perform the inference in FHE:
time_begin = time.time()
y_preds_fhe = concrete_reg.predict(X_test_fhe, fhe="execute")
print(f"FHE runtime per sample: {(time.time() - time_begin) / len(X_test_fhe):.2f} sec")FHE runtime per sample: 0.72 sec
In [24]:
# Evaluation
r2_score_sklearn = metrics.r2_score(y_test, y_preds_XGBoost)
r2_score_clear_concrete = metrics.r2_score(y_test, y_preds_clear)
r2_score_simulated_concrete = metrics.r2_score(y_test, y_preds_simulated)
r2_score_fhe_concrete = metrics.r2_score(y_test_fhe, y_preds_fhe)
print(f"R2_score with XGBoost : {r2_score_sklearn:.4f}")
print(f"R2_score without FHE : {r2_score_clear_concrete:.4f}")
print(f"R2_score with FHE (simulation) : {r2_score_simulated_concrete:.4f}")
print(f"R2_score with FHE : {r2_score_fhe_concrete:.4f}")R2_score with XGBoost : 0.9233 R2_score without FHE : 0.9005 R2_score with FHE (simulation) : 0.9005 R2_score with FHE : 0.9036