A simple way to say that a function f
should be compiled such that its outputs can be reused as inputs is to use the composable
configuration setting to True
when compiling. Doing so, we can then easily compute f(f(x))
or f**i(x) = f(f(...(f(x) ..))
for a variable non-encrypted integer i
, which is typically what happens for recursions.
Remark that this option is the equivalent of using the fhe.AllComposable
policy of modules. In particular, the same limitations may occur (see limitations documentation section).
This document explains how to compile Fully Homomorphic Encryption (FHE) modules containing multiple functions using Concrete.
Deploying a server that contains many compatible functions is important for some use cases. With Concrete, you can compile FHE modules containing as many functions as needed.
These modules support the composition of different functions, meaning that the encrypted result of one function can be used as the input for another function without needing to decrypt it first. Additionally, a module is deployed in a single artifact, making it as simple to use as a single-function project.
The following example demonstrates how to create an FHE module:
Then, you can compile the FHE module Counter
using the compile
method. To do that, you need to provide a dictionary of input-sets for every function:
After the module is compiled, you can encrypt and call the different functions as follows:
Composition is not limited to single input / single output. Here is an example that computes the 10 first elements of the Fibonacci sequence in FHE:
Executing this script will provide the following output:
With the previous example, we see that modules allow iteration with cleartext iterands to some extent. Specifically, loops with the following structure are supported:
With this pattern, we can also support unbounded loops or complex dynamic condition, as long as this condition is computed in pure cleartext python. Here is an example that computes the Collatz sequence:
This script prints the following output:
In this example, a while loop iterates until the decrypted value equals 1. The loop body is implemented in FHE, but the iteration control must be in cleartext.
By default, when using modules, all inputs and outputs of every function are compatible, sharing the same precision and crypto-parameters. This approach applies the crypto-parameters of the most costly code path to all code paths. This simplicity may be costly and unnecessary for some use cases.
To optimize runtime, we provide finer-grained control over the composition policy via the composition
module attribute. Here is an example:
You have 3 options for the composition
attribute:
fhe.AllComposable
(default): This policy ensures that all ciphertexts used in the module are compatible. It is the least restrictive policy but the most costly in terms of performance.
fhe.NotComposable
: This policy is the most restrictive but the least costly. It is suitable when you do not need any composition and only want to pack multiple functions in a single artifact.
fhe.Wired
: This policy allows you to define custom composition rules. You can specify which outputs of a function can be forwarded to which inputs of another function.
Here is an example:
In this case, the policy states that the first output of the collatz
function can be forwarded to the first input of collatz
, but not the second output (which is decrypted every time, and used for control flow).
You can use the fhe.Wire
between any two functions. It is also possible to define wires with fhe.AllInputs
and fhe.AllOutputs
ends. For instance, in the previous example:
This policy would be equivalent to using the fhe.AllComposable
policy.
Depending on the functions, composition may add a significant overhead compared to a non-composable version.
To be composable, a function must meet the following condition: every output that can be forwarded as input (according to the composition policy) must contain a noise-refreshing operation. Since adding a noise refresh has a noticeable impact on performance, Concrete does not automatically include it.
For instance, to implement a function that doubles an encrypted value, you might write:
This function is valid with the fhe.NotComposable
policy. However, if compiled with the fhe.AllComposable
policy, it will raise a RuntimeError: Program cannot be composed: ...
, indicating that an extra Programmable Bootstrapping (PBS) step must be added.
To resolve this and make the circuit valid, add a PBS at the end of the circuit:
In various cases, deploying a server that contains many compatible functions is important. By compatible, we mean that the functions will be used together, with outputs of some of them being used as inputs of some other ones, without decryption in the middle. It also encompasses the use of recursive functions.
To support this feature in Concrete, we have two ways:
using the composable
flag in the compilation, when there is a unique function. This option is described in this document
using the Concrete modules, when there are several functions, or when there is a unique function for which we want to more precisely detail how outputs are reused as further inputs. This functionality is described in this document