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))The wrapped function must follow these criteria:
No side effects: For example, no modification of global state
Deterministic: For example, no random number generation.
Shape consistency:
output.shapeshould be the same withinput.shapeElement-wise mapping: Each output element must correspond to a single input element, for example.
output[0]should only depend oninput[0]of all inputs.
Violating these constraints may result in undefined outcome.
fhe.multivariate(function)
Wraps any multivariate function into a table lookup:
The wrapped functions must follow these criteria:
No side effects: For example, avoid modifying global state.
Deterministic: For example, no random number generation.
Broadcastable shapes:
input.shapeshould be broadcastable tooutput.shapefor all inputs.Element-wise mapping: Each output element must correspond to a single input element, for example,
output[0]should only depend oninput[0]of all inputs.
Violating these constraints may result in undefined outcome.
Multivariate functions cannot be called with rounded inputs.
fhe.conv(...)
Perform a convolution operation, with the same semantic as onnx.Conv:
Only 2D convolutions without padding and with one group are currently supported.
fhe.maxpool(...)
Perform a maxpool operation, with the same semantic as onnx.MaxPool:
Only 2D maxpooling without padding and up to 15-bits is currently supported.
fhe.array(...)
Create encrypted arrays:
Currently, only scalars can be used to create 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
uint8orint8.
To fix f using hints, you can do:
Hints are only applied to the value being hinted, and no other value. If you want the hint to be applied to multiple values, you need to hint all of them.
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:
The script will show the following figure:

Conversion with the second method (using chunks) only works in Native encoding, which is usually selected when all table lookups in the circuit are below or equal to 8 bits.
fhe.if_then_else(condition, x, y)
Perform ternary if operation, with the same semantic as x if condition else y:
fhe.identity(value)
Copy the value:
Identity extension only works in Native encoding, which is usually selected when all table lookups in the circuit are below or equal to 8 bits.
fhe.refresh(value)
It is similar to fhe.identity but with the extra guarantee that encryption noise is refreshed.
Refresh extension only works in Native encoding, which is usually selected when all table lookups in the circuit are below or equal to 8 bits.
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?