arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 59

2.5

Loading...

Getting Started

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Application Tutorials

Loading...

Loading...

Loading...

How To

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Explanations

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Developer

Loading...

Loading...

Loading...

Loading...

What is Concrete?

📁 Githubarrow-up-right | 💛 Community supportarrow-up-right | 🟨 Zama Bounty Programarrow-up-right

Concrete is an open source framework which simplifies the use of Fully Homomorphic Encryption (FHE).

FHE is a powerful cryptographic tool, allowing computation to be performed directly on encrypted data without needing to decrypt it. With FHE, you can build services that preserve privacy for all users. FHE also offers ideal protection against data breaches as everything is done on encrypted data. Even if the server is compromised, no sensitive data is leaked.

Since writing FHE programs is a difficult task, Concrete framework contains a TFHE Compiler based on LLVMarrow-up-right to make this process easier for developers.

hashtag
Organization of this documentation

This documentation is split into several sections:

  • Getting Started gives you the basics,

  • Tutorials provides essential examples on various features of the library,

  • How to helps you perform specific tasks,

hashtag
Looking for support? Ask our team!

  • Support forum: (we answer in less than 24 hours).

  • Live discussion on the FHE.org discord server: (inside the #concrete channel).

  • Do you have a question about Zama? Write us on or send us an email at: [email protected]

hashtag
How is Concrete different from Concrete Numpy?

Concrete Numpy was the former name of the Python frontend of the Concrete Compiler. Concrete Compiler is now open source, and the package name is updated from concrete-numpy to concrete-python (as concrete is already booked for a non FHE-related project).

Users from Concrete Numpy can safely update to Concrete, with a few required changes, as explained in the .

hashtag
How is it different from the previous version of Concrete?

Before v1.0, Concrete was a set of Rust libraries implementing Zama's variant of TFHE. Starting with v1, Concrete is now Zama's TFHE Compiler framework only. The Rust library is now called .

Installation

Concrete is natively supported on Linux and macOS from Python 3.8 to 3.11 inclusive. If you have Docker in your platform, you can use the docker image to use Concrete.

hashtag
Using PyPI

You can install Concrete from PyPI:

pip install -U pip wheel setuptools
pip install concrete-python

hashtag
Using Docker

You can also get the Concrete docker image:

Developer explains the inner workings of the library and everything related to contributing to the project.

https://community.zama.aiarrow-up-right
https://discord.fhe.orgarrow-up-right
Twitterarrow-up-right
upgrading documentarrow-up-right
TFHE-rsarrow-up-right
docker pull zamafhe/concrete-python:v2.0.0
docker run --rm -it zamafhe/concrete-python:latest /bin/bash

Basics of FHE programs

hashtag
Operations on encrypted values

The idea of homomorphic encryption is that you can compute on ciphertexts without knowing the messages they encrypt. A scheme is said to be fully homomorphicarrow-up-right, if an unlimited number of additions and multiplications are supported (xxx is a plaintext and E[x]E[x]E[x] is the corresponding ciphertext):

  • homomorphic addition:

  • homomorphic multiplication:

hashtag
Noise and Bootstrap

FHE encrypts data as LWE ciphertexts. These ciphertexts can be visually represented as a bit vector with the encrypted message in the higher-order (yellow) bits as well as a random part (gray), that guarantees the security of the encrypted message, called noise.

Under the hood, each time you perform an operation on an encrypted value, the noise grows and at a certain point, it may overlap with the message and corrupt its value.

There is a way to decrease the noise of a ciphertext with the Bootstrap operation. The bootstrap operation takes as input a noisy ciphertext and generates a new ciphertext encrypting the same message, but with a lower noise. This allows additional operations to be performed on the encrypted message.

A typical FHE program will be made up of a series of operations followed by a Bootstrap, this is then repeated many times.

hashtag
Probability of Error

The amount of noise in a ciphertext is not as bounded as it may appear in the above illustration. As the errors are drawn randomly from a Gaussian distribution, they can be of varying size. This means that we need to be careful to ensure the noise terms do not affect the message bits. If the error terms do overflow into the message bits, this can cause an incorrect output (failure) when bootstrapping.

The default failure probability in Concrete is set for the whole program and is by default. This means that 1 execution of every 100,000 may result in an incorrect output. To have a lower probability of error, you need to change the cryptographic parameters, likely resulting in worse performance. On the other side of this trade-off, allowing a higher probability of error will likely speed-up operations.

hashtag
Function evaluation

So far, we only introduced arithmetic operations but a typical program usually also involves functions (maximum, minimum, square root…)

During the Bootstrap operation, in TFHE, you could perform a table lookup simultaneously to reduce noise, turning the Bootstrap operation into a Programmable Bootstrap (PBS).

Concrete uses the PBS to support function evaluation:

  • homomorphic univariate function evaluation:

Let's take a simple example. A function (or circuit) that takes a 4 bits input variable and output the maximum value between a clear constant and the encrypted input:

example:

could be turned into a table lookup:

The Lookup table lut being applied during the Programmable Bootstrap.

hashtag
PBS management

You should not worry about PBS, they are completely managed by Concrete during the compilation process. Each function evaluation will be turned into a Lookup table and evaluated by a PBS.

See this in action with the previous example, if you dump the MLIR code produced by the frontend, you will see (forget about MLIR syntax, just see the Lookup table value on the 4th line):

The only thing you should keep in mind is that it adds a constraint on the input type, and that is the reason behind having a maximum bit-width supported in Concrete.

Second takeaway is that PBS are the most costly operations in FHE, the less PBS in your circuit the faster it will run. It is an interesting metrics to optimize (you will see that Concrete could give you the number of PBS used in your circuit).

Note also that PBS cost varies with the input variable precision (a circuit with 8 bit PBS will run faster than one with 16 bits PBS).

hashtag
Development Workflow

Allowing computation on encrypted data is particularly interesting in the client/server model, especially when the client data are sensitive and the server not trusted. You could split the workflow in two main steps: development and deployment.

hashtag
Development

During development, you will turn your program into its FHE equivalent. Concrete automates this task with the compilation process but you can make this process even easier by reducing the precision required, reducing the number of PBSs or allowing more parallelization in your code (e.g. working on bit chunks instead of high bit-width variables).

Once happy with the code, the development process is over and you will create the compiler artifact that will be used during deployment.

hashtag
Deployment

A typical Concrete deployment will host on a server the compilation artifact: Client specifications required by the compiled circuits and the fhe executable itself. Client will ask for the circuit requirements, generate keys accordingly, then it will send an encrypted payload and receive an encrypted result.

For more information on deployment, see

Quick Start

To compute on encrypted data, you first need to define the function you want to compute, then compile it into a Concrete Circuit, which you can use to perform homomorphic evaluation.

Here is the full example that we will walk through:

hashtag
Importing the library

Everything you need to perform homomorphic evaluation is included in a single module:

hashtag
Defining the function to compile

In this example, we compile a simple addition function:

hashtag
Creating a compiler

To compile the function, you need to create a Compiler by specifying the function to compile and the encryption status of its inputs:

hashtag
Defining an inputset

An inputset is a collection representing the typical inputs to the function. It is used to determine the bit widths and shapes of the variables within the function.

It should be in iterable, yielding tuples, of the same length as the number of arguments of the function being compiled:

circle-exclamation

All inputs in the inputset will be evaluated in the graph, which takes time. If you're experiencing long compilation times, consider providing a smaller inputset.

hashtag
Compiling the function

You can use the compile method of a Compiler class with an inputset to perform the compilation and get the resulting circuit back:

hashtag
Performing homomorphic evaluation

You can use the encrypt_run_decrypt method of a Circuit class to perform homomorphic evaluation:

circle-info

circuit.encrypt_run_decrypt(*args) is just a convenient way to do everything at once. It is implemented as circuit.decrypt(circuit.run(circuit.encrypt(*args))).

from concrete import fhe

def add(x, y):
    return x + y

compiler = fhe.Compiler(add, {"x": "encrypted", "y": "clear"})

inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)]
circuit = compiler.compile(inputset)

x = 4
y = 4

clear_evaluation = add(x, y)
homomorphic_evaluation = circuit.encrypt_run_decrypt(x, y)

print(x, "+", y, "=", clear_evaluation, "=", homomorphic_evaluation)
from concrete import fhe
def add(x, y):
    return x + y
compiler = fhe.Compiler(add, {"x": "encrypted", "y": "clear"})
inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)]
circuit = compiler.compile(inputset)
homomorphic_evaluation = circuit.encrypt_run_decrypt(4, 4)
E[x]+E[y]=E[x+y]E[x] + E[y] = E[x + y]E[x]+E[y]=E[x+y]
E[x]∗E[y]=E[x∗y]E[x] * E[y] = E[x * y]E[x]∗E[y]=E[x∗y]
1100000\frac{1}{100000}1000001​
f(E[x])=E[f(x)]f(E[x]) = E[f(x)]f(E[x])=E[f(x)]
Howto - Deploy
import numpy as np

def encrypted_max(x: uint4):
    return np.maximum(5, x)
def encrypted_max(x: uint4):
    lut = [5, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
    return lut[x]
module {
  func.func @main(%arg0: !FHE.eint<4>) -> !FHE.eint<4> {
    %cst = arith.constant dense<[5, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]> : tensor<16xi64>
    %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
    return %0 : !FHE.eint<4>
  }
}

Key Value Database

This is an interactive tutorial written as a Jupyter Notebook, which you can find herearrow-up-right.

SHA-256

This is an interactive tutorial written as a Jupyter Notebook, which you can find herearrow-up-right.

Formatting

You can convert your compiled circuit into its textual representation by converting it to string:

str(circuit)

If you just want to see the output on your terminal, you can directly print it as well:

print(circuit)
circle-exclamation

Formatting is just for debugging purposes. It's not possible to create the circuit back from its textual representation. See How to Deploy if that's your goal.

Decorator

If you are trying to compile a regular function, you can use the decorator interface instead of the explicit Compiler interface to simplify your code:

from concrete import fhe

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

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

assert circuit.encrypt_run_decrypt(10) == f(10)
circle-info

This decorator is a way to add the compile method to the function object without changing its name elsewhere.

Multi Precision

Each integer in the circuit has a certain bit-width, which is determined by the inputset. These bit-widths can be observed when graphs are printed:

However, it's not possible to add 3-bit and 4-bit numbers together because their encoding is different:

The result of such an addition is a 5-bit number, which also has a different encoding:

Because of these encoding differences, we perform a graph processing step called bit-width assignment, which takes the graph and updates the bit-widths to be compatible with FHE.

After this graph processing step, the graph would look like:

Most operations cannot change the encoding, which means that the input and output bit-widths need to be the same. However, there is an operation which can change the encoding: the table lookup operation.

Performance

One of the most common operations in Concrete is Table Lookups (TLUs). All operations except addition, subtraction, multiplication with non-encrypted values, tensor manipulation operations, and a few operations built with those primitive operations (e.g. matmul, conv) are converted to Table Lookups under the hood:

is exactly the same as

Table Lookups are very flexible. They allow Concrete to support many operations, but they are expensive. The exact cost depends on many variables (hardware used, error probability, etc.), but they are always much more expensive compared to other operations. You should try to avoid them as much as possible. It's not always possible to avoid them completely, but you might remove the number of TLUs or replace some of them with other primitive operations.

circle-info

Frontend fusing

Fusing is the act of combining multiple nodes into a single node, which is converted to a table lookup.

hashtag
How is it done?

Code related to fusing is in the frontends/concrete-python/concrete/fhe/compilation/utils.py file. Fusing can be performed using the fuse function.

Multi Parameters

Integers in Concrete are encrypted and processed according to a set of cryptographic parameters. By default, multiple sets of such parameters are selected by the Concrete Optimizer. This might not be the best approach for every use case, and there is the option to use mono parameters instead.

When multi parameters are enabled, a different set of parameters are selected for each bit-width in the circuit, which results in:

  • Faster execution (generally).

Simulation

During development, the speed of homomorphic execution can be a blocker for fast prototyping. You could call the function you're trying to compile directly, of course, but it won't be exactly the same as FHE execution, which has a certain probability of error (see ).

To overcome this issue, simulation is introduced:

After the simulation runs, it prints the following:

circle-exclamation

There are some operations which are not supported in simulation yet. They will result in compilation failures. You can revert to simulation using graph execution using

Within fuse:
  1. We loop until there are no more subgraphs to fuse.

  2. Within each iteration: 2.1. We find a subgraph to fuse.

    2.2. We search for a terminal node that is appropriate for fusing.

    2.3. We crawl backwards to find the closest integer nodes to this node.

    2.4. If there is a single node as such, we return the subgraph from this node to the terminal node.

    2.5. Otherwise, we try to find the lowest common ancestor (lca) of this list of nodes.

    2.6. If an lca doesn't exist, we say this particular terminal node is not fusable, and we go back to search for another subgraph.

    2.7. Otherwise, we use this lca as the input of the subgraph and continue with subgraph node creation below.

    2.8. We convert the subgraph into a subgraph node by checking fusability status of the nodes of the subgraph in this step.

    2.9. We substitute the subgraph node to the original graph.

hashtag
Limitations

With the current implementation, we cannot fuse subgraphs that depend on multiple encrypted values where those values don't have a common lca (e.g., np.round(np.sin(x) + np.cos(y))).

Slower key generation.
  • Larger keys.

  • Larger memory usage during execution.

  • To disable it, you can use parameter_selection_strategy=fhe.ParameterSelectionStrategy.MONO configuration option.

    Contribute

    There are two ways to contribute to Concrete. You can:

    • Open issues to report bugs and typos or suggest ideas;

    • Request to become an official contributor by emailing [email protected]. Only approved contributors can send pull requests (PRs), so get in touch before you do.

    Concrete automatically parallelizes TLUs if they are applied to tensors.
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return x ** 2
    
    inputset = range(2 ** 4)
    circuit = f.compile(inputset)
    from concrete import fhe
    
    table = fhe.LookupTable([x ** 2 for x in range(2 ** 4)])
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return table[x]
    
    inputset = range(2 ** 4)
    circuit = f.compile(inputset)
    circuit.graph(...)
    instead of
    circuit.simulate(...)
    , which won't simulate FHE, but it will evaluate the computation graph, which is like simulating the operations without any errors due to FHE.
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return (x + 1) ** 2
    
    inputset = [np.random.randint(0, 10, size=(10,)) for _ in range(10)]
    circuit = f.compile(inputset, p_error=0.1, fhe_simulation=True)
    
    sample = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    
    actual = f(sample)
    simulation = circuit.simulate(sample)
    
    print(actual.tolist())
    print(simulation.tolist())
    [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
    [1, 4, 9, 16, 16, 36, 49, 64, 81, 100]
    Exactness

    Let's say you have this graph:

    This is the graph for (x**2) + y where x is 2-bits and y is 5-bits. If the table lookup operation wasn't able to change the encoding, we'd need to make everything 6-bits. However, since the encoding can be changed, the bit-widths can be assigned like so:

    In this case, we kept x as 2-bits, but set the table lookup result and y to be 6-bits, so that the addition can be performed.

    This style of bit-width assignment is called multi-precision, and it is enabled by default. To disable it and use a single precision across the circuit, you can use the single_precision=True configuration option.

    %0 = x                  # EncryptedScalar<uint3>              ∈ [0, 7]
    %1 = y                  # EncryptedScalar<uint4>              ∈ [0, 15]
    %2 = add(%0, %1)        # EncryptedScalar<uint5>              ∈ [2, 22]
    return %2                                     ^ these are       ^^^^^^^
                                                    the assigned    based on
                                                    bit-widths      these bounds
    D: data
    N: noise
    
    3-bit number
    ------------
    D2 D1 D0 0 0 0 ... 0 0 0 N N N N
    
    4-bit number
    ------------
    D3 D2 D1 D0 0 0 0 ... 0 0 0 N N N N
    5-bit number
    ------------
    D4 D3 D2 D1 D0 0 0 0 ... 0 0 0 N N N N
    %0 = x                  # EncryptedScalar<uint5>
    %1 = y                  # EncryptedScalar<uint5>
    %2 = add(%0, %1)        # EncryptedScalar<uint5>
    return %2
    %0 = x                    # EncryptedScalar<uint2>        ∈ [0, 3]
    %1 = y                    # EncryptedScalar<uint5>        ∈ [0, 31]
    %2 = 2                    # ClearScalar<uint2>            ∈ [2, 2]
    %3 = power(%0, %2)        # EncryptedScalar<uint4>        ∈ [0, 9]
    %4 = add(%3, %1)          # EncryptedScalar<uint6>        ∈ [1, 39]
    return %4
    %0 = x                    # EncryptedScalar<uint2>        ∈ [0, 3]
    %1 = y                    # EncryptedScalar<uint6>        ∈ [0, 31]
    %2 = 2                    # ClearScalar<uint2>            ∈ [2, 2]
    %3 = power(%0, %2)        # EncryptedScalar<uint6>        ∈ [0, 9]
    %4 = add(%3, %1)          # EncryptedScalar<uint6>        ∈ [1, 39]
    return %4

    Exactness

    One of the most common operations in Concrete is Table Lookups (TLUs). TLUs are performed with an FHE operation called Programmable Bootstrapping (PBS). PBS's have a certain probability of error, which, when triggered, result in inaccurate results.

    Let's say you have the table:

    lut = [0, 1, 4, 9, 16, 25, 36, 49, 64]

    And you perform a Table Lookup using 4. The result you should get is lut[4] = 16, but because of the possibility of error, you could get any other value in the table.

    The probability of this error can be configured through the p_error and global_p_error configuration options. The difference between these two options is that, p_error is for individual TLUs but global_p_error is for the whole circuit.

    If you set p_error to 0.01, for example, it means every TLU in the circuit will have a 99% chance of being exact with a 1% probability of error. If you have a single TLU in the circuit, global_p_error would be 1% as well. But if you have 2 TLUs for example, global_p_error would be almost 2% (1 - (0.99 * 0.99)).

    However, if you set global_p_error to 0.01, the whole circuit will have 1% probability of error, no matter how many Table Lookups are included.

    If you set both of them, both will be satisfied. Essentially, the stricter one will be used.

    By default, both p_error and global_p_error is set to None, which results in a global_p_error of 1 / 100_000 being used.

    Feel free to play with these configuration options to pick the one best suited for your needs! See to learn how you can set a custom p_error and/or global_p_error.

    circle-info

    Configuring either of those variables impacts computation time (compilation, keys generation, circuit execution) and space requirements (size of the keys on disk and in memory). Lower error probabilities would result in longer computation times and larger space requirements.

    Terminology and Structure

    hashtag
    Terminology

    Some terms used throughout the project include:

    • computation graph: A data structure to represent a computation. This is basically a directed acyclic graph in which nodes are either inputs, constants, or operations on other nodes.

    • tracing: A technique that takes a Python function from the user and generates a corresponding computation graph.

    • bounds: Before computation graphs are converted to MLIR, we need to know which value should have which type (e.g., uint3 vs int5). We use inputsets for this purpose. We simulate the graph with the inputs in the inputset to remember the minimum and the maximum value for each node, which is what we call bounds, and use bounds to determine the appropriate type for each node.

    • circuit: The result of compilation. A circuit is made of the client and server components. It has methods for everything from printing to evaluation.

    hashtag
    Module structure

    In this section, we briefly discuss the module structure of Concrete Python. You are encouraged to check individual .py files to learn more.

    • concrete

      • fhe

        • dtypes: data type specifications (e.g., int4, uint5, float32)

    Manage Keys

    Concrete generates keys for you implicitly when they are needed and if they have not already been generated. This is useful for development, but it's not flexible (or secure!) for production. Explicit key management API is introduced to be used in such cases to easily generate and re-use keys.

    hashtag
    Definition

    Let's start by defining a circuit:

    Circuits have a property called keys

    Game of Life

    In the associated , you can run the Game of Life, written in Concrete Python.

    hashtag
    Installation

    In addition to Concrete, you must install pygame in your virtual environment:

    Automatic Crypto Parameters choice

    concrete-optimizer is a tool that selects appropriate cryptographic parameters for a given fully homomorphic encryption (FHE) computation. These parameters have an impact on the security, correctness, and efficiency of the computation.

    The computation is guaranteed to be secure with the given level of security (see for details) which is typically 128 bits. The correctness of the computation is guaranteed up to a given failure probability. A surrogate of the execution time is minimized which allows for efficient FHE computation.

    The cryptographic parameters are degrees of freedom in the FHE algorithms (bootstrapping, keyswitching, etc.) that need to be fixed. The search space for possible crypto-parameters is finite but extremely large. The role of the optimizer is to quickly find the most efficient crypto-parameters possible while guaranteeing security and correctness.

    Call FHE circuits from other languages

    After doing a compilation, we end up with a couple of artifacts, including crypto parameters and a binary file containing the executable circuit. In order to be able to encrypt and run the circuit properly, we need to know how to interpret these artifacts, and there are a couple of utility functions which can be used to load them. These utility functions can be accessed through a variety of languages, including Python and C++.

    hashtag
    Demo

    We will use a really simple example for a demo, but the same steps can be done for any other circuit. example.mlir will contain the MLIR below:

    Adding a new backend

    hashtag
    Context

    The concrete backends are implementations of the cryptographic primitives of the Zama variant of .

    There are client features (private and public key generation, encryption and decryption) and server features (homomorphic operations on ciphertexts using public keys).

    Considering that

    Reuse Arguments

    Encryption can take quite some time, memory, and network bandwidth if encrypted data is to be transported. Some applications use the same argument, or a set of arguments as one of the inputs. In such applications, it doesn't make sense to encrypt and transfer the arguments each time. Instead, arguments can be encrypted separately, and reused:

    If you have multiple arguments, the encrypt method would return a tuple, and if you specify None as one of the arguments, None is placed at the same location in the resulting tuple (e.g., circuit.encrypt(a, None, b, c, None) would return (encrypted_a, None, encrypted_b, encrypted_c, None)). Each value returned by

    Project layout

    Concrete is a modular framework composed by sub-projects using different technologies, all having theirs own build system and test suite. Each sub-project have is own README that explain how to setup the developer environment, how to build it and how to run tests commands.

    Concrete is made of 4 main categories of sub-project that are organized in subdirectories from the root of the concrete repo:

    • frontends contains high-level transpilers that target end users developers who want to use the Concrete stack easily from their usual environment. There are for now only one frontend provided by the Concrete project: a Python frontend named concrete-python.

    Tagging

    When you have big circuits, keeping track of which node corresponds to which part of your code becomes difficult. A tagging system can simplify such situations:

    When you compile f with inputset of range(10), you get the following graph:

    If you get an error, you'll see exactly where the error occurred (e.g., which layer of the neural network, if you tag layers).

    circle-info

    Compiler backend

    The concrete backends are implementations of the cryptographic primitives of the Zama variant of . The compiler emits code which combines call into these backends to perform more complex homomorphic operations.

    There are client and server features.

    Client features are:

    • private (G)LWE key generation (currently random bits)

    values: value specifications (i.e., data type + shape + encryption status)

  • representation: representation of computation (e.g., computation graphs, nodes)

  • tracing: tracing of python functions

  • extensions: custom functionality (see Extensions)

  • mlir: computation graph to mlir conversion

  • compilation: configuration, compiler, artifacts, circuit, client/server, and anything else related to compilation

  • compilers contains the sub-projects in charge of actually solving the compilation problem of an high-level abstraction of FHE to an actual executable. concrete-optimizer is a Rust based project that solves the optimization problems of an FHE dag to a TFHE dag and concrete-compiler which use concrete-optimizer is an end-to-end MLIR-based compiler that takes a crypto free FHE dialect and generates compilation artifacts both for the client and the server. concrete-compiler project provide in addition of the compilation engine, a client and server library in order to easily play with the compilation artifacts to implement a client and server protocol.

  • backends contains CAPI that can be called by the concrete-compiler runtime to perform the cryptographic operations. There are currently two backends:

    • concrete-cpu, using TFHE-rs that implement the fastest implementation of TFHE on CPU.

    • concrete-cuda that provides a GPU acceleration of TFHE primitives.

  • tools are basically every other sub-projects that cannot be classified in the three previous categories and which are used as a common support by the others.

  • How to Configure
    hashtag
    Security, Correctness, and Efficiency

    hashtag
    Security

    The security level is chosen by the user. We typically operate at a fixed security level, such as 128 bits, to ensure that there is never a trade-off between security and efficiency. This constraint imposes a minimum amount of noise in all ciphertexts.

    An independent public research tool, the lattice estimatorarrow-up-right, is used to estimate the security level. The lattice estimator is maintained by FHE experts. For a given set of crypto-parameters, this tool considers all possible attacks and returns a security level.

    For each security level, a parameter curve of the appropriate minimal error level is pre-computed using the lattice estimator, and is used as an input to the optimizer. Learn more about the parameter curves here.

    hashtag
    Correctness

    Correctness decreases as the level of noise increases. Noise accumulates during homomorphic computation until it is actively reduced via bootstrapping. Too much noise can lead to the result of a computation being inaccurate or completely incorrect.

    Before optimization, we compute a noise bound that guarantees a given error level (under the assumption that noise growth is correctly managed via bootstrapping). The noise growth depends on a critical quantity: the 2-norm of any dot product (or equivalent) present in the calculus. This 2-norm changes the scale of the noise, so we must reduce it sufficiently for the next dot product operation whenever we reduce the noise.

    The user can control error probability in two ways: via the PBS error probability and the global error probability.

    The PBS error probability controls correctness locally (i.e., represents the error probability of a single PBS operation), while the global error probability focuses on the overall computation result (i.e., represents the error probability of the entire computation). These probabilities are related, and choosing which one to use may depend on the specific use case.

    hashtag
    Efficiency

    Efficiency decreases as more precision is required, e.g. 7-bits versus 8-bits. The larger the 2-norm is, the bigger the noise will be after a dot product. To remain below the noise bound, we must ensure that the inputs to the dot product have a sufficiently small noise level. The smaller this noise is, the slower the previous bootstrapping will be. Therefore, the larger the 2norm is, the slower the computation will be.

    hashtag
    How are the parameters optimized

    The optimization prioritizes security and correctness. This means that the security level (or the probability of correctness) could, in practice, be a bit higher than the level which is requested by the user.

    In the simplest case, the optimizer performs an exhaustive search in the full parameter space and selects the best solution. While the space to explore is huge, exact lower bound cuts are used to avoid exploring regions which are guaranteed to not contain an optimal point. This makes the process both fast and exhaustive. This case is called mono-parameter, where all parameters are shared by the whole computation graph.

    In more complex cases, the optimizer iteratively performs an exhaustive search, with lower bound cuts in a wide subspace of the full parameter space, until it converges to a locally optimal solution. Since the wide subspace is large and multi-dimensional, it should not be trapped in a poor locally optimal solution. The more complex case is called multi-parameter, where different calculus operations have tailored parameters.

    hashtag
    How can I determine, understand, and explore crypto-parameters

    One can have a look at reference crypto-parametersarrow-up-right for each security level (but for a given correctness). This provides insight between the calcululs content (i.e. maximum precision, maximum dot 2-norm, etc.,) and the cost.

    Then one can manually explore crypto-parameters space using a CLI toolarrow-up-right.

    hashtag
    Citing

    If you use this tool in your work, please cite:

    Bergerat, Loris and Boudi, Anas and Bourgerie, Quentin and Chillotti, Ilaria and Ligier, Damien and Orfila Jean-Baptiste and Tap, Samuel, Parameter Optimization and Larger Precision for (T)FHE, Journal of Cryptology, 2023, Volume 36

    A pre-print is available as Cryptology ePrint Archive Paper 2022/704arrow-up-right

    here
    encryption of ciphertexts using a private key
  • public key generation from private keys for keyswitch, bootstrap or private packing

  • (de)serialization of ciphertexts and public keys (also needed server side)

  • Server features are homomorphic operations on ciphertexts:

    • linear operations (multisums with plain weights)

    • keyswitch

    • simple PBS

    • WoP PBS

    There are currently 2 backends:

    • concrete-cpu which implements both client and server features targeting the CPU.

    • concrete-cuda which implements only server features targeting GPUs to accelerate homomorphic circuit evalutation.

    The compiler uses concrete-cpu for the client and can use either concrete-cpu or concrete-cuda for the server.

    TFHEarrow-up-right
    spinner
    encrypt
    can be stored and reused anytime.
    circle-exclamation

    The ordering of the arguments must be kept consistent! Encrypting an x and using it as a y could result in undefined behavior.

    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted", "y": "encrypted"})
    def add(x, y):
        return x + y
    
    inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1), (3, 2), (6, 1), (1, 7), (4, 5), (5, 4)]
    circuit = add.compile(inputset)
    
    sample_y = 4
    _, encrypted_y = circuit.encrypt(None, sample_y)
    
    for sample_x in range(3, 6):
        encrypted_x, _ = circuit.encrypt(sample_x, None)
    
        encrypted_result = circuit.run(encrypted_x, encrypted_y)
        result = circuit.decrypt(encrypted_result)
    
        assert result == sample_x + sample_y
    In the future, we plan to use tags for additional features (e.g., to measure performance of tagged regions), so it's a good idea to start utilizing them for big circuits.
    def g(z):
        with fhe.tag("def"):
            a = 120 - z
            b = a // 4
        return b
    
    
    def f(x):
        with fhe.tag("abc"):
            x = x * 2
            with fhe.tag("foo"):
                y = x + 42
            z = np.sqrt(y).astype(np.int64)
    
        return g(z + 3) * 2
    of type
    fhe.Keys
    , which has several utility functions dedicated to key management!

    hashtag
    Generation

    To explicitly generate keys for a circuit, you can use:

    circle-info

    Generated keys are stored in memory upon generation, unencrypted.

    And it's possible to set a custom seed for reproducibility:

    circle-exclamation

    Do not specify the seed manually in a production environment!

    hashtag
    Serialization

    To serialize keys, say to send it across the network:

    circle-exclamation

    Keys are not serialized in encrypted form! Please make sure you keep them in a safe environment, or encrypt them manually after serialization.

    hashtag
    Deserialization

    To deserialize the keys back, after receiving serialized keys:

    hashtag
    Assignment

    Once you have a valid fhe.Keys object, you can directly assign it to the circuit:

    circle-exclamation

    If assigned keys are generated for a different circuit, an exception will be raised.

    hashtag
    Saving

    You can also use the filesystem to store the keys directly, without needing to deal with serialization and file management yourself:

    circle-exclamation

    Keys are not saved encrypted! Please make sure you store them in a safe environment, or encrypt them manually after saving.

    hashtag
    Loading

    After keys are saved to disk, you can load them back via:

    hashtag
    Automatic Management

    If you want to generate keys in the first run and reuse the keys in consecutive runs:

    pip3 install pygame

    Once done, if you go to frontends/concrete-python/examples/game_of_life, python game_of_life.py --help should give you the manpage:

    hashtag
    Running

    Then, you can play with the different options, and in particular:

    • dimension, to chose the size of the grid; the larger, the slower

    • method, to chose which implementation is used for the grid update

    • log2_global_p_error and log2_p_error, to chose the probability of correctness (see the Concrete documentation for more information)

    • simulate, to do computations only in simulation, i.e., not in FHE

    hashtag
    Typical Executions

    In simulation: python3 game_of_life.py --dimension 100 --refresh_every 50 --simulate

    In FHE: python3 game_of_life.py --dimension 6 --refresh_every 8 --log2_p_error -40 --method method_4b

    hashtag
    Technical Explanations

    A blog is currently in the process of being written, and a link will be added here when it's available. In the meantime, some explanations are given in the code.

    Python filearrow-up-right
    Game of Life
    You can use the concretecompiler binary to compile this MLIR program. Same can be done with concrete-python, as we only need the compilation artifacts at the end.

    You should be able to see artifacts listed in the python-demo directory

    Now we want to use the Python bindings in order to call the compiled circuit.

    The main struct to manage compilation artifacts is LibrarySupport. You will have to create one with the path you used during compilation, then load the result of the compilation

    Using the compilation result, you can load the server lambda (the entrypoint to the executable compiled circuit) as well as the client parameters (containing crypto parameters)

    The client parameters will serve the client to generate keys and encrypt arguments for the circuit

    Only evaluation keys are required for the execution of the circuit. You can execute the circuit on the encrypted arguments via server_lambda_call

    At this point you have the encrypted result and can decrypt it using the keyset which holds the secret key

    There is also a couple of tests in test_compilation.pyarrow-up-right that can show how to both compile and run a circuit between a client and server using serialization.

    func.func @main(%arg0: tensor<4x4x!FHE.eint<6>>, %arg1: tensor<4x2xi7>) -> tensor<4x2x!FHE.eint<6>> {
       %0 = "FHELinalg.matmul_eint_int"(%arg0, %arg1): (tensor<4x4x!FHE.eint<6>>, tensor<4x2xi7>) -> (tensor<4x2x!FHE.eint<6>>)
       %tlu = arith.constant dense<[40, 13, 20, 62, 47, 41, 46, 30, 59, 58, 17, 4, 34, 44, 49, 5, 10, 63, 18, 21, 33, 45, 7, 14, 24, 53, 56, 3, 22, 29, 1, 39, 48, 32, 38, 28, 15, 12, 52, 35, 42, 11, 6, 43, 0, 16, 27, 9, 31, 51, 36, 37, 55, 57, 54, 2, 8, 25, 50, 23, 61, 60, 26, 19]> : tensor<64xi64>
       %result = "FHELinalg.apply_lookup_table"(%0, %tlu): (tensor<4x2x!FHE.eint<6>>, tensor<64xi64>) -> (tensor<4x2x!FHE.eint<6>>)
       return %result: tensor<4x2x!FHE.eint<6>>
    }

    performance improvements are mostly beneficial for the server operations

  • the client needs to be portable for the variety of clients that may exist, we expect mostly server backend to be added to the compiler to improve performance (e.g. by using specialized hardware)

  • hashtag
    What is needed in the server backend

    The server backend should expose C or C++ functions to do TFHE operations using the current ciphertext and key memory representation (or functions to change representation). A backend can support only a subset of the current TFHE operations.

    The most common operations one would be expected to add are WP-PBS (standard TFHE programmable bootstrap), keyswitch and WoP (without padding bootsrap).

    Linear operations may also be supported but may need more work since their introduction may interfere with other compilation passes. The following example does not include this.

    hashtag
    Concrete-cuda example

    We will detail how concrete-cuda is integrated in the compiler. Adding a new server feature backend (for non linear operations) should be quite similar. However, if you want to integrate a backend but it does not fit with this description, please open an issue or contact us to discuss the integration.

    In compilers/concrete-compiler/Makefile

    • the variable CUDA_SUPPORT has been added and set to OFF (CUDA_SUPPORT?=OFF) by default

    • the variables CUDA_SUPPORT and CUDA_PATH are passed to CMake

    In compilers/concrete-compiler/compiler/include/concretelang/Runtime/context.h, the RuntimeContext struct is enriched with state to manage the backend ressources (behind a #ifdef CONCRETELANG_CUDA_SUPPORT).

    In compilers/concrete-compiler/compiler/lib/Runtime/wrappers.cpp, the cuda backend server functions are added (behind a #ifdef CONCRETELANG_CUDA_SUPPORT)

    The pass ConcreteToCAPI is modified to have a flag to insert calls to these new wrappers instead of the cpu ones (the code calling this pass is modified accordingly).

    It may be possible to replace the cpu wrappers (with a compilation flag) instead of adding new ones to avoid having to change the pass.

    In compilers/concrete-compiler/CMakeLists.txt a Section #Concrete Cuda Configuration has been added Other CMakeLists.txt have also been modified (or added) with if(CONCRETELANG_CUDA_SUPPORT) guard to handle header includes, linking...

    TFHEarrow-up-right
     %0 = x                            # EncryptedScalar<uint4>        ∈ [0, 9]
     %1 = 2                            # ClearScalar<uint2>            ∈ [2, 2]            @ abc
     %2 = multiply(%0, %1)             # EncryptedScalar<uint5>        ∈ [0, 18]           @ abc
     %3 = 42                           # ClearScalar<uint6>            ∈ [42, 42]          @ abc.foo
     %4 = add(%2, %3)                  # EncryptedScalar<uint6>        ∈ [42, 60]          @ abc.foo
     %5 = subgraph(%4)                 # EncryptedScalar<uint3>        ∈ [6, 7]            @ abc
     %6 = 3                            # ClearScalar<uint2>            ∈ [3, 3]
     %7 = add(%5, %6)                  # EncryptedScalar<uint4>        ∈ [9, 10]
     %8 = 120                          # ClearScalar<uint7>            ∈ [120, 120]        @ def
     %9 = subtract(%8, %7)             # EncryptedScalar<uint7>        ∈ [110, 111]        @ def
    %10 = 4                            # ClearScalar<uint3>            ∈ [4, 4]            @ def
    %11 = floor_divide(%9, %10)        # EncryptedScalar<uint5>        ∈ [27, 27]          @ def
    %12 = 2                            # ClearScalar<uint2>            ∈ [2, 2]
    %13 = multiply(%11, %12)           # EncryptedScalar<uint6>        ∈ [54, 54]
    return %13
    
    Subgraphs:
    
        %5 = subgraph(%4):
    
            %0 = input                         # EncryptedScalar<uint2>          @ abc.foo
            %1 = sqrt(%0)                      # EncryptedScalar<float64>        @ abc
            %2 = astype(%1, dtype=int_)        # EncryptedScalar<uint1>          @ abc
            return %2
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return x ** 2
    
    inputset = range(10)
    circuit = f.compile(inputset)
    circuit.keys.generate()
    circuit.keys.generate(seed=420)
    serialized_keys: bytes = circuit.keys.serialize()
    keys: fhe.Keys = fhe.Keys.deserialize(serialized_keys)
    circuit.keys = keys
    circuit.keys.save("/path/to/keys")
    circuit.keys.load("/path/to/keys")
    circuit.keys.load_if_exists_generate_and_save_otherwise("/path/to/keys")
    Game of Life in Concrete Python.
    
    options:
      -h, --help            show this help message and exit
      --dimension DIMENSION
                            Dimension of the grid
      --refresh_every REFRESH_EVERY
                            Refresh the grid every X steps
      --method {method_3b,method_4b,method_5b,method_basic}
                            Method for refreshing the grid
      --log2_global_p_error LOG2_GLOBAL_P_ERROR
                            Probability of correctness issue (full circuit)
      --log2_p_error LOG2_P_ERROR
                            Probability of correctness issue (individual TLU)
      --simulate            Simulate instead of running computations in FHE
      --show_mlir           Show the MLIR
      --stop_after_compilation
                            Stop after compilation
      --text_output         Print a text output of the grid
    $ concretecompiler --action=compile -o python-demo example.mlir
    $ ls python-demo/
    client_parameters.concrete.params.json  compilation_feedback.json  fhecircuit-client.h  sharedlib.so  staticlib.a
    from concrete.compiler import (ClientSupport, LambdaArgument, LibrarySupport)
    lib_support = LibrarySupport.new("/path/to/your/python-demo/")
    compilation_result = lib_support.reload()
    server_lambda = lib_support.load_server_lambda(compilation_result)
    client_params = lib_support.load_client_parameters(compilation_result)
    client_support = ClientSupport.new()
    key_set = client_support.key_set(client_params)
    args = [
    	LambdaArgument.from_tensor_u8([1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], [4, 4]),
    	LambdaArgument.from_tensor_u8([1, 2, 1, 2, 1, 2, 1, 2], [4, 2])
    ]
    encrypted_args = client_support.encrypt_arguments(client_params, key_set, args)
    eval_keys = key_set.get_evaluation_keys()
    encrypted_result = lib_support.server_call(server_lambda, encrypted_args, eval_keys)
    result_arg = client_support.decrypt_result(client_params, key_set, encrypted_result)
    print("result tensor dims: {}".format(result_arg.n_values()))
    print("result tensor data: {}".format(result_arg.get_values()))
    -DCONCRETELANG_CUDA_SUPPORT=${CUDA_SUPPORT}
    -DCUDAToolkit_ROOT=$(CUDA_PATH)

    Tracing Dialect

    Tracing dialect A dialect to print program values at runtime.

    hashtag
    Operation definition

    hashtag
    Tracing.trace_ciphertext (::mlir::concretelang::Tracing::TraceCiphertextOp)

    Prints a ciphertext.

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Tracing.trace_message (::mlir::concretelang::Tracing::TraceMessageOp)

    Prints a message.

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Tracing.trace_plaintext (::mlir::concretelang::Tracing::TracePlaintextOp)

    Prints a plaintext.

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    Direct Circuits

    circle-exclamation

    Direct circuits are still experimental. It is very easy to make mistakes (e.g., due to no overflow checks or type coercion) while using direct circuits, so utilize them with care.

    For some applications, the data types of inputs, intermediate values, and outputs are known (e.g., for manipulating bytes, you would want to use uint8). Using inputsets to determine bounds in these cases is not necessary, and can even be error-prone. Therefore, another interface for defining such circuits is introduced:

    There are a few differences between direct circuits and traditional circuits:

    Progressbar

    Big circuits can take a long time to execute, and waiting for execution to finish without having any indication of its progress can be frustrating. For this reason, progressbar feature is introduced:

    When you run this code, you will see a progressbar like:

    And as the circuit progresses, this progressbar would fill:

    circle-info

    It is not a uniform progressbar. For example, when the progressbar shows 50%, this does not mean that half of the execution is performed in terms of seconds. Instead, it means that half of the nodes in the graph have been calculated. Since different node types can take a different amount of time, this should not be used to get an ETA.

    Security curves

    To select secure cryptographic parameters for usage in Concrete, we utilize the . In particular, we use the following workflow:

    1. Data Acquisition

      • For a given value of we obtain raw data from the Lattice Estimator, which ultimately leads to a security level . All relevant attacks in the Lattice Estimator are considered.

    Floating Points

    Concrete partly supports floating points. There is no support for floating point inputs or outputs. However, there is support for intermediate values to be floating points (under certain constraints).

    hashtag
    Floating points as intermediate values

    Concrete-Compile, which is used for compiling the circuit, doesn't support floating points at all. However, it supports table lookups which take an integer and map it to another integer. The constraints of this operation are that there should be a single integer input, and a single integer output.

    Deploy

    After developing your circuit, you may want to deploy it. However, sharing the details of your circuit with every client might not be desirable. As well as this, you might want to perform the computation on dedicated servers. In this case, you can use the Client and Server features of Concrete.

    hashtag
    Development of the circuit

    You can develop your circuit using the techniques discussed in previous chapters. Here is a simple example:

    msg

    ::mlir::StringAttr

    string attribute

    nmsb

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ciphertext

    msg

    ::mlir::StringAttr

    string attribute

    msg

    ::mlir::StringAttr

    string attribute

    nmsb

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    plaintext

    integer

    Remember that the resulting dtype for each operation will be determined by its inputs. This can lead to some unexpected results if you're not careful (e.g., if you do -x where x: fhe.uint8, you won't receive a negative value as the result will be fhe.uint8 as well)

  • There is no inputset evaluation when using fhe types in .astype(...) calls (e.g., np.sqrt(x).astype(fhe.uint4)), so the bit width of the output cannot be determined.

  • Specify the resulting data type in univariate extension (e.g., fhe.univariate(function, outputs=fhe.uint4)(x)), for the same reason as above.

  • Be careful with overflows. With inputset evaluation, you'll get bigger bit widths but no overflows. With direct definition, you must ensure that there aren't any overflows manually.

  • Let's review a more complicated example to see how direct circuits behave:

    This prints:

    Here is the breakdown of the assigned data types:

    As you can see, %8 is subtraction of two unsigned values, and the result is unsigned as well. In the case that c > d, we have an overflow, and this results in undefined behavior.

    Increase the value of σ\sigmaσ, until the tuple (n,q=264,σ)(n, q = 2^{64}, \sigma)(n,q=264,σ) satisfies the target level of security λtarget\lambda_{target}λtarget​.

  • Repeat for several values of nnn.

  • Model Generation for λ=λtarget\lambda = \lambda_{target}λ=λtarget​.

    • At this point, we have several sets of points {(n,q=264,σ)}\{(n, q = 2^{64}, \sigma)\}{(n,q=264,σ)} satisfying the target level of security λtarget\lambda_{target}λtarget​. From here, we fit a model to this raw data (σ\sigmaσ as a function of nnn).

  • Model Verification.

    • For each model, we perform a verification check to ensure that the values output from the function σ(n)\sigma(n)σ(n) provide the claimed level of security, λtarget\lambda_{target}λtarget​.

  • These models are then used as input for Concrete, to ensure that the parameter space explored by the compiler attains the required security level. Note that we consider the RC.BDGL16 lattice reduction cost model within the Lattice Estimator. Therefore, when computing our security estimates, we use the call LWE.estimate(params, red_cost_model = RC.BDGL16) on the input parameter set params.

    hashtag
    Usage

    To generate the raw data from the lattice estimator, use::

    by default, this script will generate parameter curves for {80, 112, 128, 192} bits of security, using log2(q)=64log_2(q) = 64log2​(q)=64.

    To compare the current curves with the output of the lattice estimator, use:

    this will compare the four curves generated above against the output of the version of the lattice estimator found in the third_party folderarrow-up-right.

    To generate the associated cpp and rust code, use::

    further advanced options can be found inside the Makefile.

    hashtag
    Example

    To look at the raw data gathered in step 1., we can look in the sage-object folderarrow-up-right. These objects can be loaded in the following way using SageMath:

    entries are tuples of the form: (n,log2(q),log2(σ),λ)(n, log_2(q), log_2(\sigma), \lambda)(n,log2​(q),log2​(σ),λ). We can view individual entries via::

    To view the interpolated curves we load the verified_curves.sobj object inside the sage-object folderarrow-up-right.

    This object is a tuple containing the information required for the four security curves ({80, 112, 128, 192} bits of security). Looking at one of the entries:

    Here we can see the linear model parameters (a=−0.026599462343105267,b=2.981543184145991)(a = -0.026599462343105267, b = 2.981543184145991)(a=−0.026599462343105267,b=2.981543184145991) along with the security level 128. This linear model can be used to generate secure parameters in the following way: for q=264q = 2^{64}q=264, if we have an LWE dimension of n=1536n = 1536n=1536, then the required noise size is:

    σ=a∗n+b=−37.85\sigma = a * n + b = -37.85σ=a∗n+b=−37.85

    This value corresponds to the logarithm of the relative error size. Using the parameter set (n,log(q),σ=264−37.85)(n, log(q), \sigma = 2^{64 - 37.85})(n,log(q),σ=264−37.85) in the Lattice Estimator confirms a 128-bit security level.

    (n,q=264,σ)(n, q = 2^{64}, \sigma)(n,q=264,σ)
    λ\lambdaλ
    Lattice-Estimatorarrow-up-right
    As long as your floating point operations comply with those constraints, Concrete automatically converts them to a table lookup operation:

    In the example above, a, b, and c are floating point intermediates. They are used to calculate d, which is an integer with a value dependent upon x, which is also an integer. Concrete detects this and fuses all of these operations into a single table lookup from x to d.

    This approach works for a variety of use cases, but it comes up short for others:

    This results in:

    The reason for the error is that d no longer depends solely on x; it depends on y as well. Concrete cannot fuse these operations, so it raises an exception instead.

    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        a = x + 1.5
        b = np.sin(x)
        c = np.around(a + b)
        d = c.astype(np.int64)
        return d
    
    inputset = range(8)
    circuit = f.compile(inputset)
    
    for x in range(8):
        assert circuit.encrypt_run_decrypt(x) == f(x)

    Once you have your circuit, you can save everything the server needs:

    Then, send server.zip to your computation server.

    hashtag
    Setting up a server

    You can load the server.zip you get from the development machine:

    You will need to wait for requests from clients. The first likely request is for ClientSpecs.

    Clients need ClientSpecs to generate keys and request computation. You can serialize ClientSpecs:

    Then, you can send it to the clients requesting it.

    hashtag
    Setting up clients

    After getting the serialized ClientSpecs from a server, you can create the client object:

    hashtag
    Generating keys (on the client)

    Once you have the Client object, you can perform key generation:

    This method generates encryption/decryption keys and evaluation keys.

    The server needs access to the evaluation keys that were just generated. You can serialize your evaluation keys as shown:

    After serialization, send the evaluation keys to the server.

    circle-info

    Serialized evaluation keys are very large, so you may want to cache them on the server instead of sending them with each request.

    hashtag
    Encrypting inputs (on the client)

    The next step is to encrypt your inputs and request the server to perform some computation. This can be done in the following way:

    Then, send the serialized arguments to the server.

    hashtag
    Performing computation (on the server)

    Once you have serialized evaluation keys and serialized arguments, you can deserialize them:

    You can perform the computation, as well:

    Then, send the serialized result back to the client. After this, the client can decrypt to receive the result of the computation.

    hashtag
    Decrypting the result (on the client)

    Once you have received the serialized result of the computation from the server, you can deserialize it:

    Then, decrypt the result:

    from concrete import fhe
    
    @fhe.circuit({"x": "encrypted"})
    def circuit(x: fhe.uint8):
        return x + 42
    
    assert circuit.encrypt_run_decrypt(10) == 52
    from concrete import fhe
    import numpy as np
    
    def square(value):
        return value ** 2
    
    @fhe.circuit({"x": "encrypted", "y": "encrypted"})
    def circuit(x: fhe.uint8, y: fhe.int2):
        a = x + 10
        b = y + 10
    
        c = np.sqrt(a).round().astype(fhe.uint4)
        d = fhe.univariate(square, outputs=fhe.uint8)(b)
    
        return d - c
    
    print(circuit)
    %0 = x                       # EncryptedScalar<uint8>
    %1 = y                       # EncryptedScalar<int2>
    %2 = 10                      # ClearScalar<uint4>
    %3 = add(%0, %2)             # EncryptedScalar<uint8>
    %4 = 10                      # ClearScalar<uint4>
    %5 = add(%1, %4)             # EncryptedScalar<int4>
    %6 = subgraph(%3)            # EncryptedScalar<uint4>
    %7 = square(%5)              # EncryptedScalar<uint8>
    %8 = subtract(%7, %6)        # EncryptedScalar<uint8>
    return %8
    
    Subgraphs:
    
        %6 = subgraph(%3):
    
            %0 = input                         # EncryptedScalar<uint8>
            %1 = sqrt(%0)                      # EncryptedScalar<float64>
            %2 = around(%1, decimals=0)        # EncryptedScalar<float64>
            %3 = astype(%2)                    # EncryptedScalar<uint4>
            return %3
    %0 is uint8 because it's specified in the definition
    %1 is  int2 because it's specified in the definition
    %2 is uint4 because it's the constant 10
    %3 is uint8 because it's the addition between uint8 and uint4
    %4 is uint4 because it's the constant 10
    %5 is  int4 because it's the addition between int2 and uint4
    %6 is uint4 because it's specified in astype
    %7 is uint8 because it's specified in univariate
    %8 is uint8 because it's subtraction between uint8 and uint4
    make generate-curves
    make compare-curves
    make generate-code
    sage: X = load("128.sobj")
    sage: X["128"][0]
    (2366, 64.0, 4.0, 128.51)
    sage: curves = load("verified_curves.sobj")
    sage: curves[2][:3]
    (-0.026599462343105267, 2.981543184145991, 128)
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted", "y": "encrypted"})
    def f(x, y):
        a = x + 1.5
        b = np.sin(y)
        c = np.around(a + b)
        d = c.astype(np.int64)
        return d
    
    inputset = [(1, 2), (3, 0), (2, 2), (1, 3)]
    circuit = f.compile(inputset)
    
    for x in range(8):
        assert circuit.encrypt_run_decrypt(x) == f(x)
    RuntimeError: Function you are trying to compile cannot be converted to MLIR
    
    %0 = x                             # EncryptedScalar<uint2>
    %1 = 1.5                           # ClearScalar<float64>
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported
    %2 = y                             # EncryptedScalar<uint2>
    %3 = add(%0, %1)                   # EncryptedScalar<float64>
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported
    %4 = sin(%2)                       # EncryptedScalar<float64>
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported
    %5 = add(%3, %4)                   # EncryptedScalar<float64>
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported
    %6 = around(%5)                    # EncryptedScalar<float64>
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported
    %7 = astype(%6, dtype=int_)        # EncryptedScalar<uint3>
    return %7
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def function(x):
        return x + 42
    
    inputset = range(10)
    circuit = function.compile(inputset)
    circuit.server.save("server.zip")
    from concrete import fhe
    
    server = fhe.Server.load("server.zip")
    serialized_client_specs: str = server.client_specs.serialize()
    client_specs = fhe.ClientSpecs.deserialize(serialized_client_specs)
    client = fhe.Client(client_specs)
    client.keys.generate()
    serialized_evaluation_keys: bytes = client.evaluation_keys.serialize()
    arg: fhe.Value = client.encrypt(7)
    serialized_arg: bytes = arg.serialize()
    deserialized_evaluation_keys = fhe.EvaluationKeys.deserialize(serialized_evaluation_keys)
    deserialized_arg = fhe.Value.deserialize(serialized_arg)
    result: fhe.Value = server.run(deserialized_arg, evaluation_keys=deserialized_evaluation_keys)
    serialized_result: bytes = result.serialize()
    deserialized_result = fhe.Value.deserialize(serialized_result)
    decrypted_result = client.decrypt(deserialized_result)
    assert decrypted_result == 49

    Once the progressbar fills and execution completes, you will see the following figure:

    Evaluation:  10% |█████.............................................|  10% (scaling.r)
    ^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^
    Title        Progressbar                                                   Tag
    Evaluation:  30% |███████████████...................................|  30% (scaling.g)
    Evaluation:  50% |█████████████████████████.........................|  50% (scaling.b)

    Statistics

    Concrete analyzes all compiled circuits and calculates some statistics. These statistics can be used to find bottlenecks and compare circuits. Statistics are calculated in terms of basic operations. There are 6 basic operations in Concrete:

    • clear addition: x + y where x is encrypted and y is clear

    • encrypted addition: x + y where both x and y are encrypted

    import time
    
    import matplotlib.pyplot as plt
    import numpy as np
    import randimage
    from concrete import fhe
    
    configuration = fhe.Configuration(
        enable_unsafe_features=True,
        use_insecure_key_cache=True,
        insecure_key_cache_location=".keys",
    
        # To enable displaying progressbar
        show_progress=True,
        # To enable showing tags in the progressbar (does not work in notebooks)
        progress_tag=True,
        # To give a title to the progressbar
        progress_title="Evaluation:",
    )
    
    @fhe.compiler({"image": "encrypted"})
    def to_grayscale(image):
        with fhe.tag("scaling.r"):
            r = image[:, :, 0]
            r = (r * 0.30).astype(np.int64)
    
        with fhe.tag("scaling.g"):
            g = image[:, :, 1]
            g = (g * 0.59).astype(np.int64)
    
        with fhe.tag("scaling.b"):
            b = image[:, :, 2]
            b = (b * 0.11).astype(np.int64)
    
        with fhe.tag("combining.rgb"):
            gray = r + g + b
            
        with fhe.tag("creating.result"):
            gray = np.expand_dims(gray, axis=2)
            result = np.concatenate((gray, gray, gray), axis=2)
        
        return result
    
    image_size = (16, 16)
    image_data = (randimage.get_random_image(image_size) * 255).round().astype(np.int64)
    
    print()
    
    print(f"Compilation started @ {time.strftime('%H:%M:%S', time.localtime())}")
    start = time.time()
    inputset = [np.random.randint(0, 256, size=image_data.shape) for _ in range(100)]
    circuit = to_grayscale.compile(inputset, configuration)
    end = time.time()
    print(f"(took {end - start:.3f} seconds)")
    
    print()
    
    print(f"Key generation started @ {time.strftime('%H:%M:%S', time.localtime())}")
    start = time.time()
    circuit.keygen()
    end = time.time()
    print(f"(took {end - start:.3f} seconds)")
    
    print()
    
    print(f"Evaluation started @ {time.strftime('%H:%M:%S', time.localtime())}")
    start = time.time()
    grayscale_image_data = circuit.encrypt_run_decrypt(image_data)
    end = time.time()
    print(f"(took {end - start:.3f} seconds)")
    
    fig, axs = plt.subplots(1, 2)
    axs = axs.flatten()
    
    axs[0].set_title("Original")
    axs[0].imshow(image_data)
    axs[0].axis("off")
    
    axs[1].set_title("Grayscale")
    axs[1].imshow(grayscale_image_data)
    axs[1].axis("off")
    
    plt.show()

    clear multiplication: x * y where x is encrypted and y is clear

  • encrypted negation: -x where x is encrypted

  • key switch: building block for table lookups

  • packing key switch: building block for table lookups

  • programmable bootstrapping: building block for table lookups

  • You can print all statistics using the show_statistics configuration option:

    This code will print:

    circle-info

    Each of these properties can be directly accessed on the circuit (e.g., circuit.programmable_bootstrap_count).

    hashtag
    Tags

    Circuit analysis also considers tags!

    Imagine you have a neural network with 10 layers, each of them tagged. You can easily see the number of additions and multiplications required for matrix multiplications per layer:

    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return (x**2) + (2*x) + 4
    
    inputset = range(2**2)
    circuit = f.compile(inputset, show_statistics=True)
    Statistics
    --------------------------------------------------------------------------------
    size_of_secret_keys: 22648
    size_of_bootstrap_keys: 51274176
    size_of_keyswitch_keys: 64092720
    size_of_inputs: 16392
    size_of_outputs: 16392
    p_error: 9.627450598589458e-06
    global_p_error: 9.627450598589458e-06
    complexity: 99198195.0
    programmable_bootstrap_count: 1
    programmable_bootstrap_count_per_parameter: {
        BootstrapKeyParam(polynomial_size=2048, glwe_dimension=1, input_lwe_dimension=781, level=1, base_log=23, variance=9.940977002694397e-32): 1
    }
    key_switch_count: 1
    key_switch_count_per_parameter: {
        KeyswitchKeyParam(level=5, base_log=3, variance=1.939836732335308e-11): 1
    }
    packing_key_switch_count: 0
    clear_addition_count: 1
    clear_addition_count_per_parameter: {
        LweSecretKeyParam(dimension=2048): 1
    }
    encrypted_addition_count: 1
    encrypted_addition_count_per_parameter: {
        LweSecretKeyParam(dimension=2048): 1
    }
    clear_multiplication_count: 1
    clear_multiplication_count_per_parameter: {
        LweSecretKeyParam(dimension=2048): 1
    }
    encrypted_negation_count: 0
    --------------------------------------------------------------------------------
    Statistics
    --------------------------------------------------------------------------------
    clear_multiplication_count_per_tag: {
        /model/model: 53342
        /model/model.0/Gemm: 14720
        /model/model.0/Gemm.matmul: 14720
        /model/model.2/Gemm: 11730
        /model/model.2/Gemm.matmul: 11730
        /model/model.4/Gemm: 9078
        /model/model.4/Gemm.matmul: 9078
        /model/model.6/Gemm: 6764
        /model/model.6/Gemm.matmul: 6764
        /model/model.8/Gemm: 4788
        /model/model.8/Gemm.matmul: 4788
        /model/model.10/Gemm: 3150
        /model/model.10/Gemm.matmul: 3150
        /model/model.12/Gemm: 1850
        /model/model.12/Gemm.matmul: 1850
        /model/model.14/Gemm: 888
        /model/model.14/Gemm.matmul: 888
        /model/model.16/Gemm: 264
        /model/model.16/Gemm.matmul: 264
        /model/model.18/Gemm: 110
        /model/model.18/Gemm.matmul: 110
    }
    encrypted_addition_count_per_tag: {
        /model/model: 53342
        /model/model.0/Gemm: 14720
        /model/model.0/Gemm.matmul: 14720
        /model/model.2/Gemm: 11730
        /model/model.2/Gemm.matmul: 11730
        /model/model.4/Gemm: 9078
        /model/model.4/Gemm.matmul: 9078
        /model/model.6/Gemm: 6764
        /model/model.6/Gemm.matmul: 6764
        /model/model.8/Gemm: 4788
        /model/model.8/Gemm.matmul: 4788
        /model/model.10/Gemm: 3150
        /model/model.10/Gemm.matmul: 3150
        /model/model.12/Gemm: 1850
        /model/model.12/Gemm.matmul: 1850
        /model/model.14/Gemm: 888
        /model/model.14/Gemm.matmul: 888
        /model/model.16/Gemm: 264
        /model/model.16/Gemm.matmul: 264
        /model/model.18/Gemm: 110
        /model/model.18/Gemm.matmul: 110
    }
    --------------------------------------------------------------------------------

    Bit Extraction

    Some applications require directly manipulating bits of integers. Concrete provides a bit extraction operation for such applications.

    Bit extraction is capable of extracting a slice of bits from an integer. Index 0 corresponds to the lowest significant bit. The cost of this operation is proportional to the highest significant bit index.

    circle-exclamation

    Bit extraction only works in the Native encoding, which is usually selected when all table lookups in the circuit are less than or equal to 8 bits.

    Slices can be used for indexing fhe.bits(value) as well.

    Even slices with negative steps are supported!

    Signed integers are supported as well.

    Lastly, here is a practical use case of bit extraction.

    prints

    hashtag
    Limitations

    • Bits cannot be extracted using a negative index.

      • Which means fhe.bits(x)[-1] or fhe.bits(x)[-4:-1] is not supported for example.

    hashtag
    Performance Considerations

    hashtag
    A Chain of Individual Bit Extractions

    Key Concept: Extracting a specific bit requires clearing all the preceding lower bits. This involves extracting these previous bits as intermediate values and then subtracting them from the input.

    Implications:

    • Bits are extracted sequentially, starting from the least significant bit to the more significant ones. The cost is proportional to the index of the highest extracted bit plus one.

    • No parallelization is possible. The computation time is proportional to the cost, independent of the number of CPUs.

    Examples:

    • Extracting fhe.bits(x)[4] is approximately five times costlier than extracting fhe.bits(x)[0].

    • Extracting fhe.bits(x)[4] takes around five times more wall clock time than fhe.bits(x)[0].

    hashtag
    Reuse of Intermediate Extracted Bits

    Key Concept: Common sub-expression elimination is applied to intermediate extracted bits.

    Implications:

    • The overall cost for a series of fhe.bits(x)[m:n] calls on the same input x is almost equivalent to the cost of the single most computationally expensive extraction in the series, i.e. fhe.bits(x)[n].

    • The order of extraction in that series does not affect the overall cost.

    Example:

    The combined operation fhe.bit(x)[3] + fhe.bit(x)[2] + fhe.bit(x)[1] has almost the same cost as fhe.bits(x)[3].

    hashtag
    TLUs of 1b input precision

    Each extracted bit incurs a cost of approximately one TLU of 1-bit input precision. Therefore, fhe.bits(x)[0] is generally faster than any other TLU operation.

    Configure

    Concrete can be customized using Configurations:

    from concrete import fhe
    import numpy as np
    
    configuration = fhe.Configuration(p_error=0.01, dataflow_parallelize=True)
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return x + 42
    
    inputset = range(10)
    circuit = f.compile(inputset, configuration=configuration)

    You can overwrite individual options as kwargs to the compile method:

    Or you can combine both:

    circle-info

    Additional kwargs to compile functions take higher precedence. So if you set the option in both configuration and compile methods, the value in the compile method will be used.

    hashtag
    Options

    • show_graph: Optional[bool] = None

      • Print computation graph during compilation. True means always print, False means never print, None means print depending on verbose configuration below.

    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return fhe.bits(x)[0], fhe.bits(x)[3]
    
    inputset = range(32)
    circuit = f.compile(inputset)
    
    assert circuit.encrypt_run_decrypt(0b_00000) == (0, 0)
    assert circuit.encrypt_run_decrypt(0b_00001) == (1, 0)
    
    assert circuit.encrypt_run_decrypt(0b_01100) == (0, 1)
    assert circuit.encrypt_run_decrypt(0b_01101) == (1, 1)
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return x + 42
    
    inputset = range(10)
    circuit = f.compile(inputset, p_error=0.01, dataflow_parallelize=True)
    from concrete import fhe
    import numpy as np
    
    configuration = fhe.Configuration(p_error=0.01)
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return x + 42
    
    inputset = range(10)
    circuit = f.compile(inputset, configuration=configuration, loop_parallelize=True)

    show_mlir: Optional[bool] = None

    • Print MLIR during compilation. True means always print, False means never print, None means print depending on verbose configuration below.

  • show_optimizer: Optional[bool] = None

    • Print optimizer output during compilation. True means always print, False means never print, None means print depending on verbose configuration below.

  • show_statistics: Optional[bool] = None

    • Print circuit statistics during compilation. True means always print, False means never print, None means print depending on verbose configuration below.

  • verbose: bool = False

    • Print details related to compilation.

  • dump_artifacts_on_unexpected_failures: bool = True

    • Export debugging artifacts automatically on compilation failures.

  • auto_adjust_rounders: bool = False

    • Adjust rounders automatically.

  • p_error: Optional[float] = None

    • Error probability for individual table lookups. If set, all table lookups will have the probability of a non-exact result smaller than the set value. See Exactness to learn more.

  • global_p_error: Optional[float] = None

    • Global error probability for the whole circuit. If set, the whole circuit will have the probability of a non-exact result smaller than the set value. See Exactness to learn more.

  • single_precision: bool = False

    • Use single precision for the whole circuit.

  • parameter_selection_strategy: (fhe.ParameterSelectionStrategy) = fhe.ParameterSelectionStrategy.MULTI

    • Set how cryptographic parameters are selected.

  • loop_parallelize: bool = True

    • Enable loop parallelization in the compiler.

  • dataflow_parallelize: bool = False

    • Enable dataflow parallelization in the compiler.

  • auto_parallelize: bool = False

    • Enable auto parallelization in the compiler.

  • enable_unsafe_features: bool = False

    • Enable unsafe features.

  • use_insecure_key_cache: bool = False (Unsafe)

    • Use the insecure key cache.

  • insecure_key_cache_location: Optional[Union[Path, str]] = None

    • Location of insecure key cache.

  • show_progress: bool = False,

    • Display a progress bar during circuit execution

  • progress_title: str = "",

    • Title of the progress bar

  • progress_tag: Union[bool, int] = False,

    • How many nested tag elements to display with the progress bar. True means all tag elements and False disables the display. 2 will display elmt1.elmt2

  • fhe_simulation: bool = False

    • Enable FHE simulation. Can be enabled later using circuit.enable_fhe_simulation().

  • fhe_execution: bool = True

    • Enable FHE execution. Can be enabled later using circuit.enable_fhe_execution().

  • compiler_debug_mode: bool = False,

    • Enable/disable debug mode of the compiler. This can show a lot of information, including passes and pattern rewrites.

  • compiler_verbose_mode: bool = False,

    • Enable/disable verbose mode of the compiler. This mainly shows logs from the compiler, and is less verbose than the debug mode.

  • comparison_strategy_preference: Optional[Union[ComparisonStrategy, str, List[Union[ComparisonStrategy, str]]]] = None

    • Specify preference for comparison strategies, can be a single strategy or an ordered list of strategies. See Comparisons to learn more.

  • bitwise_strategy_preference: Optional[Union[BitwiseStrategy, str, List[Union[BitwiseStrategy, str]]]] = None

    • Specify preference for bitwise strategies, can be a single strategy or an ordered list of strategies. See Bitwise to learn more.

  • shifts_with_promotion: bool = True,

    • Enable promotions in encrypted shifts instead of casting in runtime. See Bitwise#Shifts to learn more.

  • composable: bool = False,

    • Specify that the function must be composable with itself.

  • The reason for this is that we don't know in advance (i.e., before inputset evaluation) how many bits x has.
    • For example, let's say you have x == 10 == 0b_000...0001010, and you want to do fhe.bits(x)[-1]. If the value is 4-bits (i.e., 0b_1010), the result needs to be 1, but if it's 6-bits (i.e., 0b_001010), the result needs to be 0. Since we don't know the bit-width of x before inputset evaluation, we cannot calculate fhe.bits(x)[-1].

  • When extracting bits using slices in reverse order (i.e., step < 0), the start bit needs to be provided explicitly.

    • Which means fhe.bits(x)[::-1] or fhe.bits(x)[:2:-1] is not supported for example.

    • The reason is the same as above.

  • When extracting bits of signed values using slices, the stop bit needs to be provided explicitly.

    • Which means fhe.bits(x)[1:] or fhe.bits(x)[1::2] is not supported for example.

    • The reason is similar to above.

      • To explain a bit more, signed integers use representation. In this representation, negative values have their most significant bits set to 1 (e.g., -1 == 0b_11111, -2 == 0b_11110, -3 == 0b_11101). Extracting bits always returns a positive value (e.g., fhe.bits(-1)[1:3] == 0b_11 == 3) This means if you were to do fhe.bits(x)[1:] where x == -1, if x is 4 bits, the result would be 0b_111 == 7, but if x

  • Bits of floats cannot be extracted.

    • Floats are partially supported but extracting their bits is not supported at all.

  • The cost of extracting fhe.bits(x)[0:5] is almost the same as that of fhe.bits(x)[5].

    Composition

    concrete-python supports circuit composition, which allows the output of a circuit execution to be used directly as an input without decryption. We can execute the circuit as many time as we want by forwarding outputs without decrypting intermediate values. This feature enables a new range of applications, including support for control flow in pure (cleartext) python.

    Here is a first simple example that uses composition to implement a simple counter in FHE:

    Note the use of the composable flag in the compile call. It instructs the compiler to ensure the circuit can be called on its own outputs (see Limitations section for more details). Executing this script should give the following output:

    hashtag
    Multi inputs, multi outputs

    Composition is not limited to 1-to-1 circuits, it can also be used with circuits with multiple inputs and multiple outputs. Here is an example that computes the 10 first elements of the Fibonacci sequence in FHE:

    Executing this script will provide the following output:

    Though it is not visible in this example, there is no limitations on the number of inputs and outputs. There is also no need for a specific logic regarding how we forward values from outputs to inputs; those could be switched for instance.

    circle-info

    See below in the , for explanations about the use of noise_reset.

    hashtag
    Iteration support

    With the previous example we see that to some extent, composition allows to support iteration with cleartext iterands. That is, loops with the following shape :

    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 :

    Which prints:

    Here we use a while loop that keeps iterating as long as the decryption of the running value is different from 1. Again, the loop body is implemented in FHE, but the iteration control has to be in the clear.

    hashtag
    Limitations

    Depending on the circuit, supporting composition may add a non-negligeable overhead when compared to a non-composable version. Indeed, to be composable a circuit must verify two conditions:

    1. All inputs and outputs must share the same precision and the same crypto-parameters: the most expensive parameters that would otherwise be used for a single input or output, are generalized to all inputs and outputs.

    2. There must be a noise refresh in every path between an input and an output: some circuits will need extra PBSes to be added to allow composability.

    The first point is handled automatically by the compiler, no change to the circuit is needed to ensure the right precisions are used.

    For the second point, since adding a PBS has an impact on performance, we do not ade them on behalf of the user. For instance, to implement a circuit that doubles an encrypted value, we would write something like:

    This is a valid circuit when composable is not used, but when compiled with composition activated, a RuntimeError: Program can not be composed: ... error is reported, signalling that an extra PBS must be added. To solve this situation, and turn this circuit into a composable one, one can use the following snippet to add a PBS at the end of your circuit:

    Compilation

    There are two main entry points to the Concrete Compiler. The first is to use the Concrete Python frontend. The second is to use the Compiler directly, which takes MLIRarrow-up-right as input. Concrete Python is more high level and uses the Compiler under the hood.

    Compilation begins in the frontend with tracing to get an easy-to-manipulate representation of the function. We call this representation a Computation Graph, which is a Directed Acyclic Graph (DAG) containing nodes representing computations done in the function. Working with graphs is useful because they have been studied extensively and there are a lot of available algorithms to manipulate them. Internally, we use networkxarrow-up-right, which is an excellent graph library for Python.

    The next step in compilation is transforming the computation graph. There are many transformations we perform, and these are discussed in their own sections. The result of a transformation is another computation graph.

    After transformations are applied, we need to determine the bounds (i.e., the minimum and the maximum values) of each intermediate node. This is required because FHE allows limited precision for computations. Measuring these bounds helps determine the required precision for the function.

    The frontend is almost done at this stage and only needs to transform the computation graph to equivalent MLIR code. Once the MLIR is generated, our Compiler backend takes over. Any other frontend wishing to use the Compiler needs to plugin at this stage.

    The Compiler takes MLIR code that makes use of both the FHE and FHELinalg for scalar and tensor operations respectively.

    Compilation then ends with a series of that generates a native binary which contains executable code. Crypto parameters are generated along the way as well.

    hashtag
    Tracing

    We start with a Python function f, such as this one:

    The goal of tracing is to create the following computation graph without requiring any change from the user.

    (Note that the edge labels are for non-commutative operations. To give an example, a subtraction node represents (predecessor with edge label 0) - (predecessor with edge label 1))

    To do this, we make use of Tracers, which are objects that record the operation performed during their creation. We create a Tracer for each argument of the function and call the function with those Tracers. Tracers make use of the operator overloading feature of Python to achieve their goal:

    2 * y will be performed first, and * is overloaded for Tracer to return another tracer: Tracer(computation=Multiply(Constant(2), self.computation)), which is equal to Tracer(computation=Multiply(Constant(2), Input("y"))).

    x + (2 * y) will be performed next, and + is overloaded for Tracer to return another tracer: Tracer(computation=Add(self.computation, (2 * y).computation)), which is equal to Tracer(computation=Add(Input("x"), Multiply(Constant(2), Input("y"))).

    In the end, we will have output tracers that can be used to create the computation graph. The implementation is a bit more complex than this, but the idea is the same.

    Tracing is also responsible for indicating whether the values in the node would be encrypted or not. The rule for that is: if a node has an encrypted predecessor, it is encrypted as well.

    hashtag
    Topological transforms

    The goal of topological transforms is to make more functions compilable.

    With the current version of Concrete, floating-point inputs and floating-point outputs are not supported. However, if the floating-point operations are intermediate operations, they can sometimes be fused into a single table lookup from integer to integer, thanks to some specific transforms.

    Let's take a closer look at the transforms we can currently perform.

    hashtag
    Fusing.

    We have allocated a whole new chapter to explaining fusing. You can find it .

    hashtag
    Bounds measurement

    Given a computation graph, the goal of the bounds measurement step is to assign the minimal data type to each node in the graph.

    If we have an encrypted input that is always between 0 and 10, we should assign the type EncryptedScalar<uint4> to the node of this input as EncryptedScalar<uint4>. This is the minimal encrypted integer that supports all values between 0 and 10.

    If there were negative values in the range, we could have used intX instead of uintX.

    Bounds measurement is necessary because FHE supports limited precision, and we don't want unexpected behaviour while evaluating the compiled functions.

    Let's take a closer look at how we perform bounds measurement.

    hashtag
    Inputset evaluation

    This is a simple approach that requires an inputset to be provided by the user.

    The inputset is not to be confused with the dataset, which is classical in ML, as it doesn't require labels. Rather, the inputset is a set of values which are typical inputs of the function.

    The idea is to evaluate each input in the inputset and record the result of each operation in the computation graph. Then we compare the evaluation results with the current minimum/maximum values of each node and update the minimum/maximum accordingly. After the entire inputset is evaluated, we assign a data type to each node using the minimum and maximum values it contains.

    Here is an example, given this computation graph where x is encrypted:

    and this inputset:

    Evaluation result of 2:

    • x: 2

    • 2: 2

    • *: 4

    New bounds:

    • x: [2, 2]

    • 2: [2, 2]

    Evaluation result of 3:

    • x: 3

    • 2: 2

    • *: 6

    New bounds:

    • x: [2, 3]

    • 2: [2, 2]

    • *

    Evaluation result of 1:

    • x: 1

    • 2: 2

    • *: 2

    New bounds:

    • x: [1, 3]

    • 2: [2, 2]

    • *

    Assigned data types:

    • x: EncryptedScalar<uint2>

    • 2: ClearScalar<uint2>

    • *

    hashtag
    MLIR Compiler Passes

    We describe below some of the main passes in the compilation pipeline.

    hashtag
    FHE to TFHE

    This pass converts high level operations which are not crypto specific to lower level operations from the TFHE scheme. Ciphertexts get introduced in the code as well. TFHE operations and ciphertexts require some parameters which need to be chosen, and the pass does just that.

    hashtag
    TFHE Parameterization

    TFHE Parameterization takes care of introducing the chosen parameters in the Intermediate Representation (IR). After this pass, you should be able to see the dimension of ciphertexts, as well as other parameters in the IR.

    hashtag
    TFHE to Concrete

    This pass lowers TFHE operations to low level operations that are closer to the backend implementation, working on tensors and memory buffers (after a bufferization pass).

    hashtag
    Concrete to LLVM

    This pass lowers everything to LLVM-IR in order to generate the final binary.

    Common Workarounds

    As explained in the Basics of FHE, the challenge for developers is to adapt their code to fit FHE constraints. In this document we have collected some common examples to illustrate the kind of optimization one can do to get better performance.

    circle-info

    All code snippets provided here are temporary workarounds. In future versions of Concrete, some functions described here could be directly available in a more generic and efficient form. These code snippets are coming from support answers in our community forumarrow-up-right

    hashtag
    Minimum for Two values

    In this first example, we compute a minimum by creating the difference between two numbers y and x and conditionally remove this diff from y to either get x if y>x or y if x>y:

    hashtag
    Maximum for Two values

    The companion example of above with the maximum value of two integers instead of the minimum:

    hashtag
    Minimum for several values

    And an extension for more than two values:

    hashtag
    Retrieving a value within an encrypted array with an encrypted index

    This example shows how to deal with an array and an encrypted index. It will create a "selection" array filled with 0 except for the requested index that will be 1, and sum the products of all array values by this selection array:

    hashtag
    Filter an array with comparison (>)

    This example filters an encrypted array with an encrypted condition, here a greater than with an encrypted value. It packs all values with a selection bit, resulting from the comparison that allow the unpacking of only the filtered values:

    hashtag
    Matrix Row/Col means

    In this example Matrix operation, we are introducing a key concept when using Concrete: trying to maximize the parallelization. Here instead of sequentially summing all values to create a mean value, we split the values in sub-groups, and do the mean of the sub-group means:

    SDFG Dialect

    Dialect for the construction of static data flow graphs A dialect for the construction of static data flow graphs. The data flow graph is composed of a set of processes, connected through data streams. Special streams allow for data to be injected into and to be retrieved from the data flow graph.

    hashtag
    Operation definition

    hashtag

    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
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return fhe.bits(x)[3:0:-1]
    
    inputset = range(32)
    circuit = f.compile(inputset)
    
    assert circuit.encrypt_run_decrypt(0b_01101) == 0b_011
    assert circuit.encrypt_run_decrypt(0b_01011) == 0b_101
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return fhe.bits(x)[1:3]
    
    inputset = range(-16, 16)
    circuit = f.compile(inputset)
    
    assert circuit.encrypt_run_decrypt(-14) == 0b_01  # -14 == 0b_10010 (in two's complement)
    assert circuit.encrypt_run_decrypt(-12) == 0b_10  # -12 == 0b_10100 (in two's complement)
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def is_even(x):
        return 1 - fhe.bits(x)[0]
    
    inputset = [
        np.random.randint(-16, 16, size=(5,))
        for _ in range(100)
    ]
    circuit = is_even.compile(inputset)
    
    sample = np.random.randint(-16, 16, size=(5,))
    for value, value_is_even in zip(sample, circuit.encrypt_run_decrypt(sample)):
        print(f"{value} is {'even' if value_is_even else 'odd'}")
    13 is odd
    0 is even
    -15 is odd
    2 is even
    -6 is even
    from concrete import fhe
    
    @fhe.compiler({"counter": "encrypted"})
    def increment(counter):
       return (counter + 1) % 100
    
    print("Compiling `increment` function")
    increment_fhe = increment.compile(list(range(0, 100)), composable=True)
    
    print("Generating keyset ...")
    increment_fhe.keygen()
    
    print("Encrypting the initial counter value")
    counter = 0
    counter_enc = increment_fhe.encrypt(counter)
    
    print(f"| iteration || decrypted | cleartext |")
    for i in range(10):
        counter_enc = increment_fhe.run(counter_enc)
        counter = increment(counter)
    
        # For demo purpose; no decryption is needed.
        counter_dec = increment_fhe.decrypt(counter_enc)
        print(f"|     {i}     || {counter_dec:<9} | {counter:<9} |")
    Compiling `increment` function
    Generating keyset ...
    Encrypting the initial counter value
    | iteration || decrypted | cleartext |
    |     0     || 1         | 1         |
    |     1     || 2         | 2         |
    |     2     || 3         | 3         |
    |     3     || 4         | 4         |
    |     4     || 5         | 5         |
    |     5     || 6         | 6         |
    |     6     || 7         | 7         |
    |     7     || 8         | 8         |
    |     8     || 9         | 9         |
    |     9     || 10        | 10        |
    is 5 bits the result would be
    0b_1111 == 15
    . Since we don't know the bit-width of
    x
    before inputset evaluation, we cannot calculate
    fhe.bits(x)[1:]
    .
    two's complementarrow-up-right
    Limitations section
    Collatz sequencearrow-up-right

    3: 3

  • +: 7

  • *: [4, 4]
  • 3: [3, 3]

  • +: [7, 7]

  • 3: 3

  • +: 9

  • : [4,
    6
    ]
  • 3: [3, 3]

  • +: [7, 9]

  • 3: 3

  • +: 5

  • : [
    2
    , 6]
  • 3: [3, 3]

  • +: [5, 9]

  • : EncryptedScalar<
    uint3
    >
  • 3: ClearScalar<uint2>

  • +: EncryptedScalar<uint4>

  • dialectsarrow-up-right
    passes
    here
    TFHE Parameterization
    SDFG.get (::mlir::concretelang::SDFG::Get)

    Retrieves a data element from a stream

    Retrieves a single data element from the specified stream (i.e., an instance of the element type of the stream).

    Example:

    hashtag
    Operands:

    Operand
    Description

    stream

    An SDFG data stream

    hashtag
    Results:

    Result
    Description

    data

    any type

    hashtag
    SDFG.init (::mlir::concretelang::SDFG::Init)

    Initializes the streaming framework

    Initializes the streaming framework. This operation must be performed before control reaches any other operation from the dialect.

    Example:

    hashtag
    Results:

    Result
    Description

    «unnamed»

    An SDFG data flow graph

    hashtag
    SDFG.make_process (::mlir::concretelang::SDFG::MakeProcess)

    Creates a new SDFG process

    Creates a new SDFG process and connects it to the input and output streams.

    Example:

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    type

    ::mlir::concretelang::SDFG::ProcessKindAttr

    Process kind

    hashtag
    Operands:

    Operand
    Description

    dfg

    An SDFG data flow graph

    streams

    An SDFG data stream

    hashtag
    SDFG.make_stream (::mlir::concretelang::SDFG::MakeStream)

    Returns a new SDFG stream

    Returns a new SDFG stream, transporting data either between processes on the device, from the host to the device or from the device to the host. All streams are typed, allowing data to be read / written through SDFG.get and SDFG.put only using the stream's type.

    Example:

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    name

    ::mlir::StringAttr

    string attribute

    type

    ::mlir::concretelang::SDFG::StreamKindAttr

    Stream kind

    hashtag
    Operands:

    Operand
    Description

    dfg

    An SDFG data flow graph

    hashtag
    Results:

    Result
    Description

    «unnamed»

    An SDFG data stream

    hashtag
    SDFG.put (::mlir::concretelang::SDFG::Put)

    Writes a data element to a stream

    Writes the input operand to the specified stream. The operand's type must meet the element type of the stream.

    Example:

    hashtag
    Operands:

    Operand
    Description

    stream

    An SDFG data stream

    data

    any type

    hashtag
    SDFG.shutdown (::mlir::concretelang::SDFG::Shutdown)

    Shuts down the streaming framework

    Shuts down the streaming framework. This operation must be performed after any other operation from the dialect.

    Example:

    hashtag
    Operands:

    Operand
    Description

    dfg

    An SDFG data flow graph

    hashtag
    SDFG.start (::mlir::concretelang::SDFG::Start)

    Finalizes the creation of an SDFG and starts execution of its processes

    Finalizes the creation of an SDFG and starts execution of its processes. Any creation of streams and processes must take place before control reaches this operation.

    Example:

    hashtag
    Operands:

    Operand
    Description

    dfg

    An SDFG data flow graph

    hashtag
    Attribute definition

    hashtag
    ProcessKindAttr

    Process kind

    Syntax:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    value

    ::mlir::concretelang::SDFG::ProcessKind

    an enum of type ProcessKind

    hashtag
    StreamKindAttr

    Stream kind

    Syntax:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    value

    ::mlir::concretelang::SDFG::StreamKind

    an enum of type StreamKind

    hashtag
    Type definition

    hashtag
    DFGType

    An SDFG data flow graph

    Syntax: !SDFG.dfg

    A handle to an SDFG data flow graph

    hashtag
    StreamType

    An SDFG data stream

    An SDFG stream to connect SDFG processes.

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    elementType

    Type

    from concrete import fhe
    
    def noise_reset(x):
       return fhe.univariate(lambda x: x)(x)
    
    @fhe.compiler({"n1th": "encrypted", "nth": "encrypted"})
    def fib(n1th, nth):
       return noise_reset(nth), noise_reset(n1th + nth)
    
    print("Compiling `fib` function ...")
    inputset = list(zip(range(0, 100), range(0, 100)))
    fib_fhe = fib.compile(inputset, composable=True)
    
    print("Generating keyset ...")
    fib_fhe.keygen()
    
    print("Encrypting initial values")
    n1th = 1
    nth = 2
    (n1th_enc, nth_enc) = fib_fhe.encrypt(n1th, nth)
    
    print(f"|           ||        (n-1)-th       |         n-th          |")
    print(f"| iteration || decrypted | cleartext | decrypted | cleartext |")
    for i in range(10):
       (n1th_enc, nth_enc) = fib_fhe.run(n1th_enc, nth_enc)
       (n1th, nth) = fib(n1th, nth)
       
        # For demo purpose; no decryption is needed.
       (n1th_dec, nth_dec) = fib_fhe.decrypt(n1th_enc, nth_enc)
       print(f"|     {i}     || {n1th_dec:<9} | {n1th:<9} | {nth_dec:<9} | {nth:<9} |")
    Compiling `fib` function ...
    Generating keyset ...
    Encrypting initial values
    |           ||        (n-1)-th       |         n-th          |
    | iteration || decrypted | cleartext | decrypted | cleartext |
    |     0     || 2         | 2         | 3         | 3         |
    |     1     || 3         | 3         | 5         | 5         |
    |     2     || 5         | 5         | 8         | 8         |
    |     3     || 8         | 8         | 13        | 13        |
    |     4     || 13        | 13        | 21        | 21        |
    |     5     || 21        | 21        | 34        | 34        |
    |     6     || 34        | 34        | 55        | 55        |
    |     7     || 55        | 55        | 89        | 89        |
    |     8     || 89        | 89        | 144       | 144       |
    |     9     || 144       | 144       | 233       | 233       |
    for i in some_cleartext_constant_range:
        # Do something in FHE in the loop body, implement as an FHE circuit.
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def collatz(x):
    
        y = x // 2
        z = 3 * x + 1
    
        is_x_odd = fhe.bits(x)[0]
    
        # In a fast way, compute ans = is_x_odd * (z - y) + y
        ans = fhe.multivariate(lambda b, x: b * x)(is_x_odd, z - y) + y
    
        is_one = ans == 1
    
        return ans, is_one
    
    
    print("Compiling `collatz` function ...")
    inputset = [i for i in range(63)]
    collatz_fhe = collatz.compile(inputset, composable=True)
    
    print("Generating keyset ...")
    collatz_fhe.keygen()
    
    print("Encrypting initial value")
    x = 19
    x_enc = collatz_fhe.encrypt(x)
    is_one_enc = None
    
    print(f"| decrypted | cleartext |")
    while is_one_enc is None or not collatz_fhe.decrypt(is_one_enc):
        x_enc, is_one_enc = collatz_fhe.run(x_enc)
        x, is_one = collatz(x)
    
        # For demo purpose; no decryption is needed.
        x_dec = collatz_fhe.decrypt(x_enc)
        print(f"| {x_dec:<9} | {x:<9} |")
    Compiling `collatz` function ...
    Generating keyset ...
    Encrypting initial value
    | decrypted | cleartext |
    | 58        | 58        |
    | 29        | 29        |
    | 88        | 88        |
    | 44        | 44        |
    | 22        | 22        |
    | 11        | 11        |
    | 34        | 34        |
    | 17        | 17        |
    | 52        | 52        |
    | 26        | 26        |
    | 13        | 13        |
    | 40        | 40        |
    | 20        | 20        |
    | 10        | 10        |
    | 5         | 5         |
    | 16        | 16        |
    | 8         | 8         |
    | 4         | 4         |
    | 2         | 2         |
    | 1         | 1         |
    @fhe.compiler({"counter": "encrypted"})
    def double(counter):
       return counter * 2
    def noise_reset(x):
       return fhe.univariate(lambda x: x)(x)
    
    @fhe.compiler({"counter": "encrypted"})
    def double(counter):
       return noise_reset(counter * 2)
    def f(x):
        return (2 * x) + 3
    def f(x, y):
        return x + 2 * y
    
    x = Tracer(computation=Input("x"))
    y = Tracer(computation=Input("y"))
    
    resulting_tracer = f(x, y)
    [2, 3, 1]
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted", "y": "encrypted"})
    def min_two(x, y):
    	diff = y - x
    	min_x_y = y - np.maximum(y - x, 0)
    	return min_x_y
    
    inputset = [tuple(np.random.randint(0, 16, size=2)) for _ in range(50)]
    circuit = min_two.compile(inputset)
    
    x, y = np.random.randint(0, 16, size=2)
    assert circuit.encrypt_run_decrypt(x, y) == min(x, y)
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted", "y": "encrypted"})
    def max_two(x, y):
    	diff = y - x
    	max_x_y = y - np.minimum(y - x, 0)
    	return max_x_y
    
    inputset = [tuple(np.random.randint(0, 16, size=2)) for _ in range(50)]
    circuit = max_two.compile(inputset)
    
    x, y = np.random.randint(0, 16, size=2)
    assert circuit.encrypt_run_decrypt(x, y) == max(x, y)
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"args": "encrypted"})
    def fhe_min(args):
        remaining = list(args)
        while len(remaining) > 1:
            a = remaining.pop()
            b = remaining.pop()
            min_a_b = b - np.maximum(b - a, 0)
            remaining.insert(0, min_a_b)
        return remaining[0]
    
    inputset = [np.random.randint(0, 16, size=5) for _ in range(50)]
    circuit = fhe_min.compile(inputset)
    
    x1, x2, x3, x4, x5 = np.random.randint(0, 16, size=5)
    assert circuit.encrypt_run_decrypt([x1, x2, x3, x4, x5]) == min(x1, x2, x3, x4, x5)
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"array": "encrypted", "index": "encrypted"})
    def indexed_value(array, index):
        all_indices = np.arange(array.size)
        index_selection = index == all_indices
        selection_and_zeros = array * index_selection
        selection = np.sum(selection_and_zeros)
        return selection
    
    inputset = [(np.random.randint(0, 16, size=5), np.random.randint(0, 5)) for _ in range(50)]
    circuit = indexed_value.compile(inputset)
    
    array = np.random.randint(0, 16, size=5)
    
    index = np.random.randint(0, 5)
    assert circuit.encrypt_run_decrypt(array, index) == array[index]
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"numbers": "encrypted", "threshold": "encrypted"})
    def filtering(numbers, threshold):
        is_greater = numbers > threshold
    
        shifted_numbers = numbers * 2  # open space for a single bit at the end
        combined_numbers_and_is_greater = shifted_numbers + is_greater  # put is_greater to that bit
    
        def extract(combination):
            is_greater = (combination % 2) == 1  # extract is_greater back from packing
            if_true = combination // 2  # if is greater is true, we unpack the number and use it
            if_false = 0  # otherwise we set the element to zero
            return np.where(is_greater, if_true, if_false)  # and apply the operation
    
        return fhe.univariate(extract)(combined_numbers_and_is_greater)
    
    inputset = [(np.random.randint(0, 16, size=5), np.random.randint(0, 16)) for _ in range(50)]
    circuit = filtering.compile(inputset)
    
    numbers = np.random.randint(0, 16, size=5)
    threshold = np.random.randint(0, 16)
    assert np.array_equal(circuit.encrypt_run_decrypt(numbers, threshold), list(map(lambda x: x if x > threshold else 0, numbers)))
    
    import numpy as np
    from concrete import fhe
    
    def smallest_prime_divisor(n):
        if n % 2 == 0:
            return 2
    
        for i in range(3, int(np.sqrt(n)) + 1):
            if n % i == 0:
                return i
    
        return n
    
    def mean_of_vector(x):
        assert x.size != 0
        if x.size == 1:
            return x[0]
    
        group_size = smallest_prime_divisor(x.size)
        if x.size == group_size:
            return np.round(np.sum(x) / x.size).astype(np.int64)
    
        groups = []
        for i in range(x.size // group_size):
            start = i * group_size
            end = start + group_size
            groups.append(x[start:end])
    
        mean_of_groups = []
        for group in groups:
            mean_of_groups.append(np.round(np.sum(group) / group_size).astype(np.int64))
    
        return mean_of_vector(fhe.array(mean_of_groups))
    
    @fhe.compiler(({"x": "encrypted"}))
    def mean_of_matrix(x):
        return mean_of_vector(x.flatten())
    
    @fhe.compiler(({"x": "encrypted"}))
    def mean_of_rows_of_matrix(x):
        means = []
        for i in range(x.shape[0]):
            means.append(mean_of_vector(x[i]))
        return fhe.array(means)
    
    @fhe.compiler(({"x": "encrypted"}))
    def mean_of_columns_of_matrix(x):
        means = []
        for i in range(x.shape[1]):
            means.append(mean_of_vector(x[:, i]))
        return fhe.array(means)
    
    
    inputset = [np.random.randint(0, 16, size=(5,5)) for _ in range(50)]
    matrix = np.random.randint(0, 16, size=(5, 5))
    
    circuit = mean_of_matrix.compile(inputset)
    assert circuit.encrypt_run_decrypt(matrix) == round(matrix.mean())
    
    circuit = mean_of_rows_of_matrix.compile(inputset)
    assert np.array_equal(circuit.encrypt_run_decrypt(matrix), [round(x) for x in matrix.mean(1)])
    
    circuit = mean_of_columns_of_matrix.compile(inputset)
    assert np.array_equal(circuit.encrypt_run_decrypt(matrix), [round(x) for x in matrix.mean(0)])
    "SDFG.get" (%stream) : (!SDFG.stream<1024xi64>) -> (tensor<1024xi64>)
    "SDFG.init" : () -> !SDFG.dfg
    %in0 = "SDFG.make_stream" { type = #SDFG.stream_kind<host_to_device> }(%dfg) : (!SDFG.dfg) -> !SDFG.stream<tensor<1024xi64>>
    %in1 = "SDFG.make_stream" { type = #SDFG.stream_kind<host_to_device> }(%dfg) : (!SDFG.dfg) -> !SDFG.stream<tensor<1024xi64>>
    %out = "SDFG.make_stream" { type = #SDFG.stream_kind<device_to_host> }(%dfg) : (!SDFG.dfg) -> !SDFG.stream<tensor<1024xi64>>
    "SDFG.make_process" { type = #SDFG.process_kind<add_eint> }(%dfg, %in0, %in1, %out) :
      (!SDFG.dfg, !SDFG.stream<tensor<1024xi64>>, !SDFG.stream<tensor<1024xi64>>, !SDFG.stream<tensor<1024xi64>>) -> ()
    "SDFG.make_stream" { name = "stream", type = #SDFG.stream_kind<host_to_device> }(%dfg)
      : (!SDFG.dfg) -> !SDFG.stream<tensor<1024xi64>>
    "SDFG.put" (%stream, %data) : (!SDFG.stream<1024xi64>, tensor<1024xi64>) -> ()
    "SDFG.shutdown" (%dfg) : !SDFG.dfg
    "SDFG.start"(%dfg) : !SDFG.dfg
    #SDFG.process_kind<
      ::mlir::concretelang::SDFG::ProcessKind   # value
    >
    #SDFG.stream_kind<
      ::mlir::concretelang::SDFG::StreamKind   # value
    >

    Runtime Dialect

    Runtime dialect A dialect for representation the abstraction needed for the runtime.

    hashtag
    Operation definition

    hashtag

    RT.await_future (::mlir::concretelang::RT::AwaitFutureOp)

    Wait for a future and access its data.

    The results of a dataflow task are always futures which could be further used as inputs to subsequent tasks. When the result of a task is needed in the outer execution context, the result future needs to be synchronized and its data accessed using RT.await_future.

    hashtag
    Operands:

    Operand
    Description

    input

    Future with a parameterized element type

    hashtag
    Results:

    Result
    Description

    output

    any type

    hashtag
    RT.build_return_ptr_placeholder (::mlir::concretelang::RT::BuildReturnPtrPlaceholderOp)

    hashtag
    Results:

    Result
    Description

    output

    Pointer to a parameterized element type

    hashtag
    RT.clone_future (::mlir::concretelang::RT::CloneFutureOp)

    Interfaces: AllocationOpInterface, MemoryEffectOpInterface

    hashtag
    Operands:

    Operand
    Description

    input

    Future with a parameterized element type

    hashtag
    Results:

    Result
    Description

    output

    Future with a parameterized element type

    hashtag
    RT.create_async_task (::mlir::concretelang::RT::CreateAsyncTaskOp)

    Create a dataflow task.

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    workfn

    ::mlir::SymbolRefAttr

    symbol reference attribute

    hashtag
    Operands:

    Operand
    Description

    list

    any type

    hashtag
    RT.dataflow_task (::mlir::concretelang::RT::DataflowTaskOp)

    Dataflow task operation

    RT.dataflow_task allows to specify a task that will be concurrently executed when their operands are ready. Operands are either the results of computation in other RT.dataflow_task (dataflow dependences) or obtained from the execution context (immediate operands). Operands are synchronized using futures and, in the case of immediate operands, copied when the task is created. Caution is required when the operand is a pointer as no deep copy will occur.

    Example:

    Traits: AutomaticAllocationScope, SingleBlockImplicitTerminator

    Interfaces: AllocationOpInterface, MemoryEffectOpInterface, RegionBranchOpInterface

    hashtag
    Operands:

    Operand
    Description

    inputs

    any type

    hashtag
    Results:

    Result
    Description

    outputs

    any type

    hashtag
    RT.dataflow_yield (::mlir::concretelang::RT::DataflowYieldOp)

    Dataflow yield operation

    RT.dataflow_yield is a special terminator operation for blocks inside the region in RT.dataflow_task. It allows to specify the return values of a RT.dataflow_task.

    Example:

    Traits: ReturnLike, Terminator

    hashtag
    Operands:

    Operand
    Description

    values

    any type

    hashtag
    RT.deallocate_future_data (::mlir::concretelang::RT::DeallocateFutureDataOp)

    hashtag
    Operands:

    Operand
    Description

    input

    Future with a parameterized element type

    hashtag
    RT.deallocate_future (::mlir::concretelang::RT::DeallocateFutureOp)

    hashtag
    Operands:

    Operand
    Description

    input

    any type

    hashtag
    RT.deref_return_ptr_placeholder (::mlir::concretelang::RT::DerefReturnPtrPlaceholderOp)

    hashtag
    Operands:

    Operand
    Description

    input

    Pointer to a parameterized element type

    hashtag
    Results:

    Result
    Description

    output

    Future with a parameterized element type

    hashtag
    RT.deref_work_function_argument_ptr_placeholder (::mlir::concretelang::RT::DerefWorkFunctionArgumentPtrPlaceholderOp)

    hashtag
    Operands:

    Operand
    Description

    input

    Pointer to a parameterized element type

    hashtag
    Results:

    Result
    Description

    output

    any type

    hashtag
    RT.make_ready_future (::mlir::concretelang::RT::MakeReadyFutureOp)

    Build a ready future.

    Data passed to dataflow tasks must be encapsulated in futures, including immediate operands. These must be converted into futures using RT.make_ready_future.

    Interfaces: AllocationOpInterface, MemoryEffectOpInterface

    hashtag
    Operands:

    Operand
    Description

    input

    any type

    memrefCloned

    any type

    hashtag
    Results:

    Result
    Description

    output

    Future with a parameterized element type

    hashtag
    RT.register_task_work_function (::mlir::concretelang::RT::RegisterTaskWorkFunctionOp)

    Register the task work-function with the runtime system.

    hashtag
    Operands:

    Operand
    Description

    list

    any type

    hashtag
    RT.work_function_return (::mlir::concretelang::RT::WorkFunctionReturnOp)

    hashtag
    Operands:

    Operand
    Description

    in

    any type

    out

    any type

    hashtag
    Type definition

    hashtag
    FutureType

    Future with a parameterized element type

    The value of a !RT.future type represents the result of an asynchronous operation.

    Examples:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    elementType

    Type

    hashtag
    PointerType

    Pointer to a parameterized element type

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    elementType

    Type

    func @test(%0 : i64): (i64, i64) {
        // Execute right now as %0 is ready.
        %1, %2 = "RT.dataflow_task"(%0) ({
            %a = addi %0, %0 : i64
            %b = muli %0, %0 : i64
            "RT.dataflow_yield"(%a, %b) : (i64, i64) -> i64
        }) : (i64, i64) -> (i64, i64)
        // Concurrently execute both tasks below when the task above is completed.
        %3 = "RT.dataflow_task"(%1) ({
            %c = constant 1 : %i64
            %a = addi %1, %c : i64
            "RT.dataflow_yield"(%a) : (i64, i64) -> i64
        }) : (i64, i64) -> (i64, i64)
        %4 = "RT.dataflow_task"(%2) ({
            %c = constant 2 : %i64
            %a = addi %2, %c : i64
            "RT.dataflow_yield"(%a) : (i64, i64) -> i64
        }) : (i64, i64) -> (i64, i64)
        return %3, %4 : (i64, i64)
    }
    %0 = constant 1 : i64
    %1 = constant 2 : i64
    "RT.dataflow_yield" %0, %1 : i64, i64
    !RT.future<i64>

    Extensions

    Concrete supports native Python and NumPy operations as much as possible, but not everything in Python or NumPy is available. Therefore, we provide some extensions ourselves to improve your experience.

    hashtag
    fhe.univariate(function)

    Allows you to wrap any univariate function into a single table lookup:

    triangle-exclamation

    The wrapped function:

    • shouldn't have any side effects (e.g., no modification of global state)

    • should be deterministic (e.g., no random numbers)

    hashtag
    fhe.multivariate(function)

    Allows you to wrap any multivariate function into a table lookup:

    triangle-exclamation

    The wrapped function:

    • shouldn't have any side effects (e.g., no modification of global state)

    circle-exclamation

    Multivariate functions cannot be called with inputs.

    hashtag
    fhe.conv(...)

    Allows you to perform a convolution operation, with the same semantic as :

    triangle-exclamation

    Only 2D convolutions without padding and with one group are currently supported.

    hashtag
    fhe.maxpool(...)

    Allows you to perform a maxpool operation, with the same semantic as :

    triangle-exclamation

    Only 2D maxpooling without padding and up to 15-bits is currently supported.

    hashtag
    fhe.array(...)

    Allows you to create encrypted arrays:

    triangle-exclamation

    Currently, only scalars can be used to create arrays.

    hashtag
    fhe.zero()

    Allows you to create an encrypted scalar zero:

    hashtag
    fhe.zeros(shape)

    Allows you to create an encrypted tensor of zeros:

    hashtag
    fhe.one()

    Allows you to create an encrypted scalar one:

    hashtag
    fhe.ones(shape)

    Allows you to create an encrypted tensor of ones:

    hashtag
    fhe.hint(value, **kwargs)

    Allows you to 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:

    circle-exclamation

    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:

    Truncating

    Table lookups have a strict constraint on the number of bits they support. This can be limiting, especially if you don't need exact precision. As well as this, using larger bit-widths leads to slower table lookups.

    To overcome these issues, truncated table lookups are introduced. This operation provides a way to zero the least significant bits of a large integer and then apply the table lookup on the resulting (smaller) value.

    Imagine you have a 5-bit value, you can use fhe.truncate_bit_pattern(value, lsbs_to_remove=2) to truncate it (here the last 2 bits are discarded). Once truncated, value will remain in 5-bits (e.g., 22 = 0b10110 would be truncated to 20 = 0b10100), and the last 2 bits of it would be zero. Concrete uses this to optimize table lookups on the truncated value, the 5-bit table lookup gets optimized to a 3-bit table lookup, which is much faster!

    Let's see how truncation works in practice:

    prints:

    and displays:

    Now, let's see how truncating can be used in FHE.

    prints:

    circle-info

    These speed-ups can vary from system to system.

    circle-info

    The reason why the speed-up is not increasing with lsbs_to_remove is because the truncating operation itself has a cost: each bit removal is a PBS. Therefore, if a lot of bits are removed, truncation itself could take longer than the bigger TLU which is evaluated afterwards.

    and displays:

    hashtag
    Auto Truncators

    Truncating is very useful but, in some cases, you don't know how many bits your input contains, so it's not reliable to specify lsbs_to_remove manually. For this reason, the AutoTruncator class is introduced.

    AutoTruncator allows you to set how many of the most significant bits to keep, but they need to be adjusted using an inputset to determine how many of the least significant bits to remove. This can be done manually using fhe.AutoTruncator.adjust(function, inputset), or by setting auto_adjust_truncators configuration to True during compilation.

    Here is how auto truncators can be used in FHE:

    prints:

    and displays:

    circle-exclamation

    AutoTruncators should be defined outside the function that is being compiled. They are used to store the result of the adjustment process, so they shouldn't be created each time the function is called. Furthermore, each AutoTruncator should be used with exactly one truncate_bit_pattern call.

    Compatibility

    hashtag
    Supported operations

    Here are the operations you can use inside the function you are compiling:

    circle-info

    Min/Max Operations

    Finding the minimum or maximum of two numbers is not a native operation in Concrete, so it needs to be implemented using existing native operations (i.e., additions, clear multiplications, negations, table lookups). Concrete offers two different implementations for this.

    hashtag
    Chunked

    This is the most general implementation that can be used in any situation. The idea is:

    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))
    import matplotlib.pyplot as plt
    import numpy as np
    from concrete import fhe
    
    original_bit_width = 5
    lsbs_to_remove = 2
    
    assert 0 < lsbs_to_remove < original_bit_width
    
    original_values = list(range(2**original_bit_width))
    truncated_values = [
        fhe.truncate_bit_pattern(value, lsbs_to_remove)
        for value in original_values
    ]
    
    previous_truncated = truncated_values[0]
    for original, truncated in zip(original_values, truncated_values):
        if truncated != previous_truncated:
            previous_truncated = truncated
            print()
    
        original_binary = np.binary_repr(original, width=(original_bit_width + 1))
        truncated_binary = np.binary_repr(truncated, width=(original_bit_width + 1))
    
        print(
            f"{original:2} = 0b_{original_binary[:-lsbs_to_remove]}[{original_binary[-lsbs_to_remove:]}] "
            f"=> "
            f"0b_{truncated_binary[:-lsbs_to_remove]}[{truncated_binary[-lsbs_to_remove:]}] = {truncated}"
        )
    
    fig = plt.figure()
    ax = fig.add_subplot()
    
    plt.plot(original_values, original_values, label="original", color="black")
    plt.plot(original_values, truncated_values, label="truncated", color="green")
    plt.legend()
    
    ax.set_aspect("equal", adjustable="box")
    plt.show()

    should have the same output shape as its input (i.e., output.shape should be the same with input.shape)

  • each output element should correspond to a single input element (e.g., output[0] should only depend on input[0])

  • If any of these constraints are violated, the outcome is undefined.

    should be deterministic (e.g., no random numbers)
  • should have input shapes which are broadcastable to the output shape (i.e., input.shape should be broadcastable to output.shape for all inputs)

  • each output element should correspond to a single input element (e.g., output[0] should only depend on input[0] of all inputs)

  • If any of these constraints are violated, the outcome is undefined.

    rounded
    onnx.Convarrow-up-right
    onnx.MaxPoolarrow-up-right
    import numpy as np
    from concrete import fhe
    
    def value_if_condition_else_zero(value, condition):
        return value if condition else np.zeros_like(value, dtype=np.int64)
    
    def function(x, y):
        return fhe.multivariate(value_if_condition_else_zero)(x, y)
    
    inputset = [
        (
            np.random.randint(-2**4, 2**4, size=(2, 2)),
            np.random.randint(0, 2**1, size=()),
        )
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(function, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset)
    
    sample = [np.array([[-2, 4], [0, 1]]), 0]
    assert np.array_equal(circuit.encrypt_run_decrypt(*sample), function(*sample))
    
    sample = [np.array([[3, -1], [2, 4]]), 1]
    assert np.array_equal(circuit.encrypt_run_decrypt(*sample), function(*sample))
    import numpy as np
    from concrete import fhe
    
    weight = np.array([[2, 1], [3, 2]]).reshape(1, 1, 2, 2)
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return fhe.conv(x, weight, strides=(2, 2), dilations=(1, 1), group=1)
    
    inputset = [np.random.randint(0, 4, size=(1, 1, 4, 4)) for _ in range(10)]
    circuit = f.compile(inputset)
    
    sample = np.array(
        [
            [3, 2, 1, 0],
            [3, 2, 1, 0],
            [3, 2, 1, 0],
            [3, 2, 1, 0],
        ]
    ).reshape(1, 1, 4, 4)
    assert np.array_equal(circuit.encrypt_run_decrypt(sample), f(sample))
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return fhe.maxpool(x, kernel_shape=(2, 2), strides=(2, 2), dilations=(1, 1))
    
    inputset = [np.random.randint(0, 4, size=(1, 1, 4, 4)) for _ in range(10)]
    circuit = f.compile(inputset)
    
    sample = np.array(
        [
            [3, 2, 1, 0],
            [3, 2, 1, 0],
            [3, 2, 1, 0],
            [3, 2, 1, 0],
        ]
    ).reshape(1, 1, 4, 4)
    assert np.array_equal(circuit.encrypt_run_decrypt(sample), f(sample))
    import numpy as np
    from concrete import fhe
    
    @fhe.compiler({"x": "encrypted", "y": "encrypted"})
    def f(x, y):
        return fhe.array([x, y])
    
    inputset = [(3, 2), (7, 0), (0, 7), (4, 2)]
    circuit = f.compile(inputset)
    
    sample = (3, 4)
    assert np.array_equal(circuit.encrypt_run_decrypt(*sample), f(*sample))
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        z = fhe.zero()
        return x + z
    
    inputset = range(10)
    circuit = f.compile(inputset)
    
    for x in range(10):
        assert circuit.encrypt_run_decrypt(x) == x
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        z = fhe.zeros((2, 3))
        return x + z
    
    inputset = range(10)
    circuit = f.compile(inputset)
    
    for x in range(10):
        assert np.array_equal(circuit.encrypt_run_decrypt(x), np.array([[x, x, x], [x, x, x]]))
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        z = fhe.one()
        return x + z
    
    inputset = range(10)
    circuit = f.compile(inputset)
    
    for x in range(10):
        assert circuit.encrypt_run_decrypt(x) == x + 1
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        z = fhe.ones((2, 3))
        return x + z
    
    inputset = range(10)
    circuit = f.compile(inputset)
    
    for x in range(10):
        assert np.array_equal(circuit.encrypt_run_decrypt(x), np.array([[x, x, x], [x, x, x]]) + 1)
    from concrete import fhe
    import numpy as np
    
    @fhe.compiler({"x": "encrypted"})
    def f(x, y, z):
        a = x | y
        b = y & z
        c = a ^ b
        return c
    
    inputset = [
        (np.random.randint(0, 2**8), np.random.randint(0, 2**8), np.random.randint(0, 2**8))
        for _ in range(3)
    ]
    circuit = f.compile(inputset)
    
    print(circuit)
    %0 = x                          # EncryptedScalar<uint8>        ∈ [173, 240]
    %1 = y                          # EncryptedScalar<uint8>        ∈ [52, 219]
    %2 = z                          # EncryptedScalar<uint8>        ∈ [36, 252]
    %3 = bitwise_or(%0, %1)         # EncryptedScalar<uint8>        ∈ [243, 255]
    %4 = bitwise_and(%1, %2)        # EncryptedScalar<uint7>        ∈ [0, 112] 
                                                      ^^^^^ this can lead to bugs
    %5 = bitwise_xor(%3, %4)        # EncryptedScalar<uint8>        ∈ [131, 255]
    return %5
    @fhe.compiler({"x": "encrypted", "y": "encrypted", "z": "encrypted"})
    def f(x, y, z):
        # hint that inputs should be considered at least 8-bits
        x = fhe.hint(x, bit_width=8)
        y = fhe.hint(y, bit_width=8)
        z = fhe.hint(z, bit_width=8)
    
        # hint that intermediates should be considered at least 8-bits
        a = fhe.hint(x | y, bit_width=8)
        b = fhe.hint(y & z, bit_width=8)
        c = fhe.hint(a ^ b, bit_width=8)
    
        return c
    %0 = x                          # EncryptedScalar<uint8>        ∈ [...]
    %1 = y                          # EncryptedScalar<uint8>        ∈ [...]
    %2 = z                          # EncryptedScalar<uint8>        ∈ [...]
    %3 = bitwise_or(%0, %1)         # EncryptedScalar<uint8>        ∈ [...]
    %4 = bitwise_and(%1, %2)        # EncryptedScalar<uint8>        ∈ [...] 
    %5 = bitwise_xor(%3, %4)        # EncryptedScalar<uint8>        ∈ [...]
    return %5
    @fhe.compiler({"x": "encrypted", "y": "encrypted"})
    def is_vectors_same(x, y):
        assert x.ndim != 1
        assert y.ndim != 1
        
        assert len(x) == len(y)
        n = len(x)
        
        number_of_same_elements = np.sum(x == y)
        fhe.hint(number_of_same_elements, can_store=n)  # hint that number of same elements can go up to n
        is_same = number_of_same_elements == n
    
        return is_same
     0 = 0b_0000[00] => 0b_0000[00] = 0
     1 = 0b_0000[01] => 0b_0000[00] = 0
     2 = 0b_0000[10] => 0b_0000[00] = 0
     3 = 0b_0000[11] => 0b_0000[00] = 0
    
     4 = 0b_0001[00] => 0b_0001[00] = 4
     5 = 0b_0001[01] => 0b_0001[00] = 4
     6 = 0b_0001[10] => 0b_0001[00] = 4
     7 = 0b_0001[11] => 0b_0001[00] = 4
    
     8 = 0b_0010[00] => 0b_0010[00] = 8
     9 = 0b_0010[01] => 0b_0010[00] = 8
    10 = 0b_0010[10] => 0b_0010[00] = 8
    11 = 0b_0010[11] => 0b_0010[00] = 8
    
    12 = 0b_0011[00] => 0b_0011[00] = 12
    13 = 0b_0011[01] => 0b_0011[00] = 12
    14 = 0b_0011[10] => 0b_0011[00] = 12
    15 = 0b_0011[11] => 0b_0011[00] = 12
    
    16 = 0b_0100[00] => 0b_0100[00] = 16
    17 = 0b_0100[01] => 0b_0100[00] = 16
    18 = 0b_0100[10] => 0b_0100[00] = 16
    19 = 0b_0100[11] => 0b_0100[00] = 16
    
    20 = 0b_0101[00] => 0b_0101[00] = 20
    21 = 0b_0101[01] => 0b_0101[00] = 20
    22 = 0b_0101[10] => 0b_0101[00] = 20
    23 = 0b_0101[11] => 0b_0101[00] = 20
    
    24 = 0b_0110[00] => 0b_0110[00] = 24
    25 = 0b_0110[01] => 0b_0110[00] = 24
    26 = 0b_0110[10] => 0b_0110[00] = 24
    27 = 0b_0110[11] => 0b_0110[00] = 24
    
    28 = 0b_0111[00] => 0b_0111[00] = 28
    29 = 0b_0111[01] => 0b_0111[00] = 28
    30 = 0b_0111[10] => 0b_0111[00] = 28
    31 = 0b_0111[11] => 0b_0111[00] = 28
    import itertools
    import time
    
    import matplotlib.pyplot as plt
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        enable_unsafe_features=True,
        use_insecure_key_cache=True,
        insecure_key_cache_location=".keys",
    )
    
    input_bit_width = 6
    input_range = np.array(range(2**input_bit_width))
    
    timings = {}
    results = {}
    
    for lsbs_to_remove in range(input_bit_width):
        @fhe.compiler({"x": "encrypted"})
        def f(x):
            return fhe.truncate_bit_pattern(x, lsbs_to_remove) ** 2
        
        circuit = f.compile(inputset=[input_range], configuration=configuration)
        circuit.keygen()
        
        encrypted_sample = circuit.encrypt(input_range)
        start = time.time()
        encrypted_result = circuit.run(encrypted_sample)
        end = time.time()
        result = circuit.decrypt(encrypted_result)
        
        took = end - start
        
        timings[lsbs_to_remove] = took
        results[lsbs_to_remove] = result
    
    number_of_figures = len(results)
    
    columns = 1
    for i in range(2, number_of_figures):
        if number_of_figures % i == 0:
            columns = i
    rows = number_of_figures // columns
    
    fig, axs = plt.subplots(rows, columns)
    axs = axs.flatten()
    
    baseline = timings[0]
    for lsbs_to_remove in range(input_bit_width):
        timing = timings[lsbs_to_remove]
        speedup = baseline / timing
        print(f"lsbs_to_remove={lsbs_to_remove} => {speedup:.2f}x speedup")
    
        axs[lsbs_to_remove].set_title(f"lsbs_to_remove={lsbs_to_remove}")
        axs[lsbs_to_remove].plot(input_range, results[lsbs_to_remove])
    
    plt.show()
    lsbs_to_remove=0 => 1.00x speedup
    lsbs_to_remove=1 => 1.69x speedup
    lsbs_to_remove=2 => 3.48x speedup
    lsbs_to_remove=3 => 3.06x speedup
    lsbs_to_remove=4 => 3.46x speedup
    lsbs_to_remove=5 => 3.14x speedup
    import itertools
    import time
    
    import matplotlib.pyplot as plt
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        enable_unsafe_features=True,
        use_insecure_key_cache=True,
        insecure_key_cache_location=".keys",
        single_precision=False,
        parameter_selection_strategy=fhe.ParameterSelectionStrategy.MULTI,
    )
    
    input_bit_width = 6
    input_range = np.array(range(2**input_bit_width))
    
    timings = {}
    results = {}
    
    for target_msbs in reversed(range(1, input_bit_width + 1)):
        truncator = fhe.AutoTruncator(target_msbs)
    
        @fhe.compiler({"x": "encrypted"})
        def f(x):
            return fhe.truncate_bit_pattern(x, lsbs_to_remove=truncator) ** 2
    
        fhe.AutoTruncator.adjust(f, inputset=[input_range])
    
        circuit = f.compile(inputset=[input_range], configuration=configuration)
        circuit.keygen()
    
        encrypted_sample = circuit.encrypt(input_range)
        start = time.time()
        encrypted_result = circuit.run(encrypted_sample)
        end = time.time()
        result = circuit.decrypt(encrypted_result)
    
        took = end - start
    
        timings[target_msbs] = took
        results[target_msbs] = result
    
    number_of_figures = len(results)
    
    columns = 1
    for i in range(2, number_of_figures):
        if number_of_figures % i == 0:
            columns = i
    rows = number_of_figures // columns
    
    fig, axs = plt.subplots(rows, columns)
    axs = axs.flatten()
    
    baseline = timings[input_bit_width]
    for i, target_msbs in enumerate(reversed(range(1, input_bit_width + 1))):
        timing = timings[target_msbs]
        speedup = baseline / timing
        print(f"target_msbs={target_msbs} => {speedup:.2f}x speedup")
    
        axs[i].set_title(f"target_msbs={target_msbs}")
        axs[i].plot(input_range, results[target_msbs])
    
    plt.show()
    target_msbs=6 => 1.00x speedup
    target_msbs=5 => 1.80x speedup
    target_msbs=4 => 3.47x speedup
    target_msbs=3 => 3.02x speedup
    target_msbs=2 => 3.38x speedup
    target_msbs=1 => 3.37x speedup
    Some of these operations are not supported between two encrypted values. A detailed error will be raised if you try to do something that is not supported.

    hashtag
    Supported Python operators.

    • __abs__arrow-up-right

    • __add__arrow-up-right

    • __and__arrow-up-right

    hashtag
    Supported NumPy functions.

    • np.absolutearrow-up-right

    • np.addarrow-up-right

    • np.arccosarrow-up-right

    hashtag
    Supported ndarray methods.

    • np.ndarray.astypearrow-up-right

    • np.ndarray.cliparrow-up-right

    • np.ndarray.dotarrow-up-right

    hashtag
    Supported ndarray properties.

    • np.ndarray.shapearrow-up-right

    • np.ndarray.ndimarrow-up-right

    • np.ndarray.sizearrow-up-right

    hashtag
    Limitations

    hashtag
    Control flow constraints.

    Some Python control flow statements are not supported. You cannot have an if statement or a while statement for which the condition depends on an encrypted value. However, such statements are supported with constant values (e.g., for i in range(SOME_CONSTANT), if os.environ.get("SOME_FEATURE") == "ON":).

    hashtag
    Type constraints.

    You cannot have floating-point inputs or floating-point outputs. You can have floating-point intermediate values as long as they can be converted to an integer Table Lookup (e.g., (60 * np.sin(x)).astype(np.int64)).

    hashtag
    Bit width constraints.

    There is a limit on the bit width of encrypted values. We are constantly working on increasing this bit width. If you go above the limit, you will get an error.

    hashtag
    Notes
    • Initial comparison is chunked as well, which is already very expensive.

    • Multiplication with operands aren't allowed to increase the bit-width of the inputs, so they are very expensive as well.

    • Optimal chunk size is selected automatically to reduce the number of table lookups.

    • Chunked comparisons result in at least 9 and at most 21 table lookups.

    • It is used if no other implementation can be used.

    hashtag
    Pros

    • Can be used with any integers.

    hashtag
    Cons

    • Extremely expensive.

    hashtag
    Example

    produces

    hashtag
    Min/Max Trick

    This implementation uses the fact that [min,max](x, y) is equal to [min, max](x - y, 0) + y, which is just a subtraction, a table lookup and an addition!

    There are two major problems with this implementation though:

    1. subtraction before the TLU requires up to 2 additional bits to avoid overflows (it is 1 in most cases).

    2. subtraction and addition require the same bit-width across operands.

    What this means is that if we are comparing uint3 and uint6, we need to convert both of them to uint7 in some way to do the subtraction and proceed with the TLU in 7-bits. There are 2 ways to achieve this behavior.

    hashtag
    Requirements

    hashtag
    1. fhe.ComparisonStrategy.ONE_TLU_PROMOTED

    This strategy makes sure that during bit-width assignment, both operands are assigned the same bit-width, and that bit-width contains at least the amount of bits required to store x - y. The idea is:

    hashtag
    Pros

    • It will always result in a single table lookup.

    hashtag
    Cons

    • It will increase the bit-width of both operands and the result, and lock them together across the whole circuit, which can result in significant slowdowns if the result or the operands are used in other costly operations.

    hashtag
    Example

    produces

    hashtag
    2. fhe.ComparisonStrategy.THREE_TLU_CASTED

    This strategy will not put any constraint on bit-widths during bit-width assignment. Instead, operands are cast to a bit-width that can store x - y during runtime using table lookups. The idea is:

    hashtag
    Notes

    • It can result in a single table lookup as well, if x and y are assigned (because of other operations) the same bit-width, and that bit-width can store x - y.

    • Or in two table lookups if only one of the operands is assigned a bit-width bigger than or equal to the bit width that can store x - y.

    hashtag
    Pros

    • It will not put any constraints on bit-widths of the operands, which is amazing if they are used in other costly operations.

    • It will result in at most 3 table lookups, which is still good.

    hashtag
    Cons

    • If you are not doing anything else with the operands, or doing less costly operations compared to comparison, it will introduce up to two unnecessary table lookups and slow down execution compared to fhe.MinMaxStrategy.ONE_TLU_PROMOTED.

    hashtag
    Example

    produces

    hashtag
    Summary

    Strategy
    Minimum # of TLUs
    Maximum # of TLUs
    Can increase the bit-width of the inputs

    CHUNKED

    9

    21

    ONE_TLU_PROMOTED

    1

    1

    ✓

    circle-info

    Concrete will choose the best strategy available after bit-width assignment, regardless of the specified preference.

    circle-info

    Different strategies are good for different circuits. If you want the best runtime for your use case, you can compile your circuit with all different comparison strategy preferences, and pick the one with the lowest complexity.

    MLIR FHE Dialects

    hashtag
    Introduction

    Compilation of a Python program starts with Concrete's Python frontend, which first traces and transforms it and then converts it into an intermediate representation (IR) that is further processed by Concrete Compiler. This IR is based on the MLIR subprojectarrow-up-right of the LLVM compiler infrastructurearrow-up-right. This document provides an overview of Concrete's FHE-specific representations based on the MLIR framework.

    In contrast to traditional infrastructure for compilers, the set of operations and data types that constitute the IR, as well as the level of abstraction that the IR represents, are not fixed in MLIR and can easily be extended. All operations and data types are grouped into dialectsarrow-up-right, with each dialect representing a specific domain or a specific level of abstraction. Mixing operations and types from different dialects within the same IR is allowed and even encouraged, with all dialects--builtin or developed as an extension--being first-class citizens.

    Concrete compiler takes advantage of these concepts by defining a set of dialects, capable of representing an FHE program from an abstract specification that is independent of the actual cryptosystem down to a program that can easily be mapped to function calls of a cryptographic library. The dialects for the representation of an FHE program are:

    • The FHELinalg Dialect (, )

    • The FHE Dialect (, )

    • The TFHE Dialect (, )

    In addition, the project further defines two dialects that help expose dynamic task-parallelism and static data-flow graphs in order to benefit from multi-core, multi-accelerator and distributed systems. These are:

    • The RT Dialect (, ) and

    • The SDFG Dialect (, ).

    The figure below illustrates the relationship between the dialects and their embedding into the compilation pipeline.

    The following sections focus on the FHE-related dialects, i.e., on the FHELinalg Dialect, the FHE Dialect, the TFHE Dialect and the Concrete Dialect.

    hashtag
    The FHE and FHELinalg Dialects: An abstract specification of an FHE program

    The top part of the figure shows the components which are involved in the generation of the initial IR, ending with the step labelled MLIR translation. When the initial IR is passed on to Concrete Compiler through its Python bindings, all FHE-related operations are specified using either the FHE or FHELinalg Dialect. Both of these dialects provide operations and data types for the abstract specification of an FHE program, completely independently of a cryptosystem. At this point, the IR simply indicates whether an operand is encrypted (via the type FHE.eint<n>, where n stands for the precision in bits) and what operations are applied to encrypted values. Plaintext values simply use MLIR's builtin integer type in (e.g., i3 or i64).

    The FHE Dialect provides scalar operations on encrypted integers, such as additions (FHE.add_eint) or multiplications (FHE.mul_eint), while the FHELinalg Dialect offers operations on tensors of encrypted integers, e.g., matrix products (FHELinalg.matmul_eint_eint) or convolutions (FHELinalg.conv2d).

    In a first lowering step of the pipeline, all FHELinalg operations are lowered to operations from using scalar operations from the FHE Dialect. Consider the following example, which consists of a function that performs a multiplication of a matrix of encrypted integers and a matrix of cleartext values:

    Upon conversion, the FHELinalg.matmul operation is converted to a linalg.generic operation whose body contains a scalar multiplication (FHE.mul_eint_int) and a scalar addition (FHE.add_eint_int):

    This is then further lowered to a nest of loops from , implementing the parallel and reduction dimensions from the linalg.generic operation above:

    hashtag
    The TFHE Dialect: Binding to the TFHE cryptosystem and parametrization

    In order to obtain an executable program at the end of the compilation pipeline, the abstract specification of the FHE program must at some point be bound to a specific cryptosystem. This is the role of the TFHE Dialect, whose purpose is:

    • to indicate operations to be carried out using an implementation of the TFHE cryptosystem

    • to parametrize the cryptosystem with key sizes, and

    • to provide a mapping between keys and encrypted values

    When lowering the IR based on the FHE Dialect to the TFHE Dialect, the compiler first generates a generic form, in which FHE operations are lowered to TFHE operations and where values are converted to unparametrized TFHE.glwe values. The unparametrized form TFHE.glwe<sk?> simply indicates that a TFHE.glwe value is to be used, but without any indication of the cryptographic parameters and the actual key.

    The IR below shows the example program after lowering to unparametrized TFHE:

    All operations from the FHE dialect have been replaced with corresponding operations from the TFHE Dialect.

    During subsequent parametrization, the compiler can either use a set of default parameters or can obtain a set of parameters from Concrete's optimizer. Either way, an additional pass injects the parameters into the IR, replacing all TFHE.glwe<sk?> instances with TFHE.glwe<i,d,n>, where i is a sequential identifier for a key, d the number of GLWE dimensions and n the size of the GLWE polynomial.

    The result of such a parametrization for the example is given below:

    In this parametrization, a single key with the ID 0 is used, with a single dimension and a polynomial of size 512.

    hashtag
    The Concrete Dialect: Preparing bindings with a crypto library

    In the next step of the pipeline, operations and types are lowered to the Concrete Dialect. This dialect provides operations, which are implemented by one of Concrete's backend libraries, but still abstracts from any technical details required for interaction with an actual library. The goal is to maintain a high-level representation with value-based semantics and actual operations instead of buffer semantics and library calls, while ensuring that all operations an effectively be lowered to a library call later in the pipeline. However, the abstract types from TFHE are already lowered to tensors of integers with a suitable shape that will hold the binary data of the encrypted values.

    The result of the lowering of the example to the Concrete Dialect is shown below:

    hashtag
    Bufferization and emitting library calls

    The remaining stages of the pipeline are rather technical. Before any binding to an actual Concrete backend library, the compiler first invokes to convert the value-based IR into an IR with buffer semantics. In particular, this means that keys and encrypted values are no longer abstract values in a mathematical sense, but values backed by a memory location that holds the actual data. This form of IR is then suitable for a pass emitting actual library calls that implement the corresponding operations from the Concrete Dialect for a specific backend.

    The result for the example is given below:

    At this stage, the IR is only composed of operations from builtin Dialects and thus amenable to lowering to LLVM-IR using the lowering passes provided by MLIR.

    Table Lookups

    In this tutorial, we will review how to perform direct table lookups in Concrete.

    hashtag
    Direct table lookup

    Concrete provides a LookupTable class to create your own tables and apply them in your circuits.

    (x - y).bit_width <= MAXIMUM_TLU_BIT_WIDTH
    # (example below is for bit-width of 8 and chunk size of 4)
    
    # compare lhs and rhs
    select_lhs = lhs < rhs  # or lhs > rhs for maximum
    
    # multiply lhs with select_lhs
    lhs_contribution = lhs * select_lhs
    
    # multiply rhs with 1 - select_lhs
    rhs_contribution = rhs * (1 - select_lhs)
    
    # compute the result
    result = lhs_contribution + rhs_contribution
    import numpy as np
    from concrete import fhe
    
    def f(x, y):
        return np.minimum(x, y)
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**4))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, show_mlir=True)
    module {
    
      func.func @main(%arg0: !FHE.eint<4>, %arg1: !FHE.eint<4>) -> !FHE.eint<4> {
      
        // calculating select_x, which is x < y since we're computing the minimum
        %cst = arith.constant dense<[0, 0, 0, 0, 4, 4, 4, 4, 8, 8, 8, 8, 12, 12, 12, 12]> : tensor<16xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        %cst_0 = arith.constant dense<[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]> : tensor<16xi64>
        %1 = "FHE.apply_lookup_table"(%arg1, %cst_0) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        %2 = "FHE.add_eint"(%0, %1) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %cst_1 = arith.constant dense<[0, 1, 1, 1, 2, 0, 1, 1, 2, 2, 0, 1, 2, 2, 2, 0]> : tensor<16xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_1) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        %cst_2 = arith.constant dense<[0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12]> : tensor<16xi64>
        %4 = "FHE.apply_lookup_table"(%arg0, %cst_2) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        %cst_3 = arith.constant dense<[0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]> : tensor<16xi64>
        %5 = "FHE.apply_lookup_table"(%arg1, %cst_3) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        %6 = "FHE.add_eint"(%4, %5) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %cst_4 = arith.constant dense<[0, 4, 4, 4, 8, 0, 4, 4, 8, 8, 0, 4, 8, 8, 8, 0]> : tensor<16xi64>
        %7 = "FHE.apply_lookup_table"(%6, %cst_4) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        %8 = "FHE.add_eint"(%7, %3) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %cst_5 = arith.constant dense<[0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0]> : tensor<16xi64>
        %9 = "FHE.apply_lookup_table"(%8, %cst_5) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<1>
        
        // extracting the first 2 bits of x shifhted to left by 1 bits for packing
        %cst_6 = arith.constant dense<[0, 2, 4, 6, 0, 2, 4, 6, 0, 2, 4, 6, 0, 2, 4, 6]> : tensor<16xi64>
        %10 = "FHE.apply_lookup_table"(%arg0, %cst_6) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<3>
        
        // casting select_x to 3 bits for packing
        %cst_7 = arith.constant dense<[0, 1]> : tensor<2xi64>
        %11 = "FHE.apply_lookup_table"(%9, %cst_7) : (!FHE.eint<1>, tensor<2xi64>) -> !FHE.eint<3>
        
        // packing the first 2 bits of x with select_x
        %12 = "FHE.add_eint"(%10, %11) : (!FHE.eint<3>, !FHE.eint<3>) -> !FHE.eint<3>
        
        // calculating contribution of 0 if select_x is 0 else the first 2 bits of x
        %cst_8 = arith.constant dense<[0, 0, 0, 1, 0, 2, 0, 3]> : tensor<8xi64>
        %13 = "FHE.apply_lookup_table"(%12, %cst_8) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<4>
        
        // extracting the last 2 bits of x shifhted to the left by 1 bit for packing
        %cst_9 = arith.constant dense<[0, 0, 0, 0, 2, 2, 2, 2, 4, 4, 4, 4, 6, 6, 6, 6]> : tensor<16xi64>
        %14 = "FHE.apply_lookup_table"(%arg0, %cst_9) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<3>
        
        // packing the last 2 bits of x with select_x
        %15 = "FHE.add_eint"(%14, %11) : (!FHE.eint<3>, !FHE.eint<3>) -> !FHE.eint<3>
        
        // calculating contribution of 0 if select_x is 0 else the last 2 bits of x shifted by 2 bits for direct addition
        %cst_10 = arith.constant dense<[0, 0, 0, 4, 0, 8, 0, 12]> : tensor<8xi64>
        %16 = "FHE.apply_lookup_table"(%15, %cst_10) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<4>
        
        // computing x * select_x
        %17 = "FHE.add_eint"(%13, %16) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        
        // extracting the first 2 bits of y shifhted to the left by 1 bit for packing
        %18 = "FHE.apply_lookup_table"(%arg1, %cst_6) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<3>
        
        // packing the first 2 bits of y with select_x
        %19 = "FHE.add_eint"(%18, %11) : (!FHE.eint<3>, !FHE.eint<3>) -> !FHE.eint<3>
        
        // calculating contribution of 0 if select_x is 1 else the first 2 bits of y
        %cst_11 = arith.constant dense<[0, 0, 1, 0, 2, 0, 3, 0]> : tensor<8xi64>
        %20 = "FHE.apply_lookup_table"(%19, %cst_11) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<4>
        
        // extracting the last 2 bits of y shifhted to left by 1 bit for packing
        %21 = "FHE.apply_lookup_table"(%arg1, %cst_9) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<3>
        
        // packing the last 2 bits of y with select_x
        %22 = "FHE.add_eint"(%21, %11) : (!FHE.eint<3>, !FHE.eint<3>) -> !FHE.eint<3>
        
        // calculating contribution of 0 if select_x is 1 else the last 2 bits of y shifted by 2 bits for direct addition
        %cst_12 = arith.constant dense<[0, 0, 4, 0, 8, 0, 12, 0]> : tensor<8xi64>
        %23 = "FHE.apply_lookup_table"(%22, %cst_12) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<4>
        
        // computing y * (1 - select_x)
        %24 = "FHE.add_eint"(%20, %23) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        
        // computing the result
        %25 = "FHE.add_eint"(%17, %24) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
    
        return %25 : !FHE.eint<4>
        
      }
      
    }
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_promoted_to_uint7 - y_promoted_to_uint7] + y_promoted_to_uint7
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        min_max_strategy_preference=fhe.MinMaxStrategy.ONE_TLU_PROMOTED,
    )
    
    def f(x, y):
        return np.minimum(x, y)
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**2))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
    
      // promotions          ............         ............
      func.func @main(%arg0: !FHE.eint<5>, %arg1: !FHE.eint<5>) -> !FHE.eint<5> {
      
        // subtraction
        %0 = "FHE.to_signed"(%arg0) : (!FHE.eint<5>) -> !FHE.esint<5>
        %1 = "FHE.to_signed"(%arg1) : (!FHE.eint<5>) -> !FHE.esint<5>
        %2 = "FHE.sub_eint"(%0, %1) : (!FHE.esint<5>, !FHE.esint<5>) -> !FHE.esint<5>
        
        // tlu
        %cst = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1]> : tensor<32xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst) : (!FHE.esint<5>, tensor<32xi64>) -> !FHE.eint<5>
        
        // addition
        %4 = "FHE.add_eint"(%3, %arg1) : (!FHE.eint<5>, !FHE.eint<5>) -> !FHE.eint<5>
        
        return %4 : !FHE.eint<5>
        
      }
      
    }
    uint3_to_uint7_lut = fhe.LookupTable([...])
    x_cast_to_uint7 = uint3_to_uint7_lut[x]
    
    uint6_to_uint7_lut = fhe.LookupTable([...])
    y_cast_to_uint7 = uint6_to_uint7_lut[y]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_cast_to_uint7 - y_cast_to_uint7] + y
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        min_max_strategy_preference=fhe.MinMaxStrategy.THREE_TLU_CASTED,
    )
    
    def f(x, y):
        return np.minimum(x, y)
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**2))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
    
      // no promotions
      func.func @main(%arg0: !FHE.eint<4>, %arg1: !FHE.eint<2>) -> !FHE.eint<2> {
      
        // casting x
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]> : tensor<16xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.esint<5>
        
        // casting y
        %cst_0 = arith.constant dense<[0, 1, 2, 3]> : tensor<4xi64>
        %1 = "FHE.apply_lookup_table"(%arg1, %cst_0) : (!FHE.eint<2>, tensor<4xi64>) -> !FHE.esint<5>
        
        // subtraction
        %2 = "FHE.sub_eint"(%0, %1) : (!FHE.esint<5>, !FHE.esint<5>) -> !FHE.esint<5>
        
        // tlu
        %cst_1 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1]> : tensor<32xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_1) : (!FHE.esint<5>, tensor<32xi64>) -> !FHE.eint<2>
        
        // addition
        %4 = "FHE.add_eint"(%3, %arg1) : (!FHE.eint<2>, !FHE.eint<2>) -> !FHE.eint<2>
        
        return %4 : !FHE.eint<2>
        
      }
      
    }

    THREE_TLU_CASTED

    1

    3

    __eq__arrow-up-right
    __floordiv__arrow-up-right
    __ge__arrow-up-right
    __getitem__arrow-up-right
    __gt__arrow-up-right
    __invert__arrow-up-right
    __le__arrow-up-right
    __lshift__arrow-up-right
    __lt__arrow-up-right
    __matmul__arrow-up-right
    __mod__arrow-up-right
    __mul__arrow-up-right
    __ne__arrow-up-right
    __neg__arrow-up-right
    __or__arrow-up-right
    __pos__arrow-up-right
    __pow__arrow-up-right
    __radd__arrow-up-right
    __rand__arrow-up-right
    __rfloordiv__arrow-up-right
    __rlshift__arrow-up-right
    __rmatmul__arrow-up-right
    __rmod__arrow-up-right
    __rmul__arrow-up-right
    __ror__arrow-up-right
    __round__arrow-up-right
    __rpow__arrow-up-right
    __rrshift__arrow-up-right
    __rshift__arrow-up-right
    __rsub__arrow-up-right
    __rtruediv__arrow-up-right
    __rxor__arrow-up-right
    __sub__arrow-up-right
    __truediv__arrow-up-right
    __xor__arrow-up-right
    np.arccosharrow-up-right
    np.arcsinarrow-up-right
    np.arcsinharrow-up-right
    np.arctanarrow-up-right
    np.arctan2arrow-up-right
    np.arctanharrow-up-right
    np.aroundarrow-up-right
    np.bitwise_andarrow-up-right
    np.bitwise_orarrow-up-right
    np.bitwise_xorarrow-up-right
    np.broadcast_toarrow-up-right
    np.cbrtarrow-up-right
    np.ceilarrow-up-right
    np.cliparrow-up-right
    np.concatenatearrow-up-right
    np.copysignarrow-up-right
    np.cosarrow-up-right
    np.cosharrow-up-right
    np.deg2radarrow-up-right
    np.degreesarrow-up-right
    np.dotarrow-up-right
    np.equalarrow-up-right
    np.exparrow-up-right
    np.exp2arrow-up-right
    np.expand_dimsarrow-up-right
    np.expm1arrow-up-right
    np.fabsarrow-up-right
    np.float_powerarrow-up-right
    np.floorarrow-up-right
    np.floor_dividearrow-up-right
    np.fmaxarrow-up-right
    np.fminarrow-up-right
    np.fmodarrow-up-right
    np.gcdarrow-up-right
    np.greaterarrow-up-right
    np.greater_equalarrow-up-right
    np.heavisidearrow-up-right
    np.hypotarrow-up-right
    np.invertarrow-up-right
    np.isfinitearrow-up-right
    np.isinfarrow-up-right
    np.isnanarrow-up-right
    np.lcmarrow-up-right
    np.ldexparrow-up-right
    np.left_shiftarrow-up-right
    np.lessarrow-up-right
    np.less_equalarrow-up-right
    np.logarrow-up-right
    np.log10arrow-up-right
    np.log1parrow-up-right
    np.log2arrow-up-right
    np.logaddexparrow-up-right
    np.logaddexp2arrow-up-right
    np.logical_andarrow-up-right
    np.logical_notarrow-up-right
    np.logical_orarrow-up-right
    np.logical_xorarrow-up-right
    np.matmularrow-up-right
    np.maximumarrow-up-right
    np.minimumarrow-up-right
    np.multiplyarrow-up-right
    np.negativearrow-up-right
    np.nextafterarrow-up-right
    np.not_equalarrow-up-right
    np.ones_likearrow-up-right
    np.positivearrow-up-right
    np.powerarrow-up-right
    np.rad2degarrow-up-right
    np.radiansarrow-up-right
    np.reciprocalarrow-up-right
    np.remainderarrow-up-right
    np.reshapearrow-up-right
    np.right_shiftarrow-up-right
    np.rintarrow-up-right
    np.roundarrow-up-right
    np.signarrow-up-right
    np.signbitarrow-up-right
    np.sinarrow-up-right
    np.sinharrow-up-right
    np.spacingarrow-up-right
    np.sqrtarrow-up-right
    np.squarearrow-up-right
    np.subtractarrow-up-right
    np.sumarrow-up-right
    np.tanarrow-up-right
    np.tanharrow-up-right
    np.transposearrow-up-right
    np.true_dividearrow-up-right
    np.truncarrow-up-right
    np.wherearrow-up-right
    np.zeros_likearrow-up-right
    np.ndarray.flattenarrow-up-right
    np.ndarray.reshapearrow-up-right
    np.ndarray.transposearrow-up-right
    np.ndarray.Tarrow-up-right

    The Concrete Dialect (documentation, sourcearrow-up-right)

  • and for debugging purposes, the Tracing Dialect (documentation, sourcearrow-up-right).

  • documentation
    sourcearrow-up-right
    documentation
    sourcearrow-up-right
    documentation
    sourcearrow-up-right
    documentation
    sourcearrow-up-right
    documentation
    sourcearrow-up-right
    MLIR's builtin Linalg Dialectarrow-up-right
    MLIR's SCF Dialectarrow-up-right
    MLIR's bufferization infrastructurearrow-up-right
    func.func @main(%arg0: tensor<4x3x!FHE.eint<2>>, %arg1: tensor<3x2xi3>) -> tensor<4x2x!FHE.eint<2>> {
      %0 = "FHELinalg.matmul_eint_int"(%arg0, %arg1) : (tensor<4x3x!FHE.eint<2>>, tensor<3x2xi3>) -> tensor<4x2x!FHE.eint<2>>
      return %0 : tensor<4x2x!FHE.eint<2>>
    }
    #map = affine_map<(d0, d1, d2) -> (d0, d2)>
    #map1 = affine_map<(d0, d1, d2) -> (d2, d1)>
    #map2 = affine_map<(d0, d1, d2) -> (d0, d1)>
    
    func.func @main(%arg0: tensor<4x3x!FHE.eint<2>>, %arg1: tensor<3x2xi3>) -> tensor<4x2x!FHE.eint<2>> {
      %0 = "FHE.zero_tensor"() : () -> tensor<4x2x!FHE.eint<2>>
      %1 = linalg.generic {indexing_maps = [#map, #map1, #map2], iterator_types = ["parallel", "parallel", "reduction"]} ins(%arg0, %arg1 : tensor<4x3x!FHE.eint<2>>, tensor<3x2xi3>) outs(%0 : tensor<4x2x!FHE.eint<2>>) {
      ^bb0(%in: !FHE.eint<2>, %in_0: i3, %out: !FHE.eint<2>):
        %2 = "FHE.mul_eint_int"(%in, %in_0) : (!FHE.eint<2>, i3) -> !FHE.eint<2>
        %3 = "FHE.add_eint"(%out, %2) : (!FHE.eint<2>, !FHE.eint<2>) -> !FHE.eint<2>
        linalg.yield %3 : !FHE.eint<2>
      } -> tensor<4x2x!FHE.eint<2>>
      return %1 : tensor<4x2x!FHE.eint<2>>
    }
    func.func @main(%arg0: tensor<4x3x!FHE.eint<2>>, %arg1: tensor<3x2xi3>) -> tensor<4x2x!FHE.eint<2>> {
      %c0 = arith.constant 0 : index
      %c4 = arith.constant 4 : index
      %c1 = arith.constant 1 : index
      %c2 = arith.constant 2 : index
      %c3 = arith.constant 3 : index
      %0 = "FHE.zero_tensor"() : () -> tensor<4x2x!FHE.eint<2>>
      %1 = scf.for %arg2 = %c0 to %c4 step %c1 iter_args(%arg3 = %0) -> (tensor<4x2x!FHE.eint<2>>) {
        %2 = scf.for %arg4 = %c0 to %c2 step %c1 iter_args(%arg5 = %arg3) -> (tensor<4x2x!FHE.eint<2>>) {
          %3 = scf.for %arg6 = %c0 to %c3 step %c1 iter_args(%arg7 = %arg5) -> (tensor<4x2x!FHE.eint<2>>) {
            %extracted = tensor.extract %arg0[%arg2, %arg6] : tensor<4x3x!FHE.eint<2>>
            %extracted_0 = tensor.extract %arg1[%arg6, %arg4] : tensor<3x2xi3>
            %extracted_1 = tensor.extract %arg7[%arg2, %arg4] : tensor<4x2x!FHE.eint<2>>
            %4 = "FHE.mul_eint_int"(%extracted, %extracted_0) : (!FHE.eint<2>, i3) -> !FHE.eint<2>
            %5 = "FHE.add_eint"(%extracted_1, %4) : (!FHE.eint<2>, !FHE.eint<2>) -> !FHE.eint<2>
            %inserted = tensor.insert %5 into %arg7[%arg2, %arg4] : tensor<4x2x!FHE.eint<2>>
            scf.yield %inserted : tensor<4x2x!FHE.eint<2>>
          }
          scf.yield %3 : tensor<4x2x!FHE.eint<2>>
        }
        scf.yield %2 : tensor<4x2x!FHE.eint<2>>
      }
      return %1 : tensor<4x2x!FHE.eint<2>>
    }
    func.func @main(%arg0: tensor<4x3x!TFHE.glwe<sk?>>, %arg1: tensor<3x2xi3>) -> tensor<4x2x!TFHE.glwe<sk?>> {
      %c0 = arith.constant 0 : index
      %c4 = arith.constant 4 : index
      %c1 = arith.constant 1 : index
      %c2 = arith.constant 2 : index
      %c3 = arith.constant 3 : index
      %0 = "TFHE.zero_tensor"() : () -> tensor<4x2x!TFHE.glwe<sk?>>
      %1 = scf.for %arg2 = %c0 to %c4 step %c1 iter_args(%arg3 = %0) -> (tensor<4x2x!TFHE.glwe<sk?>>) {
        %2 = scf.for %arg4 = %c0 to %c2 step %c1 iter_args(%arg5 = %arg3) -> (tensor<4x2x!TFHE.glwe<sk?>>) {
          %3 = scf.for %arg6 = %c0 to %c3 step %c1 iter_args(%arg7 = %arg5) -> (tensor<4x2x!TFHE.glwe<sk?>>) {
            %extracted = tensor.extract %arg0[%arg2, %arg6] : tensor<4x3x!TFHE.glwe<sk?>>
            %extracted_0 = tensor.extract %arg1[%arg6, %arg4] : tensor<3x2xi3>
            %extracted_1 = tensor.extract %arg7[%arg2, %arg4] : tensor<4x2x!TFHE.glwe<sk?>>
            %4 = arith.extsi %extracted_0 : i3 to i64
            %5 = "TFHE.mul_glwe_int"(%extracted, %4) : (!TFHE.glwe<sk?>, i64) -> !TFHE.glwe<sk?>
            %6 = "TFHE.add_glwe"(%extracted_1, %5) : (!TFHE.glwe<sk?>, !TFHE.glwe<sk?>) -> !TFHE.glwe<sk?>
            %inserted = tensor.insert %6 into %arg7[%arg2, %arg4] : tensor<4x2x!TFHE.glwe<sk?>>
            scf.yield %inserted : tensor<4x2x!TFHE.glwe<sk?>>
          }
          scf.yield %3 : tensor<4x2x!TFHE.glwe<sk?>>
        }
        scf.yield %2 : tensor<4x2x!TFHE.glwe<sk?>>
      }
      return %1 : tensor<4x2x!TFHE.glwe<sk?>>
    }
    func.func @main(%arg0: tensor<4x3x!TFHE.glwe<sk<0,1,512>>>, %arg1: tensor<3x2xi3>) -> tensor<4x2x!TFHE.glwe<sk<0,1,512>>> {
      %c0 = arith.constant 0 : index
      %c4 = arith.constant 4 : index
      %c1 = arith.constant 1 : index
      %c2 = arith.constant 2 : index
      %c3 = arith.constant 3 : index
      %0 = "TFHE.zero_tensor"() : () -> tensor<4x2x!TFHE.glwe<sk<0,1,512>>>
      %1 = scf.for %arg2 = %c0 to %c4 step %c1 iter_args(%arg3 = %0) -> (tensor<4x2x!TFHE.glwe<sk<0,1,512>>>) {
        %2 = scf.for %arg4 = %c0 to %c2 step %c1 iter_args(%arg5 = %arg3) -> (tensor<4x2x!TFHE.glwe<sk<0,1,512>>>) {
          %3 = scf.for %arg6 = %c0 to %c3 step %c1 iter_args(%arg7 = %arg5) -> (tensor<4x2x!TFHE.glwe<sk<0,1,512>>>) {
            %extracted = tensor.extract %arg0[%arg2, %arg6] : tensor<4x3x!TFHE.glwe<sk<0,1,512>>>
            %extracted_0 = tensor.extract %arg1[%arg6, %arg4] : tensor<3x2xi3>
            %extracted_1 = tensor.extract %arg7[%arg2, %arg4] : tensor<4x2x!TFHE.glwe<sk<0,1,512>>>
            %4 = arith.extsi %extracted_0 : i3 to i64
            %5 = "TFHE.mul_glwe_int"(%extracted, %4) : (!TFHE.glwe<sk<0,1,512>>, i64) -> !TFHE.glwe<sk<0,1,512>>
            %6 = "TFHE.add_glwe"(%extracted_1, %5) : (!TFHE.glwe<sk<0,1,512>>, !TFHE.glwe<sk<0,1,512>>) -> !TFHE.glwe<sk<0,1,512>>
            %inserted = tensor.insert %6 into %arg7[%arg2, %arg4] : tensor<4x2x!TFHE.glwe<sk<0,1,512>>>
            scf.yield %inserted : tensor<4x2x!TFHE.glwe<sk<0,1,512>>>
          }
          scf.yield %3 : tensor<4x2x!TFHE.glwe<sk<0,1,512>>>
        }
        scf.yield %2 : tensor<4x2x!TFHE.glwe<sk<0,1,512>>>
      }
      return %1 : tensor<4x2x!TFHE.glwe<sk<0,1,512>>>
    }
    func.func @main(%arg0: tensor<4x3x513xi64>, %arg1: tensor<3x2xi3>) -> tensor<4x2x513xi64> {
      %c0 = arith.constant 0 : index
      %c4 = arith.constant 4 : index
      %c1 = arith.constant 1 : index
      %c2 = arith.constant 2 : index
      %c3 = arith.constant 3 : index
      %generated = tensor.generate  {
      ^bb0(%arg2: index, %arg3: index, %arg4: index):
        %c0_i64 = arith.constant 0 : i64
        tensor.yield %c0_i64 : i64
      } : tensor<4x2x513xi64>
      %0 = scf.for %arg2 = %c0 to %c4 step %c1 iter_args(%arg3 = %generated) -> (tensor<4x2x513xi64>) {
        %1 = scf.for %arg4 = %c0 to %c2 step %c1 iter_args(%arg5 = %arg3) -> (tensor<4x2x513xi64>) {
          %2 = scf.for %arg6 = %c0 to %c3 step %c1 iter_args(%arg7 = %arg5) -> (tensor<4x2x513xi64>) {
            %extracted_slice = tensor.extract_slice %arg0[%arg2, %arg6, 0] [1, 1, 513] [1, 1, 1] : tensor<4x3x513xi64> to tensor<513xi64>
            %extracted = tensor.extract %arg1[%arg6, %arg4] : tensor<3x2xi3>
            %extracted_slice_0 = tensor.extract_slice %arg7[%arg2, %arg4, 0] [1, 1, 513] [1, 1, 1] : tensor<4x2x513xi64> to tensor<513xi64>
            %3 = arith.extsi %extracted : i3 to i64
            %4 = "Concrete.mul_cleartext_lwe_tensor"(%extracted_slice, %3) : (tensor<513xi64>, i64) -> tensor<513xi64>
            %5 = "Concrete.add_lwe_tensor"(%extracted_slice_0, %4) : (tensor<513xi64>, tensor<513xi64>) -> tensor<513xi64>
            %inserted_slice = tensor.insert_slice %5 into %arg7[%arg2, %arg4, 0] [1, 1, 513] [1, 1, 1] : tensor<513xi64> into tensor<4x2x513xi64>
            scf.yield %inserted_slice : tensor<4x2x513xi64>
          }
          scf.yield %2 : tensor<4x2x513xi64>
        }
        scf.yield %1 : tensor<4x2x513xi64>
      }
      return %0 : tensor<4x2x513xi64>
    }
    func.func @main(%arg0: memref<4x3x513xi64, strided<[?, ?, ?], offset: ?>>, %arg1: memref<3x2xi3, strided<[?, ?], offset: ?>>, %arg2: !Concrete.context) -> memref<4x2x513xi64> {
      %c0_i64 = arith.constant 0 : i64
      call @_dfr_start(%c0_i64, %arg2) : (i64, !Concrete.context) -> ()
      %c0 = arith.constant 0 : index
      %c4 = arith.constant 4 : index
      %c1 = arith.constant 1 : index
      %c2 = arith.constant 2 : index
      %c513 = arith.constant 513 : index
      %c0_i64_0 = arith.constant 0 : i64
      %c3 = arith.constant 3 : index
      %alloc = memref.alloc() {alignment = 64 : i64} : memref<4x2x513xi64>
      scf.for %arg3 = %c0 to %c4 step %c1 {
        scf.for %arg4 = %c0 to %c2 step %c1 {
          scf.for %arg5 = %c0 to %c513 step %c1 {
            memref.store %c0_i64_0, %alloc[%arg3, %arg4, %arg5] : memref<4x2x513xi64>
          }
        }
      }
      scf.for %arg3 = %c0 to %c4 step %c1 {
        scf.for %arg4 = %c0 to %c2 step %c1 {
          %subview = memref.subview %alloc[%arg3, %arg4, 0] [1, 1, 513] [1, 1, 1] : memref<4x2x513xi64> to memref<513xi64, strided<[1], offset: ?>>
          scf.for %arg5 = %c0 to %c3 step %c1 {
            %subview_1 = memref.subview %arg0[%arg3, %arg5, 0] [1, 1, 513] [1, 1, 1] : memref<4x3x513xi64, strided<[?, ?, ?], offset: ?>> to memref<513xi64, strided<[?], offset: ?>>
            %0 = memref.load %arg1[%arg5, %arg4] : memref<3x2xi3, strided<[?, ?], offset: ?>>
            %1 = arith.extsi %0 : i3 to i64
            %alloc_2 = memref.alloc() {alignment = 64 : i64} : memref<513xi64>
            %cast = memref.cast %alloc_2 : memref<513xi64> to memref<?xi64, #map>
            %cast_3 = memref.cast %subview_1 : memref<513xi64, strided<[?], offset: ?>> to memref<?xi64, #map>
            func.call @memref_mul_cleartext_lwe_ciphertext_u64(%cast, %cast_3, %1) : (memref<?xi64, #map>, memref<?xi64, #map>, i64) -> ()
            %alloc_4 = memref.alloc() {alignment = 64 : i64} : memref<513xi64>
            %cast_5 = memref.cast %alloc_4 : memref<513xi64> to memref<?xi64, #map>
            %cast_6 = memref.cast %subview : memref<513xi64, strided<[1], offset: ?>> to memref<?xi64, #map>
            %cast_7 = memref.cast %alloc_2 : memref<513xi64> to memref<?xi64, #map>
            func.call @memref_add_lwe_ciphertexts_u64(%cast_5, %cast_6, %cast_7) : (memref<?xi64, #map>, memref<?xi64, #map>, memref<?xi64, #map>) -> ()
            memref.dealloc %alloc_2 : memref<513xi64>
            memref.copy %alloc_4, %subview : memref<513xi64> to memref<513xi64, strided<[1], offset: ?>>
            memref.dealloc %alloc_4 : memref<513xi64>
          }
        }
      }
      call @_dfr_stop(%c0_i64) : (i64) -> ()
      return %alloc : memref<4x2x513xi64>
    }
    circle-info

    LookupTables 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:

    hashtag
    With scalars.

    You can create the lookup table using a list of integers and apply it using indexing:

    hashtag
    With tensors.

    When you apply a table lookup to a tensor, the scalar table lookup is applied to each element of the tensor:

    hashtag
    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:

    hashtag
    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 LookupTables:

    In this example, we applied a squared table to the first column and a cubed table to the second column.

    hashtag
    Fused table lookup

    Concrete tries to fuse some operations into table lookups automatically so that lookup tables don't need to be created manually:

    circle-info

    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:

    circle-info

    Fusing makes the code more readable and easier to modify, so try to utilize it over manual LookupTables as much as possible.

    Debug

    In this section, you will learn how to debug the compilation process easily and find help in the case that you cannot resolve your issue.

    hashtag
    Compiler debug and verbose modes

    There are two configuration options that you can use to understand what's happening under the hood during the compilation process.

    • compiler_verbose_mode will print the passes applied by the compiler and let you see the transformations done by the compiler. Also, in the case of a crash, it could narrow down the crash location.

    • compiler_debug_mode is a lot more detailed version of the verbose mode. This is even better for crashes.

    circle-exclamation

    These flags might not work as expected in Jupyter notebooks as they output to stderr directly from C++.

    hashtag
    Debug artifacts

    Concrete has an artifact system to simplify the process of debugging issues.

    hashtag
    Automatic export.

    In case of compilation failures, artifacts are exported automatically to the .artifacts directory under the working directory. Let's intentionally create a compilation failure to show what is exported.

    This function fails to compile because Concrete does not support floating-point outputs. When you try to compile it, an exception will be raised and the artifacts will be exported automatically. If you go to the .artifacts directory under the working directory, you'll see the following files:

    hashtag
    environment.txt

    This file contains information about your setup (i.e., your operating system and python version).

    hashtag
    requirements.txt

    This file contains information about Python packages and their versions installed on your system.

    hashtag
    function.txt

    This file contains information about the function you tried to compile.

    hashtag
    parameters.txt

    This file contains information about the encryption status of the parameters of the function you tried to compile.

    hashtag
    1.initial.graph.txt

    This file contains the textual representation of the initial computation graph right after tracing.

    hashtag
    2.final.graph.txt

    This file contains the textual representation of the final computation graph right before MLIR conversion.

    hashtag
    traceback.txt

    This file contains information about the error that was received.

    hashtag
    Manual exports.

    Manual exports are mostly used for visualization. They can be very useful for demonstrations. Here is how to perform one:

    If you go to the /tmp/custom/export/path directory, you'll see the following files:

    hashtag
    1.initial.graph.txt

    This file contains the textual representation of the initial computation graph right after tracing.

    hashtag
    2.after-fusing.graph.txt

    This file contains the textual representation of the intermediate computation graph after fusing.

    hashtag
    3.final.graph.txt

    This file contains the textual representation of the final computation graph right before MLIR conversion.

    hashtag
    mlir.txt

    This file contains information about the MLIR of the function which was compiled using the provided inputset.

    hashtag
    client_parameters.json

    This file contains information about the client parameters chosen by Concrete.

    hashtag
    Asking the community

    You can seek help with your issue by asking a question directly in the .

    hashtag
    Submitting an issue

    If you cannot find a solution in the community forum, or if you have found a bug in the library, you could create an issue in our GitHub repository.

    In case of a bug, try to:

    • minimize randomness;

    • minimize your function as much as possible while keeping the bug - this will help to fix the bug faster;

    • include your inputset in the issue;

    In case of a feature request, try to:

    • give a minimal example of the desired behavior;

    • explain your use case.

    IndexError: index 10 is out of bounds for axis 0 with size 6
    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
    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]]
    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
    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]]
    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)
    include reproduction steps in the issue;
  • include debug artifacts in the issue.

  • community forumarrow-up-right
    def f(x):
        return np.sin(x)
    Linux-5.12.13-arch1-2-x86_64-with-glibc2.29 #1 SMP PREEMPT Fri, 25 Jun 2021 22:56:51 +0000
    Python 3.8.10
    astroid==2.15.0
    attrs==22.2.0
    auditwheel==5.3.0
    ...
    wheel==0.40.0
    wrapt==1.15.0
    zipp==3.15.0
    def f(x):
        return np.sin(x)
    x :: encrypted
    %0 = x              # EncryptedScalar<uint3>
    %1 = sin(%0)        # EncryptedScalar<float64>
    return %1
    %0 = x              # EncryptedScalar<uint3>
    %1 = sin(%0)        # EncryptedScalar<float64>
    return %1
    Traceback (most recent call last):
      File "/path/to/your/script.py", line 9, in <module>
        circuit = f.compile(inputset)
      File "/usr/local/lib/python3.10/site-packages/concrete/fhe/compilation/decorators.py", line 159, in compile
        return self.compiler.compile(inputset, configuration, artifacts, **kwargs)
      File "/usr/local/lib/python3.10/site-packages/concrete/fhe/compilation/compiler.py", line 437, in compile
        mlir = GraphConverter.convert(self.graph)
      File "/usr/local/lib/python3.10/site-packages/concrete/fhe/mlir/graph_converter.py", line 677, in convert
        GraphConverter._check_graph_convertibility(graph)
      File "/usr/local/lib/python3.10/site-packages/concrete/fhe/mlir/graph_converter.py", line 240, in _check_graph_convertibility
        raise RuntimeError(message)
    RuntimeError: Function you are trying to compile cannot be converted to MLIR
    
    %0 = x              # EncryptedScalar<uint3>          ∈ [3, 5]
    %1 = sin(%0)        # EncryptedScalar<float64>        ∈ [-0.958924, 0.14112]
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported
                                                                                 /path/to/your/script.py:6
    return %1
    from concrete import fhe
    import numpy as np
    
    artifacts = fhe.DebugArtifacts("/tmp/custom/export/path")
    
    @fhe.compiler({"x": "encrypted"})
    def f(x):
        return 127 - (50 * (np.sin(x) + 1)).astype(np.int64)
    
    inputset = range(2 ** 3)
    circuit = f.compile(inputset, artifacts=artifacts)
    
    artifacts.export()
    %0 = x                             # EncryptedScalar<uint1>
    %1 = sin(%0)                       # EncryptedScalar<float64>
    %2 = 1                             # ClearScalar<uint1>
    %3 = add(%1, %2)                   # EncryptedScalar<float64>
    %4 = 50                            # ClearScalar<uint6>
    %5 = multiply(%4, %3)              # EncryptedScalar<float64>
    %6 = astype(%5, dtype=int_)        # EncryptedScalar<uint1>
    %7 = 127                           # ClearScalar<uint7>
    %8 = subtract(%7, %6)              # EncryptedScalar<uint1>
    return %8
    %0 = x                       # EncryptedScalar<uint1>
    %1 = subgraph(%0)            # EncryptedScalar<uint1>
    %2 = 127                     # ClearScalar<uint7>
    %3 = subtract(%2, %1)        # EncryptedScalar<uint1>
    return %3
    
    Subgraphs:
    
        %1 = subgraph(%0):
    
            %0 = input                         # EncryptedScalar<uint1>
            %1 = sin(%0)                       # EncryptedScalar<float64>
            %2 = 1                             # ClearScalar<uint1>
            %3 = add(%1, %2)                   # EncryptedScalar<float64>
            %4 = 50                            # ClearScalar<uint6>
            %5 = multiply(%4, %3)              # EncryptedScalar<float64>
            %6 = astype(%5, dtype=int_)        # EncryptedScalar<uint1>
            return %6
    %0 = x                       # EncryptedScalar<uint3>        ∈ [0, 7]
    %1 = subgraph(%0)            # EncryptedScalar<uint7>        ∈ [2, 95]
    %2 = 127                     # ClearScalar<uint7>            ∈ [127, 127]
    %3 = subtract(%2, %1)        # EncryptedScalar<uint7>        ∈ [32, 125]
    return %3
    
    Subgraphs:
    
        %1 = subgraph(%0):
    
            %0 = input                         # EncryptedScalar<uint1>
            %1 = sin(%0)                       # EncryptedScalar<float64>
            %2 = 1                             # ClearScalar<uint1>
            %3 = add(%1, %2)                   # EncryptedScalar<float64>
            %4 = 50                            # ClearScalar<uint6>
            %5 = multiply(%4, %3)              # EncryptedScalar<float64>
            %6 = astype(%5, dtype=int_)        # EncryptedScalar<uint1>
            return %6
    module {
      func.func @main(%arg0: !FHE.eint<7>) -> !FHE.eint<7> {
        %c127_i8 = arith.constant 127 : i8
        %cst = arith.constant dense<"..."> : tensor<128xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<7>, tensor<128xi64>) -> !FHE.eint<7>
        %1 = "FHE.sub_int_eint"(%c127_i8, %0) : (i8, !FHE.eint<7>) -> !FHE.eint<7>
        return %1 : !FHE.eint<7>
      }
    }
    {
        "bootstrapKeys": [
            {
                "baseLog": 22,
                "glweDimension": 1,
                "inputLweDimension": 908,
                "inputSecretKeyID": 1,
                "level": 1,
                "outputSecretKeyID": 0,
                "polynomialSize": 8192,
                "variance": 4.70197740328915e-38
            }
        ],
        "functionName": "main",
        "inputs": [
            {
                "encryption": {
                    "encoding": {
                        "isSigned": false,
                        "precision": 7
                    },
                    "secretKeyID": 0,
                    "variance": 4.70197740328915e-38
                },
                "shape": {
                    "dimensions": [],
                    "sign": false,
                    "size": 0,
                    "width": 7
                }
            }
        ],
        "keyswitchKeys": [
            {
                "baseLog": 3,
                "inputSecretKeyID": 0,
                "level": 6,
                "outputSecretKeyID": 1,
                "variance": 1.7944329123150665e-13
            }
        ],
        "outputs": [
            {
                "encryption": {
                    "encoding": {
                        "isSigned": false,
                        "precision": 7
                    },
                    "secretKeyID": 0,
                    "variance": 4.70197740328915e-38
                },
                "shape": {
                    "dimensions": [],
                    "sign": false,
                    "size": 0,
                    "width": 7
                }
            }
        ],
        "packingKeyswitchKeys": [],
        "secretKeys": [
            {
                "dimension": 8192
            },
            {
                "dimension": 908
            }
        ]
    }

    Bitwise Operations

    Bitwise operations are not native operations in Concrete, so they need to be implemented using existing native operations (i.e., additions, clear multiplications, negations, table lookups). Concrete offers two different implementations for performing bitwise operations.

    hashtag
    Chunked

    This is the most general implementation that can be used in any situation. The idea is:

    hashtag
    Notes

    • Signed bitwise operations are not supported.

    • The optimal chunk size is selected automatically to reduce the number of table lookups.

    • Chunked bitwise operations result in at least 4 and at most 9 table lookups.

    hashtag
    Pros

    • Can be used with any integers.

    hashtag
    Cons

    • Very expensive.

    hashtag
    Example

    produces

    hashtag
    Packing Trick

    This implementation uses the fact that we can combine two values into a single value and apply a single table lookup to this combined value!

    There are two major problems with this implementation:

    1. packing requires the same bit-width across operands.

    2. packing requires the bit-width of at least x.bit_width + y.bit_width and that bit-width cannot exceed maximum TLU bit-width, which is 16 at the moment.

    What this means is if we are comparing uint3 and uint6, we need to convert both of them to uint9 in some way to do the packing and proceed with the TLU in 9-bits. There are 4 ways to achieve this behavior.

    hashtag
    Requirements

    hashtag
    1. fhe.BitwiseStrategy.ONE_TLU_PROMOTED

    This strategy makes sure that during bit-width assignment, both operands are assigned the same bit-width, and that bit-width contains at least the amount of bits required to store pack(x, y). The idea is:

    hashtag
    Pros

    • It will always result in a single table lookup.

    hashtag
    Cons

    • It will significantly increase the bit-width of both operands and lock them to each other across the whole circuit, which can result in significant slowdowns if the operands are used in other costly operations.

    hashtag
    Example

    produces

    hashtag
    2. fhe.BitwiseStrategy.THREE_TLU_CASTED

    This strategy will not put any constraint on bit-widths during bit-width assignment, instead operands are cast to a bit-width that can store pack(x, y) during runtime using table lookups. The idea is:

    hashtag
    Notes

    • It can result in a single table lookup as well, if x and y are assigned (because of other operations) the same bit-width, and that bit-width can store pack(x, y).

    • Or in two table lookups if only one of the operands is assigned a bit-width bigger than or equal to the bit width that can store pack(x, y).

    hashtag
    Pros

    • It will not put any constraints on bit-widths of the operands, which is amazing if they are used in other costly operations.

    • It will result in at most 3 table lookups, which is still good.

    hashtag
    Cons

    • If you are not doing anything else with the operands, or doing less costly operations compared to bitwise, it will introduce up to two unnecessary table lookups and slow down execution compared to fhe.BitwiseStrategy.ONE_TLU_PROMOTED.

    hashtag
    Example

    produces

    hashtag
    3. fhe.BitwiseStrategy.TWO_TLU_BIGGER_PROMOTED_SMALLER_CASTED

    This strategy can be viewed as a middle ground between the two strategies described above. With this strategy, only the bigger operand will be constrained to have at least the required bit-width to store pack(x, y), and the smaller operand will be cast to that bit-width during runtime. The idea is:

    hashtag
    Notes

    • It can result in a single table lookup as well, if the smaller operand is assigned (because of other operations) the same bit-width as the bigger operand.

    hashtag
    Pros

    • It will only put a constraint on the bigger operand, which is great if the smaller operand is used in other costly operations.

    • It will result in at most 2 table lookups, which is great.

    hashtag
    Cons

    • It will significantly increase the bit-width of the bigger operand which can result in significant slowdowns if the bigger operand is used in other costly operations.

    • If you are not doing anything else with the smaller operand, or doing less costly operations compared to comparison, it could introduce an unnecessary table lookup and slow down execution compared to fhe.BitwiseStrategy.THREE_TLU_CASTED.

    hashtag
    Example

    produces

    hashtag
    4. fhe.BitwiseStrategy.TWO_TLU_BIGGER_CASTED_SMALLER_PROMOTED

    This strategy is like the exact opposite of the strategy above. With this, only the smaller operand will be constrained to have at least the required bit-width, and the bigger operand will be cast during runtime. The idea is:

    hashtag
    Notes

    • It can result in a single table lookup as well, if the bigger operand is assigned (because of other operations) the same bit-width as the smaller operand.

    hashtag
    Pros

    • It will only put constraint on the smaller operand, which is great if the bigger operand is used in other costly operations.

    • It will result in at most 2 table lookups, which is great.

    hashtag
    Cons

    • It will increase the bit-width of the smaller operand which can result in significant slowdowns if the smaller operand is used in other costly operations.

    • If you are not doing anything else with the bigger operand, or doing less costly operations compared to comparison, it could introduce an unnecessary table lookup and slow down execution compared to fhe.BitwiseStrategy.THREE_TLU_CASTED.

    hashtag
    Example

    produces

    hashtag
    Summary

    Strategy
    Minimum # of TLUs
    Maximum # of TLUs
    Can increase the bit-width of the inputs
    circle-info

    Concrete will choose the best strategy available after bit-width assignment, regardless of the specified preference.

    circle-info

    Different strategies are good for different circuits. If you want the best runtime for your use case, you can compile your circuit with all different comparison strategy preferences, and pick the one with the lowest complexity.

    hashtag
    Shifts

    The same configuration option is used to modify the behavior of encrypted shift operations, and shifts are much more complex to implement, so we'll not go over the details. What is important is, the end the result is computed using additions or subtractions on the original shifted operand. Since additions and subtractions require the same bit-width across operands, input and output bit-widths need to be synchronized at some point. There are two ways to do this:

    hashtag
    With promotion

    Here, the shifted operand and shift result are assigned the same bit-width during bit-width assignment, which avoids an additional TLU on the shifted operand. On the other hand, it might increase the bit-width of the result or the shifted operand, and if they're used in other costly operations, it could result in significant slowdowns. This is the default behavior.

    produces

    hashtag
    With casting

    The approach described above could be suboptimal for some circuits, so it is advised to check the complexity with it disabled before production. Here is how the implementation changes with it disabled.

    produces

    Rounding

    Table lookups have a strict constraint on the number of bits they support. This can be limiting, especially if you don't need exact precision. As well as this, using larger bit-widths leads to slower table lookups.

    To overcome these issues, rounded table lookups are introduced. This operation provides a way to round the least significant bits of a large integer and then apply the table lookup on the resulting (smaller) value.

    Imagine you have a 5-bit value, but you want to have a 3-bit table lookup. You can call fhe.round_bit_pattern(input, lsbs_to_remove=2) and use the 3-bit value you receive as input to the table lookup.

    Let's see how rounding works in practice:

    prints:

    and displays:

    circle-info

    If the rounded number is one of the last 2**(lsbs_to_remove - 1) numbers in the input range [0, 2**original_bit_width), an overflow will happen.

    By default, if an overflow is encountered during inputset evaluation, bit-widths will be adjusted accordingly. This results in a loss of speed, but ensures accuracy.

    You can turn this overflow protection off (e.g., for performance) by using fhe.round_bit_pattern(..., overflow_protection=False)

    Now, let's see how rounding can be used in FHE.

    prints:

    circle-info

    These speed-ups can vary from system to system.

    circle-info

    The reason why the speed-up is not increasing with lsbs_to_remove is because the rounding operation itself has a cost: each bit removal is a PBS. Therefore, if a lot of bits are removed, rounding itself could take longer than the bigger TLU which is evaluated afterwards.

    and displays:

    circle-info

    Feel free to disable overflow protection and see what happens.

    hashtag
    Auto Rounders

    Rounding is very useful but, in some cases, you don't know how many bits your input contains, so it's not reliable to specify lsbs_to_remove manually. For this reason, the AutoRounder class is introduced.

    AutoRounder allows you to set how many of the most significant bits to keep, but they need to be adjusted using an inputset to determine how many of the least significant bits to remove. This can be done manually using fhe.AutoRounder.adjust(function, inputset), or by setting auto_adjust_rounders configuration to True during compilation.

    Here is how auto rounders can be used in FHE:

    prints:

    and displays:

    circle-exclamation

    AutoRounders should be defined outside the function that is being compiled. They are used to store the result of the adjustment process, so they shouldn't be created each time the function is called. Furthermore, each AutoRounder should be used with exactly one round_bit_pattern call.

    # (example below is for bit-width of 8 and chunk size of 4)
    
    # extract chunks of lhs using table lookups
    lhs_chunks = [lhs.bits[0:4], lhs.bits[4:8]]
    
    # extract chunks of rhs using table lookups
    rhs_chunks = [rhs.bits[0:4], rhs.bits[4:8]]
    
    # pack chunks of lhs and rhs using clear multiplications and additions 
    packed_chunks = []
    for lhs_chunk, rhs_chunk in zip(lhs_chunks, rhs_chunks):
        shifted_lhs_chunk = lhs_chunk * 2**4  # (i.e., lhs_chunk << 4)
        packed_chunks.append(shifted_lhs_chunk + rhs_chunk)
    
    # apply comparison table lookup to packed chunks
    bitwise_table = fhe.LookupTable([...])
    result_chunks = bitwise_table[packed_chunks]
    
    # sum resulting chunks obtain the result
    result = np.sum(result_chunks)
    import matplotlib.pyplot as plt
    import numpy as np
    from concrete import fhe
    
    original_bit_width = 5
    lsbs_to_remove = 2
    
    assert 0 < lsbs_to_remove < original_bit_width
    
    original_values = list(range(2**original_bit_width))
    rounded_values = [
        fhe.round_bit_pattern(value, lsbs_to_remove)
        for value in original_values
    ]
    
    previous_rounded = rounded_values[0]
    for original, rounded in zip(original_values, rounded_values):
        if rounded != previous_rounded:
            previous_rounded = rounded
            print()
    
        original_binary = np.binary_repr(original, width=(original_bit_width + 1))
        rounded_binary = np.binary_repr(rounded, width=(original_bit_width + 1))
    
        print(
            f"{original:2} = 0b_{original_binary[:-lsbs_to_remove]}[{original_binary[-lsbs_to_remove:]}] "
            f"=> "
            f"0b_{rounded_binary[:-lsbs_to_remove]}[{rounded_binary[-lsbs_to_remove:]}] = {rounded}"
        )
    
    fig = plt.figure()
    ax = fig.add_subplot()
    
    plt.plot(original_values, original_values, label="original", color="black")
    plt.plot(original_values, rounded_values, label="rounded", color="green")
    plt.legend()
    
    ax.set_aspect("equal", adjustable="box")
    plt.show()
     0 = 0b_0000[00] => 0b_0000[00] = 0
     1 = 0b_0000[01] => 0b_0000[00] = 0
    
     2 = 0b_0000[10] => 0b_0001[00] = 4
     3 = 0b_0000[11] => 0b_0001[00] = 4
     4 = 0b_0001[00] => 0b_0001[00] = 4
     5 = 0b_0001[01] => 0b_0001[00] = 4
    
     6 = 0b_0001[10] => 0b_0010[00] = 8
     7 = 0b_0001[11] => 0b_0010[00] = 8
     8 = 0b_0010[00] => 0b_0010[00] = 8
     9 = 0b_0010[01] => 0b_0010[00] = 8
    
    10 = 0b_0010[10] => 0b_0011[00] = 12
    11 = 0b_0010[11] => 0b_0011[00] = 12
    12 = 0b_0011[00] => 0b_0011[00] = 12
    13 = 0b_0011[01] => 0b_0011[00] = 12
    
    14 = 0b_0011[10] => 0b_0100[00] = 16
    15 = 0b_0011[11] => 0b_0100[00] = 16
    16 = 0b_0100[00] => 0b_0100[00] = 16
    17 = 0b_0100[01] => 0b_0100[00] = 16
    
    18 = 0b_0100[10] => 0b_0101[00] = 20
    19 = 0b_0100[11] => 0b_0101[00] = 20
    20 = 0b_0101[00] => 0b_0101[00] = 20
    21 = 0b_0101[01] => 0b_0101[00] = 20
    
    22 = 0b_0101[10] => 0b_0110[00] = 24
    23 = 0b_0101[11] => 0b_0110[00] = 24
    24 = 0b_0110[00] => 0b_0110[00] = 24
    25 = 0b_0110[01] => 0b_0110[00] = 24
    
    26 = 0b_0110[10] => 0b_0111[00] = 28
    27 = 0b_0110[11] => 0b_0111[00] = 28
    28 = 0b_0111[00] => 0b_0111[00] = 28
    29 = 0b_0111[01] => 0b_0111[00] = 28
    
    30 = 0b_0111[10] => 0b_1000[00] = 32
    31 = 0b_0111[11] => 0b_1000[00] = 32
    It is used if no other implementation can be used.

    1

    3

    TWO_TLU_BIGGER_PROMOTED_SMALLER_CASTED

    1

    2

    ✓

    TWO_TLU_BIGGER_CASTED_SMALLER_PROMOTED

    1

    2

    ✓

    CHUNKED

    4

    9

    ONE_TLU_PROMOTED

    1

    1

    ✓

    THREE_TLU_CASTED

    . However, this could lead to unexpected behavior at runtime.
    import numpy as np
    from concrete import fhe
    
    def f(x, y):
        return x & y
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**4))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, show_mlir=True)
    module {
      
      // no promotions
      func.func @main(%arg0: !FHE.eint<4>, %arg1: !FHE.eint<4>) -> !FHE.eint<4> {
    
        // extracting the first chunk of x, adjusted for shifting
        %cst = arith.constant dense<[0, 0, 0, 0, 4, 4, 4, 4, 8, 8, 8, 8, 12, 12, 12, 12]> : tensor<16xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
            
        // extracting the first chunk of y
        %cst_0 = arith.constant dense<[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]> : tensor<16xi64>
        %1 = "FHE.apply_lookup_table"(%arg1, %cst_0) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
            
        // packing the first chunks
        %2 = "FHE.add_eint"(%0, %1) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
            
        // applying the bitwise operation to the first chunks, adjusted for addition in the end
        %cst_1 = arith.constant dense<[0, 0, 0, 0, 0, 4, 0, 4, 0, 0, 8, 8, 0, 4, 8, 12]> : tensor<16xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_1) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
            
        // extracting the second chunk of x, adjusted for shifting
        %cst_2 = arith.constant dense<[0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12]> : tensor<16xi64>
        %4 = "FHE.apply_lookup_table"(%arg0, %cst_2) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
            
        // extracting the second chunk of y
        %cst_3 = arith.constant dense<[0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]> : tensor<16xi64>
        %5 = "FHE.apply_lookup_table"(%arg1, %cst_3) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
            
        // packing the second chunks
        %6 = "FHE.add_eint"(%4, %5) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
            
        // applying the bitwise operation to second chunks
        %cst_4 = arith.constant dense<[0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 0, 1, 2, 3]> : tensor<16xi64>
        %7 = "FHE.apply_lookup_table"(%6, %cst_4) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
            
        // adding resulting chunks to obtain the result
        %8 = "FHE.add_eint"(%7, %3) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
            
        return %8 : !FHE.eint<4>
    
      }
      
    }
    x.bit_width + y.bit_width <= MAXIMUM_TLU_BIT_WIDTH
    bitwise_lut = fhe.LookupTable([...])
    result = bitwise_lut[pack(x_promoted_to_uint9, y_promoted_to_uint9)]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        bitwise_strategy_preference=fhe.BitwiseStrategy.ONE_TLU_PROMOTED,
    )
    
    def f(x, y):
        return x & y
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**4))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // promotions          ............         ............
      func.func @main(%arg0: !FHE.eint<8>, %arg1: !FHE.eint<8>) -> !FHE.eint<4> {
        
        // packing
        %c16_i9 = arith.constant 16 : i9
        %0 = "FHE.mul_eint_int"(%arg0, %c16_i9) : (!FHE.eint<8>, i9) -> !FHE.eint<8>
        %1 = "FHE.add_eint"(%0, %arg1) : (!FHE.eint<8>, !FHE.eint<8>) -> !FHE.eint<8>
            
        // computing the result
        %cst = arith.constant dense<"..."> : tensor<256xi64>
        %2 = "FHE.apply_lookup_table"(%1, %cst) : (!FHE.eint<8>, tensor<256xi64>) -> !FHE.eint<4>
            
        return %2 : !FHE.eint<4>
            
      }
      
    }
    uint3_to_uint9_lut = fhe.LookupTable([...])
    x_cast_to_uint9 = uint3_to_uint9_lut[x]
    
    uint6_to_uint9_lut = fhe.LookupTable([...])
    y_cast_to_uint9 = uint6_to_uint9_lut[y]
    
    bitwise_lut = fhe.LookupTable([...])
    result = bitwise_lut[pack(x_cast_to_uint9, y_cast_to_uint9)]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        comparison_strategy_preference=fhe.BitwiseStrategy.THREE_TLU_CASTED,
    )
    
    def f(x, y):
        return x & y
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**4))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // no promotions
      func.func @main(%arg0: !FHE.eint<4>, %arg1: !FHE.eint<4>) -> !FHE.eint<4> {
        
        // casting
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]> : tensor<16xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<8>
        %1 = "FHE.apply_lookup_table"(%arg1, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<8>
    
        // packing
        %c16_i9 = arith.constant 16 : i9
        %2 = "FHE.mul_eint_int"(%0, %c16_i9) : (!FHE.eint<8>, i9) -> !FHE.eint<8>
        %3 = "FHE.add_eint"(%2, %1) : (!FHE.eint<8>, !FHE.eint<8>) -> !FHE.eint<8>
            
        // computing the result
        %cst_0 = arith.constant dense<"..."> : tensor<256xi64>
        %4 = "FHE.apply_lookup_table"(%3, %cst_0) : (!FHE.eint<8>, tensor<256xi64>) -> !FHE.eint<4>
            
        return %4 : !FHE.eint<4>
            
      }
      
    }
    uint3_to_uint9_lut = fhe.LookupTable([...])
    x_cast_to_uint9 = uint3_to_uint9_lut[x]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_cast_to_uint9 - y_promoted_to_uint9]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        bitwise_strategy_preference=fhe.BitwiseStrategy.TWO_TLU_BIGGER_PROMOTED_SMALLER_CASTED,
    )
    
    def f(x, y):
        return x & y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**6))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // promotions                               ............
      func.func @main(%arg0: !FHE.eint<3>, %arg1: !FHE.eint<8>) -> !FHE.eint<3> {
        
        // casting smaller operand
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7]> : tensor<8xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<8>
            
        // packing
        %c32_i9 = arith.constant 32 : i9
        %1 = "FHE.mul_eint_int"(%0, %c32_i9) : (!FHE.eint<8>, i9) -> !FHE.eint<8>
        %2 = "FHE.add_eint"(%1, %arg1) : (!FHE.eint<8>, !FHE.eint<8>) -> !FHE.eint<8>
            
        // computing the result
        %cst_0 = arith.constant dense<"..."> : tensor<256xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_0) : (!FHE.eint<8>, tensor<256xi64>) -> !FHE.eint<3>
            
        return %3 : !FHE.eint<3>
            
      }
      
    }
    uint6_to_uint9_lut = fhe.LookupTable([...])
    y_cast_to_uint9 = uint6_to_uint9_lut[y]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_promoted_to_uint9 - y_cast_to_uint9]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        bitwise_strategy_preference=fhe.BitwiseStrategy.TWO_TLU_BIGGER_CASTED_SMALLER_PROMOTED,
    )
    
    def f(x, y):
        return x | y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**6))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // promotions          ............
      func.func @main(%arg0: !FHE.eint<9>, %arg1: !FHE.eint<6>) -> !FHE.eint<6> {
        
        // casting bigger operand
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]> : tensor<64xi64>
        %0 = "FHE.apply_lookup_table"(%arg1, %cst) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.eint<9>
            
        // packing
        %c64_i10 = arith.constant 64 : i10
        %1 = "FHE.mul_eint_int"(%arg0, %c64_i10) : (!FHE.eint<9>, i10) -> !FHE.eint<9>
        %2 = "FHE.add_eint"(%1, %0) : (!FHE.eint<9>, !FHE.eint<9>) -> !FHE.eint<9>
            
        // computing the result
        %cst_0 = arith.constant dense<"..."> : tensor<512xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_0) : (!FHE.eint<9>, tensor<512xi64>) -> !FHE.eint<6>
            
        return %3 : !FHE.eint<6>
    
      }
      
    }
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        shifts_with_promotion=True,
    )
    
    def f(x, y):
        return x << y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**2))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // promotions          ............
      func.func @main(%arg0: !FHE.eint<6>, %arg1: !FHE.eint<2>) -> !FHE.eint<6> {
        
        // shifting for the second bit of y
        %cst = arith.constant dense<[0, 0, 1, 1]> : tensor<4xi64>
        %0 = "FHE.apply_lookup_table"(%arg1, %cst) : (!FHE.eint<2>, tensor<4xi64>) -> !FHE.eint<4>
        %cst_0 = arith.constant dense<[0, 0, 0, 2, 2, 2, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]> : tensor<64xi64>
        %1 = "FHE.apply_lookup_table"(%arg0, %cst_0) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.eint<4>
        %2 = "FHE.add_eint"(%1, %0) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %cst_1 = arith.constant dense<[0, 0, 0, 8, 0, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 56]> : tensor<16xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_1) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %cst_2 = arith.constant dense<[0, 6, 12, 2, 8, 14, 4, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]> : tensor<64xi64>
        %4 = "FHE.apply_lookup_table"(%arg0, %cst_2) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.eint<4>
        %5 = "FHE.add_eint"(%4, %0) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %cst_3 = arith.constant dense<[0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7]> : tensor<16xi64>
        %6 = "FHE.apply_lookup_table"(%5, %cst_3) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %7 = "FHE.add_eint"(%3, %6) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
        %8 = "FHE.add_eint"(%7, %arg0) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
        
        // shifting for the first bit of y
        %cst_4 = arith.constant dense<[0, 1, 0, 1]> : tensor<4xi64>
        %9 = "FHE.apply_lookup_table"(%arg1, %cst_4) : (!FHE.eint<2>, tensor<4xi64>) -> !FHE.eint<4>
        %cst_5 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 14, 14, 14, 14, 14, 14, 14, 14]> : tensor<64xi64>
        %10 = "FHE.apply_lookup_table"(%8, %cst_5) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.eint<4>
        %11 = "FHE.add_eint"(%10, %9) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %12 = "FHE.apply_lookup_table"(%11, %cst_1) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %cst_6 = arith.constant dense<[0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14]> : tensor<64xi64>
        %13 = "FHE.apply_lookup_table"(%8, %cst_6) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.eint<4>
        %14 = "FHE.add_eint"(%13, %9) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %15 = "FHE.apply_lookup_table"(%14, %cst_3) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %16 = "FHE.add_eint"(%12, %15) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
        %17 = "FHE.add_eint"(%16, %8) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
            
        return %17 : !FHE.eint<6>
            
      }
      
    }
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        shifts_with_promotion=False,
    )
    
    def f(x, y):
        return x << y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**2))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // no promotions
      func.func @main(%arg0: !FHE.eint<3>, %arg1: !FHE.eint<2>) -> !FHE.eint<6> {
        
        // shifting for the second bit of y
        %cst = arith.constant dense<[0, 0, 1, 1]> : tensor<4xi64>
        %0 = "FHE.apply_lookup_table"(%arg1, %cst) : (!FHE.eint<2>, tensor<4xi64>) -> !FHE.eint<4>
        %cst_0 = arith.constant dense<[0, 0, 0, 2, 2, 2, 4, 4]> : tensor<8xi64>
        %1 = "FHE.apply_lookup_table"(%arg0, %cst_0) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<4>
        %2 = "FHE.add_eint"(%1, %0) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %cst_1 = arith.constant dense<[0, 0, 0, 8, 0, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 56]> : tensor<16xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_1) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %cst_2 = arith.constant dense<[0, 6, 12, 2, 8, 14, 4, 10]> : tensor<8xi64>
        %4 = "FHE.apply_lookup_table"(%arg0, %cst_2) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<4>
        %5 = "FHE.add_eint"(%4, %0) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %cst_3 = arith.constant dense<[0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7]> : tensor<16xi64>
        %6 = "FHE.apply_lookup_table"(%5, %cst_3) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %7 = "FHE.add_eint"(%3, %6) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
            
        // cast x to 6-bits to compute the result using addition/subtraction
        %cst_4 = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7]> : tensor<8xi64>
        %8 = "FHE.apply_lookup_table"(%arg0, %cst_4) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.eint<6>
        // this was done using promotion instead of casting in runtime when the flag was turned on
            
        %9 = "FHE.add_eint"(%7, %8) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
            
        // shifting for the first bit of y
        %cst_5 = arith.constant dense<[0, 1, 0, 1]> : tensor<4xi64>
        %10 = "FHE.apply_lookup_table"(%arg1, %cst_5) : (!FHE.eint<2>, tensor<4xi64>) -> !FHE.eint<4>
        %cst_6 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 8, 8, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 12, 12, 12, 12, 12, 12, 12, 12, 14, 14, 14, 14, 14, 14, 14, 14]> : tensor<64xi64>
        %11 = "FHE.apply_lookup_table"(%9, %cst_6) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.eint<4>
        %12 = "FHE.add_eint"(%11, %10) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %13 = "FHE.apply_lookup_table"(%12, %cst_1) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %cst_7 = arith.constant dense<[0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14]> : tensor<64xi64>
        %14 = "FHE.apply_lookup_table"(%9, %cst_7) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.eint<4>
        %15 = "FHE.add_eint"(%14, %10) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        %16 = "FHE.apply_lookup_table"(%15, %cst_3) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<6>
        %17 = "FHE.add_eint"(%13, %16) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
        %18 = "FHE.add_eint"(%17, %9) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6>
            
        return %18 : !FHE.eint<6>
      }
      
    }
    import itertools
    import time
    
    import matplotlib.pyplot as plt
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        enable_unsafe_features=True,
        use_insecure_key_cache=True,
        insecure_key_cache_location=".keys",
        single_precision=False,
        parameter_selection_strategy=fhe.ParameterSelectionStrategy.MULTI,
    )
    
    input_bit_width = 6
    input_range = np.array(range(2**input_bit_width))
    
    timings = {}
    results = {}
    
    for lsbs_to_remove in range(input_bit_width):
        @fhe.compiler({"x": "encrypted"})
        def f(x):
            return fhe.round_bit_pattern(x, lsbs_to_remove) ** 2
        
        circuit = f.compile(inputset=[input_range], configuration=configuration)
        circuit.keygen()
        
        encrypted_sample = circuit.encrypt(input_range)
        start = time.time()
        encrypted_result = circuit.run(encrypted_sample)
        end = time.time()
        result = circuit.decrypt(encrypted_result)
        
        took = end - start
        
        timings[lsbs_to_remove] = took
        results[lsbs_to_remove] = result
    
    number_of_figures = len(results)
    
    columns = 1
    for i in range(2, number_of_figures):
        if number_of_figures % i == 0:
            columns = i
    rows = number_of_figures // columns
    
    fig, axs = plt.subplots(rows, columns)
    axs = axs.flatten()
    
    baseline = timings[0]
    for lsbs_to_remove in range(input_bit_width):
        timing = timings[lsbs_to_remove]
        speedup = baseline / timing
        print(f"lsbs_to_remove={lsbs_to_remove} => {speedup:.2f}x speedup")
    
        axs[lsbs_to_remove].set_title(f"lsbs_to_remove={lsbs_to_remove}")
        axs[lsbs_to_remove].plot(input_range, results[lsbs_to_remove])
    
    plt.show()
    lsbs_to_remove=0 => 1.00x speedup
    lsbs_to_remove=1 => 1.20x speedup
    lsbs_to_remove=2 => 2.17x speedup
    lsbs_to_remove=3 => 3.75x speedup
    lsbs_to_remove=4 => 2.64x speedup
    lsbs_to_remove=5 => 2.61x speedup
    import itertools
    import time
    
    import matplotlib.pyplot as plt
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        enable_unsafe_features=True,
        use_insecure_key_cache=True,
        insecure_key_cache_location=".keys",
        single_precision=False,
        parameter_selection_strategy=fhe.ParameterSelectionStrategy.MULTI,
    )
    
    input_bit_width = 6
    input_range = np.array(range(2**input_bit_width))
    
    timings = {}
    results = {}
    
    for target_msbs in reversed(range(1, input_bit_width + 1)):
        rounder = fhe.AutoRounder(target_msbs)
    
        @fhe.compiler({"x": "encrypted"})
        def f(x):
            return fhe.round_bit_pattern(x, rounder) ** 2
    
        fhe.AutoRounder.adjust(f, inputset=[input_range])
    
        circuit = f.compile(inputset=[input_range], configuration=configuration)
        circuit.keygen()
    
        encrypted_sample = circuit.encrypt(input_range)
        start = time.time()
        encrypted_result = circuit.run(encrypted_sample)
        end = time.time()
        result = circuit.decrypt(encrypted_result)
    
        took = end - start
    
        timings[target_msbs] = took
        results[target_msbs] = result
    
    number_of_figures = len(results)
    
    columns = 1
    for i in range(2, number_of_figures):
        if number_of_figures % i == 0:
            columns = i
    rows = number_of_figures // columns
    
    fig, axs = plt.subplots(rows, columns)
    axs = axs.flatten()
    
    baseline = timings[input_bit_width]
    for i, target_msbs in enumerate(reversed(range(1, input_bit_width + 1))):
        timing = timings[target_msbs]
        speedup = baseline / timing
        print(f"target_msbs={target_msbs} => {speedup:.2f}x speedup")
    
        axs[i].set_title(f"target_msbs={target_msbs}")
        axs[i].plot(input_range, results[target_msbs])
    
    plt.show()
    target_msbs=6 => 1.00x speedup
    target_msbs=5 => 1.22x speedup
    target_msbs=4 => 1.95x speedup
    target_msbs=3 => 3.11x speedup
    target_msbs=2 => 2.23x speedup
    target_msbs=1 => 2.34x speedup

    Comparisons

    Comparisons are not native operations in Concrete, so they need to be implemented using existing native operations (i.e., additions, clear multiplications, negations, table lookups). Concrete offers three different implementations for performing comparisons.

    hashtag
    Chunked

    This is the most general implementation that can be used in any situation. The idea is:

    hashtag
    Notes
    • Signed comparisons are more complex to explain, but they are supported!

    • The optimal chunk size is selected automatically to reduce the number of table lookups.

    • Chunked comparisons result in at least 5 and at most 13 table lookups.

    • It is used if no other implementation can be used.

    • == and != are using a different chunk comparison and reduction strategy with less table lookups.

    hashtag
    Pros

    • Can be used with any integers.

    hashtag
    Cons

    • Very expensive.

    hashtag
    Example

    produces

    hashtag
    Subtraction Trick

    This implementation uses the fact that x [<,<=,==,!=,>=,>] y is equal to x - y [<,<=,==,!=,>=,>] 0, which is just a subtraction and a table lookup!

    There are two major problems with this implementation:

    1. subtraction before the TLU requires up to 2 additional bits to avoid overflows (it is 1 in most cases).

    2. subtraction requires the same bit-width across operands.

    What this means is if we are comparing uint3 and uint6, we need to convert both of them to uint7 in some way to do the subtraction and proceed with the TLU in 7-bits. There are 4 ways to achieve this behavior.

    hashtag
    Requirements

    hashtag
    1. fhe.ComparisonStrategy.ONE_TLU_PROMOTED

    This strategy makes sure that during bit-width assignment, both operands are assigned the same bit-width, and that bit-width contains at least the number of bits required to store x - y. The idea is:

    hashtag
    Pros

    • It will always result in a single table lookup.

    hashtag
    Cons

    • It will increase the bit-width of both operands and lock them to each other across the whole circuit, which can result in significant slowdowns if the operands are used in other costly operations.

    hashtag
    Example

    produces

    hashtag
    2. fhe.ComparisonStrategy.THREE_TLU_CASTED

    This strategy will not put any constraint on bit-widths during bit-width assignment, instead operands are cast to a bit-width that can store x - y during runtime using table lookups. The idea is:

    hashtag
    Notes

    • It can result in a single table lookup, if x and y are assigned (because of other operations) the same bit-width and that bit-width can store x - y.

    • Alternatively, two table lookups can be used if only one of the operands is assigned a bit-width bigger than or equal to the bit width that can store x - y.

    hashtag
    Pros

    • It will not put any constraints on the bit-widths of the operands, which is amazing if they are used in other costly operations.

    • It will result in at most 3 table lookups, which is still good.

    hashtag
    Cons

    • If you are not doing anything else with the operands, or doing less costly operations compared to comparison, it will introduce up to two unnecessary table lookups and slow down execution compared to fhe.ComparisonStrategy.ONE_TLU_PROMOTED.

    hashtag
    Example

    produces

    hashtag
    3. fhe.ComparisonStrategy.TWO_TLU_BIGGER_PROMOTED_SMALLER_CASTED

    This strategy can be seen as a middle ground between the two strategies described above. With this strategy, only the bigger operand will be constrained to have at least the required bit-width to store x - y, and the smaller operand will be cast to that bit-width during runtime. The idea is:

    hashtag
    Notes

    • It can result in a single table lookup, if the smaller operand is assigned (because of other operations) the same bit-width as the bigger operand.

    hashtag
    Pros

    • It will only put a constraint on the bigger operand, which is great if the smaller operand is used in other costly operations.

    • It will result in at most 2 table lookups, which is great.

    hashtag
    Cons

    • It will increase the bit-width of the bigger operand, which can result in significant slowdowns if the bigger operand is used in other costly operations.

    • If you are not doing anything else with the smaller operand, or doing less costly operations compared to comparison, it could introduce an unnecessary table lookup and slow down execution compared to fhe.ComparisonStrategy.THREE_TLU_CASTED.

    hashtag
    Example

    produces

    hashtag
    4. fhe.ComparisonStrategy.TWO_TLU_BIGGER_CASTED_SMALLER_PROMOTED

    This strategy can be seen as the exact opposite of the strategy above. With this, only the smaller operand will be constrained to have at least the required bit-width, and the bigger operand will be cast during runtime. The idea is:

    hashtag
    Notes

    • It can result in a single table lookup, if the bigger operand is assigned (because of other operations) the same bit-width as the smaller operand.

    hashtag
    Pros

    • It will only put a constraint on the smaller operand, which is great if the bigger operand is used in other costly operations.

    • It will result in at most 2 table lookups, which is great.

    hashtag
    Cons

    • It will increase the bit-width of the smaller operand, which can result in significant slowdowns if the smaller operand is used in other costly operations.

    • If you are not doing anything else with the bigger operand, or doing less costly operations compared to comparison, it could introduce an unnecessary table lookup and slow down execution compared to fhe.ComparisonStrategy.THREE_TLU_CASTED.

    hashtag
    Example

    produces

    hashtag
    Clipping Trick

    This implementation uses the fact that the subtraction trick is not optimal in terms of the required intermediate bit width. The comparison result does not change if we compare(3, 40) or compare(3, 4), so why not clipping the bigger operand and then doing the subtraction to use less bits!

    There are two major problems with this implementation:

    1. it can not be used when the bit-widths are the same (for some cases even when they differ by only one bit)

    2. subtraction still requires the same bit-width across operands.

    What this means is if we are comparing uint3 and uint6, we need to convert both of them to uint4 in some way to do the subtraction and proceed with the TLU in 7-bits. There are 2 ways to achieve this behavior.

    hashtag
    Requirements

    hashtag
    1. fhe.ComparisonStrategy.THREE_TLU_BIGGER_CLIPPED_SMALLER_CASTED

    This strategy will not put any constraint on bit-widths during bit-width assignment, instead the smaller operand is cast to a bit-width that can store clipped(bigger) - smaller or smaller - clipped(bigger) during runtime using table lookups. The idea is:

    hashtag
    Notes

    • This is a fallback implementation, so if there is a difference of 1-bit (or in some cases 2-bits) and the subtraction trick cannot be used optimally, this implementation will be used instead of fhe.ComparisonStrategy.CHUNKED.

    • It can result in two table lookups if the smaller operand is assigned a bit-width bigger than or equal to the bit width that can store clipped(bigger) - smaller or smaller - clipped(bigger).

    hashtag
    Pros

    • It will not put any constraints on the bit-widths of the operands, which is amazing if they are used in other costly operations.

    • It will result in at most 3 table lookups, which is still good.

    • These table lookups will be on smaller bit-widths, which is great.

    hashtag
    Cons

    • Cannot be used to compare integers with the same bit-width, which is very common.

    hashtag
    Example

    produces

    hashtag
    2. fhe.ComparisonStrategy.TWO_TLU_BIGGER_CLIPPED_SMALLER_PROMOTED

    This strategy is similar to the strategy described above. The difference is that with this strategy, the smaller operand will be constrained to have at least the required bit-width to store clipped(bigger) - smaller or smaller - clipped(bigger). The bigger operand will still be clipped to that bit-width during runtime. The idea is:

    hashtag
    Pros

    • It will only put a constraint on the smaller operand, which is great if the bigger operand is used in other costly operations.

    • It will result in exactly 2 table lookups, which is great.

    hashtag
    Cons

    • It will increase the bit-width of the bigger operand, which can result in significant slowdowns if the bigger operand is used in other costly operations.

    hashtag
    Example

    produces

    hashtag
    Summary

    Strategy
    Minimum # of TLUs
    Maximum # of TLUs
    Can increase the bit-width of the inputs

    CHUNKED

    5

    13

    ONE_TLU_PROMOTED

    1

    1

    ✓

    circle-info

    Concrete will choose the best strategy available after bit-width assignment, regardless of the specified preference.

    circle-info

    Different strategies are good for different circuits. If you want the best runtime for your use case, you can compile your circuit with all different comparison strategy preferences, and pick the one with the lowest complexity.

    (x - y).bit_width <= MAXIMUM_TLU_BIT_WIDTH
    x.bit_width != y.bit_width
    smaller = x if x.bit_width < y.bit_width else y
    bigger = x if x.bit_width > y.bit_width else y
    clipped = lambda value: np.clip(value, smaller.min() - 1, smaller.max() + 1)
    any(
        (
            bit_width <= MAXIMUM_TLU_BIT_WIDTH and
            bit_width <= bigger.dtype.bit_width and
            bit_width > smaller.dtype.bit_width
        )
        for bit_width in [
            (smaller - clipped(bigger)).bit_width,
            (clipped(bigger) - smaller).bit_width,
        ]
      )
    # (example below is for bit-width of 8 and chunk size of 4)
    
    # extract chunks of lhs using table lookups
    lhs_chunks = [lhs.bits[0:4], lhs.bits[4:8]]
    
    # extract chunks of rhs using table lookups
    rhs_chunks = [rhs.bits[0:4], rhs.bits[4:8]]
    
    # pack chunks of lhs and rhs using clear multiplications and additions 
    packed_chunks = []
    for lhs_chunk, rhs_chunk in zip(lhs_chunks, rhs_chunks):
        shifted_lhs_chunk = lhs_chunk * 2**4  # (i.e., lhs_chunk << 4)
        packed_chunks.append(shifted_lhs_chunk + rhs_chunk)
    
    # apply comparison table lookup to packed chunks
    comparison_table = fhe.LookupTable([...])
    chunk_comparisons = comparison_table[packed_chunks]
    
    # reduce chunk comparisons to comparison of numbers
    result = chunk_comparisons[0]
    for chunk_comparison in chunk_comparisons[1:]:
        chunk_reduction_table = fhe.LookupTable([...])
        shifted_chunk_comparison= chunk_comparison * 2**2  # (i.e., lhs_chunk << 2)
        result = chunk_reduction_table[result + shifted_chunk_comparison]
    import numpy as np
    from concrete import fhe
    
    def f(x, y):
        return x < y
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**4))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, show_mlir=True)
    module {
      func.func @main(%arg0: !FHE.eint<4>, %arg1: !FHE.eint<4>) -> !FHE.eint<1> {
      
        // extracting the first chunk of x, adjusted for shifting
        %cst = arith.constant dense<[0, 0, 0, 0, 4, 4, 4, 4, 8, 8, 8, 8, 12, 12, 12, 12]> : tensor<16xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        
        // extracting the first chunk of y
        %cst_0 = arith.constant dense<[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]> : tensor<16xi64>
        %1 = "FHE.apply_lookup_table"(%arg1, %cst_0) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        
        // packing first chunks
        %2 = "FHE.add_eint"(%0, %1) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        
        // comparing first chunks
        %cst_1 = arith.constant dense<[0, 1, 1, 1, 2, 0, 1, 1, 2, 2, 0, 1, 2, 2, 2, 0]> : tensor<16xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_1) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        
        // extracting the second chunk of x, adjusted for shifting
        %cst_2 = arith.constant dense<[0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12]> : tensor<16xi64>
        %4 = "FHE.apply_lookup_table"(%arg0, %cst_2) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        
        // extracting the second chunk of y
        %cst_3 = arith.constant dense<[0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]> : tensor<16xi64>
        %5 = "FHE.apply_lookup_table"(%arg1, %cst_3) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        
        // packing second chunks
        %6 = "FHE.add_eint"(%4, %5) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        
        // comparing second chunks
        %cst_4 = arith.constant dense<[0, 4, 4, 4, 8, 0, 4, 4, 8, 8, 0, 4, 8, 8, 8, 0]> : tensor<16xi64>
        %7 = "FHE.apply_lookup_table"(%6, %cst_4) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<4>
        
        // packing comparisons
        %8 = "FHE.add_eint"(%7, %3) : (!FHE.eint<4>, !FHE.eint<4>) -> !FHE.eint<4>
        
        // reducing comparisons to result
        %cst_5 = arith.constant dense<[0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0]> : tensor<16xi64>
        %9 = "FHE.apply_lookup_table"(%8, %cst_5) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.eint<1>
        
        return %9 : !FHE.eint<1>
        
      }
    }
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_promoted_to_uint7 - y_promoted_to_uint7]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        comparison_strategy_preference=fhe.ComparisonStrategy.ONE_TLU_PROMOTED,
    )
    
    def f(x, y):
        return x < y
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**4))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      // promotions          ............         ............
      func.func @main(%arg0: !FHE.eint<5>, %arg1: !FHE.eint<5>) -> !FHE.eint<1> {
        
        // subtraction
        %0 = "FHE.to_signed"(%arg0) : (!FHE.eint<5>) -> !FHE.esint<5>
        %1 = "FHE.to_signed"(%arg1) : (!FHE.eint<5>) -> !FHE.esint<5>
        %2 = "FHE.sub_eint"(%0, %1) : (!FHE.esint<5>, !FHE.esint<5>) -> !FHE.esint<5>
        
        // computing the result
        %cst = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]> : tensor<32xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst) : (!FHE.esint<5>, tensor<32xi64>) -> !FHE.eint<1>
        
        return %3 : !FHE.eint<1>
        
      }
      
    }
    uint3_to_uint7_lut = fhe.LookupTable([...])
    x_cast_to_uint7 = uint3_to_uint7_lut[x]
    
    uint6_to_uint7_lut = fhe.LookupTable([...])
    y_cast_to_uint7 = uint6_to_uint7_lut[y]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_cast_to_uint7 - y_cast_to_uint7]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        comparison_strategy_preference=fhe.ComparisonStrategy.THREE_TLU_CASTED,
    )
    
    def f(x, y):
        return x < y
    
    inputset = [
        (np.random.randint(0, 2**4), np.random.randint(0, 2**4))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // no promotions
      func.func @main(%arg0: !FHE.eint<3>, %arg1: !FHE.eint<6>) -> !FHE.eint<1> {
        
        // casting
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]> : tensor<16xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.esint<5>
        %1 = "FHE.apply_lookup_table"(%arg1, %cst) : (!FHE.eint<4>, tensor<16xi64>) -> !FHE.esint<5>
        
        // subtraction
        %2 = "FHE.sub_eint"(%0, %1) : (!FHE.esint<5>, !FHE.esint<5>) -> !FHE.esint<5>
        
        // computing the result
        %cst_0 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]> : tensor<32xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_0) : (!FHE.esint<5>, tensor<32xi64>) -> !FHE.eint<1>
        
        return %3 : !FHE.eint<1>
        
      }
      
    }
    uint3_to_uint7_lut = fhe.LookupTable([...])
    x_cast_to_uint7 = uint3_to_uint7_lut[x]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_cast_to_uint7 - y_promoted_to_uint7]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        comparison_strategy_preference=fhe.ComparisonStrategy.TWO_TLU_BIGGER_PROMOTED_SMALLER_CASTED,
    )
    
    def f(x, y):
        return x < y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**5))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // promotions                               ............
      func.func @main(%arg0: !FHE.eint<3>, %arg1: !FHE.eint<6>) -> !FHE.eint<1> {
        
        // casting the smaller operand
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7]> : tensor<8xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.esint<6>
        
        // subtraction
        %1 = "FHE.to_signed"(%arg1) : (!FHE.eint<6>) -> !FHE.esint<6>
        %2 = "FHE.sub_eint"(%0, %1) : (!FHE.esint<6>, !FHE.esint<6>) -> !FHE.esint<6>
        
        // computing the result
        %cst_0 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]> : tensor<64xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_0) : (!FHE.esint<6>, tensor<64xi64>) -> !FHE.eint<1>
        
        return %3 : !FHE.eint<1>
        
      }
      
    }
    uint6_to_uint7_lut = fhe.LookupTable([...])
    y_cast_to_uint7 = uint6_to_uint7_lut[y]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_promoted_to_uint7 - y_cast_to_uint7]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        comparison_strategy_preference=fhe.ComparisonStrategy.TWO_TLU_BIGGER_PROMOTED_SMALLER_CASTED,
    )
    
    def f(x, y):
        return x < y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**5))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // promotions          ............
      func.func @main(%arg0: !FHE.eint<6>, %arg1: !FHE.eint<5>) -> !FHE.eint<1> {
        
        // casting the bigger operand
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]> : tensor<32xi64>
        %0 = "FHE.apply_lookup_table"(%arg1, %cst) : (!FHE.eint<5>, tensor<32xi64>) -> !FHE.esint<6>
        
        // subtraction
        %1 = "FHE.to_signed"(%arg0) : (!FHE.eint<6>) -> !FHE.esint<6>
        %2 = "FHE.sub_eint"(%1, %0) : (!FHE.esint<6>, !FHE.esint<6>) -> !FHE.esint<6>
        
        // computing the result
        %cst_0 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]> : tensor<64xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_0) : (!FHE.esint<6>, tensor<64xi64>) -> !FHE.eint<1>
        
        return %3 : !FHE.eint<1>
        
      }
      
    }
    uint3_to_uint4_lut = fhe.LookupTable([...])
    x_cast_to_uint4 = uint3_to_uint4_lut[x]
    
    clipper = fhe.LookupTable([...])
    y_clipped = clipper[y]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_cast_to_uint4 - y_clipped]
    # or
    another_comparison_lut = fhe.LookupTable([...])
    result = another_comparison_lut[y_clipped - x_cast_to_uint4]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        comparison_strategy_preference=fhe.ComparisonStrategy.THREE_TLU_BIGGER_CLIPPED_SMALLER_CASTED
    )
    
    def f(x, y):
        return x < y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**6))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // no promotions
      func.func @main(%arg0: !FHE.eint<3>, %arg1: !FHE.eint<6>) -> !FHE.eint<1> {
        
        // casting the smaller operand 
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7]> : tensor<8xi64>
        %0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<3>, tensor<8xi64>) -> !FHE.esint<4>
        
        // clipping the bigger operand
        %cst_0 = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]> : tensor<64xi64>
        %1 = "FHE.apply_lookup_table"(%arg1, %cst_0) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.esint<4>
        
        // subtraction
        %2 = "FHE.sub_eint"(%0, %1) : (!FHE.esint<4>, !FHE.esint<4>) -> !FHE.esint<4>
        
        // computing the result
        %cst_1 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]> : tensor<16xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_1) : (!FHE.esint<4>, tensor<16xi64>) -> !FHE.eint<1>
        
        return %3 : !FHE.eint<1>
        
      }
      
    }
    clipper = fhe.LookupTable([...])
    y_clipped = clipper[y]
    
    comparison_lut = fhe.LookupTable([...])
    result = comparison_lut[x_promoted_to_uint4 - y_clipped]
    # or
    another_comparison_lut = fhe.LookupTable([...])
    result = another_comparison_lut[y_clipped - x_promoted_to_uint4]
    import numpy as np
    from concrete import fhe
    
    configuration = fhe.Configuration(
        comparison_strategy_preference=fhe.ComparisonStrategy.TWO_TLU_BIGGER_CLIPPED_SMALLER_PROMOTED
    )
    
    def f(x, y):
        return x < y
    
    inputset = [
        (np.random.randint(0, 2**3), np.random.randint(0, 2**6))
        for _ in range(100)
    ]
    
    compiler = fhe.Compiler(f, {"x": "encrypted", "y": "encrypted"})
    circuit = compiler.compile(inputset, configuration, show_mlir=True)
    module {
      
      // promotions          ............
      func.func @main(%arg0: !FHE.eint<4>, %arg1: !FHE.eint<6>) -> !FHE.eint<1> {
        
        // clipping the bigger operand
        %cst = arith.constant dense<[0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]> : tensor<64xi64>
        %0 = "FHE.apply_lookup_table"(%arg1, %cst) : (!FHE.eint<6>, tensor<64xi64>) -> !FHE.esint<4>
        
        // subtraction
        %1 = "FHE.to_signed"(%arg0) : (!FHE.eint<4>) -> !FHE.esint<4>
        %2 = "FHE.sub_eint"(%1, %0) : (!FHE.esint<4>, !FHE.esint<4>) -> !FHE.esint<4>
            
        // computing the result
        %cst_0 = arith.constant dense<[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]> : tensor<16xi64>
        %3 = "FHE.apply_lookup_table"(%2, %cst_0) : (!FHE.esint<4>, tensor<16xi64>) -> !FHE.eint<1>
        
        return %3 : !FHE.eint<1>
        
      }
      
    }

    THREE_TLU_CASTED

    1

    3

    TWO_TLU_BIGGER_PROMOTED_SMALLER_CASTED

    1

    2

    ✓

    TWO_TLU_BIGGER_CASTED_SMALLER_PROMOTED

    1

    2

    ✓

    THREE_TLU_BIGGER_CLIPPED_SMALLER_CASTED

    2

    3

    TWO_TLU_BIGGER_CLIPPED_SMALLER_PROMOTED

    2

    2

    ✓

    TFHE Dialect

    High Level Fully Homomorphic Encryption dialect A dialect for representation of high level operation on fully homomorphic ciphertext.

    hashtag
    Operation definition

    hashtag
    TFHE.batched_add_glwe_cst_int (::mlir::concretelang::TFHE::ABatchedAddGLWECstIntOp)

    Batched version of AddGLWEIntOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_add_glwe_int_cst (::mlir::concretelang::TFHE::ABatchedAddGLWEIntCstOp)

    Batched version of AddGLWEIntOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_add_glwe_int (::mlir::concretelang::TFHE::ABatchedAddGLWEIntOp)

    Batched version of AddGLWEIntOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_add_glwe (::mlir::concretelang::TFHE::ABatchedAddGLWEOp)

    Batched version of AddGLWEOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.add_glwe_int (::mlir::concretelang::TFHE::AddGLWEIntOp)

    Returns the sum of a clear integer and an lwe ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BatchableOpInterface, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.add_glwe (::mlir::concretelang::TFHE::AddGLWEOp)

    Returns the sum of two lwe ciphertexts

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BatchableOpInterface, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_bootstrap_glwe (::mlir::concretelang::TFHE::BatchedBootstrapGLWEOp)

    Batched version of KeySwitchGLWEOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_keyswitch_glwe (::mlir::concretelang::TFHE::BatchedKeySwitchGLWEOp)

    Batched version of KeySwitchGLWEOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_mapped_bootstrap_glwe (::mlir::concretelang::TFHE::BatchedMappedBootstrapGLWEOp)

    Batched version of KeySwitchGLWEOp which also batches the lookup table

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_mul_glwe_cst_int (::mlir::concretelang::TFHE::BatchedMulGLWECstIntOp)

    Batched version of MulGLWEIntOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_mul_glwe_int_cst (::mlir::concretelang::TFHE::BatchedMulGLWEIntCstOp)

    Batched version of MulGLWEIntOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_mul_glwe_int (::mlir::concretelang::TFHE::BatchedMulGLWEIntOp)

    Batched version of MulGLWEIntOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.batched_neg_glwe (::mlir::concretelang::TFHE::BatchedNegGLWEOp)

    Batched version of NegGLWEOp

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.bootstrap_glwe (::mlir::concretelang::TFHE::BootstrapGLWEOp)

    Programmable bootstraping of a GLWE ciphertext with a lookup table

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BatchableOpInterface, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.encode_expand_lut_for_bootstrap (::mlir::concretelang::TFHE::EncodeExpandLutForBootstrapOp)

    Encode and expand a lookup table so that it can be used for a bootstrap.

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.encode_lut_for_crt_woppbs (::mlir::concretelang::TFHE::EncodeLutForCrtWopPBSOp)

    Encode and expand a lookup table so that it can be used for a wop pbs.

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.encode_plaintext_with_crt (::mlir::concretelang::TFHE::EncodePlaintextWithCrtOp)

    Encodes a plaintext by decomposing it on a crt basis.

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.keyswitch_glwe (::mlir::concretelang::TFHE::KeySwitchGLWEOp)

    Change the encryption parameters of a glwe ciphertext by applying a keyswitch

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BatchableOpInterface, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.mul_glwe_int (::mlir::concretelang::TFHE::MulGLWEIntOp)

    Returns the product of a clear integer and an lwe ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BatchableOpInterface, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.neg_glwe (::mlir::concretelang::TFHE::NegGLWEOp)

    Negates a glwe ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BatchableOpInterface, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.sub_int_glwe (::mlir::concretelang::TFHE::SubGLWEIntOp)

    Substracts an integer and a GLWE ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.wop_pbs_glwe (::mlir::concretelang::TFHE::WopPBSGLWEOp)

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.zero (::mlir::concretelang::TFHE::ZeroGLWEOp)

    Returns a trivial encryption of 0

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Results:

    Result
    Description

    hashtag
    TFHE.zero_tensor (::mlir::concretelang::TFHE::ZeroTensorGLWEOp)

    Returns a tensor containing trivial encryptions of 0

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Results:

    Result
    Description

    hashtag
    Attribute definition

    hashtag
    GLWEBootstrapKeyAttr

    An attribute representing bootstrap key.

    Syntax:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    hashtag
    GLWEKeyswitchKeyAttr

    An attribute representing keyswitch key.

    Syntax:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    hashtag
    GLWEPackingKeyswitchKeyAttr

    An attribute representing Wop Pbs key.

    Syntax:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    hashtag
    Type definition

    hashtag
    GLWECipherTextType

    A GLWE ciphertext

    An GLWE cipher text

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    cbsLevels

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    cbsBaseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    levels

    int

    baseLog

    int

    index

    int

    index

    int

    glweDim

    int

    levels

    int

    baseLog

    int

    index

    int

    ciphertext

    A GLWE ciphertext

    plaintexts

    1D tensor of integer values

    result

    1D tensor of A GLWE ciphertext values

    ciphertexts

    1D tensor of A GLWE ciphertext values

    plaintext

    integer

    result

    1D tensor of A GLWE ciphertext values

    ciphertexts

    1D tensor of A GLWE ciphertext values

    plaintexts

    1D tensor of integer values

    result

    1D tensor of A GLWE ciphertext values

    ciphertexts_a

    1D tensor of A GLWE ciphertext values

    ciphertexts_b

    1D tensor of A GLWE ciphertext values

    result

    1D tensor of A GLWE ciphertext values

    a

    A GLWE ciphertext

    b

    integer

    «unnamed»

    A GLWE ciphertext

    a

    A GLWE ciphertext

    b

    A GLWE ciphertext

    «unnamed»

    A GLWE ciphertext

    key

    ::mlir::concretelang::TFHE::GLWEBootstrapKeyAttr

    An attribute representing bootstrap key.

    ciphertexts

    1D tensor of A GLWE ciphertext values

    lookup_table

    1D tensor of 64-bit signless integer values

    result

    1D tensor of A GLWE ciphertext values

    key

    ::mlir::concretelang::TFHE::GLWEKeyswitchKeyAttr

    An attribute representing keyswitch key.

    ciphertexts

    1D tensor of A GLWE ciphertext values

    result

    1D tensor of A GLWE ciphertext values

    key

    ::mlir::concretelang::TFHE::GLWEBootstrapKeyAttr

    An attribute representing bootstrap key.

    ciphertexts

    1D tensor of A GLWE ciphertext values

    lookup_table

    2D tensor of 64-bit signless integer values

    result

    1D tensor of A GLWE ciphertext values

    ciphertext

    A GLWE ciphertext

    cleartexts

    1D tensor of integer values

    result

    1D tensor of A GLWE ciphertext values

    ciphertexts

    1D tensor of A GLWE ciphertext values

    cleartext

    integer

    result

    1D tensor of A GLWE ciphertext values

    ciphertexts

    1D tensor of A GLWE ciphertext values

    cleartexts

    1D tensor of integer values

    result

    1D tensor of A GLWE ciphertext values

    ciphertexts

    1D tensor of A GLWE ciphertext values

    result

    1D tensor of A GLWE ciphertext values

    key

    ::mlir::concretelang::TFHE::GLWEBootstrapKeyAttr

    An attribute representing bootstrap key.

    ciphertext

    A GLWE ciphertext

    lookup_table

    1D tensor of 64-bit signless integer values

    result

    A GLWE ciphertext

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    outputBits

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    isSigned

    ::mlir::BoolAttr

    bool attribute

    input_lookup_table

    1D tensor of 64-bit signless integer values

    result

    1D tensor of 64-bit signless integer values

    crtDecomposition

    ::mlir::ArrayAttr

    64-bit integer array attribute

    crtBits

    ::mlir::ArrayAttr

    64-bit integer array attribute

    modulusProduct

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    isSigned

    ::mlir::BoolAttr

    input_lookup_table

    1D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    mods

    ::mlir::ArrayAttr

    64-bit integer array attribute

    modsProd

    ::mlir::IntegerAttr

    64-bit signless integer attribute

    input

    64-bit signless integer

    result

    1D tensor of 64-bit signless integer values

    key

    ::mlir::concretelang::TFHE::GLWEKeyswitchKeyAttr

    An attribute representing keyswitch key.

    ciphertext

    A GLWE ciphertext

    result

    A GLWE ciphertext

    a

    A GLWE ciphertext

    b

    integer

    «unnamed»

    A GLWE ciphertext

    a

    A GLWE ciphertext

    «unnamed»

    A GLWE ciphertext

    a

    integer

    b

    A GLWE ciphertext

    «unnamed»

    A GLWE ciphertext

    ksk

    ::mlir::concretelang::TFHE::GLWEKeyswitchKeyAttr

    An attribute representing keyswitch key.

    bsk

    ::mlir::concretelang::TFHE::GLWEBootstrapKeyAttr

    An attribute representing bootstrap key.

    pksk

    ::mlir::concretelang::TFHE::GLWEPackingKeyswitchKeyAttr

    An attribute representing Wop Pbs key.

    crtDecomposition

    ::mlir::ArrayAttr

    ciphertexts

    lookupTable

    2D tensor of 64-bit signless integer values

    result

    out

    A GLWE ciphertext

    tensor

    inputKey

    mlir::concretelang::TFHE::GLWESecretKey

    outputKey

    mlir::concretelang::TFHE::GLWESecretKey

    polySize

    int

    glweDim

    int

    inputKey

    mlir::concretelang::TFHE::GLWESecretKey

    outputKey

    mlir::concretelang::TFHE::GLWESecretKey

    levels

    int

    baseLog

    int

    inputKey

    mlir::concretelang::TFHE::GLWESecretKey

    outputKey

    mlir::concretelang::TFHE::GLWESecretKey

    outputPolySize

    int

    innerLweDim

    int

    key

    mlir::concretelang::TFHE::GLWESecretKey

    bool attribute

    64-bit integer array attribute

    #TFHE.bsk<
      mlir::concretelang::TFHE::GLWESecretKey,   # inputKey
      mlir::concretelang::TFHE::GLWESecretKey,   # outputKey
      int,   # polySize
      int,   # glweDim
      int,   # levels
      int,   # baseLog
      int   # index
    >
    #TFHE.ksk<
      mlir::concretelang::TFHE::GLWESecretKey,   # inputKey
      mlir::concretelang::TFHE::GLWESecretKey,   # outputKey
      int,   # levels
      int,   # baseLog
      int   # index
    >
    #TFHE.pksk<
      mlir::concretelang::TFHE::GLWESecretKey,   # inputKey
      mlir::concretelang::TFHE::GLWESecretKey,   # outputKey
      int,   # outputPolySize
      int,   # innerLweDim
      int,   # glweDim
      int,   # levels
      int,   # baseLog
      int   # index
    >

    FHE Dialect

    High Level Fully Homomorphic Encryption dialect A dialect for representation of high level operation on fully homomorphic ciphertext.

    hashtag
    Operation definition

    hashtag
    FHE.add_eint_int (::mlir::concretelang::FHE::AddEintIntOp)

    Adds an encrypted integer and a clear integer

    The clear integer must have at most one more bit than the encrypted integer and the result must have the same width and the same signedness as the encrypted integer.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.add_eint (::mlir::concretelang::FHE::AddEintOp)

    Adds two encrypted integers

    The encrypted integers and the result must have the same width and the same signedness.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BinaryEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.apply_lookup_table (::mlir::concretelang::FHE::ApplyLookupTableEintOp)

    Applies a clear lookup table to an encrypted integer

    The width of the result can be different than the width of the operand. The lookup table must be a tensor of size 2^p where p is the width of the encrypted integer.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.and (::mlir::concretelang::FHE::BoolAndOp)

    Applies an AND gate to two encrypted boolean values

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.nand (::mlir::concretelang::FHE::BoolNandOp)

    Applies a NAND gate to two encrypted boolean values

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.not (::mlir::concretelang::FHE::BoolNotOp)

    Applies a NOT gate to an encrypted boolean value

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.or (::mlir::concretelang::FHE::BoolOrOp)

    Applies an OR gate to two encrypted boolean values

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.xor (::mlir::concretelang::FHE::BoolXorOp)

    Applies an XOR gate to two encrypted boolean values

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.from_bool (::mlir::concretelang::FHE::FromBoolOp)

    Cast a boolean to an unsigned integer

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.gen_gate (::mlir::concretelang::FHE::GenGateOp)

    Applies a truth table based on two boolean inputs

    Truth table must be a tensor of four boolean values.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.lsb (::mlir::concretelang::FHE::LsbEintOp)

    Extract the lowest significant bit at a given precision.

    This operation extracts the lsb of a ciphertext in a specific precision.

    Extracting the lsb with the smallest precision:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.max_eint (::mlir::concretelang::FHE::MaxEintOp)

    Retrieve the maximum of two encrypted integers.

    Retrieve the maximum of two encrypted integers using the formula, 'max(x, y) == max(x - y, 0) + y'. The input and output types should be the same.

    If `x - y`` inside the max overflows or underflows, the behavior is undefined. To support the full range, you should increase the bit-width by 1 manually.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BinaryEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.mul_eint_int (::mlir::concretelang::FHE::MulEintIntOp)

    Multiply an encrypted integer with a clear integer

    The clear integer must have one more bit than the encrypted integer and the result must have the same width and the same signedness as the encrypted integer.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.mul_eint (::mlir::concretelang::FHE::MulEintOp)

    Multiplies two encrypted integers

    The encrypted integers and the result must have the same width and signedness. Also, due to the current implementation, one supplementary bit of width must be provided, in addition to the number of bits needed to encode the largest output value.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BinaryEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.mux (::mlir::concretelang::FHE::MuxOp)

    Multiplexer for two encrypted boolean inputs, based on an encrypted condition

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.neg_eint (::mlir::concretelang::FHE::NegEintOp)

    Negates an encrypted integer

    The result must have the same width and the same signedness as the encrypted integer.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.reinterpret_precision (::mlir::concretelang::FHE::ReinterpretPrecisionEintOp)

    Reinterpret the ciphertext with a different precision.

    Changing the precision of a ciphertext. It changes both the precision, the value, and in certain cases the correctness of the ciphertext.

    Changing to - a bigger precision is always safe. This is equivalent to a shift left for the value. - a smaller precision is only safe if you clear the lowest bits that are discarded. If not, you can assume small errors on the next TLU. This is equivalent to a shift right for the value.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.round (::mlir::concretelang::FHE::RoundEintOp)

    Rounds a ciphertext to a smaller precision.

    Assuming a ciphertext whose message is implemented over p bits, this operation rounds it to fit to q bits with p>q.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.sub_eint_int (::mlir::concretelang::FHE::SubEintIntOp)

    Subtract a clear integer from an encrypted integer

    The clear integer must have one more bit than the encrypted integer and the result must have the same width and the same signedness as the encrypted integer.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.sub_eint (::mlir::concretelang::FHE::SubEintOp)

    Subtract an encrypted integer from an encrypted integer

    The encrypted integers and the result must have the same width and the same signedness.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: BinaryEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.sub_int_eint (::mlir::concretelang::FHE::SubIntEintOp)

    Subtract an encrypted integer from a clear integer

    The clear integer must have one more bit than the encrypted integer and the result must have the same width and the same signedness as the encrypted integer.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: Binary, BinaryIntEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.to_bool (::mlir::concretelang::FHE::ToBoolOp)

    Cast an unsigned integer to a boolean

    The input must be of width one or two. Two being the current representation of an encrypted boolean, leaving one bit for the carry.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.to_signed (::mlir::concretelang::FHE::ToSignedOp)

    Cast an unsigned integer to a signed one

    The result must have the same width as the input.

    The behavior is undefined on overflow/underflow.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.to_unsigned (::mlir::concretelang::FHE::ToUnsignedOp)

    Cast a signed integer to an unsigned one

    The result must have the same width as the input.

    The behavior is undefined on overflow/underflow.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.zero (::mlir::concretelang::FHE::ZeroEintOp)

    Returns a trivial encrypted integer of 0

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Results:

    Result
    Description

    hashtag
    FHE.zero_tensor (::mlir::concretelang::FHE::ZeroTensorOp)

    Creates a new tensor with all elements initialized to an encrypted zero.

    Creates a new tensor with the shape specified in the result type and initializes its elements with an encrypted zero.

    Example:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Results:

    Result
    Description

    hashtag
    Type definition

    hashtag
    EncryptedBooleanType

    An encrypted boolean

    Syntax: !FHE.ebool

    An encrypted boolean.

    hashtag
    EncryptedSignedIntegerType

    An encrypted signed integer

    An encrypted signed integer with width bits to performs FHE Operations.

    Examples:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    hashtag
    EncryptedUnsignedIntegerType

    An encrypted unsigned integer

    An encrypted unsigned integer with width bits to performs FHE Operations.

    Examples:

    hashtag
    Parameters:

    Parameter
    C++ type
    Description

    a

    b

    integer

    «unnamed»

    a

    b

    «unnamed»

    a

    lut

    tensor of integer values

    «unnamed»

    left

    An encrypted boolean

    right

    An encrypted boolean

    «unnamed»

    An encrypted boolean

    left

    An encrypted boolean

    right

    An encrypted boolean

    «unnamed»

    An encrypted boolean

    value

    An encrypted boolean

    «unnamed»

    An encrypted boolean

    left

    An encrypted boolean

    right

    An encrypted boolean

    «unnamed»

    An encrypted boolean

    left

    An encrypted boolean

    right

    An encrypted boolean

    «unnamed»

    An encrypted boolean

    input

    An encrypted boolean

    «unnamed»

    An encrypted unsigned integer

    left

    An encrypted boolean

    right

    An encrypted boolean

    truth_table

    tensor of integer values

    «unnamed»

    An encrypted boolean

    input

    «unnamed»

    x

    y

    «unnamed»

    a

    b

    integer

    «unnamed»

    rhs

    lhs

    «unnamed»

    cond

    An encrypted boolean

    c1

    An encrypted boolean

    c2

    An encrypted boolean

    «unnamed»

    An encrypted boolean

    a

    «unnamed»

    input

    «unnamed»

    input

    «unnamed»

    a

    b

    integer

    «unnamed»

    a

    b

    «unnamed»

    a

    integer

    b

    «unnamed»

    input

    An encrypted unsigned integer

    «unnamed»

    An encrypted boolean

    input

    An encrypted unsigned integer

    «unnamed»

    An encrypted signed integer

    input

    An encrypted signed integer

    «unnamed»

    An encrypted unsigned integer

    out

    tensor

    width

    unsigned

    width

    unsigned

    // ok
    "FHE.add_eint_int"(%a, %i) : (!FHE.eint<2>, i3) -> !FHE.eint<2>
    "FHE.add_eint_int"(%a, %i) : (!FHE.esint<2>, i3) -> !FHE.esint<2>
    
    // error
    "FHE.add_eint_int"(%a, %i) : (!FHE.eint<2>, i4) -> !FHE.eint<2>
    "FHE.add_eint_int"(%a, %i) : (!FHE.eint<2>, i3) -> !FHE.eint<3>
    "FHE.add_eint_int"(%a, %i) : (!FHE.eint<2>, i3) -> !FHE.esint<2>
    // ok
    "FHE.add_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.eint<2>)
    "FHE.add_eint"(%a, %b): (!FHE.esint<2>, !FHE.esint<2>) -> (!FHE.esint<2>)
    
    // error
    "FHE.add_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<3>) -> (!FHE.eint<2>)
    "FHE.add_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.eint<3>)
    "FHE.add_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.esint<2>)
    "FHE.add_eint"(%a, %b): (!FHE.esint<2>, !FHE.eint<2>) -> (!FHE.eint<2>)
    // ok
    "FHE.apply_lookup_table"(%a, %lut): (!FHE.eint<2>, tensor<4xi64>) -> (!FHE.eint<2>)
    "FHE.apply_lookup_table"(%a, %lut): (!FHE.eint<2>, tensor<4xi64>) -> (!FHE.eint<3>)
    "FHE.apply_lookup_table"(%a, %lut): (!FHE.eint<3>, tensor<4xi64>) -> (!FHE.eint<2>)
    
    // error
    "FHE.apply_lookup_table"(%a, %lut): (!FHE.eint<2>, tensor<8xi64>) -> (!FHE.eint<2>)
    "FHE.and"(%a, %b): (!FHE.ebool, !FHE.ebool) -> (!FHE.ebool)
    "FHE.nand"(%a, %b): (!FHE.ebool, !FHE.ebool) -> (!FHE.ebool)
    "FHE.not"(%a): (!FHE.ebool) -> (!FHE.ebool)
    "FHE.or"(%a, %b): (!FHE.ebool, !FHE.ebool) -> (!FHE.ebool)
    "FHE.xor"(%a, %b): (!FHE.ebool, !FHE.ebool) -> (!FHE.ebool)
    "FHE.from_bool"(%x) : (!FHE.ebool) -> !FHE.eint<1>
    "FHE.from_bool"(%x) : (!FHE.ebool) -> !FHE.eint<2>
    "FHE.from_bool"(%x) : (!FHE.ebool) -> !FHE.eint<4>
    // ok
    "FHE.gen_gate"(%a, %b, %ttable): (!FHE.ebool, !FHE.ebool, tensor<4xi64>) -> (!FHE.ebool)
    
    // error
    "FHE.gen_gate"(%a, %b, %ttable): (!FHE.ebool, !FHE.ebool, tensor<7xi64>) -> (!FHE.ebool)
     // Checking if even or odd
     %even = "FHE.lsb"(%a): (!FHE.eint<4>) -> (!FHE.eint<1>)
    
    Usually when you extract the lsb bit, you also need to extract the next one.
    In that case you first need to clear the first lsb of the input to be able to reduce its precision and extract the next one.
    To be able to clear the lsb just extracted, you can get it in the original precision.
    
    Example:
    ```mlir
     // Extracting the first lsb with original precision
     %lsb_0 = "FHE.lsb"(%input): (!FHE.eint<4>) -> (!FHE.eint<4>)
     // Clearing the first lsb from original input
     %input_lsb0_cleared = "FHE.sub_eint"(%input, %lsb_0) : (!FHE.eint<4>, !FHE.eint<4>) -> (!FHE.eint<4>)
     // Reducing the precision of the input
     %input_3b = "FHE.reinterpret_precision(%input) : (!FHE.eint<4>) -> !FHE.eint<3>
     // Now, we can do it again with the second lsb
     %lsb_1 = "FHE.lsb"(%input_3b): (!FHE.eint<3>) -> (!FHE.eint<3>)
     ...
     // later if you need %b_lsb at same position as in the input
     %lsb_1_at_input_position = "FHE.reinterpret_precision(%b_lsb)" : (!FHE.eint<3>) -> !FHE.eint<4>
     // that way you can recombine the extracted bits
     %input_mod_4 = "FHE.add_eint"(%lsb_0, %lsb_1_at_input_position) : (!FHE.eint<4>, !FHE.eint<4>) -> (!FHE.eint<4>)
    // ok
    "FHE.max_eint"(%x, %y) : (!FHE.eint<2>, !FHE.eint<2>) -> !FHE.eint<2>
    "FHE.max_eint"(%x, %y) : (!FHE.esint<3>, !FHE.esint<3>) -> !FHE.esint<3>
    
    // error
    "FHE.max_eint"(%x, %y) : (!FHE.eint<2>, !FHE.eint<3>) -> !FHE.eint<2>
    "FHE.max_eint"(%x, %y) : (!FHE.eint<2>, !FHE.eint<2>) -> !FHE.esint<2>
    "FHE.max_eint"(%x, %y) : (!FHE.esint<2>, !FHE.eint<2>) -> !FHE.eint<2>
    // ok
    "FHE.mul_eint_int"(%a, %i) : (!FHE.eint<2>, i3) -> !FHE.eint<2>
    "FHE.mul_eint_int"(%a, %i) : (!FHE.esint<2>, i3) -> !FHE.esint<2>
    
    // error
    "FHE.mul_eint_int"(%a, %i) : (!FHE.eint<2>, i4) -> !FHE.eint<2>
    "FHE.mul_eint_int"(%a, %i) : (!FHE.eint<2>, i3) -> !FHE.eint<3>
    "FHE.mul_eint_int"(%a, %i) : (!FHE.eint<2>, i3) -> !FHE.esint<2>
    // ok
    "FHE.mul_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.eint<2>)
    "FHE.mul_eint"(%a, %b): (!FHE.eint<3>, !FHE.eint<3>) -> (!FHE.eint<3>)
    "FHE.mul_eint"(%a, %b): (!FHE.esint<3>, !FHE.esint<3>) -> (!FHE.esint<3>)
    
    // error
    "FHE.mul_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<3>) -> (!FHE.eint<2>)
    "FHE.mul_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.eint<3>)
    "FHE.mul_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.esint<2>)
    "FHE.mul_eint"(%a, %b): (!FHE.esint<2>, !FHE.eint<2>) -> (!FHE.eint<2>)
    "FHE.mux"(%cond, %c1, %c2): (!FHE.ebool, !FHE.ebool, !FHE.ebool) -> (!FHE.ebool)
    // ok
    "FHE.neg_eint"(%a): (!FHE.eint<2>) -> (!FHE.eint<2>)
    "FHE.neg_eint"(%a): (!FHE.esint<2>) -> (!FHE.esint<2>)
    
    // error
    "FHE.neg_eint"(%a): (!FHE.eint<2>) -> (!FHE.eint<3>)
    "FHE.neg_eint"(%a): (!FHE.eint<2>) -> (!FHE.esint<2>)
     // assuming %a is stored as 4bits but can be stored with only 2bits
     // we can reduce its storage precision
     %shifted_a = "FHE.mul_eint_int"(%a, %c_4): (!FHE.eint<4>) -> (!FHE.eint<4>)
     %a_small_precision = "FHE.reinterpret_precision"(%shifted_a, %lsb) : (!FHE.eint<4>) -> (!FHE.eint<2>)
     // ok
     "FHE.round"(%a): (!FHE.eint<6>) -> (!FHE.eint<5>)
     "FHE.round"(%a): (!FHE.eint<5>) -> (!FHE.eint<3>)
     "FHE.round"(%a): (!FHE.eint<3>) -> (!FHE.eint<2>)
     "FHE.round"(%a): (!FHE.esint<3>) -> (!FHE.esint<2>)
    
    // error
     "FHE.round"(%a): (!FHE.eint<6>) -> (!FHE.eint<6>)
     "FHE.round"(%a): (!FHE.eint<4>) -> (!FHE.eint<5>)
     "FHE.round"(%a): (!FHE.eint<4>) -> (!FHE.esint<5>)
    
    // ok
    "FHE.sub_eint_int"(%i, %a) : (!FHE.eint<2>, i3) -> !FHE.eint<2>
    "FHE.sub_eint_int"(%i, %a) : (!FHE.esint<2>, i3) -> !FHE.esint<2>
    
    // error
    "FHE.sub_eint_int"(%i, %a) : (!FHE.eint<2>, i4) -> !FHE.eint<2>
    "FHE.sub_eint_int"(%i, %a) : (!FHE.eint<2>, i3) -> !FHE.eint<3>
    "FHE.sub_eint_int"(%i, %a) : (!FHE.eint<2>, i3) -> !FHE.esint<2>
    // ok
    "FHE.sub_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.eint<2>)
    "FHE.sub_eint"(%a, %b): (!FHE.esint<2>, !FHE.esint<2>) -> (!FHE.esint<2>)
    
    // error
    "FHE.sub_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<3>) -> (!FHE.eint<2>)
    "FHE.sub_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.eint<3>)
    "FHE.sub_eint"(%a, %b): (!FHE.eint<2>, !FHE.esint<2>) -> (!FHE.esint<2>)
    "FHE.sub_eint"(%a, %b): (!FHE.eint<2>, !FHE.eint<2>) -> (!FHE.esint<2>)
    // ok
    "FHE.sub_int_eint"(%i, %a) : (i3, !FHE.eint<2>) -> !FHE.eint<2>
    "FHE.sub_int_eint"(%i, %a) : (i3, !FHE.esint<2>) -> !FHE.esint<2>
    
    // error
    "FHE.sub_int_eint"(%i, %a) : (i4, !FHE.eint<2>) -> !FHE.eint<2>
    "FHE.sub_int_eint"(%i, %a) : (i3, !FHE.eint<2>) -> !FHE.eint<3>
    "FHE.sub_int_eint"(%i, %a) : (i3, !FHE.eint<2>) -> !FHE.esint<2>
    // ok
    "FHE.to_bool"(%x) : (!FHE.eint<1>) -> !FHE.ebool
    "FHE.to_bool"(%x) : (!FHE.eint<2>) -> !FHE.ebool
    
    // error
    "FHE.to_bool"(%x) : (!FHE.eint<3>) -> !FHE.ebool
    // ok
    "FHE.to_signed"(%x) : (!FHE.eint<2>) -> !FHE.esint<2>
    
    // error
    "FHE.to_signed"(%x) : (!FHE.eint<2>) -> !FHE.esint<3>
    // ok
    "FHE.to_unsigned"(%x) : (!FHE.esint<2>) -> !FHE.eint<2>
    
    // error
    "FHE.to_unsigned"(%x) : (!FHE.esint<2>) -> !FHE.eint<3>
    "FHE.zero"() : () -> !FHE.eint<2>
    "FHE.zero"() : () -> !FHE.esint<2>
    %tensor = "FHE.zero_tensor"() : () -> tensor<5x!FHE.eint<4>>
    %tensor = "FHE.zero_tensor"() : () -> tensor<5x!FHE.esint<4>>
    !FHE.esint<7>
    !FHE.esint<6>
    !FHE.eint<7>
    !FHE.eint<6>

    Concrete Dialect

    Low Level Fully Homomorphic Encryption dialect A dialect for representation of low level operation on fully homomorphic ciphertext.

    hashtag
    Operation definition

    hashtag
    Concrete.add_lwe_buffer (::mlir::concretelang::Concrete::AddLweBufferOp)

    Returns the sum of 2 lwe ciphertexts

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.add_lwe_tensor (::mlir::concretelang::Concrete::AddLweTensorOp)

    Returns the sum of 2 lwe ciphertexts

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.add_plaintext_lwe_buffer (::mlir::concretelang::Concrete::AddPlaintextLweBufferOp)

    Returns the sum of a clear integer and an lwe ciphertext

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.add_plaintext_lwe_tensor (::mlir::concretelang::Concrete::AddPlaintextLweTensorOp)

    Returns the sum of a clear integer and an lwe ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_add_lwe_buffer (::mlir::concretelang::Concrete::BatchedAddLweBufferOp)

    Batched version of AddLweBufferOp, which performs the same operation on multiple elements

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_add_lwe_tensor (::mlir::concretelang::Concrete::BatchedAddLweTensorOp)

    Batched version of AddLweTensorOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_add_plaintext_cst_lwe_buffer (::mlir::concretelang::Concrete::BatchedAddPlaintextCstLweBufferOp)

    Batched version of AddPlaintextLweBufferOp, which performs the same operation on multiple elements

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_add_plaintext_cst_lwe_tensor (::mlir::concretelang::Concrete::BatchedAddPlaintextCstLweTensorOp)

    Batched version of AddPlaintextLweTensorOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_add_plaintext_lwe_buffer (::mlir::concretelang::Concrete::BatchedAddPlaintextLweBufferOp)

    Batched version of AddPlaintextLweBufferOp, which performs the same operation on multiple elements

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_add_plaintext_lwe_tensor (::mlir::concretelang::Concrete::BatchedAddPlaintextLweTensorOp)

    Batched version of AddPlaintextLweTensorOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_bootstrap_lwe_buffer (::mlir::concretelang::Concrete::BatchedBootstrapLweBufferOp)

    Batched version of BootstrapLweOp, which performs the same operation on multiple elements

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_bootstrap_lwe_tensor (::mlir::concretelang::Concrete::BatchedBootstrapLweTensorOp)

    Batched version of BootstrapLweOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_keyswitch_lwe_buffer (::mlir::concretelang::Concrete::BatchedKeySwitchLweBufferOp)

    Batched version of KeySwitchLweOp, which performs the same operation on multiple elements

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_keyswitch_lwe_tensor (::mlir::concretelang::Concrete::BatchedKeySwitchLweTensorOp)

    Batched version of KeySwitchLweOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_mapped_bootstrap_lwe_buffer (::mlir::concretelang::Concrete::BatchedMappedBootstrapLweBufferOp)

    Batched, mapped version of BootstrapLweOp, which performs the same operation on multiple elements

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_mapped_bootstrap_lwe_tensor (::mlir::concretelang::Concrete::BatchedMappedBootstrapLweTensorOp)

    Batched, mapped version of BootstrapLweOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_mul_cleartext_cst_lwe_buffer (::mlir::concretelang::Concrete::BatchedMulCleartextCstLweBufferOp)

    Batched version of MulCleartextLweBufferOp, which performs the same operation on multiple elements

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_mul_cleartext_cst_lwe_tensor (::mlir::concretelang::Concrete::BatchedMulCleartextCstLweTensorOp)

    Batched version of MulCleartextLweTensorOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_mul_cleartext_lwe_buffer (::mlir::concretelang::Concrete::BatchedMulCleartextLweBufferOp)

    Batched version of MulCleartextLweBufferOp, which performs the same operation on multiple elements

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_mul_cleartext_lwe_tensor (::mlir::concretelang::Concrete::BatchedMulCleartextLweTensorOp)

    Batched version of MulCleartextLweTensorOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.batched_negate_lwe_buffer (::mlir::concretelang::Concrete::BatchedNegateLweBufferOp)

    Batched version of NegateLweBufferOp, which performs the same operation on multiple elements

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.batched_negate_lwe_tensor (::mlir::concretelang::Concrete::BatchedNegateLweTensorOp)

    Batched version of NegateLweTensorOp, which performs the same operation on multiple elements

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.bootstrap_lwe_buffer (::mlir::concretelang::Concrete::BootstrapLweBufferOp)

    Bootstraps a LWE ciphertext with a GLWE trivial encryption of the lookup table

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.bootstrap_lwe_tensor (::mlir::concretelang::Concrete::BootstrapLweTensorOp)

    Bootstraps an LWE ciphertext with a GLWE trivial encryption of the lookup table

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.encode_expand_lut_for_bootstrap_buffer (::mlir::concretelang::Concrete::EncodeExpandLutForBootstrapBufferOp)

    Encode and expand a lookup table so that it can be used for a bootstrap

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.encode_expand_lut_for_bootstrap_tensor (::mlir::concretelang::Concrete::EncodeExpandLutForBootstrapTensorOp)

    Encode and expand a lookup table so that it can be used for a bootstrap

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.encode_lut_for_crt_woppbs_buffer (::mlir::concretelang::Concrete::EncodeLutForCrtWopPBSBufferOp)

    Encode and expand a lookup table so that it can be used for a crt wop pbs

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.encode_lut_for_crt_woppbs_tensor (::mlir::concretelang::Concrete::EncodeLutForCrtWopPBSTensorOp)

    Encode and expand a lookup table so that it can be used for a wop pbs

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.encode_plaintext_with_crt_buffer (::mlir::concretelang::Concrete::EncodePlaintextWithCrtBufferOp)

    Encodes a plaintext by decomposing it on a crt basis

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.encode_plaintext_with_crt_tensor (::mlir::concretelang::Concrete::EncodePlaintextWithCrtTensorOp)

    Encodes a plaintext by decomposing it on a crt basis

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.keyswitch_lwe_buffer (::mlir::concretelang::Concrete::KeySwitchLweBufferOp)

    Performs a keyswitching operation on an LWE ciphertext

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.keyswitch_lwe_tensor (::mlir::concretelang::Concrete::KeySwitchLweTensorOp)

    Performs a keyswitching operation on an LWE ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.mul_cleartext_lwe_buffer (::mlir::concretelang::Concrete::MulCleartextLweBufferOp)

    Returns the product of a clear integer and a lwe ciphertext

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.mul_cleartext_lwe_tensor (::mlir::concretelang::Concrete::MulCleartextLweTensorOp)

    Returns the product of a clear integer and a lwe ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.negate_lwe_buffer (::mlir::concretelang::Concrete::NegateLweBufferOp)

    Negates an lwe ciphertext

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.negate_lwe_tensor (::mlir::concretelang::Concrete::NegateLweTensorOp)

    Negates an lwe ciphertext

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Concrete.wop_pbs_crt_lwe_buffer (::mlir::concretelang::Concrete::WopPBSCRTLweBufferOp)

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Concrete.wop_pbs_crt_lwe_tensor (::mlir::concretelang::Concrete::WopPBSCRTLweTensorOp)

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    Type definition

    hashtag
    ContextType

    A runtime context

    Syntax: !Concrete.context

    An abstract runtime context to pass contextual value, like public keys, ...

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    glweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    glweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    kskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    kskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    glweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    glweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    glweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    glweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::BoolAttr

    bool attribute

    ::mlir::BoolAttr

    bool attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    kskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    kskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchInputLweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchoutputPolynomialSize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchBaseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    circuitBootstrapLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    circuitBootstrapBaseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    crtDecomposition

    ::mlir::ArrayAttr

    64-bit integer array attribute

    kskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    pkskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchInputLweDimension

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchoutputPolynomialSize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    packingKeySwitchBaseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    circuitBootstrapLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    circuitBootstrapBaseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    crtDecomposition

    ::mlir::ArrayAttr

    64-bit integer array attribute

    kskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    pkskIndex

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    1D memref of 64-bit signless integer values

    lhs

    1D memref of 64-bit signless integer values

    rhs

    1D memref of 64-bit signless integer values

    lhs

    1D tensor of 64-bit signless integer values

    rhs

    1D tensor of 64-bit signless integer values

    result

    1D tensor of 64-bit signless integer values

    result

    1D memref of 64-bit signless integer values

    lhs

    1D memref of 64-bit signless integer values

    rhs

    64-bit signless integer

    lhs

    1D tensor of 64-bit signless integer values

    rhs

    64-bit signless integer

    result

    1D tensor of 64-bit signless integer values

    result

    2D memref of 64-bit signless integer values

    lhs

    2D memref of 64-bit signless integer values

    rhs

    2D memref of 64-bit signless integer values

    lhs

    2D tensor of 64-bit signless integer values

    rhs

    2D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    result

    2D memref of 64-bit signless integer values

    lhs

    2D memref of 64-bit signless integer values

    rhs

    64-bit signless integer

    lhs

    2D tensor of 64-bit signless integer values

    rhs

    64-bit signless integer

    result

    2D tensor of 64-bit signless integer values

    result

    2D memref of 64-bit signless integer values

    lhs

    2D memref of 64-bit signless integer values

    rhs

    1D memref of 64-bit signless integer values

    lhs

    2D tensor of 64-bit signless integer values

    rhs

    1D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    inputLweDim

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    2D memref of 64-bit signless integer values

    input_ciphertext

    2D memref of 64-bit signless integer values

    lookup_table

    1D memref of 64-bit signless integer values

    inputLweDim

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    input_ciphertext

    2D tensor of 64-bit signless integer values

    lookup_table

    1D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    baseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    lwe_dim_in

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    2D memref of 64-bit signless integer values

    ciphertext

    2D memref of 64-bit signless integer values

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    baseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    lwe_dim_in

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ciphertext

    2D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    inputLweDim

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    2D memref of 64-bit signless integer values

    input_ciphertext

    2D memref of 64-bit signless integer values

    lookup_table_vector

    2D memref of 64-bit signless integer values

    inputLweDim

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    input_ciphertext

    2D tensor of 64-bit signless integer values

    lookup_table_vector

    2D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    result

    2D memref of 64-bit signless integer values

    lhs

    2D memref of 64-bit signless integer values

    rhs

    64-bit signless integer

    lhs

    2D tensor of 64-bit signless integer values

    rhs

    64-bit signless integer

    result

    2D tensor of 64-bit signless integer values

    result

    2D memref of 64-bit signless integer values

    lhs

    2D memref of 64-bit signless integer values

    rhs

    1D memref of 64-bit signless integer values

    lhs

    2D tensor of 64-bit signless integer values

    rhs

    1D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    result

    2D memref of 64-bit signless integer values

    ciphertext

    2D memref of 64-bit signless integer values

    ciphertext

    2D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    inputLweDim

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    1D memref of 64-bit signless integer values

    input_ciphertext

    1D memref of 64-bit signless integer values

    lookup_table

    1D memref of 64-bit signless integer values

    inputLweDim

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    input_ciphertext

    1D tensor of 64-bit signless integer values

    lookup_table

    1D tensor of 64-bit signless integer values

    result

    1D tensor of 64-bit signless integer values

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    outputBits

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    isSigned

    ::mlir::BoolAttr

    bool attribute

    result

    1D memref of 64-bit signless integer values

    input_lookup_table

    1D memref of 64-bit signless integer values

    polySize

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    outputBits

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    isSigned

    ::mlir::BoolAttr

    bool attribute

    input_lookup_table

    1D tensor of 64-bit signless integer values

    result

    1D tensor of 64-bit signless integer values

    crtDecomposition

    ::mlir::ArrayAttr

    64-bit integer array attribute

    crtBits

    ::mlir::ArrayAttr

    64-bit integer array attribute

    modulusProduct

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    2D memref of 64-bit signless integer values

    input_lookup_table

    1D memref of 64-bit signless integer values

    crtDecomposition

    ::mlir::ArrayAttr

    64-bit integer array attribute

    crtBits

    ::mlir::ArrayAttr

    64-bit integer array attribute

    modulusProduct

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    input_lookup_table

    1D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    mods

    ::mlir::ArrayAttr

    64-bit integer array attribute

    modsProd

    ::mlir::IntegerAttr

    64-bit signless integer attribute

    result

    1D memref of 64-bit signless integer values

    input

    64-bit signless integer

    mods

    ::mlir::ArrayAttr

    64-bit integer array attribute

    modsProd

    ::mlir::IntegerAttr

    64-bit signless integer attribute

    input

    64-bit signless integer

    result

    1D tensor of 64-bit signless integer values

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    baseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    lwe_dim_in

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    1D memref of 64-bit signless integer values

    ciphertext

    1D memref of 64-bit signless integer values

    level

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    baseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    lwe_dim_in

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ciphertext

    1D tensor of 64-bit signless integer values

    result

    1D tensor of 64-bit signless integer values

    result

    1D memref of 64-bit signless integer values

    lhs

    1D memref of 64-bit signless integer values

    rhs

    64-bit signless integer

    lhs

    1D tensor of 64-bit signless integer values

    rhs

    64-bit signless integer

    result

    1D tensor of 64-bit signless integer values

    result

    1D memref of 64-bit signless integer values

    ciphertext

    1D memref of 64-bit signless integer values

    ciphertext

    1D tensor of 64-bit signless integer values

    result

    1D tensor of 64-bit signless integer values

    bootstrapLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bootstrapBaseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    keyswitchLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    result

    2D memref of 64-bit signless integer values

    ciphertext

    2D memref of 64-bit signless integer values

    lookup_table

    2D memref of 64-bit signless integer values

    bootstrapLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    bootstrapBaseLog

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    keyswitchLevel

    ::mlir::IntegerAttr

    32-bit signless integer attribute

    ciphertext

    2D tensor of 64-bit signless integer values

    lookupTable

    2D tensor of 64-bit signless integer values

    result

    2D tensor of 64-bit signless integer values

    baseLog

    baseLog

    lwe_dim_out

    lwe_dim_out

    baseLog

    baseLog

    baseLog

    baseLog

    isSigned

    isSigned

    lwe_dim_out

    lwe_dim_out

    keyswitchBaseLog

    keyswitchBaseLog

    FHELinalg Dialect

    High Level Fully Homomorphic Encryption Linalg dialect A dialect for representation of high level linalg operations on fully homomorphic ciphertexts.

    hashtag
    Operation definition

    hashtag
    FHELinalg.add_eint_int (::mlir::concretelang::FHELinalg::AddEintIntOp)

    Returns a tensor that contains the addition of a tensor of encrypted integers and a tensor of clear integers.

    Performs an addition following the broadcasting rules between a tensor of encrypted integers and a tensor of clear integers. The width of the clear integers must be less than or equal to the width of encrypted integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEintInt, TensorBroadcastingRules

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.add_eint (::mlir::concretelang::FHELinalg::AddEintOp)

    Returns a tensor that contains the addition of two tensor of encrypted integers.

    Performs an addition following the broadcasting rules between two tensors of encrypted integers. The width of the encrypted integers must be equal.

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEint, TensorBroadcastingRules

    Interfaces: BinaryEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.apply_lookup_table (::mlir::concretelang::FHELinalg::ApplyLookupTableEintOp)

    Returns a tensor that contains the result of the lookup on a table.

    For each encrypted index, performs a lookup table of clear integers.

    The %lut argument must be a tensor with one dimension, where its dimension is 2^p where p is the width of the encrypted integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.apply_mapped_lookup_table (::mlir::concretelang::FHELinalg::ApplyMappedLookupTableEintOp)

    Returns a tensor that contains the result of the lookup on a table, using a different lookup table for each element, specified by a map.

    Performs for each encrypted index a lookup table of clear integers. Multiple lookup tables are passed, and the application of lookup tables is performed following the broadcasting rules. The precise lookup is specified by a map.

    Examples:

    Others examples: // [0,1] [1, 0] = [3,2] // [3,0] lut [[1,3,5,7], [0,2,4,6]] with [0, 1] = [7,0] // [2,3] [1, 0] = [4,7]

    // [0,1] [0, 0] = [1,3] // [3,0] lut [[1,3,5,7], [0,2,4,6]] with [1, 1] = [6,0] // [2,3] [1, 0] = [4,7]

    // [0,1] [0] = [1,3] // [3,0] lut [[1,3,5,7], [0,2,4,6]] with [1] = [6,0] // [2,3] [0] = [5,7]

    // [0,1] = [1,2] // [3,0] lut [[1,3,5,7], [0,2,4,6]] with [0, 1] = [7,0] // [2,3] = [5,6]

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.apply_multi_lookup_table (::mlir::concretelang::FHELinalg::ApplyMultiLookupTableEintOp)

    Returns a tensor that contains the result of the lookup on a table, using a different lookup table for each element.

    Performs for each encrypted index a lookup table of clear integers. Multiple lookup tables are passed, and the application of lookup tables is performed following the broadcasting rules.

    The %luts argument should be a tensor with M dimension, where the first M-1 dimensions are broadcastable with the N dimensions of the encrypted tensor, and where the last dimension dimension is equal to 2^p where p is the width of the encrypted integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.concat (::mlir::concretelang::FHELinalg::ConcatOp)

    Concatenates a sequence of tensors along an existing axis.

    Concatenates several tensors along a given axis.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.conv2d (::mlir::concretelang::FHELinalg::Conv2dOp)

    Returns the 2D convolution of a tensor in the form NCHW with weights in the form FCHW

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.dot_eint_int (::mlir::concretelang::FHELinalg::Dot)

    Returns the encrypted dot product between a vector of encrypted integers and a vector of clean integers.

    Performs a dot product between a vector of encrypted integers and a vector of clear integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.dot_eint_eint (::mlir::concretelang::FHELinalg::DotEint)

    Returns the encrypted dot product between two vectors of encrypted integers.

    Performs a dot product between two vectors of encrypted integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.from_element (::mlir::concretelang::FHELinalg::FromElementOp)

    Creates a tensor with a single element.

    Creates a tensor with a single element.

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.lsb (::mlir::concretelang::FHELinalg::LsbEintOp)

    Extract the lowest significant bit at a given precision.

    This operation extracts the lsb of a ciphertext tensor in a specific precision.

    Extracting only 1 bit:

    Traits: AlwaysSpeculatableImplTrait, TensorUnaryEint

    Interfaces: ConditionallySpeculatable, ConstantNoise, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.matmul_eint_eint (::mlir::concretelang::FHELinalg::MatMulEintEintOp)

    Returns a tensor that contains the result of the matrix multiplication of a matrix of encrypted integers and a second matrix of encrypted integers.

    Performs a matrix multiplication of a matrix of encrypted integers and a second matrix of encrypted integers.

    The behavior depends on the arguments in the following way:

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEint

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.matmul_eint_int (::mlir::concretelang::FHELinalg::MatMulEintIntOp)

    Returns a tensor that contains the result of the matrix multiplication of a matrix of encrypted integers and a matrix of clear integers.

    Performs a matrix multiplication of a matrix of encrypted integers and a matrix of clear integers. The width of the clear integers must be less than or equal to the width of encrypted integers.

    The behavior depends on the arguments in the following way:

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEintInt

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.matmul_int_eint (::mlir::concretelang::FHELinalg::MatMulIntEintOp)

    Returns a tensor that contains the result of the matrix multiplication of a matrix of clear integers and a matrix of encrypted integers.

    Performs a matrix multiplication of a matrix of clear integers and a matrix of encrypted integers. The width of the clear integers must be less than or equal to the width of encrypted integers.

    The behavior depends on the arguments in the following way:

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryIntEint

    Interfaces: Binary, BinaryIntEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.maxpool2d (::mlir::concretelang::FHELinalg::Maxpool2dOp)

    Returns the 2D maxpool of a tensor in the form NCHW

    Interfaces: UnaryEint

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.mul_eint_int (::mlir::concretelang::FHELinalg::MulEintIntOp)

    Returns a tensor that contains the multiplication of a tensor of encrypted integers and a tensor of clear integers.

    Performs a multiplication following the broadcasting rules between a tensor of encrypted integers and a tensor of clear integers. The width of the clear integers must be less than or equal to the width of encrypted integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEintInt, TensorBroadcastingRules

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.mul_eint (::mlir::concretelang::FHELinalg::MulEintOp)

    Returns a tensor that contains the multiplication of two tensor of encrypted integers.

    Performs an addition following the broadcasting rules between two tensors of encrypted integers. The width of the encrypted integers must be equal.

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEint, TensorBroadcastingRules

    Interfaces: BinaryEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.neg_eint (::mlir::concretelang::FHELinalg::NegEintOp)

    Returns a tensor that contains the negation of a tensor of encrypted integers.

    Performs a negation to a tensor of encrypted integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorUnaryEint

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.reinterpret_precision (::mlir::concretelang::FHELinalg::ReinterpretPrecisionEintOp)

    Reinterpret the ciphertext tensor with a different precision.

    It's a reinterpretation cast which changes only the precision. On CRT represention, it does nothing. On Native representation, it moves the message/noise further forward, effectively changing the precision. Changing to - a bigger precision is safe, as the crypto-parameters are chosen such that only zeros will come from the noise part. This is equivalent to a shift left for the value - a smaller precision is only safe if you clear the lowest message bits first. If not, you can assume small errors with high probability and frequent bigger errors, which can be contained to small errors using margins. This is equivalent to a shift right for the value

    Example:

    Traits: AlwaysSpeculatableImplTrait, TensorUnaryEint

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.round (::mlir::concretelang::FHELinalg::RoundOp)

    Rounds a tensor of ciphertexts into a smaller precision.

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEintInt, TensorBroadcastingRules

    Interfaces: Binary, BinaryEintInt, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.sub_eint (::mlir::concretelang::FHELinalg::SubEintOp)

    Returns a tensor that contains the subtraction of two tensor of encrypted integers.

    Performs an subtraction following the broadcasting rules between two tensors of encrypted integers. The width of the encrypted integers must be equal.

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryEint, TensorBroadcastingRules

    Interfaces: BinaryEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.sub_int_eint (::mlir::concretelang::FHELinalg::SubIntEintOp)

    Returns a tensor that contains the subtraction of a tensor of clear integers and a tensor of encrypted integers.

    Performs a subtraction following the broadcasting rules between a tensor of clear integers and a tensor of encrypted integers. The width of the clear integers must be less than or equal to the width of encrypted integers.

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorBinaryIntEint, TensorBroadcastingRules

    Interfaces: Binary, BinaryIntEint, ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.sum (::mlir::concretelang::FHELinalg::SumOp)

    Returns the sum of elements of a tensor of encrypted integers along specified axes.

    Attributes:

    • keep_dims: boolean = false whether to keep the rank of the tensor after the sum operation if true, reduced axes will have the size of 1

    • axes: I64ArrayAttr = [] list of dimension to perform the sum along think of it as the dimensions to reduce (see examples below to get an intuition)

    Examples:

    Traits: AlwaysSpeculatableImplTrait, TensorUnaryEint

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface)

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.to_signed (::mlir::concretelang::FHELinalg::ToSignedOp)

    Cast an unsigned integer tensor to a signed one

    Cast an unsigned integer tensor to a signed one. The result must have the same width and the same shape as the input.

    The behavior is undefined on overflow/underflow.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.to_unsigned (::mlir::concretelang::FHELinalg::ToUnsignedOp)

    Cast a signed integer tensor to an unsigned one

    Cast a signed integer tensor to an unsigned one. The result must have the same width and the same shape as the input.

    The behavior is undefined on overflow/underflow.

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    hashtag
    FHELinalg.transpose (::mlir::concretelang::FHELinalg::TransposeOp)

    Returns a tensor that contains the transposition of the input tensor.

    Performs a transpose operation on an N-dimensional tensor.

    Attributes:

    • axes: I64ArrayAttr = [] list of dimension to perform the transposition contains a permutation of [0,1,..,N-1] where N is the number of axes think of it as a way to rearrange axes (see the example below)

    Examples:

    Traits: AlwaysSpeculatableImplTrait

    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint

    Effects: MemoryEffects::Effect{}

    hashtag
    Attributes:

    Attribute
    MLIR Type
    Description

    hashtag
    Operands:

    Operand
    Description

    hashtag
    Results:

    Result
    Description

    ::mlir::IntegerAttr

    64-bit signless integer attribute

    lhs

    rhs

    «unnamed»

    lhs

    rhs

    «unnamed»

    t

    lut

    «unnamed»

    t

    luts

    map

    «unnamed»

    t

    luts

    «unnamed»

    axis

    ::mlir::IntegerAttr

    64-bit signless integer attribute

    ins

    out

    padding

    ::mlir::DenseIntElementsAttr

    64-bit signless integer elements attribute

    strides

    ::mlir::DenseIntElementsAttr

    64-bit signless integer elements attribute

    dilations

    ::mlir::DenseIntElementsAttr

    64-bit signless integer elements attribute

    input

    weight

    bias

    «unnamed»

    lhs

    rhs

    out

    lhs

    rhs

    out

    «unnamed»

    any type

    «unnamed»

    input

    output

    lhs

    rhs

    «unnamed»

    lhs

    rhs

    «unnamed»

    lhs

    rhs

    «unnamed»

    kernel_shape

    ::mlir::DenseIntElementsAttr

    64-bit signless integer elements attribute

    strides

    ::mlir::DenseIntElementsAttr

    64-bit signless integer elements attribute

    dilations

    ::mlir::DenseIntElementsAttr

    64-bit signless integer elements attribute

    input

    «unnamed»

    lhs

    rhs

    «unnamed»

    lhs

    rhs

    «unnamed»

    input

    «unnamed»

    input

    output

    lhs

    rhs

    «unnamed»

    lhs

    rhs

    «unnamed»

    lhs

    rhs

    «unnamed»

    axes

    ::mlir::ArrayAttr

    64-bit integer array attribute

    keep_dims

    ::mlir::BoolAttr

    bool attribute

    tensor

    out

    input

    output

    input

    output

    axes

    ::mlir::ArrayAttr

    64-bit integer array attribute

    tensor

    any type

    «unnamed»

    any type

    group

    // Returns the term-by-term addition of `%a0` with `%a1`
    "FHELinalg.add_eint_int"(%a0, %a1) : (tensor<4x!FHE.eint<4>>, tensor<4xi5>) -> tensor<4x!FHE.eint<4>>
    
    // Returns the term-by-term addition of `%a0` with `%a1`, where dimensions equal to one are stretched.
    "FHELinalg.add_eint_int"(%a0, %a1) : (tensor<4x1x4x!FHE.eint<4>>, tensor<1x4x4xi5>) -> tensor<4x4x4x!FHE.eint<4>>
    
    // Returns the addition of a 3x3 matrix of encrypted integers and a 3x1 matrix (a column) of integers.
    //
    // [1,2,3]   [1]   [2,3,4]
    // [4,5,6] + [2] = [6,7,8]
    // [7,8,9]   [3]   [10,11,12]
    //
    // The dimension #1 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.add_eint_int"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<3x1xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the addition of a 3x3 matrix of encrypted integers and a 1x3 matrix (a line) of integers.
    //
    // [1,2,3]             [2,4,6]
    // [4,5,6] + [1,2,3] = [5,7,9]
    // [7,8,9]             [8,10,12]
    //
    // The dimension #2 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.add_eint_int"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<1x3xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Same behavior as the previous one, but as the dimension #2 is missing of operand #2.
    "FHELinalg.add_eint_int(%a0, %a1)" : (tensor<3x4x!FHE.eint<4>>, tensor<3xi5>) -> tensor<4x4x4x!FHE.eint<4>>
    
    // Returns the term-by-term addition of `%a0` with `%a1`
    "FHELinalg.add_eint"(%a0, %a1) : (tensor<4x!FHE.eint<4>>, tensor<4x!FHE.eint<4>>) -> tensor<4x!FHE.eint<4>>
    
    // Returns the term-by-term addition of `%a0` with `%a1`, where dimensions equal to one are stretched.
    "FHELinalg.add_eint"(%a0, %a1) : (tensor<4x1x4x!FHE.eint<4>>, tensor<1x4x4x!FHE.eint<4>>) -> tensor<4x4x4x!FHE.eint<4>>
    
    // Returns the addition of a 3x3 matrix of encrypted integers and a 3x1 matrix (a column) of encrypted integers.
    //
    // [1,2,3]   [1]   [2,3,4]
    // [4,5,6] + [2] = [6,7,8]
    // [7,8,9]   [3]   [10,11,12]
    //
    // The dimension #1 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.add_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<3x1x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the addition of a 3x3 matrix of encrypted integers and a 1x3 matrix (a line) of encrypted integers.
    //
    // [1,2,3]             [2,4,6]
    // [4,5,6] + [1,2,3] = [5,7,9]
    // [7,8,9]             [8,10,12]
    //
    // The dimension #2 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.add_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<1x3x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    
    // Same behavior as the previous one, but as the dimension #2 of operand #2 is missing.
    "FHELinalg.add_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<3x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    // The result of this operation, is a tensor that contains the result of a lookup table.
    // i.e. %res[i, ..., k] = %lut[%t[i, ..., k]]
    %res = FHELinalg.apply_lookup_table(%t, %lut): tensor<DNx...xD1x!FHE.eint<$p>>, tensor<D2^$pxi64> -> tensor<DNx...xD1x!FHE.eint<$p>>
    
    // Returns the lookup of 3x3 matrix of encrypted indices of with 2 on a table of size 4=2² of clear integers.
    //
    // [0,1,2]                 [1,3,5]
    // [3,0,1] lut [1,3,5,7] = [7,1,3]
    // [2,3,0]                 [5,7,1]
    "FHELinalg.apply_lookup_table"(%t, %lut) : (tensor<3x3x!FHE.eint<2>>, tensor<4xi64>) -> tensor<3x3x!FHE.eint<3>>
    // The result of this operation, is a tensor that contains the result of the lookup on different tables.
    // i.e. %res[i, ..., k] = %luts[ %map[i, ..., k] ][ %t[i, ..., k] ]
    %res = FHELinalg.apply_mapped_lookup_table(%t, %luts, %map): tensor<DNx...xD1x!FHE.eint<$p>>, tensor<DM x ^$p>, tensor<DNx...xD1xindex> -> tensor<DNx...xD1x!FHE.eint<$p>>
    
    // Returns the lookup of 3x2 matrix of encrypted indices of width 2 on a vector of 2 tables of size 4=2^2 of clear integers.
    //
    // [0,1]                                 [0, 1] = [1,2]
    // [3,0] lut [[1,3,5,7], [0,2,4,6]] with [0, 1] = [7,0]
    // [2,3]                                 [0, 1] = [5,6]
    "FHELinalg.apply_mapped_lookup_table"(%t, %luts, %map) : (tensor<3x2x!FHE.eint<2>>, tensor<2x4xi64>, tensor<3x2xindex>) -> tensor<3x2x!FHE.eint<3>>
    // The result of this operation, is a tensor that contains the result of the lookup on different tables.
    // i.e. %res[i, ..., k] = [ %luts[i][%t[i]], ..., %luts[k][%t[k]] ]
    %res = FHELinalg.apply_multi_lookup_table(%t, %lut): tensor<DNx...xD1x!FHE.eint<$p>>, tensor<DMx...xD1xD2^$pxi64> -> tensor<DNx...xD1x!FHE.eint<$p>>
    
    // Returns the lookup of 3x2 matrix of encrypted indices of width 2 on a vector of 2 tables of size 4=2² of clear integers.
    // The tables are broadcasted along the first dimension of the tensor.
    //
    // [0,1]                            = [1,2]
    // [3,0] lut [[1,3,5,7], [0,2,4,6]] = [7,0]
    // [2,3]                            = [5,6]
    "FHELinalg.apply_multi_lookup_table"(%t, %luts) : (tensor<3x2x!FHE.eint<2>>, tensor<2x4xi64>) -> tensor<3x2x!FHE.eint<3>>
    
    // Returns the lookup of a vector of 3 encrypted indices of width 2 on a vector of 3 tables of size 4=2² of clear integers.
    //
    // [3,0,1] lut [[1,3,5,7], [0,2,4,6], [1,2,3,4]] = [7,0,2]
    "FHELinalg.apply_multi_lookup_table"(%t, %luts) : (tensor<3x!FHE.eint<2>>, tensor<3x4xi64>) -> tensor<3x!FHE.eint<3>>
    "FHELinalg.concat"(%a, %b) { axis = 0 } : (tensor<3x3x!FHE.eint<4>>, tensor<3x3x!FHE.eint<4>>) -> tensor<6x3x!FHE.eint<4>>
    //
    //        ( [1,2,3]  [1,2,3] )   [1,2,3]
    // concat ( [4,5,6], [4,5,6] ) = [4,5,6]
    //        ( [7,8,9]  [7,8,9] )   [7,8,9]
    //                               [1,2,3]
    //                               [4,5,6]
    //                               [7,8,9]
    //
    "FHELinalg.concat"(%a, %b) { axis = 1 } : (tensor<3x3x!FHE.eint<4>>, tensor<3x3x!FHE.eint<4>>) -> tensor<3x6x!FHE.eint<4>>
    //
    //        ( [1,2,3]  [1,2,3] )   [1,2,3,1,2,3]
    // concat ( [4,5,6], [4,5,6] ) = [4,5,6,4,5,6]
    //        ( [7,8,9]  [7,8,9] )   [7,8,9,7,8,9]
    //
    // Returns the dot product of `%a0` with `%a1`
    "FHELinalg.dot_eint_int"(%a0, %a1) : (tensor<4x!FHE.eint<4>>, tensor<4xi5>) -> !FHE.eint<4>
    
    // Returns the dot product of `%a0` with `%a1`
    "FHELinalg.dot_eint_eint"(%a0, %a1) : (tensor<4x!FHE.eint<4>>, tensor<4x!FHE.eint<4>>) -> !FHE.eint<4>
    
    "FHELinalg.from_element"(%a) : (Type) -> tensor<1xType>
     // ok
     %lsb = "FHE.lsb"(%a): (tensor<1x!FHE.eint<4>)) -> (tensor<1x!FHE.eint<1>>)
    
    If you need to clear the lsb of the original ciphertext, you should extract to the same precision as the ciphertext.
    If you need to extract several bits, you can extract sequentially using explicit bitwidth change and bit clearing.
    
    Example:
    ```mlir
     // ok
     %a_lsb = "FHELinalg.lsb"(%a): (tensor<1x!FHE.eint<4>)) -> (tensor<1x!FHE.eint<4>))
     %a_lsb_cleared = "FHELinalg.sub_eint"(%a, %lsb) : (tensor<1x!FHE.eint<4>), tensor<1x!FHE.eint<4>)) -> (tensor<1x!FHE.eint<4>))
     %b = %a : tensor<1x!FHE.eint<3>>
     // now you can extract the next lsb from %b
     %b_lsb = "FHELinalg.lsb"(%b): (tensor<1x!FHE.eint<3>>) -> (tensor<1x!FHE.eint<3>>)
     // later if you need %b_lsb at the original position
     %b_lsb_as_in_a = %b_lsb : tensor<1x!FHE.eint<3>>
    - If both arguments are 2-D,
      they are multiplied like conventional matrices.
    
      e.g.,
    
      arg0: tensor<MxN> = [...]
      arg1: tensor<NxP> = [...]
    
      result: tensor<MxP> = [...]
    
    - If the first argument is a vector (1-D),
      it is treated as a matrix with a single row and standard matrix multiplication is performed.
    
      After standard matrix multiplication,
      the first dimension is removed from the result.
    
      e.g.,
    
      arg0: tensor<3> = [x, y, z]
      arg1: tensor<3xM> = [
          [_, _, ..., _, _],
          [_, _, ..., _, _],
          [_, _, ..., _, _],
      ]
    
      is treated as
    
      arg0: tensor<1x3> = [
          [x, y, z]
      ]
      arg1: tensor<3xM> = [
          [_, _, ..., _, _],
          [_, _, ..., _, _],
          [_, _, ..., _, _],
      ]
    
      and matrix multiplication is performed with the following form (1x3 @ 3xM -> 1xM)
    
      result: tensor<1xM> = [[_, _, ..., _, _]]
    
      finally, the first dimension is removed by definition so the result has the following form
    
      result: tensor<M>  = [_, _, ..., _, _]
    
    - If the second argument is 1-D,
      it is treated as a matrix with a single column and standard matrix multiplication is performed.
    
      After standard matrix multiplication,
      the last dimension is removed from the result.
    
      e.g.,
    
      arg0: tensor<Mx3> = [
          [_, _, _],
          [_, _, _],
          ...,
          [_, _, _],
          [_, _, _],
      ]
      arg1: tensor<3> = [x, y, z]
    
      is treated as
    
      arg0: tensor<Mx3> = [
          [_, _, _],
          [_, _, _],
          ...,
          [_, _, _],
          [_, _, _],
      ]
      arg1: tensor<3x1> = [
        [x],
        [y],
        [z],
      ]
    
      and matrix multiplication is performed with the following form (Mx3 @ 3x1 -> Mx1)
    
      result: tensor<Mx1> = [
        [_],
        [_],
          ...,
        [_],
        [_],
      ]
    
      finally, the last dimension is removed by definition so the result has the following form
    
      result: tensor<M> = [_, _, _]
    
    - If either argument is N-D where N > 2,
      the operation is treated as a collection of matrices residing in the last two indices and broadcasted accordingly.
    
      arg0: tensor<Kx1MxN> = [...]
      arg1: tensor<LxNxP> = [...]
    
      result: tensor<KxLxMxP> = [...]
    "FHELinalg.matmul_eint_eint(%a, %b) : (tensor<MxNx!FHE.eint<p>>, tensor<NxPx!FHE.eint<p>'>) -> tensor<MxPx!FHE.eint<p>>"
    "FHELinalg.matmul_eint_eint(%a, %b) : (tensor<KxLxMxNx!FHE.eint<p>>, tensor<KxLxNxPx!FHE.eint<p>'>) -> tensor<KxLxMxPx!FHE.eint<p>>"
    "FHELinalg.matmul_eint_eint(%a, %b) : (tensor<MxNx!FHE.eint<p>>, tensor<Nx!FHE.eint<p>'>) -> tensor<Mx!FHE.eint<p>>"
    "FHELinalg.matmul_eint_eint(%a, %b) : (tensor<Nx!FHE.eint<p>>, tensor<NxPx!FHE.eint<p>'>) -> tensor<Px!FHE.eint<p>>"
    // Returns the matrix multiplication of a 3x2 matrix of encrypted integers and a 2x3 matrix of integers.
    //         [ 1, 2, 3]
    //         [ 2, 3, 4]
    //       *
    // [1,2]   [ 5, 8,11]
    // [3,4] = [11,18,25]
    // [5,6]   [17,28,39]
    //
    "FHELinalg.matmul_eint_eint"(%a, %b) : (tensor<3x2x!FHE.eint<6>>, tensor<2x3x!FHE.eint<6>>) -> tensor<3x3x!FHE.eint<12>>
    - If both arguments are 2-D,
      they are multiplied like conventional matrices.
    
      e.g.,
    
      arg0: tensor<MxN> = [...]
      arg1: tensor<NxP> = [...]
    
      result: tensor<MxP> = [...]
    
    - If the first argument is a vector (1-D),
      it is treated as a matrix with a single row and standard matrix multiplication is performed.
    
      After standard matrix multiplication,
      the first dimension is removed from the result.
    
      e.g.,
    
      arg0: tensor<3> = [x, y, z]
      arg1: tensor<3xM> = [
          [_, _, ..., _, _],
          [_, _, ..., _, _],
          [_, _, ..., _, _],
      ]
    
      is treated as
    
      arg0: tensor<1x3> = [
          [x, y, z]
      ]
      arg1: tensor<3xM> = [
          [_, _, ..., _, _],
          [_, _, ..., _, _],
          [_, _, ..., _, _],
      ]
    
      and matrix multiplication is performed with the following form (1x3 @ 3xM -> 1xM)
    
      result: tensor<1xM> = [[_, _, ..., _, _]]
    
      finally, the first dimension is removed by definition so the result has the following form
    
      result: tensor<M>  = [_, _, ..., _, _]
    
    - If the second argument is 1-D,
      it is treated as a matrix with a single column and standard matrix multiplication is performed.
    
      After standard matrix multiplication,
      the last dimension is removed from the result.
    
      e.g.,
    
      arg0: tensor<Mx3> = [
          [_, _, _],
          [_, _, _],
          ...,
          [_, _, _],
          [_, _, _],
      ]
      arg1: tensor<3> = [x, y, z]
    
      is treated as
    
      arg0: tensor<Mx3> = [
          [_, _, _],
          [_, _, _],
          ...,
          [_, _, _],
          [_, _, _],
      ]
      arg1: tensor<3x1> = [
        [x],
        [y],
        [z],
      ]
    
      and matrix multiplication is performed with the following form (Mx3 @ 3x1 -> Mx1)
    
      result: tensor<Mx1> = [
        [_],
        [_],
          ...,
        [_],
        [_],
      ]
    
      finally, the last dimension is removed by definition so the result has the following form
    
      result: tensor<M> = [_, _, _]
    
    - If either argument is N-D where N > 2,
      the operation is treated as a collection of matrices residing in the last two indices and broadcasted accordingly.
    
      arg0: tensor<Kx1MxN> = [...]
      arg1: tensor<LxNxP> = [...]
    
      result: tensor<KxLxMxP> = [...]
    "FHELinalg.matmul_eint_int(%a, %b) : (tensor<MxNx!FHE.eint<p>>, tensor<NxPxip'>) -> tensor<MxPx!FHE.eint<p>>"
    "FHELinalg.matmul_eint_int(%a, %b) : (tensor<KxLxMxNx!FHE.eint<p>>, tensor<KxLxNxPxip'>) -> tensor<KxLxMxPx!FHE.eint<p>>"
    "FHELinalg.matmul_eint_int(%a, %b) : (tensor<MxNx!FHE.eint<p>>, tensor<Nxip'>) -> tensor<Mx!FHE.eint<p>>"
    "FHELinalg.matmul_eint_int(%a, %b) : (tensor<Nx!FHE.eint<p>>, tensor<NxPxip'>) -> tensor<Px!FHE.eint<p>>"
    // Returns the matrix multiplication of a 3x2 matrix of encrypted integers and a 2x3 matrix of integers.
    //         [ 1, 2, 3]
    //         [ 2, 3, 4]
    //       *
    // [1,2]   [ 5, 8,11]
    // [3,4] = [11,18,25]
    // [5,6]   [17,28,39]
    //
    "FHELinalg.matmul_eint_int"(%a, %b) : (tensor<3x2x!FHE.eint<6>>, tensor<2x3xi7>) -> tensor<3x3x!FHE.eint<6>>
    - If both arguments are 2-D,
      they are multiplied like conventional matrices.
    
      e.g.,
    
      arg0: tensor<MxN> = [...]
      arg1: tensor<NxP> = [...]
    
      result: tensor<MxP> = [...]
    
    - If the first argument is a vector (1-D),
      it is treated as a matrix with a single row and standard matrix multiplication is performed.
    
      After standard matrix multiplication,
      the first dimension is removed from the result.
    
      e.g.,
    
      arg0: tensor<3> = [x, y, z]
      arg1: tensor<3xM> = [
          [_, _, ..., _, _],
          [_, _, ..., _, _],
          [_, _, ..., _, _],
      ]
    
      is treated as
    
      arg0: tensor<1x3> = [
          [x, y, z]
      ]
      arg1: tensor<3xM> = [
          [_, _, ..., _, _],
          [_, _, ..., _, _],
          [_, _, ..., _, _],
      ]
    
      and matrix multiplication is performed with the following form (1x3 @ 3xM -> 1xM)
    
      result: tensor<1xM> = [[_, _, ..., _, _]]
    
      finally, the first dimension is removed by definition so the result has the following form
    
      result: tensor<M>  = [_, _, ..., _, _]
    
    - If the second argument is 1-D,
      it is treated as a matrix with a single column and standard matrix multiplication is performed.
    
      After standard matrix multiplication,
      the last dimension is removed from the result.
    
      e.g.,
    
      arg0: tensor<Mx3> = [
          [_, _, _],
          [_, _, _],
          ...,
          [_, _, _],
          [_, _, _],
      ]
      arg1: tensor<3> = [x, y, z]
    
      is treated as
    
      arg0: tensor<Mx3> = [
          [_, _, _],
          [_, _, _],
          ...,
          [_, _, _],
          [_, _, _],
      ]
      arg1: tensor<3x1> = [
        [x],
        [y],
        [z],
      ]
    
      and matrix multiplication is performed with the following form (Mx3 @ 3x1 -> Mx1)
    
      result: tensor<Mx1> = [
        [_],
        [_],
          ...,
        [_],
        [_],
      ]
    
      finally, the last dimension is removed by definition so the result has the following form
    
      result: tensor<M> = [_, _, _]
    
    - If either argument is N-D where N > 2,
      the operation is treated as a collection of matrices residing in the last two indices and broadcasted accordingly.
    
      arg0: tensor<Kx1MxN> = [...]
      arg1: tensor<LxNxP> = [...]
    
      result: tensor<KxLxMxP> = [...]
    "FHELinalg.matmul_int_eint(%a, %b) : (tensor<MxNxip'>, tensor<NxPxFHE.eint<p>>) -> tensor<MxPx!FHE.eint<p>>"
    "FHELinalg.matmul_int_eint(%a, %b) : (tensor<KxLxMxNxip'>, tensor<KxLxNxPxFHE.eint<p>>) -> tensor<KxLxMxPx!FHE.eint<p>>"
    "FHELinalg.matmul_int_eint(%a, %b) : (tensor<MxNxip'>, tensor<NxFHE.eint<p>>) -> tensor<Mx!FHE.eint<p>>"
    "FHELinalg.matmul_int_eint(%a, %b) : (tensor<Nxip'>, tensor<NxPxFHE.eint<p>>) -> tensor<Px!FHE.eint<p>>"
    // Returns the matrix multiplication of a 3x2 matrix of clear integers and a 2x3 matrix of encrypted integers.
    //         [ 1, 2, 3]
    //         [ 2, 3, 4]
    //       *
    // [1,2]   [ 5, 8,11]
    // [3,4] = [11,18,25]
    // [5,6]   [17,28,39]
    //
    "FHELinalg.matmul_int_eint"(%a, %b) : (tensor<3x2xi7>, tensor<2x3x!FHE.eint<6>>) -> tensor<3x3x!FHE.eint<6>>
    // Returns the term-by-term multiplication of `%a0` with `%a1`
    "FHELinalg.mul_eint_int"(%a0, %a1) : (tensor<4x!FHE.eint<4>>, tensor<4xi5>) -> tensor<4x!FHE.eint<4>>
    
    // Returns the term-by-term multiplication of `%a0` with `%a1`, where dimensions equal to one are stretched.
    "FHELinalg.mul_eint_int"(%a0, %a1) : (tensor<4x1x4x!FHE.eint<4>>, tensor<1x4x4xi5>) -> tensor<4x4x4x!FHE.eint<4>>
    
    // Returns the multiplication of a 3x3 matrix of encrypted integers and a 3x1 matrix (a column) of integers.
    //
    // [1,2,3]   [1]   [1,2,3]
    // [4,5,6] * [2] = [8,10,18]
    // [7,8,9]   [3]   [21,24,27]
    //
    // The dimension #1 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.mul_eint_int"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<3x1xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the multiplication of a 3x3 matrix of encrypted integers and a 1x3 matrix (a line) of integers.
    //
    // [1,2,3]             [2,4,6]
    // [4,5,6] * [1,2,3] = [5,7,9]
    // [7,8,9]             [8,10,12]
    //
    // The dimension #2 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.mul_eint_int"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<1x3xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Same behavior as the previous one, but as the dimension #2 is missing of operand #2.
    "FHELinalg.mul_eint_int"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<3xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the term-by-term multiplication of `%a0` with `%a1`
    "FHELinalg.mul_eint"(%a0, %a1) : (tensor<4x!FHE.eint<8>>, tensor<4x!FHE.eint<8>>) -> tensor<4x!FHE.eint<8>>
    
    // Returns the term-by-term multiplication of `%a0` with `%a1`, where dimensions equal to one are stretched.
    "FHELinalg.mul_eint"(%a0, %a1) : (tensor<4x1x4x!FHE.eint<8>>, tensor<1x4x4x!FHE.eint<8>>) -> tensor<4x4x4x!FHE.eint<8>>
    
    // Returns the multiplication of a 3x3 matrix of encrypted integers and a 3x1 matrix (a column) of encrypted integers.
    //
    // [1,2,3]   [1]   [1,2,3]
    // [4,5,6] * [2] = [8,10,12]
    // [7,8,9]   [3]   [21,24,27]
    //
    // The dimension #1 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.mul_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<8>>, tensor<3x1x!FHE.eint<8>>) -> tensor<3x3x!FHE.eint<8>>
    
    // Returns the multiplication of a 3x3 matrix of encrypted integers and a 1x3 matrix (a line) of encrypted integers.
    //
    // [1,2,3]             [1,4,9]
    // [4,5,6] * [1,2,3] = [4,10,18]
    // [7,8,9]             [7,16,27]
    //
    // The dimension #2 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.mul_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<8>>, tensor<1x3x!FHE.eint<8>>) -> tensor<3x3x!FHE.eint<8>>
    
    // Same behavior as the previous one, but as the dimension #2 of operand #2 is missing.
    "FHELinalg.mul_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<8>>, tensor<3x!FHE.eint<8>>) -> tensor<3x3x!FHE.eint<8>>
    // Returns the term-by-term negation of `%a0`
    "FHELinalg.neg_eint"(%a0) : (tensor<3x3x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    //
    //        ( [1,2,3] )   [31,30,29]
    // negate ( [4,5,6] ) = [28,27,26]
    //        ( [7,8,9] )   [25,24,23]
    //
    // The negation is computed as `2**(p+1) - a` where p=4 here.
     // assuming a is encoded as 4bits but can be stored in 2bits
     // we can obtain a to a smaller 2 bits precision
     %shifted_a = "FHELinalg.mul_eint_intlsb"(%a, %c_4): (tensor<1x!FHE.eint<4>>) -> (tensor<1x!FHE.eint<2>>)
     %a_small_precision = "FHELinalg.reinterpret_precision"(%shifted_a, %lsb) : (tensor<1x!FHE.eint<4>>) -> (tensor<1x!FHE.eint<2>>)
      Assuming a ciphertext whose message is implemented over `p` bits, this
      operation rounds it to fit to `q` bits where `p>q`.
    
      Example:
      ```mlir
      // ok
      "FHELinalg.round"(%a): (tensor<3x!FHE.eint<6>>) -> (tensor<3x!FHE.eint<5>>)
      "FHELinalg.round"(%a): (tensor<3x!FHE.eint<5>>) -> (tensor<3x!FHE.eint<3>>)
      "FHELinalg.round"(%a): (tensor<3x!FHE.eint<3>>) -> (tensor<3x!FHE.eint<2>>)
      "FHELinalg.round"(%a): (tensor<3x!FHE.esint<3>>) -> (tensor<3x!FHE.esint<2>>)
    
      // error
      "FHELinalg.round"(%a): (tensor<3x!FHE.eint<6>>) -> (tensor<3x!FHE.eint<6>>)
      "FHELinalg.round"(%a): (tensor<3x!FHE.eint<4>>) -> (tensor<3x!FHE.eint<5>>)
      "FHELinalg.round"(%a): (tensor<3x!FHE.eint<4>>) -> (tensor<3x!FHE.esint<2>>)
    
    Traits: AlwaysSpeculatableImplTrait, TensorUnaryEint
    
    Interfaces: ConditionallySpeculatable, NoMemoryEffect (MemoryEffectOpInterface), UnaryEint
    
    Effects: MemoryEffects::Effect{}
    
    #### Operands:
    
    | Operand | Description |
    | :-----: | ----------- |
    | `input` | 
    
    #### Results:
    
    | Result | Description |
    | :----: | ----------- |
    | `output` | 
    
    ### `FHELinalg.sub_eint_int` (::mlir::concretelang::FHELinalg::SubEintIntOp)
    
    Returns a tensor that contains the subtraction of a tensor of clear integers from a tensor of encrypted integers.
    
    Performs a subtraction following the broadcasting rules between a tensor of clear integers from a tensor of encrypted integers.
    The width of the clear integers must be less than or equal to the width of encrypted integers.
    
    Examples:
    ```mlir
    // Returns the term-by-term subtraction of `%a0` with `%a1`
    "FHELinalg.sub_eint_int"(%a0, %a1) : (tensor<4x!FHE.eint<4>>, tensor<4xi5>) -> tensor<4x!FHE.eint<4>>
    
    // Returns the term-by-term subtraction of `%a0` with `%a1`, where dimensions equal to one are stretched.
    "FHELinalg.sub_eint_int"(%a0, %a1) : (tensor<1x4x4x!FHE.eint<4>>, tensor<4x1x4xi5>) -> tensor<4x4x4x!FHE.eint<4>>
    
    // Returns the subtraction of a 3x3 matrix of integers and a 3x1 matrix (a column) of encrypted integers.
    //
    // [1,2,3]   [1]   [0,2,3]
    // [4,5,6] - [2] = [2,3,4]
    // [7,8,9]   [3]   [4,5,6]
    //
    // The dimension #1 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.sub_eint_int"(%a0, %a1) : (tensor<3x1x!FHE.eint<4>>, tensor<3x3xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the subtraction of a 3x3 matrix of integers and a 1x3 matrix (a line) of encrypted integers.
    //
    // [1,2,3]             [0,0,0]
    // [4,5,6] - [1,2,3] = [3,3,3]
    // [7,8,9]             [6,6,6]
    //
    // The dimension #2 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.sub_eint_int"(%a0, %a1) : (tensor<1x3x!FHE.eint<4>>, tensor<3x3xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Same behavior as the previous one, but as the dimension #2 is missing of operand #2.
    "FHELinalg.sub_eint_int"(%a0, %a1) : (tensor<3x!FHE.eint<4>>, tensor<3x3xi5>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the term-by-term subtraction of `%a0` with `%a1`
    "FHELinalg.sub_eint"(%a0, %a1) : (tensor<4x!FHE.eint<4>>, tensor<4x!FHE.eint<4>>) -> tensor<4x!FHE.eint<4>>
    
    // Returns the term-by-term subtraction of `%a0` with `%a1`, where dimensions equal to one are stretched.
    "FHELinalg.sub_eint"(%a0, %a1) : (tensor<4x1x4x!FHE.eint<4>>, tensor<1x4x4x!FHE.eint<4>>) -> tensor<4x4x4x!FHE.eint<4>>
    
    // Returns the subtraction of a 3x3 matrix of integers and a 3x1 matrix (a column) of encrypted integers.
    //
    // [1,2,3]   [1]   [0,2,3]
    // [4,5,6] - [2] = [2,3,4]
    // [7,8,9]   [3]   [4,5,6]
    //
    // The dimension #1 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.sub_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<3x1x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the subtraction of a 3x3 matrix of integers and a 1x3 matrix (a line) of encrypted integers.
    //
    // [1,2,3]             [0,0,0]
    // [4,5,6] - [1,2,3] = [3,3,3]
    // [7,8,9]             [6,6,6]
    //
    // The dimension #2 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.sub_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<1x3x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    
    // Same behavior as the previous one, but as the dimension #2 of operand #2 is missing.
    "FHELinalg.sub_eint"(%a0, %a1) : (tensor<3x3x!FHE.eint<4>>, tensor<3x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    // Returns the term-by-term subtraction of `%a0` with `%a1`
    "FHELinalg.sub_int_eint"(%a0, %a1) : (tensor<4xi5>, tensor<4x!FHE.eint<4>>) -> tensor<4x!FHE.eint<4>>
    
    // Returns the term-by-term subtraction of `%a0` with `%a1`, where dimensions equal to one are stretched.
    "FHELinalg.sub_int_eint"(%a0, %a1) : (tensor<4x1x4xi5>, tensor<1x4x4x!FHE.eint<4>>) -> tensor<4x4x4x!FHE.eint<4>>
    
    // Returns the subtraction of a 3x3 matrix of integers and a 3x1 matrix (a column) of encrypted integers.
    //
    // [1,2,3]   [1]   [0,2,3]
    // [4,5,6] - [2] = [2,3,4]
    // [7,8,9]   [3]   [4,5,6]
    //
    // The dimension #1 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.sub_int_eint"(%a0, %a1) : (tensor<3x3xi5>, tensor<3x1x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the subtraction of a 3x3 matrix of integers and a 1x3 matrix (a line) of encrypted integers.
    //
    // [1,2,3]             [0,0,0]
    // [4,5,6] - [1,2,3] = [3,3,3]
    // [7,8,9]             [6,6,6]
    //
    // The dimension #2 of operand #2 is stretched as it is equal to 1.
    "FHELinalg.sub_int_eint"(%a0, %a1) : (tensor<3x3xi5>, tensor<1x3x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    
    // Same behavior as the previous one, but as the dimension #2 is missing of operand #2.
    "FHELinalg.sub_int_eint"(%a0, %a1) : (tensor<3x3xi5>, tensor<3x!FHE.eint<4>>) -> tensor<3x3x!FHE.eint<4>>
    
    // Returns the sum of all elements of `%a0`
    "FHELinalg.sum"(%a0) : (tensor<3x3x!FHE.eint<4>>) -> !FHE.eint<4>
    //
    //     ( [1,2,3] )
    // sum ( [4,5,6] ) = 45
    //     ( [7,8,9] )
    //
    // Returns the sum of all elements of `%a0` along columns
    "FHELinalg.sum"(%a0) { axes = [0] } : (tensor<3x2x!FHE.eint<4>>) -> tensor<2x!FHE.eint<4>>
    //
    //     ( [1,2] )
    // sum ( [3,4] ) = [9, 12]
    //     ( [5,6] )
    //
    // Returns the sum of all elements of `%a0` along columns while preserving dimensions
    "FHELinalg.sum"(%a0) { axes = [0], keep_dims = true } : (tensor<3x2x!FHE.eint<4>>) -> tensor<1x2x!FHE.eint<4>>
    //
    //     ( [1,2] )
    // sum ( [3,4] ) = [[9, 12]]
    //     ( [5,6] )
    //
    // Returns the sum of all elements of `%a0` along rows
    "FHELinalg.sum"(%a0) { axes = [1] } : (tensor<3x2x!FHE.eint<4>>) -> tensor<3x!FHE.eint<4>>
    //
    //     ( [1,2] )
    // sum ( [3,4] ) = [3, 7, 11]
    //     ( [5,6] )
    //
    // Returns the sum of all elements of `%a0` along rows while preserving dimensions
    "FHELinalg.sum"(%a0) { axes = [1], keep_dims = true } : (tensor<3x2x!FHE.eint<4>>) -> tensor<3x1x!FHE.eint<4>>
    //
    //     ( [1,2] )   [3]
    // sum ( [3,4] ) = [7]
    //     ( [5,6] )   [11]
    //
    // ok
    "FHELinalg.to_signed"(%x) : (tensor<3x2x!FHE.eint<2>>) -> tensor<3x2x!FHE.esint<2>>
    
    // error
    "FHELinalg.to_signed"(%x) : (tensor<3x2x!FHE.eint<2>>) -> tensor<3x2x!FHE.esint<3>>
    // ok
    "FHELinalg.to_unsigned"(%x) : (tensor<3x2x!FHE.esint<2>>) -> tensor<3x2x!FHE.eint<2>>
    
    // error
    "FHELinalg.to_unsigned"(%x) : (tensor<3x2x!FHE.esint<2>>) -> tensor<3x2x!FHE.eint<3>>
    "FHELinalg.transpose"(%a) : (tensor<n0xn1x...xnNxType>) -> tensor<nNx...xn1xn0xType>
    // Transpose the input tensor
    // [1,2]    [1, 3, 5]
    // [3,4] => [2, 4, 6]
    // [5,6]
    //
    "FHELinalg.transpose"(%a) : (tensor<3x2xi7>) -> tensor<2x3xi7>
    "FHELinalg.transpose"(%a) { axes = [1, 3, 0, 2] } : (tensor<2x3x4x5xi7>) -> tensor<3x5x2x4xi7>