Concrete v2.5: Multiple-Outputs and Iterative Functions, TFHE-rs Under the Hood, and New Truncate-PBS Operator

January 19, 2024
Quentin Bourgerie

This new version of Concrete introduces support for multi-output functions and iterative use in loops, alongside new operators for quicker bit-operation management. Additionally, it marks the completion of TFHE-rs integration into the compiler.

Support for multiple-output

Before, compiling a function with several outputs wasn't an option. Developers often had to combine these outputs into a single tensor, which wasn't user-friendly and required additional coding on both the server and client sides. More importantly, this process forced the compiler to seek a shared encoding for all outputs, potentially leading to less-than-ideal parameters. Recognizing this, one of the most frequent requests from the community has been for multi-output support.

Concrete v2.5 is boosting productivity and enhancing program speed by introducing this much-anticipated feature. Here's a simple example to illustrate how it now efficiently compiles:

from concrete import fhe

@fhe.compiler({"x": "encrypted", "y": "encrypted"})
def f(x, y):
  return x**2, y/2

circuit = f.compile([(x, y) for x in range(16) for y in range(16)])

(x, y) = circuit.encrypt_run_decrypt(8, 8)
assert (x, y) == (64, 4)

An advanced example demonstrating this new feature is showcased in the FHE training from the last Concrete ML release v1.4.

Support for composition

Concrete compilers efficiently balance security, accuracy, and optimal cryptographic parameters, considering the unique demands of the computation flow. Specifically, encrypted inputs typically have less encryption noise compared to outputs. This implies that reusing an output without re-encrypting it could lead to a higher likelihood of incorrect computations than initially specified (or the default probability, if not specified).

Concrete v2.5 introduces a new feature that signals the optimizer that the compiled function is designed to be self-composable.

from concrete import fhe
import numpy as np

@fhe.compiler({"x": "encrypted"})
def f(x):
   return (x + 1) % 15

# composable flag indicates that the function should be composable with itself.
circuit = f.compile([i for i in range(0, 15)], composable=True)
x = 6
x_enc = circuit.encrypt(x)

for i in range(100):
   x = f(x)
   x_enc = circuit.run(x_enc)
   x_dec = circuit.decrypt(x_enc)
   assert x == x_dec

This feature enables iteration over an encrypted state, a key aspect in encrypted database applications.

While the introduction of composability is a significant step, it's important to acknowledge its current limitations. For instance, it's not yet possible to compose a set of functions, nor can you specify that only certain inputs/outputs should be compatible. We encourage you to explore this new capability and share your feedback. Enhanced support and functionalities are planned for upcoming releases.

Faster bit operations and truncate PBS operator

Concrete Compiler now introduces a new primitive for swift least significant bit (LSB) extraction and bitwidth casting. This addition enhances frontends, enabling faster and more versatile bit manipulation than previously possible. Utilizing these advanced compiler primitives, Concrete Python in v2.5 brings forth new operators.

Firstly, Concrete v2.5 includes a truncate operator. This new tool enables developers to accelerate FHE evaluations by reducing the bit width of encrypted integers, similar to the previously introduced rounding feature. Comprehensive documentation on the rounding operator, including its usage and benefits, can be found here.

Next, Concrete in v2.5 introduces an operator that enables the extraction of a specific subset of bits from an encrypted integer. The accompanying code example demonstrates how to select and extract these bits. A key advantage of this operator is its speed, being much faster than similar functionalities achieved through table lookup. Complete documentation for this feature is available here.

from concrete import fhe

@fhe.compiler({"x": "encrypted"})
def f(x):
   return fhe.bits(x)[1:4]

inputset = range(32)
circuit = f.compile(inputset)

assert circuit.encrypt_run_decrypt(0b_01101) == 0b_110
assert circuit.encrypt_run_decrypt(0b_01011) == 0b_101

As highlighted earlier, the new bit extraction feature in Concrete v2.5 outpaces the equivalent table lookup method. This speed advantage suggests many operators could leverage this for faster performance. However, not all operators that could benefit from bit extraction have been updated in this release. These refinements are planned for future releases, so anticipate further speed enhancements soon.

TFHE-rs integration and key compression

Before v2.5, Concrete's CPU backend was 'concrete-cpu', utilizing Zama TFHE primitives. Starting with v2.5, 'concrete-cpu' has transitioned from being a direct implementation of these primitives to serving as a facade for TFHE-rs. This significant shift in our CPU backend strategy aims to streamline and capitalize on Zama's advancements in speeding up cryptographic primitives and introducing new features. As a result of this change, you can anticipate faster key generation and homomorphic evaluation. Additionally, it brings a novel feature: seeded key compression, which can be activated with a single compilation flag.

from concrete import fhe
import numpy as np

table = fhe.LookupTable([i for i in range(0, 32)])

@fhe.compiler({"x": "encrypted"})
def f(x):
   return table[x]

circuit = f.compile([i for i in range(0, 32)], compress_evaluation_keys=False)
print(
   "evaluation keys length without compression",
   len(circuit.client.evaluation_keys.serialize()),
)

circuit = f.compile([i for i in range(0, 32)], compress_inputs=True)
print(
   "evaluation keys length with compression",
   len(circuit.client.evaluation_keys.serialize()),
)

With the new key compression feature, you'll see significant bandwidth savings during the transfer of evaluation keys between the client and server. However, it's important to note that decompression, which occurs on the server side before evaluation, is a necessary step in this process.

Additional links

Read more related posts