Extensions

This document introduces some extensions of Concrete, including functions for wrapping univariate and multivariate functions, performing convolution and maxpool operations, creating encrypted arrays, and more.

fhe.univariate(function)

Wraps any univariate function into a single table lookup:

import numpy as np
from concrete import fhe

def complex_univariate_function(x):

    def per_element(element):
        result = 0
        for i in range(element):
            result += i
        return result

    return np.vectorize(per_element)(x)

@fhe.compiler({"x": "encrypted"})
def f(x):
    return fhe.univariate(complex_univariate_function)(x)

inputset = [np.random.randint(0, 5, size=(3, 2)) for _ in range(10)]
circuit = f.compile(inputset)

sample = np.array([
    [0, 4],
    [2, 1],
    [3, 0],
])
assert np.array_equal(circuit.encrypt_run_decrypt(sample), complex_univariate_function(sample))

fhe.multivariate(function)

Wraps any multivariate function into a table lookup:

fhe.conv(...)

Perform a convolution operation, with the same semantic as onnx.Conv:

fhe.maxpool(...)

Perform a maxpool operation, with the same semantic as onnx.MaxPool:

fhe.array(...)

Create encrypted arrays:

fhe.zero()

Create an encrypted scalar zero:

fhe.zeros(shape)

Create an encrypted tensor of zeros:

fhe.one()

Create an encrypted scalar one:

fhe.ones(shape)

Create an encrypted tensor of ones:

fhe.constant(value)

Allows you to create an encrypted constant of a given value.

This extension is also compatible with constant arrays.

fhe.hint(value, **kwargs)

Hint properties of a value. Imagine you have this circuit:

You'd expect all of a, b, and c to be 8-bits, but because inputset is very small, this code could print:

The first solution in these cases should be to use a bigger inputset, but it can still be tricky to solve with the inputset. That's where the hint extension comes into play. Hints are a way to provide extra information to compilation process:

  • Bit-width hints are for constraining the minimum number of bits in the encoded value. If you hint a value to be 8-bits, it means it should be at least uint8 or int8.

To fix f using hints, you can do:

you'll always see:

regardless of the bounds.

Alternatively, you can use it to make sure a value can store certain integers:

fhe.relu(value)

Perform ReLU operation, with the same semantic as x if x >= 0 else 0:

ReLU Conversion methods

The ReLU operation can be implemented in two ways:

  • Single TLU (Table Lookup Unit) on the original bit-width: Suitable for small bit-widths, as it requires fewer resources.

  • Multiple TLUs on smaller bit-widths: Better for large bit-widths, avoiding the high cost of a single large TLU.

Configuration options

The method of conversion is controlled by the relu_on_bits_threshold: int = 7 option. For example, setting relu_on_bits_threshold=5 means:

  • Bit-widths from 1 to 4 will use a single TLU.

  • Bit-widths of 5 and above will use multiple TLUs.

Another option to fine-tune the implementation is relu_on_bits_chunk_size: int = 2. For example, setting relu_on_bits_chunk_size=4 means that when using second implementation (using chunks), the input is split to 4-bit chunks using fhe.bits, and then the ReLU is applied to those chunks, which are then combined back.

Here is a script showing how execution cost is impacted when changing these values:

You might need to run the script twice to avoid crashing when plotting.

The script will show the following figure:

The default values of these options are set based on simple circuits. How they affect performance will depend on the circuit, so play around with them to get the most out of this extension.

fhe.if_then_else(condition, x, y)

Perform ternary if operation, with the same semantic as x if condition else y:

fhe.if_then_else is just an alias for np.where.

fhe.identity(value)

Copy the value:

The fhe.identity extension is useful for cloning an input with a different bit-width.

fhe.refresh(value)

It is similar to fhe.identity but with the extra guarantee that encryption noise is refreshed.

Refresh is useful when you want to control precisely where encryption noise is refreshed in your circuit. For instance if you are using modules, sometimes compilation rejects the module because it's not composable. This happens because a function of the module never refresh the encryption noise. Adding a return fhe.refresh(result) on the function result solves the issue.

fhe.inputset(...)

Create a random inputset with the given specifications:

The result will have 100 inputs by default which can be customized using the size keyword argument:

Last updated

Was this helpful?