mirror of
https://github.com/luxfi/torus-ml.git
synced 2026-10-30 15:15:23 +00:00
The distribution called itself torus-ml while shipping a package directory named concrete, so the name a user installs and the name they import disagreed — and most of the tree already said torus.ml, which could not resolve against src/concrete. Moved src/concrete to src/torus and pointed the include at it. The dependency was Zama's concrete-python from their index. It is torus-fhe from ours, which is what luxfhe/torus already ships: that frontend declares torus-fhe and ships a torus/fhe module, so every 'from torus.fhe' in this tree was already naming our compiler and only the dependency lagged. Two distributions share one namespace, which is the shape the upstream this forked from used and the reason the split works: torus-fhe gives torus.fhe, torus-ml gives torus.ml. Swept every other spelling — identifiers, enum members, workflow variables, filenames, notebooks. LICENSE and NOTICE keep the upstream attribution a fork owes; the figures carry the old name in JPEG metadata a text sweep cannot reach.
13 KiB
13 KiB
In [1]:
from pprint import pprint
import numpy as np
from torus.fhe.tracing import Tracer
from quant_framework import DualArray, Quantizer
from torus import fheIn [2]:
np.random.seed(42)
n_values = 10
x_calib = np.random.randn(n_values)In [3]:
# Create the quantizer. Higher N_BITS will give better results for the floating point comparison
N_BITS = 8
quantizer = Quantizer(n_bits=N_BITS)
def finalize(x):
"""Finalize the output value.
If the DualArray's integer array is a Tracer, an object used during compilation, return it
as is. Else, return the DualArray. This is called at the end of the run_numpy method because
the compiler can only consider Tracer objects or Numpy arrays as input and outputs.
"""
if isinstance(x.int_array, Tracer):
return x.int_array
return x
def f(q_x):
"""Function made of quantized operators to compile."""
# Convert the inputs to a DualArray instance using the stored calibration data. This is
# necessary as Torus FHE can only compile functions that inputs Numpy arrays, while we
# still want to be able to propagate the quantization parameters throughout the different
# operators
dual_x = DualArray(float_array=x_calib, int_array=q_x, quantizer=quantizer)
dual_y = dual_x.exp(key="exp")
dual_x = dual_x.add(dual_y, "add")
dual_x = dual_x.truediv(3, key="truediv")
dual_x = dual_x.quantize(key="output")
return finalize(dual_x)In [4]:
def expected_f(x):
"""Expected function made of float operators."""
return (np.exp(x) + x) / 3In [5]:
# Convert the input to a DualArray and quantize it
dual_x = DualArray(x_calib, quantizer=quantizer).quantize(key="input")
# Calibrate the function in order to compute and store the quantization parameters
dual_result = f(dual_x.int_array)In [6]:
# At this step, the quantizer contains all the needed quantization parameters (scale, zero_point)
pprint(quantizer.scale_dict){'add_sub_add_requant': (0.012416286069347535, 0),
'add_sub_add_self': (0.012416286069347535, 0),
'exp': (0.012416286069347535, 0),
'input': (0.012434746578798358, 0),
'output': (0.016833222749391915, 0),
'truediv': (0.012405140942975472, 0)}
In [7]:
expected_output = expected_f(x_calib)
# De-quantize the output
float_output = dual_result.dequantize("output").float_array
# Compare the float output with the expected output using the MAE score
output_mae = np.mean(np.abs(float_output - expected_output))
print(f"MAE between the expected result and the computed float result: {output_mae:.4f}")MAE between the expected result and the computed float result: 0.0051
In [8]:
# Create a new input and quantize it
new_x = np.random.randn(10)
new_int_x = quantizer.quantize(new_x, key="input")
# Retrieve the integer result
new_int_result = f(new_int_x).int_array
# Dequantize the output
new_float_output = quantizer.dequantize(new_int_result, key="output")
# Compare the float output with the expected output using the MAE score
new_expected_output = expected_f(new_x)
new_output_mae = np.mean(np.abs(new_float_output - new_expected_output))
print(f"MAE between the expected result and the computed float result: {new_output_mae:.4f}")MAE between the expected result and the computed float result: 0.0028
In [9]:
# Instantiate the compiler
compiler = fhe.Compiler(f, {"q_x": "encrypted"})
# Build the inputset as a batch of single quantized input
inputset = list(quantizer.quantize(x_calib, key="input"))
# Compile the function using the inputset and print the computation graph
circuit = compiler.compile(inputset, show_graph=True)
# Generate the keys
circuit.keygen()Computation Graph
--------------------------------------------------------------------------------
%0 = q_x # EncryptedScalar<int8> ∈ [-38, 127]
%1 = 0 # ClearScalar<uint1> ∈ [0, 0]
%2 = subtract(%0, %1) # EncryptedScalar<int8> ∈ [-38, 127]
%3 = 0 # ClearScalar<uint1> ∈ [0, 0]
%4 = subtract(%0, %3) # EncryptedScalar<int8> ∈ [-38, 127]
%5 = subgraph(%4) # EncryptedScalar<int8> ∈ [-38, 127]
%6 = subgraph(%2) # EncryptedScalar<uint9> ∈ [50, 390]
%7 = 1 # ClearScalar<uint1> ∈ [1, 1]
%8 = multiply(%7, %6) # EncryptedScalar<uint9> ∈ [50, 390]
%9 = add(%5, %8) # EncryptedScalar<uint10> ∈ [12, 517]
%10 = 0 # ClearScalar<uint1> ∈ [0, 0]
%11 = subtract(%9, %10) # EncryptedScalar<uint10> ∈ [12, 517]
%12 = subgraph(%11) # EncryptedScalar<uint7> ∈ [3, 127]
return %12
Subgraphs:
%5 = subgraph(%4):
%0 = input # EncryptedScalar<uint1>
%1 = 0.012416286069347535 # ClearScalar<float64>
%2 = multiply(%0, %1) # EncryptedScalar<float64>
%3 = 0.012416286069347535 # ClearScalar<float64>
%4 = divide(%2, %3) # EncryptedScalar<float64>
%5 = 0 # ClearScalar<uint1>
%6 = add(%4, %5) # EncryptedScalar<float64>
%7 = rint(%6) # EncryptedScalar<float64>
%8 = astype(%7, dtype=int_) # EncryptedScalar<uint1>
return %8
%6 = subgraph(%2):
%0 = input # EncryptedScalar<uint1>
%1 = 0.012416286069347535 # ClearScalar<float64>
%2 = multiply(%0, %1) # EncryptedScalar<float64>
%3 = exp(%2) # EncryptedScalar<float64>
%4 = 0.012416286069347535 # ClearScalar<float64>
%5 = divide(%3, %4) # EncryptedScalar<float64>
%6 = 0 # ClearScalar<uint1>
%7 = add(%5, %6) # EncryptedScalar<float64>
%8 = rint(%7) # EncryptedScalar<float64>
%9 = astype(%8, dtype=int_) # EncryptedScalar<uint1>
return %9
%12 = subgraph(%11):
%0 = input # EncryptedScalar<uint1>
%1 = 0.012405140942975472 # ClearScalar<float64>
%2 = multiply(%0, %1) # EncryptedScalar<float64>
%3 = 3 # ClearScalar<uint2>
%4 = divide(%2, %3) # EncryptedScalar<float64>
%5 = 0.016833222749391915 # ClearScalar<float64>
%6 = divide(%4, %5) # EncryptedScalar<float64>
%7 = 0 # ClearScalar<uint1>
%8 = add(%6, %7) # EncryptedScalar<float64>
%9 = rint(%8) # EncryptedScalar<float64>
%10 = astype(%9, dtype=int_) # EncryptedScalar<uint1>
return %10
--------------------------------------------------------------------------------
In [10]:
input_0 = new_int_x[0]
# Compute the result in the clear directly using the quantized operators
clear_evaluation = f(input_0)
# Compute the result in the clear using FHE simulation
simulated_evaluation = circuit.simulate(input_0)
# Compute the result in FHE
fhe_evaluation = circuit.encrypt_run_decrypt(input_0)
print((clear_evaluation.int_array, simulated_evaluation, fhe_evaluation))
print(
"Results are identical:",
all((clear_evaluation.int_array, simulated_evaluation, fhe_evaluation)),
)(3, 3, 3) Results are identical: True