In this tutorial, we will review how to perform direct table lookups in Concrete .
Direct table lookup
Concrete provides a LookupTable
class to create your own tables and apply them in your circuits.
LookupTable
s can have any number of elements. Let's call the number of elements N . As long as the lookup variable is within the range [-N , N ), the Table Lookup is valid.
If you go outside of this range, you will receive the following error:
Copy IndexError: index 10 is out of bounds for axis 0 with size 6
With scalars.
You can create the lookup table using a list of integers and apply it using indexing:
Copy from concrete import fhe
table = fhe . LookupTable ([ 2 , - 1 , 3 , 0 ])
@fhe . compiler ({ "x" : "encrypted" })
def f ( x ):
return table [ x ]
inputset = range ( 4 )
circuit = f . compile (inputset)
assert circuit . encrypt_run_decrypt ( 0 ) == table [ 0 ] == 2
assert circuit . encrypt_run_decrypt ( 1 ) == table [ 1 ] == - 1
assert circuit . encrypt_run_decrypt ( 2 ) == table [ 2 ] == 3
assert circuit . encrypt_run_decrypt ( 3 ) == table [ 3 ] == 0
With tensors.
When you apply a table lookup to a tensor, the scalar table lookup is applied to each element of the tensor:
Copy from concrete import fhe
import numpy as np
table = fhe . LookupTable ([ 2 , - 1 , 3 , 0 ])
@fhe . compiler ({ "x" : "encrypted" })
def f ( x ):
return table [ x ]
inputset = [np . random . randint ( 0 , 4 , size = ( 2 , 3 )) for _ in range ( 10 ) ]
circuit = f . compile (inputset)
sample = [
[ 0 , 1 , 3 ] ,
[ 2 , 3 , 1 ] ,
]
expected_output = [
[ 2 , - 1 , 0 ] ,
[ 3 , 0 , - 1 ] ,
]
actual_output = circuit . encrypt_run_decrypt (np. array (sample))
for i in range ( 2 ):
for j in range ( 3 ):
assert actual_output [ i ] [j] == expected_output [ i ] [j] == table [ sample [ i ] [j] ]
With negative values.
LookupTable
mimics array indexing in Python, which means if the lookup variable is negative, the table is looked up from the back:
Copy from concrete import fhe
table = fhe . LookupTable ([ 2 , - 1 , 3 , 0 ])
@fhe . compiler ({ "x" : "encrypted" })
def f ( x ):
return table [ - x ]
inputset = range ( 1 , 5 )
circuit = f . compile (inputset)
assert circuit . encrypt_run_decrypt ( 1 ) == table [ - 1 ] == 0
assert circuit . encrypt_run_decrypt ( 2 ) == table [ - 2 ] == 3
assert circuit . encrypt_run_decrypt ( 3 ) == table [ - 3 ] == - 1
assert circuit . encrypt_run_decrypt ( 4 ) == table [ - 4 ] == 2
Direct multi-table lookup
If you want to apply a different lookup table to each element of a tensor, you can have a LookupTable
of LookupTable
s:
Copy from concrete import fhe
import numpy as np
squared = fhe . LookupTable ([i ** 2 for i in range ( 4 )])
cubed = fhe . LookupTable ([i ** 3 for i in range ( 4 )])
table = fhe . LookupTable ([
[squared, cubed],
[squared, cubed],
[squared, cubed],
])
@fhe . compiler ({ "x" : "encrypted" })
def f ( x ):
return table [ x ]
inputset = [np . random . randint ( 0 , 4 , size = ( 3 , 2 )) for _ in range ( 10 ) ]
circuit = f . compile (inputset)
sample = [
[ 0 , 1 ] ,
[ 2 , 3 ] ,
[ 3 , 0 ] ,
]
expected_output = [
[ 0 , 1 ] ,
[ 4 , 27 ] ,
[ 9 , 0 ]
]
actual_output = circuit . encrypt_run_decrypt (np. array (sample))
for i in range ( 3 ):
for j in range ( 2 ):
if j == 0 :
assert actual_output [ i ] [j] == expected_output [ i ] [j] == squared [ sample [ i ] [j] ]
else :
assert actual_output [ i ] [j] == expected_output [ i ] [j] == cubed [ sample [ i ] [j] ]
In this example, we applied a squared
table to the first column and a cubed
table to the second column.
Fused table lookup
Concrete tries to fuse some operations into table lookups automatically so that lookup tables don't need to be created manually:
Copy from concrete import fhe
import numpy as np
@fhe . compiler ({ "x" : "encrypted" })
def f ( x ):
return ( 42 * np . sin (x) ) . astype (np.int64) // 10
inputset = range ( 8 )
circuit = f . compile (inputset)
for x in range ( 8 ):
assert circuit . encrypt_run_decrypt (x) == f (x)
All lookup tables need to be from integers to integers. So, without .astype(np.int64)
, Concrete will not be able to fuse.
The function is first traced into:
Concrete then fuses appropriate nodes:
Fusing makes the code more readable and easier to modify, so try to utilize it over manual LookupTable
s as much as possible.