mirror of
https://github.com/luxfhe/torus.git
synced 2026-10-30 17:07:26 +00:00
The MLIR dialect, its conversion passes (TFHEToConcrete, ConcreteToCAPI, ConcreteToLLVM), the optimizer component, the compiler binary, the Rust crate and the program_info parameter file all still carried the upstream project's name. They now carry this one's. Two things deliberately keep the word, because in them it is not the name. mlir::OpTrait CRTP parameters mean "the derived type", so they are spelled Derived and OpTy — LLVM's other conventional spelling for the same idiom. concreteShape is a capnp field: renaming it would change the serialized protocol, and the prose describing it is ordinary English. This also repairs references the earlier partial rename left dangling: Security.cpp included concrete/curves.gen.h and the directory is torus/, torus-cpu.h included concrete-num_complex.h, the Torus dialect CMakeLists named ConcreteOps.td, and compiler/setup.py asked find_namespace_packages for a "concrete" package that no longer exists — so it built without the package it ships.
26 KiB
26 KiB
In [1]:
# Importing some libraries
import time
import numpy as np
from torus import fheIn [2]:
def make_one_hot_vector(index: int, size: int) -> np.ndarray:
one_hot_vector = np.zeros(shape=(size,), dtype=np.int8)
one_hot_vector[index] = 1
return one_hot_vectorIn [3]:
x = make_one_hot_vector(0, size=5)
assert np.array_equal(x, np.array([1, 0, 0, 0, 0]))
x = make_one_hot_vector(2, size=7)
assert np.array_equal(x, np.array([0, 0, 1, 0, 0, 0, 0]))In [4]:
def get_ith_element_of_database(one_hot_vector: np.ndarray, database: np.ndarray) -> int:
return np.dot(one_hot_vector, database)In [5]:
database = np.array([3, 5, 21, 7, 11, 13, 2, 17])
assert database.ndim == 1
database_length = database.shape[0]
database_output_bits = np.ceil(np.log2(np.max(database))).astype(np.int32)
assert database_output_bits == 5
# For now, we have not compiled our functions so here, all the computations
# in the following asserts are done in the clear, just to check the semantic
# of the functions
assert (
get_ith_element_of_database(make_one_hot_vector(0, size=database_length), database)
== database[0]
)
assert (
get_ith_element_of_database(make_one_hot_vector(3, size=database_length), database)
== database[3]
)
assert (
get_ith_element_of_database(make_one_hot_vector(4, size=database_length), database)
== database[4]
)In [ ]:
def compile_function(database, **kwargs):
assert database.ndim == 1
database_length = database.shape[0]
inputset_length = 100
inputset = [
(make_one_hot_vector(np.random.randint(database_length), database_length), database)
for _ in range(inputset_length)
]
# Also add the extreme value, which would actually be sufficient alone, as it reaches the
# maximal values in get_ith_element_of_database
inputset.append((make_one_hot_vector(np.argmax(database), database_length), database))
compiler = fhe.Compiler(
get_ith_element_of_database, {"one_hot_vector": "encrypted", "database": "clear"}
)
circuit = compiler.compile(inputset, **kwargs)
return circuit
circuit = compile_function(database, show_mlir=True, show_graph=True)Computation Graph
--------------------------------------------------------------------------------
%0 = one_hot_vector # EncryptedTensor<uint1, shape=(8,)> ∈ [0, 1]
%1 = database # ClearTensor<uint5, shape=(8,)> ∈ [2, 21]
%2 = dot(%0, %1) # EncryptedScalar<uint5> ∈ [2, 21]
return %2
--------------------------------------------------------------------------------
MLIR
--------------------------------------------------------------------------------
module {
func.func @main(%arg0: tensor<8x!FHE.eint<5>>, %arg1: tensor<8xi6>) -> !FHE.eint<5> {
%0 = "FHELinalg.to_signed"(%arg0) : (tensor<8x!FHE.eint<5>>) -> tensor<8x!FHE.esint<5>>
%1 = "FHELinalg.dot_eint_int"(%0, %arg1) : (tensor<8x!FHE.esint<5>>, tensor<8xi6>) -> !FHE.esint<5>
%2 = "FHE.to_unsigned"(%1) : (!FHE.esint<5>) -> !FHE.eint<5>
return %2 : !FHE.eint<5>
}
}
--------------------------------------------------------------------------------
In [ ]:
def test_encrypted_queries(database, circuit, how_many_tests=1, verbose=True):
times = []
for _ in range(how_many_tests):
database_length = database.shape[0]
log_database_length = np.ceil(np.log2(database_length)).astype(np.int32)
# Random index in the database
random_index = np.random.randint(database_length)
# Turn it into one hot vector
x = make_one_hot_vector(random_index, database_length)
# Encrypt the query, on the client side
encrypted_x, _ = circuit.encrypt(x, None)
# Run the FHE computation on the server side
time_begin = time.time()
encrypted_y = circuit.run(encrypted_x, database)
time_end = time.time()
times.append(time_end - time_begin)
if verbose:
print(
f"FHE computation done in {(time_end - time_begin) * 1000:.1f} milliseconds "
f"-- database is {database_length} (2**{log_database_length}) elements of "
f"{database_output_bits} bits"
)
# Decrypt the result on the client side
y = circuit.decrypt(encrypted_y)
# And check the computations worked fine
assert y == database[random_index]
return times
_ = test_encrypted_queries(database, circuit)FHE computation done in 2.6 milliseconds -- database is 8 (2**3) elements of 5 bits
In [ ]:
how_many_tests = 10
sample_list = [
(4, 8),
(4, 16),
(8, 8),
(8, 16),
(9, 8),
(9, 16),
(10, 4),
(10, 8),
(12, 4),
(12, 8),
(14, 4),
(14, 8),
]
timings_dic = {}
for database_input_bits, database_output_bits in sample_list:
# Take a random database of expected size and output_bits
database_length = 2**database_input_bits
database = np.array(
[np.random.randint(2**database_output_bits) for _ in range(database_length)]
)
circuit = compile_function(database, show_mlir=False, show_graph=False)
# Benchmark
times = test_encrypted_queries(database, circuit, how_many_tests=how_many_tests, verbose=False)
mean_time = np.mean(times)
timings_dic[(database_input_bits, database_output_bits)] = mean_time
print(
f"For a database of 2**{str(database_input_bits):>2s} elements of "
f"{str(database_output_bits):>2s} bits, average execution time is "
f"{1000 * mean_time:.1f} milliseconds"
)For a database of 2** 4 elements of 8 bits, average execution time is 1.2 milliseconds For a database of 2** 4 elements of 16 bits, average execution time is 1.3 milliseconds For a database of 2** 8 elements of 8 bits, average execution time is 4.5 milliseconds For a database of 2** 8 elements of 16 bits, average execution time is 5.0 milliseconds For a database of 2** 9 elements of 8 bits, average execution time is 6.8 milliseconds For a database of 2** 9 elements of 16 bits, average execution time is 13.4 milliseconds For a database of 2**10 elements of 4 bits, average execution time is 8.1 milliseconds For a database of 2**10 elements of 8 bits, average execution time is 13.3 milliseconds For a database of 2**12 elements of 4 bits, average execution time is 36.2 milliseconds For a database of 2**12 elements of 8 bits, average execution time is 55.1 milliseconds For a database of 2**14 elements of 4 bits, average execution time is 156.5 milliseconds For a database of 2**14 elements of 8 bits, average execution time is 221.3 milliseconds
In [ ]:
# We'll have 4 subdatabases of 8b each
number_of_subdatabases = 4
database_output_bits_subdatabases = 8
database_output_bits = database_output_bits * number_of_subdatabases
def get_ith_element_of_database(
one_hot_vector: np.ndarray,
database0: np.ndarray,
database1: np.ndarray,
database2: np.ndarray,
database3: np.ndarray,
) -> (int, int, int, int):
return (
np.dot(one_hot_vector, database0),
np.dot(one_hot_vector, database1),
np.dot(one_hot_vector, database2),
np.dot(one_hot_vector, database3),
)
# Take a random database of expected size and output_bits
database_length = 2**database_input_bits
database = np.array([np.random.randint(2**database_output_bits) for _ in range(database_length)])
database0 = (database >> 0) & 0xFF
database1 = (database >> 8) & 0xFF
database2 = (database >> 16) & 0xFF
database3 = (database >> 24) & 0xFF
def compile_function_split_database(database0, database1, database2, database3, **kwargs):
database_length = database0.shape[0]
inputset_length = 100
inputset = [
(
make_one_hot_vector(np.random.randint(database_length), database_length),
database0,
database1,
database2,
database3,
)
for _ in range(inputset_length)
]
compiler = fhe.Compiler(
get_ith_element_of_database,
{
"one_hot_vector": "encrypted",
"database0": "clear",
"database1": "clear",
"database2": "clear",
"database3": "clear",
},
)
circuit = compiler.compile(inputset, **kwargs)
return circuit
circuit = compile_function_split_database(
database0, database1, database2, database3, show_mlir=False, show_graph=False
)
def test_encrypted_queries_split_database(database, circuit, how_many_tests=1, verbose=True):
times = []
for _ in range(how_many_tests):
database_length = database.shape[0]
log_database_length = np.ceil(np.log2(database_length)).astype(np.int32)
# Random index in the database
random_index = np.random.randint(database_length)
# Turn it into one hot vector
x = make_one_hot_vector(random_index, database_length)
# Encrypt the query, on the client side
encrypted_x, _, _, _, _ = circuit.encrypt(x, None, None, None, None)
# Run the FHE computation on the server side
time_begin = time.time()
encrypted_y = circuit.run(encrypted_x, database0, database1, database2, database3)
time_end = time.time()
times.append(time_end - time_begin)
if verbose:
print(
f"FHE computation done in {(time_end - time_begin) * 1000:.1f} milliseconds -- "
f"database is {database_length} (2**{log_database_length}) elements of "
f"{database_output_bits} bits"
)
# Decrypt the result on the client side
y_bits = circuit.decrypt(encrypted_y)
# Relinearize the result
y = y_bits[0] + y_bits[1] * 256 + y_bits[2] * 256**2 + y_bits[3] * 256**3
# And check the computations worked fine
assert (
y == database[random_index]
), (
f"{y} {y:x} {y_bits} {y_bits[0]:x} {y_bits[1]:x} {y_bits[2]:x} "
f"{y_bits[3]:x} {database[random_index]:x}"
)
return times
# Benchmark
times = test_encrypted_queries_split_database(
database, circuit, how_many_tests=how_many_tests, verbose=False
)
print(
f"For a database of 2**{str(database_input_bits):>2s} elements of "
f"{str(database_output_bits):>2s} bits with 4 sub-databases, "
f"average execution time is {1000 * np.mean(times):.1f} milliseconds"
)For a database of 2**14 elements of 32 bits with 4 sub-databases, average execution time is 276.4 milliseconds
In [ ]:
# Finding the best combination
def find_best_combination(expected_total_bits):
best_combination = None
for database_input_bits, database_output_bits in timings_dic.keys():
remaining_bits = expected_total_bits - database_input_bits
assert remaining_bits > 0
number_of_subdatabases = np.ceil(2**remaining_bits / database_output_bits).astype(np.int32)
estimated_time = np.ceil(
number_of_subdatabases * timings_dic[(database_input_bits, database_output_bits)]
)
print(
f"Estimated time would be {str(estimated_time):>8s} seconds for "
f"{str(number_of_subdatabases):>8s} DBs of "
f"{(database_input_bits, database_output_bits)}"
)
if best_combination is None or estimated_time < best_combination[0]:
best_combination = (
estimated_time,
number_of_subdatabases,
database_input_bits,
database_output_bits,
)
print(
f"\nBest combination: {best_combination[0]} seconds for a DB of "
f"{expected_total_bits} bits\n"
)
find_best_combination(30)
find_best_combination(20)Estimated time would be 10070.0 seconds for 8388608 DBs of (4, 8) Estimated time would be 5356.0 seconds for 4194304 DBs of (4, 16) Estimated time would be 2362.0 seconds for 524288 DBs of (8, 8) Estimated time would be 1308.0 seconds for 262144 DBs of (8, 16) Estimated time would be 1775.0 seconds for 262144 DBs of (9, 8) Estimated time would be 1757.0 seconds for 131072 DBs of (9, 16) Estimated time would be 2135.0 seconds for 262144 DBs of (10, 4) Estimated time would be 1740.0 seconds for 131072 DBs of (10, 8) Estimated time would be 2372.0 seconds for 65536 DBs of (12, 4) Estimated time would be 1806.0 seconds for 32768 DBs of (12, 8) Estimated time would be 2564.0 seconds for 16384 DBs of (14, 4) Estimated time would be 1814.0 seconds for 8192 DBs of (14, 8) Best combination: 1308.0 seconds for a DB of 30 bits Estimated time would be 10.0 seconds for 8192 DBs of (4, 8) Estimated time would be 6.0 seconds for 4096 DBs of (4, 16) Estimated time would be 3.0 seconds for 512 DBs of (8, 8) Estimated time would be 2.0 seconds for 256 DBs of (8, 16) Estimated time would be 2.0 seconds for 256 DBs of (9, 8) Estimated time would be 2.0 seconds for 128 DBs of (9, 16) Estimated time would be 3.0 seconds for 256 DBs of (10, 4) Estimated time would be 2.0 seconds for 128 DBs of (10, 8) Estimated time would be 3.0 seconds for 64 DBs of (12, 4) Estimated time would be 2.0 seconds for 32 DBs of (12, 8) Estimated time would be 3.0 seconds for 16 DBs of (14, 4) Estimated time would be 2.0 seconds for 8 DBs of (14, 8) Best combination: 2.0 seconds for a DB of 20 bits