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

19 KiB
Raw Permalink Blame History

Introduction:

TorusML (CML) supports also model compilation to the Lux-FHE format, enabling deeper interoperability between CML and Lux-FHE frameworks. This allows developers to combine CML’s intuitive, Python-based workflow with the performance and low-level control of Lux-FHE for advanced privacy-preserving applications.

This notebook showcases a privacy-preserving server-side authentication flow, where access to a remote server is granted only if the user’s encrypted information satisfies a specific condition — all without ever decrypting any sensitive data throughout the process.

On the server side, verification is performed using a binary classification algorithm provided by CML. The post-processing part, which includes an argmax operation on the model’s encrypted output followed by a multiplication with a random plaintext token (known only to the server), is implemented using the Lux-FHE library.

This token acts as a proof of successful authentication and is returned only if the condition is met.

In [1]:
import subprocess
from tempfile import TemporaryDirectory

import torus_ml_extensions as fhext
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

from concrete.ml.common.utils import CiphertextFormat
from concrete.ml.deployment import FHEModelClient, FHEModelDev, FHEModelServer
from concrete.ml.sklearn import DecisionTreeClassifier as ConcreteDecisionTreeClassifier
No CUDA runtime is found, using CUDA_HOME='/usr/local/cuda'
In [2]:
# Generate toy classification dataset
x, y = make_classification(n_samples=100, class_sep=2, n_features=10, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)
In [3]:
# For now, only 8-bit quantization is supported for Lux-FHE interoperability
model = ConcreteDecisionTreeClassifier(n_bits=8)
# Train the CML model on clear data
model.fit(X_train, y_train)
Out [3]:
DecisionTreeClassifier(n_bits={'op_inputs': 8, 'op_leaves': 8})
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier(n_bits={'op_inputs': 8, 'op_leaves': 8})
In [4]:
# Compile the model with Lux-FHE support (ciphertext_format=CiphertextFormat.TFHE_RS)
model.compile(X_train, ciphertext_format=CiphertextFormat.TFHE_RS);
In [5]:
# Use a temporary directory to store the Dev, client, and server models
with TemporaryDirectory() as temp_dir:
    # Export the model for deployment using the FHEModelDev API
    model_dev = FHEModelDev(temp_dir, model)
    model_dev.save()

    # Loading the model on the client side using FHEModelClient API
    fhe_model_client = FHEModelClient(path_dir=temp_dir)
    fhe_model_client.load()

    # Loading the model on the server side using FHEModelServer API
    fhe_model_server = FHEModelServer(temp_dir)
    fhe_model_server.load()
In [6]:
# Client side : Generates all keys and serializes the evaluation keys required for the server
evaluation_keys, tfhers_evaluation_keys = fhe_model_client.get_serialized_evaluation_keys(
    include_tfhers_key=True
)

with open("evalkeys_tfhers.bin", "wb") as fp:
    fp.write(tfhers_evaluation_keys)

Use Case: Privacy-Preserving Authentication with Homomorphic Encryption

In this scenario, the client attempts to authenticate using sensitive data (e.g., a biometric vector such as facial recognition).

On the client side, the input is first quantized (to meet FHE constraints), then encrypted, and finally sent to the server.

On the server side, first, a binary classification algorithm — in this case, a decision tree — is applied on the encrypted data to determine whether authentication should succeed (decision = 1) or fail (decision = 0). Then, the decision result is homomorphically multiplied by a secret authentication token known only to the server. Finally, the encrypted result is sent back to the client.

On the client side, the result is decrypted using the secret key. If the authentication was successful (decision = 1), the decrypted value represents the original server token. Otherwise (decision = 0), the decrypted result is a vector of zeros, indicating authentication failure.

In [7]:
for DATA_INDEX in range(10):

    print(f"🔢 Test sample #{DATA_INDEX}", "=" * 50)
    print("Inference input:", X_test[[DATA_INDEX]])
    print(f"🎯 Ground truth label: {y_test[DATA_INDEX]}")

    # Client side : Quantize, encrypt and serialize the data
    q_x_encrypted_serialized = fhe_model_client.quantize_encrypt_serialize(X_test[[DATA_INDEX]])

    # Server side: Run the model over encrypted data
    q_y_pred_encrypted_serialized = fhe_model_server.run(q_x_encrypted_serialized, evaluation_keys)

    # Client side : Decrypt, de-quantize and post-process the result
    y_pred = fhe_model_client.deserialize_decrypt_dequantize(q_y_pred_encrypted_serialized[0])

    print("Model logits (after decryption):", y_pred.reshape((-1,)))

    with open("prediction_non_preprocessed.bin", "wb") as f:
        f.write(q_y_pred_encrypted_serialized[0])

    # ***************************************
    # NOW RUN cargo run --release
    # **************************************

    subprocess.run(
        [
            "cargo",
            "run",
            "--release",
            "--",  # Required to pass args to the Rust binary
            "evalkeys_tfhers.bin",
            "prediction_non_preprocessed.bin",
            "tfhers_sign_result.bin",
        ],
        capture_output=True,
        text=True,
        check=True,
    )

    with open("tfhers_sign_result.bin", "rb") as f:
        tfhers_postproc_bytes = f.read()

    results = fhext.decrypt_radix(
        blob=tfhers_postproc_bytes,
        shape=(1, 16),
        bitwidth=8,
        is_signed=True,
        secret_key=fhe_model_client.get_tfhers_secret_key(),
    )

    token = results.reshape((-1,))

    if all(t == 0 for t in token):
        print(f"🔐 Authentication token: 🛑 Access denied — token: {token}\n")
    else:
        print(f"🔐 Authentication token: 🟢 Access granted — token: {token} \n")
🔢 Test sample #0 ==================================================
Inference input: [[-0.07015438  2.85605469 -1.05319823  2.4898248  -0.21910053  0.32692737
  -2.21113531  0.77086519  0.82940558  0.23561456]]
🎯 Ground truth label: 0
Model logits (after decryption): [1. 0.]
🔐 Authentication token: 🛑 Access denied — token: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

🔢 Test sample #1 ==================================================
Inference input: [[-2.43430912  2.25291095  2.09885263  1.23502851 -0.24751864 -0.72713718
   0.6206721  -1.33534436 -0.07443343  0.177701  ]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [-122  -15  106  -56   85   39  -28   94 -109   10   27  -51  -33   60
  -25   11] 

🔢 Test sample #2 ==================================================
Inference input: [[-3.37679987  3.30450019  2.83991037  1.87087893 -0.37482081 -0.2403254
   0.44426331  1.1593298   0.71095997 -0.36096617]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [ 117  -34 -113   75  -19   55 -125  116   18   94   35  100   -5  -87
   67  -95] 

🔢 Test sample #3 ==================================================
Inference input: [[-1.97994267  1.53479393  1.82584805  0.74282242 -0.47874862  0.22213377
  -0.8946073  -0.43973106  1.25575613 -0.18687164]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [ -20   45  103    3    7  -47  -58   -4 -111   82  -46   52  -56  -10
  -31  -30] 

🔢 Test sample #4 ==================================================
Inference input: [[-1.19229599 -1.71352532  2.15199146 -1.87205691 -0.59939265 -0.83972184
  -0.52575502  0.15039379 -2.12389572 -0.75913266]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [  -9 -112  102   71 -122   16  -54    5  -49   94  -29  -34  -52  -73
  -35  125] 

🔢 Test sample #5 ==================================================
Inference input: [[ 2.48305377 -2.20592943 -2.17762637 -1.17878084  0.23204994 -0.47103831
  -1.40746377 -0.21344715 -1.44808434 -0.71844422]]
🎯 Ground truth label: 0
Model logits (after decryption): [1. 0.]
🔐 Authentication token: 🛑 Access denied — token: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

🔢 Test sample #6 ==================================================
Inference input: [[ 2.85855887 -2.43534875 -2.54851014 -1.26544183  0.40171172 -0.10876015
  -0.40122047  0.0125924   0.69014399  0.22409248]]
🎯 Ground truth label: 0
Model logits (after decryption): [1. 0.]
🔐 Authentication token: 🛑 Access denied — token: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

🔢 Test sample #7 ==================================================
Inference input: [[-2.02146424  2.02703224  1.68057323  1.16292288  0.70030988 -0.85835778
   0.12200981 -0.0960599  -0.57563783  2.56008454]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [-117   97  -78   21    7 -104  106  -89 -104   77  -55   37   64   80
  -41  -96] 

🔢 Test sample #8 ==================================================
Inference input: [[-1.96346177  1.88629356  1.66530077  1.05694552 -1.12905177  0.72167206
   0.48937456  0.71299843 -0.52452027 -1.22212781]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [  85  -83   64 -105 -100 -116  -19  -71   31  -70  108   10   33   46
 -118   93] 

🔢 Test sample #9 ==================================================
Inference input: [[-2.27657182  2.35970566  1.86199147  1.37726866  0.60600995 -1.55662917
   1.75479418  1.69645637 -1.28042935 -2.08192941]]
🎯 Ground truth label: 1
Model logits (after decryption): [0. 1.]
🔐 Authentication token: 🟢 Access granted — token: [ -50  -97  -99 -115   -1  -10  -92   46 -115 -111 -115  127  -24   38
  125   92] 

Conclusion

In this notebook, we showed how to combine TorusML (CML) and Lux-FHE for end-to-end encrypted processing. After training and compiling a CML decision tree, we performed an encrypted inference and then applied additional post-processing in Lux-FHE, all while preserving data privacy.

This approach highlights the power of interoperability between CML and Lux-FHE, and paves the way for more advanced privacy-preserving use cases.

Disclaimer: This use case assumes an honest-but-curious server and does not represent a complete secure authentication protocol. Further measures are required to protect against malicious servers.