chore(frontend/python): doc fixes

GITBOOK-1
This commit is contained in:
Jeremy Shulman
2023-04-12 08:28:42 +00:00
committed by aquint-zama
parent 767a4137d5
commit 3249a443f0
21 changed files with 170 additions and 179 deletions

View File

@@ -122,7 +122,7 @@ Various tutorials are proposed in the documentation to help you start writing ho
- How to use Concrete with [Decorators](https://docs.zama.ai/concrete/tutorials/decorator)
- Partial support of [Floating Points](https://docs.zama.ai/concrete/tutorials/floating_points)
- How to perform [Table Lookup](https://docs.zama.ai/concrete/tutorials/table_lookup)
- How to perform [Table Lookup](https://docs.zama.ai/concrete/tutorials/table_lookups)
More generally, if you have built awesome projects using Concrete, feel free to let us know and we'll link to it!

View File

@@ -4,18 +4,18 @@
<figure><img src="_static/zama_home_docs.png" alt=""><figcaption></figcaption></figure>
**Concrete** is an open-source framework which simplifies the use of fully homomorphic encryption (FHE).
**Concrete** is an open-source framework which simplifies the use of Fully Homomorphic Encryption (FHE).
FHE is a powerful cryptographic tool, which allows computation to be performed directly on encrypted data without needing to decrypt it first. With FHE, you can build services that preserve privacy for all users. FHE is also great against data breaches as everything is done on encrypted data. Even if the server is compromised, in the end no sensitive data is leaked.
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 is 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 program is hard, concrete framework contains a TFHE Compiler based on LLVM to make this process easier for developers.
Since writing FHE programs are hard, Concrete framework contains a TFHE Compiler based on LLVM to make this process easier for developers.
## Organization of this documentation
This documentation is split into several sections:
* **Getting Started** gives you the basics,
* **Tutorials** gives you some essential examples on various features of the library,
* **Tutorials** provides essential examples on various features of the library,
* **How to** helps you perform specific tasks,
* **Developer** explains the inner workings of the library and everything related to contributing to the project.
@@ -23,14 +23,14 @@ This documentation is split into several sections:
* Support forum: [https://community.zama.ai](https://community.zama.ai) (we answer in less than 24 hours).
* Live discussion on the FHE.org discord server: [https://discord.fhe.org](https://discord.fhe.org) (inside the #**concrete** channel).
* Do you have a question about Zama? You can write us on [Twitter](https://twitter.com/zama\_fhe) or send us an email at: **hello@zama.ai**
* Do you have a question about Zama? Write us on [Twitter](https://twitter.com/zama\_fhe) or send us an email at: **hello@zama.ai**
## How is it different from Concrete Numpy?
## How is Concrete different from Concrete Numpy?
Concrete Numpy was the former name of Concrete Compiler Python's frontend. Starting from v1, Concrete Compiler is now open sourced and the package name is updated from `concrete-numpy` to `concrete-python` (as `concrete` is already booked for a non FHE-related project).
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 could safely update to Concrete with few changes explained in the [upgrading document](https://github.com/zama-ai/concrete/blob/main/UPGRADING.md).
Users from Concrete Numpy could safely update to Concrete with few changes explained in the [upgrading document](https://github.com/zama-ai/concrete/blob/main/UPGRADING.md).
## How is it different from the previous Concrete (v0.x)?
## 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. Rust library could be found under [TFHE-rs](https://github.com/zama-ai/tfhe-rs) project.
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 [TFHE-rs](https://github.com/zama-ai/tfhe-rs).

View File

@@ -1,23 +1,23 @@
# Compilation
The compilation journey begins with tracing to get an easy-to-manipulate representation of the function. We call this representation a `Computation Graph`, which is basically a Directed Acyclic Graph (DAG) containing nodes representing the computations done in the function. Working with graphs is good because they have been studied extensively over the years and there are a lot of algorithms to manipulate them. Internally, we use [networkx](https://networkx.org), which is an excellent graph library for Python.
Compilation begins with tracing to get an easy-to-manipulate representation of the function. We call this representation a `Computation Graph`, which is basically a Directed Acyclic Graph (DAG) containing nodes representing computations done in the function. Working with graphs is good because they have been studied extensively and there are a lot of available algorithms to manipulate them. Internally, we use [networkx](https://networkx.org), which is an excellent graph library for Python.
The next step in the compilation is transforming the computation graph. There are many transformations we perform, and they will be discussed in their own sections. In any case, the result of transformations is just another computation graph.
The next step in compilation is transforming the computation graph. There are many transformations we perform, and they will be discussed in their own sections. The result of transformations is just 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 currently allows a limited precision for computations. Bound measurement is our way to know what is the required precision for the function.
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 currently allows limited precision for computations. Bound measurement helps determine the required precision for the function.
The final step is to transform the computation graph to equivalent `MLIR` code. Once the MLIR is generated, our compiler backend compiles it down to native binaries.
The final step is to transform the computation graph to equivalent `MLIR` code. Once the MLIR is generated, our Compiler backend compiles it down to native binaries.
## Tracing
Given a Python function `f` such as this one:
We start with a Python function `f`, such as this one:
```
def f(x):
return (2 * x) + 3
```
...the goal of tracing is to create the following computation graph without needing any change from the user.
The goal of tracing is to create the following computation graph without requiring any change from the user.
![](../\_static/compilation-pipeline/two\_x\_plus\_three.png)
@@ -41,7 +41,7 @@ resulting_tracer = f(x, 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, and the rule for that is if a node has an encrypted predecessor, it is encrypted as well.
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.
## Topological transforms
@@ -57,9 +57,9 @@ We have allocated a whole new chapter to explaining fusing. You can find it afte
## Bounds measurement
Given a computation graph, the goal of the bound measurement step is to assign the minimal data type to each node in the graph.
Given a computation graph, the goal of the bounds measurement step is to assign the minimal data type to each node in the graph.
Let's say 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>` is the minimal encrypted integer that supports all values between `0` and `10`.
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`.
@@ -73,7 +73,7 @@ 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, it 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 the maximum values it contains.
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:
@@ -85,7 +85,7 @@ and this inputset:
[2, 3, 1]
```
Evaluation Result of `2`:
Evaluation result of `2`:
* `x`: 2
* `2`: 2
@@ -93,7 +93,7 @@ Evaluation Result of `2`:
* `3`: 3
* `+`: 7
New Bounds:
New bounds:
* `x`: \[**2**, **2**]
* `2`: \[**2**, **2**]
@@ -101,7 +101,7 @@ New Bounds:
* `3`: \[**3**, **3**]
* `+`: \[**7**, **7**]
Evaluation Result of `3`:
Evaluation result of `3`:
* `x`: 3
* `2`: 2
@@ -109,7 +109,7 @@ Evaluation Result of `3`:
* `3`: 3
* `+`: 9
New Bounds:
New bounds:
* `x`: \[2, **3**]
* `2`: \[2, 2]
@@ -117,7 +117,7 @@ New Bounds:
* `3`: \[3, 3]
* `+`: \[7, **9**]
Evaluation Result of `1`:
Evaluation result of `1`:
* `x`: 1
* `2`: 2
@@ -125,7 +125,7 @@ Evaluation Result of `1`:
* `3`: 3
* `+`: 5
New Bounds:
New bounds:
* `x`: \[**1**, 3]
* `2`: \[2, 2]
@@ -133,7 +133,7 @@ New Bounds:
* `3`: \[3, 3]
* `+`: \[**5**, 9]
Assigned Data Types:
Assigned data types:
* `x`: EncryptedScalar<**uint2**>
* `2`: ClearScalar<**uint2**>

View File

@@ -1,6 +1,6 @@
# Contribute
There are two ways to contribute to **Concrete**:
There are two ways to contribute to **Concrete**. You can:
* You can open issues to report bugs and typos and to suggest ideas.
* You can ask to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests (PRs), so please make sure to get in touch before you do!
* Open issues to report bugs and typos or suggest ideas;
* Request to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests (PRs), so get in touch before you do.

View File

@@ -4,21 +4,21 @@
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 to do that. 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.
* **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.
## Module structure
In this section, we will briefly discuss the module structure of **Concrete-Python**. You are encouraged to check individual `.py` files to learn more.
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)
* 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](../tutorial/extensions.md))
* mlir: computation graph to mlir conversion
* compilation: configuration, compiler, artifacts, circuit, client/server, and anything else related to compilation
* **dtypes:** data type specifications (e.g., int4, uint5, float32)
* **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](../tutorial/extensions.md))
* **mlir:** computation graph to mlir conversion
* **compilation:** configuration, compiler, artifacts, circuit, client/server, and anything else related to compilation

View File

@@ -2,7 +2,7 @@
## Supported operations
Here are the operations you can use inside the function you are compiling.
Here are the operations you can use inside the function you are compiling:
{% hint style="info" %}
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.
@@ -166,11 +166,11 @@ Some of these operations are not supported between two encrypted values. A detai
### Control flow constraints.
Some Python control flow statements are not supported. For example, 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":`).
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":`).
### Type constraints.
Another constraint is that 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)`).
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)`).
### Bit width constraints.

View File

@@ -1,6 +1,6 @@
# Exactness
One of the most common operations in **Concrete** is `Table Lookups` (TLUs). TLUs are performed with an FHE operation called `Programmable Bootstrapping` (PBS). PBSes have a certain probability of error, which, when triggered, result in inaccurate results.
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:
@@ -8,20 +8,20 @@ Let's say you have the table:
[0, 1, 4, 9, 16, 25, 36, 49, 64]
```
And you performed a table lookup using `4`. The result you should get is `16`, but because of the possibility of error, you can get any other value in the table.
And you perform a Table Lookup using `4`. The result you should get is `16`, but because of the possibility of error, you can 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.
Here is an example, if you set `p_error` to `0.01`, it means every TLU in the circuit will have a 1% chance of not being exact and 99% chance of being exact. 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)`).
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 being not exact, no matter how many table lookups are there.
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 `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!
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.&#x20;
See [How to Configure](../howto/configure.md) to learn how you can set a custom `p_error` and/or `global_p_error`.
Feel free to play with these configuration options to pick the one best suited for your needs! See [How to Configure](../howto/configure.md) to learn how you can set a custom `p_error` and/or `global_p_error`.
{% hint style="info" %}
Configuring either of those variables would affect computation time (compilation, keys generation, circuit execution) and space requirements (size of the keys on disk and in memory). Lower error probability would result in longer computation time and larger space requirements.
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.
{% endhint %}

View File

@@ -1,6 +1,6 @@
# Installation
**Concrete** is natively supported on Linux and macOS from Python 3.8 to 3.10 inclusive, but if you have Docker in your platform, you can use the docker image to use **Concrete**.
**Concrete** is natively supported on Linux and macOS from Python 3.8 to 3.10 inclusive. If you have Docker in your platform, you can use the docker image to use **Concrete**.
## Using PyPI

View File

@@ -1,6 +1,6 @@
# 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:
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:
```python
from concrete import fhe
@@ -28,8 +28,8 @@ inputset = range(2 ** 4)
circuit = f.compile(inputset)
```
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. Therefore, you should try to avoid them as much as possible. In most cases, it's not possible to avoid them completely, but you might remove the number of TLUs or replace some of them with other primitive operations.
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.
{% hint style="info" %}
Concrete automatically parallelize TLUs if they are applied to tensors.
Concrete automatically parallelizes TLUs if they are applied to tensors.
{% endhint %}

View File

@@ -1,6 +1,6 @@
# Quick Start
To compute on encrypted data, you first need to define the function that you want to compute, then compile it into a Concrete `Circuit`, which you can use to perform homomorphic evaluation.
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:
@@ -35,7 +35,7 @@ from concrete import fhe
## Defining the function to compile
In this example, we will compile a simple addition function:
In this example, we compile a simple addition function:
<!--pytest-codeblocks:skip-->
```python
@@ -45,7 +45,7 @@ def add(x, y):
## Creating a compiler
To compile the function, you need to create a `Compiler` by specifying the function to compile and encryption status of its inputs:
To compile the function, you need to create a `Compiler` by specifying the function to compile and the encryption status of its inputs:
<!--pytest-codeblocks:skip-->
```python
@@ -56,7 +56,7 @@ compiler = fhe.Compiler(add, {"x": "encrypted", "y": "clear"})
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 an iterable, yielding tuples of the same length as the number of arguments of the function being compiled:
It should be in iterable, yielding tuples, of the same length as the number of arguments of the function being compiled:
<!--pytest-codeblocks:skip-->
```python

View File

@@ -1,6 +1,6 @@
# Configure
The behavior of **Concrete** can be customized using `Configuration`s:
**Concrete** can be customized using `Configuration`s:
```python
from concrete import fhe
@@ -16,7 +16,7 @@ inputset = range(10)
circuit = f.compile(inputset, configuration=configuration)
```
Alternatively, you can overwrite individual options as kwargs to `compile` method:
You can overwrite individual options as kwargs to the `compile` method:
```python
from concrete import fhe
@@ -47,58 +47,40 @@ circuit = f.compile(inputset, configuration=configuration, loop_parallelize=True
```
{% hint style="info" %}
Additional kwarg to `compile` function have higher precedence. So if you set an option in both `configuration` and in `compile` methods, the value in the `compile` method will be used.
Additional kwarg 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.
{% endhint %}
## Options
* **show\_graph**: Optional[bool] = None
* Whether to print computation graph during compilation.
`True` means always to print, `False` means always to not print, `None` means print depending on verbose configuration below.
* **show\_mlir**: Optional[bool] = None
* Whether to print MLIR during compilation.
`True` means always to print, `False` means always to not print, `None` means print depending on verbose configuration below.
* **show\_optimizer**: Optional[bool] = None
* Whether to print optimizer output during compilation.
`True` means always to print, `False` means always to not print, `None` means print depending on verbose configuration below.
* **show\_graph**: Optional\[bool] = None
* Whether to print computation graph during compilation. `True` means always print, `False` means never print, `None` means print depending on verbose configuration below.
* **show\_mlir**: Optional\[bool] = None
* Whether to 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
* Whether to print optimizer output during compilation. `True` means always print, `False` means never print, `None` means print depending on verbose configuration below.
* **verbose**: bool = False
* Whether to print details related to compilation.
* **dump\_artifacts\_on\_unexpected\_failures**: bool = True
* Whether to export debugging artifacts automatically on compilation failures.
* **auto\_adjust\_rounders**: bool = False
* Whether to adjust rounders automatically.
* **p\_error**: Optional[float] = None
* Error probability for individual table lookups. If set, all table lookups will have the probability of non-exact result smaller than the set value. See [Exactness](../getting-started/exactness.md) 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 non-exact result smaller than the set value. See [Exactness](../getting-started/exactness.md) to learn more.
* **single_precision**: bool = True
* Whether to 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](../getting-started/exactness.md) 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](../getting-started/exactness.md) to learn more.
* **single\_precision**: bool = True
* Whether to use single precision for the whole circuit.
* **jit**: bool = False
* Whether to use JIT compilation.
* **loop\_parallelize**: bool = True
* Whether to enable loop parallelization in the compiler.
* **dataflow\_parallelize**: bool = False
* Whether to enable dataflow parallelization in the compiler.
* **auto\_parallelize**: bool = False
* Whether to enable auto parallelization in the compiler.
* **enable\_unsafe\_features**: bool = False
* Whether to enable unsafe features.
* **use\_insecure\_key\_cache**: bool = False _(Unsafe)_
* Whether to use the insecure key cache.
* **insecure\_key\_cache\_location**: Optional\[Union\[Path, str]] = None
* Location of insecure key cache.

View File

@@ -1,21 +1,21 @@
# Debug
In this section, you will learn how to debug the compilation process easily as well as how to get help in case you cannot resolve your issue.
In this section, you will learn how to debug the compilation process easily and get help in case you cannot resolve your issue.
## Debug Artifacts
## Debug artifacts
**Concrete** has an artifact system to simplify the process of debugging issues.
### 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 kinds of things are exported.
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.
```python
def f(x):
return np.sin(x)
```
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 the `.artifacts` directory under the working directory, you'll see the following files:
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:
#### environment.txt
@@ -28,7 +28,7 @@ Python 3.8.10
#### requirements.txt
This file contains information about python packages and their versions installed on your system.
This file contains information about Python packages and their versions installed on your system.
```
astroid==2.15.0
@@ -102,9 +102,9 @@ RuntimeError: Function you are trying to compile cannot be converted to MLIR
return %1
```
### Manual export.
### Manual exports.
Manual exports are mostly used for visualization. Nonetheless, they can be very useful for demonstrations. Here is how to perform one:
Manual exports are mostly used for visualization. They can be very useful for demonstrations. Here is how to perform one:
```python
from concrete import fhe
@@ -207,7 +207,7 @@ module {
}
```
#### client_parameters.json
#### client\_parameters.json
This file contains information about the client parameters chosen by **Concrete**.
@@ -291,15 +291,15 @@ You can seek help with your issue by asking a question directly in the [communit
If you cannot find a solution in the community forum, or you found a bug in the library, you could create an issue in our GitHub repository.
In case of a bug:
In case of a bug, try to:
* try to minimize randomness
* try to minimize your function as much as possible while keeping the bug - this will help to fix the bug faster
* try to include your inputset in the issue
* try to include reproduction steps in the issue
* try to include debug artifacts in the issue
* 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;
* include reproduction steps in the issue;
* include debug artifacts in the issue.
In case of a feature request:
In case of a feature request, try to:
* try to give a minimal example of the desired behavior
* try to explain your use case
* give a minimal example of the desired behavior;
* explain your use case.

View File

@@ -1,6 +1,6 @@
# 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. Further, you might want to perform the computation in dedicated servers. In this case, you can use the `Client` and `Server` features of **Concrete**.
After developing your circuit, you may want to deploy it. However, sharing the details of your circuit with every client might not be desirable. You might want to perform the computation in dedicated servers, as well. In this case, you can use the `Client` and `Server` features of **Concrete**.
## Development of the circuit
@@ -18,18 +18,18 @@ inputset = range(10)
circuit = function.compile(inputset)
```
Once you have your circuit, you can save everything the server needs like so:
Once you have your circuit, you can save everything the server needs:
<!--pytest-codeblocks:skip-->
```python
circuit.server.save("server.zip")
```
All you need to do now is to send `server.zip` to your computation server.
Then, send `server.zip` to your computation server.
## Setting up a server
You can load the `server.zip` you get from the development machine as follows:
You can load the `server.zip` you get from the development machine:
<!--pytest-codeblocks:skip-->
```python
@@ -38,9 +38,9 @@ from concrete import fhe
server = fhe.Server.load("server.zip")
```
At this point, you will need to wait for requests from clients. The first likely request is for `ClientSpecs`.
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` like so:
Clients need `ClientSpecs` to generate keys and request computation. You can serialize `ClientSpecs`:
<!--pytest-codeblocks:skip-->
```python
@@ -51,7 +51,7 @@ Then, you can send it to the clients requesting it.
## Setting up clients
After getting the serialized `ClientSpecs` from a server, you can create the client object like this:
After getting the serialized `ClientSpecs` from a server, you can create the client object:
<!--pytest-codeblocks:skip-->
```python
@@ -70,14 +70,14 @@ client.keys.generate()
This method generates encryption/decryption keys and evaluation keys.
The server requires evaluation keys linked to the encryption keys that you just generated. You can serialize your evaluation keys as shown below:
The server requires evaluation keys linked to the encryption keys that you just generated. You can serialize your evaluation keys as shown:
<!--pytest-codeblocks:skip-->
```python
serialized_evaluation_keys: bytes = client.evaluation_keys.serialize()
```
After serialization, you can send the evaluation keys to the server.
After serialization, send the evaluation keys to the server.
{% hint style="info" %}
Serialized evaluation keys are very big in size, so you may want to cache them on the server instead of sending them with each request.
@@ -85,18 +85,18 @@ Serialized evaluation keys are very big in size, so you may want to cache them o
## Encrypting inputs (on the client)
You are now ready to encrypt your inputs and request the server to perform the computation. You can do it like so:
Now encrypt your inputs and request the server to perform the computation. You can do it like so:
<!--pytest-codeblocks:skip-->
```python
serialized_args: bytes = client.encrypt(7).serialize()
```
The only thing left to do is to send serialized args to the server.
Then, send serialized args to the server.
## Performing computation (on the server)
Upon having the serialized evaluation keys and serialized arguments, you can deserialize them like so:
Once you have serialized evaluation keys and serialized arguments, you can deserialize them:
<!--pytest-codeblocks:skip-->
```python
@@ -104,7 +104,7 @@ deserialized_evaluation_keys = fhe.EvaluationKeys.deserialize(serialized_evaluat
deserialized_args = server.client_specs.deserialize_public_args(serialized_args)
```
And you can perform the computation as well:
You can perform the computation, as well:
<!--pytest-codeblocks:skip-->
```python
@@ -112,7 +112,7 @@ public_result = server.run(deserialized_args, deserialized_evaluation_keys)
serialized_public_result: bytes = public_result.serialize()
```
Finally, you can send the serialized public result back to the client, so they can decrypt it and get the result of the computation.
Then, send the serialized public result back to the client, so they can decrypt it and get the result of the computation.
## Decrypting the result (on the client)
@@ -123,7 +123,7 @@ Once you have received the public result of the computation from the server, you
deserialized_public_result = client.specs.deserialize_public_result(serialized_public_result)
```
Finally, you can decrypt the result like so:
Then, decrypt the result:
<!--pytest-codeblocks:skip-->
```python

View File

@@ -1,10 +1,10 @@
# Direct Circuits
{% hint style="warning" %}
Direct circuits are still experimental, and it's very easy to shoot yourself in the foot (e.g., no overflow checks, no type coercion) while using them so utilize them with care.
Direct circuits are still experimental. It is very easy to shoot yourself in the foot (e.g., no overflow checks, no type coercion) while using direct circuits, so utilize them with care.
{% endhint %}
For some applications, data types of inputs, intermediate values and outputs are known (e.g., for manipulating bytes, you would want to use uint8). For such cases, using inputsets to determine bounds are not necessary, or even error-prone. Therefore, another interface for defining such circuits, is introduced:
For some applications, 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 are not necessary, or even error-prone. Therefore, another interface for defining such circuits is introduced:
```python
from concrete import fhe
@@ -16,14 +16,14 @@ def circuit(x: fhe.uint8):
assert circuit.encrypt_run_decrypt(10) == 52
```
There are a few differences between direct circuits and traditional circuits though:
There are a few differences between direct circuits and traditional circuits:
- You need to remember that 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'll not get the negative value as the result will be `fhe.uint8` as well)
- You need to use fhe types in `.astype(...)` calls (e.g., `np.sqrt(x).astype(fhe.uint4)`). This is because there are no inputset evaluation, so cannot determine the bit-width of the output.
- You need to specify the resulting data type in [univariate](./extensions.md#fheunivariatefunction) extension (e.g., `fhe.univariate(function, outputs=fhe.uint4)(x)`), because of the same reason as above.
- You need to be careful with overflows. With inputset evaluation, you'll get bigger bit-widths but no overflows, with direct definition, you're responsible to ensure there aren't any overflows!
* 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'll fail to get the negative value as the result will be `fhe.uint8` as well)
* Use fhe types in `.astype(...)` calls (e.g., `np.sqrt(x).astype(fhe.uint4)`). There is no inputset evaluation, so the bit width of the output cannot be determined.
* Specify the resulting data type in [univariate](extensions.md#fheunivariatefunction) 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 there aren't any overflows!
Let's go over a more complicated example to see how direct circuits behave:
Let's review a more complicated example to see how direct circuits behave:
```python
from concrete import fhe
@@ -44,7 +44,9 @@ def circuit(x: fhe.uint8, y: fhe.int2):
print(circuit)
```
prints
This prints:
```
%0 = x # EncryptedScalar<uint8>
%1 = y # EncryptedScalar<int2>
@@ -67,7 +69,9 @@ Subgraphs:
%3 = astype(%2) # EncryptedScalar<uint4>
return %3
```
And here is the breakdown of assigned data types:
Here is the breakdown of assigned data types:
```
%0 is uint8 because it's specified in the definition
%1 is int2 because it's specified in the definition
@@ -80,4 +84,4 @@ And here is the breakdown of assigned data types:
%8 is uint8 because it's subtraction between uint8 and uint4
```
As you can see, `%8` is subtraction of two unsigned values, and it's unsigned as well. In an overflow condition where `c > d`, it'll result in undefined behavior.
As you can see, `%8` is subtraction of two unsigned values, and it's unsigned as well. In an overflow condition where `c > d`, it results in undefined behavior.

View File

@@ -1,6 +1,6 @@
# Extensions
**Concrete** tries to support native Python and NumPy operations as much as possible, but not everything is available in Python or NumPy. So, we provide some extensions ourselves to improve your experience.
**Concrete** supports native Python and NumPy operations as much as possible, but not everything is available in Python or NumPy. So, we provide some extensions ourselves to improve your experience.
## fhe.univariate(function)
@@ -41,7 +41,7 @@ The wrapped function shouldn't have any side effects, and it should be determini
## fhe.conv(...)
Allows you to perform convolution operation, with the same semantic of [onnx.Conv](https://github.com/onnx/onnx/blob/main/docs/Operators.md#conv):
Allows you to perform a convolution operation, with the same semantic of [onnx.Conv](https://github.com/onnx/onnx/blob/main/docs/Operators.md#conv):
```python
import numpy as np
@@ -73,7 +73,7 @@ Only 2D convolutions without padding and with one groups are supported for the t
## fhe.maxpool(...)
Allows you to perform maxpool operation, with the same semantic of [onnx.MaxPool](https://github.com/onnx/onnx/blob/main/docs/Operators.md#maxpool):
Allows you to perform a maxpool operation, with the same semantic of [onnx.MaxPool](https://github.com/onnx/onnx/blob/main/docs/Operators.md#maxpool):
```python
import numpy as np

View File

@@ -8,9 +8,9 @@
## As intermediate values
**Concrete-Compile**, which is used for compiling the circuit, doesn't support floating points at all. However, it supports table lookups. They take an integer and map it to another integer. It does not care how the lookup table is calculated. Further, the constraints of this operation are such that there should be a single integer input and it should result in a single integer output.
**Concrete-Compile**, which is used for compiling the circuit, doesn't support floating points at all. However, it supports table lookups. They take an integer and map it to another integer. It does not care how the lookup table is calculated. The constraints of this operation are such that there should be a single integer input, and it should result in a single integer output.
As long as your floating point operations comply with those constraints, **Concrete** automatically converts your operations to a table lookup operation:
As long as your floating point operations comply with those constraints, **Concrete** automatically converts them to a table lookup operation:
```python
from concrete import fhe
@@ -31,7 +31,7 @@ for x in range(8):
assert circuit.encrypt_run_decrypt(x) == f(x)
```
In the example above, `a`, `b`, and `c` are all floating point intermediates. However, they are just used to calculate `d`, which is an integer and value of `d` dependent upon `x` , which is another integer. **Concrete** detects this and fuses all of those operations into a single table lookup from `x` to `d`.
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` , another 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 some:
@@ -55,7 +55,7 @@ for x in range(8):
assert circuit.encrypt_run_decrypt(x) == f(x)
```
... results in:
This results in:
```
RuntimeError: Function you are trying to compile cannot be converted to MLIR
@@ -76,4 +76,4 @@ RuntimeError: Function you are trying to compile cannot be converted to MLIR
return %7
```
The reason for this 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.
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.

View File

@@ -15,5 +15,5 @@ print(circuit)
```
{% hint style="warning" %}
Formatting is just for debugging. It's not possible to create the circuit back from its textual representation. See [How to Deploy](../howto/deploy.md) if that's your goal.
Formatting is just for debugging purposes. It's not possible to create the circuit back from its textual representation. See [How to Deploy](../howto/deploy.md) if that's your goal.
{% endhint %}

View File

@@ -1,16 +1,17 @@
# Rounded Table Lookups
{% hint style="warning" %}
Rounded table lookups are not compilable yet. API is stable and will not change, so it's documented, but you might not be able to run the code samples in this document.
Rounded table lookups are not yet compilable. API is stable and will not change, so it's documented, but you might not be able to run the code samples provided in this document.
{% endhint %}
Table lookups have a strict constraint on number of bits they support. This can be quite limiting, especially if you don't need the exact precision.
Table lookups have a strict constraint on the number of bits they support. This can limiting, especially if you don't need exact precision.
To overcome such shortcomings, rounded table lookup operation is introduced. It's a way to extract most significant bits of a large integer and then applying the table lookup to those bits.
To overcome this, a rounded table lookup operation is introduced. It's a way to extract the most significant bits of a large integer and then apply the table lookup to those bits.
Imagine you have an 8-bit value, but you want to have a 5-bit table lookup, you can call `fhe.round_bit_pattern(input, lsbs_to_remove=3)` and use the value you get in the table lookup.
Imagine you have an 8-bit value, but you want to have a 5-bit table lookup. You can call `fhe.round_bit_pattern(input, lsbs_to_remove=3)` and use the value you get in the table lookup.
In Python, evaluation will work like this:
In Python, evaluation will work like the following:
```
0b_0000_0000 => 0b_0000_0000
0b_0000_0001 => 0b_0000_0000
@@ -49,7 +50,8 @@ In Python, evaluation will work like the following:
0b_1011_1111 => 0b_1100_0000
```
and during homomorphic execution, it'll be converted like this:
During homomorphic execution, it'll be converted like this:
```
0b_0000_0000 => 0b_00000
0b_0000_0001 => 0b_00000
@@ -88,9 +90,9 @@ and during homomorphic execution, it'll be converted like this:
0b_1011_1111 => 0b_11000
```
and then a modified table lookup would be applied to the resulting 5-bits.
A modified table lookup would be applied to the resulting 5 bits.
Here is a concrete example, let's say you want to apply ReLU to an 18-bit value. Let's see what the original ReLU looks like first:
If you want to apply ReLU to an 18-bit value., first look at the original ReLU:
```python
import matplotlib.pyplot as plt
@@ -105,9 +107,9 @@ plt.plot(xs, ys)
plt.show()
```
![](../_static/rounded-tlu/relu.png)
![](../\_static/rounded-tlu/relu.png)
Input range is [-100_000, 100_000), which means 18-bit table lookups are required, but they are not supported yet, you can apply rounding operation to the input before passing it to `ReLU` function:
The input range is \[-100\_000, 100\_000), which means 18-bit table lookups are required, but they are not yet supported. You can apply a rounding operation to the input before passing it to the `ReLU` function:
```python
from concrete import fhe
@@ -132,15 +134,15 @@ plt.plot(xs, ys)
plt.show()
```
in this case we've removed 10 least significant bits of the input and then applied ReLU function to this value to get:
We've removed the 10 least significant bits of the input and then applied the ReLU function to this value to get:
![](../_static/rounded-tlu/10-bits-removed.png)
![](../\_static/rounded-tlu/10-bits-removed.png)
which is close enough to original ReLU for some cases. If your application is more flexible, you could remove more bits, let's say 12 to get:
This is close enough to original ReLU for some cases. If your application is more flexible, you could remove more bits, let's say 12, to get:
![](../_static/rounded-tlu/12-bits-removed.png)
![](../\_static/rounded-tlu/12-bits-removed.png)
This is very useful, but in some cases, you don't know how many bits your input have, so it's not reliable to specify `lsbs_to_remove` manually. For this reason, `AutoRounder` class is introduced.
This 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, `AutoRounder` class is introduced.
```python
from concrete import fhe
@@ -170,14 +172,14 @@ plt.show()
`AutoRounder`s allow 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` to `True` during compilation.
In the example above, `6` of the most significant bits are kept to get:
In this case, `6` of the most significant bits are kept to get:
![](../_static/rounded-tlu/6-bits-kept.png)
![](../\_static/rounded-tlu/6-bits-kept.png)
You can adjust `target_msbs` depending on your requirements. If you set it to `4` for example, you'd get:
You can adjust `target_msbs` depending on your requirements. If you set it to `4`, you get:
![](../_static/rounded-tlu/4-bits-kept.png)
![](../\_static/rounded-tlu/4-bits-kept.png)
{% hint style="warning" %}
`AutoRounder`s should be defined outside the function being compiled. They are used to store the result of adjustment process, so they shouldn't be created each time the function is called.
`AutoRounder`s should be defined outside the function 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.
{% endhint %}

View File

@@ -1,9 +1,8 @@
# Simulation
During development, speed of homomorphic execution is a big 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 [Exactness](../getting-started/exactness.md)).
During development, the speed of homomorphic execution is a big 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 [Exactness](../getting-started/exactness.md)).
Considering these, simulation is introduced:
Considering this, simulation is introduced:
```python
from concrete import fhe
@@ -25,7 +24,7 @@ print(actual.tolist())
print(simulation.tolist())
```
prints
After the simulation runs, it prints this:
```
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
@@ -33,5 +32,9 @@ prints
```
{% hint style="warning" %}
Currently, simulation is better than directly calling from Python, but it's not exactly the same with FHE execution. The reason is that it is implemented in Python. Imagine you have an identity table lookup, it might be ommitted from the generated FHE code by the compiler, but it'll be present in Python as optimizations are not done in Python. This will result in a bigger error in simulation. Furthermore, some operations have multiple table lookups within them, and those cannot be simulated unless the actual implementations of said operations are ported to Python. In the future, simulation functionality will be provided by the compiler so all of these issues would be addressed. Until then, keep these in mind.
Currently, simulation is better than directly calling from Python, but it's not exactly the same with FHE execution. This is because it is implemented in Python.&#x20;
Imagine you have an identity table lookup. It might be omitted from the generated FHE code by the Compiler, but it will still be present as optimizations are not done in Python. This will result in a bigger error in simulation.&#x20;
Some operations also have multiple table lookups within them, and those cannot be simulated unless their actual implementations are ported to Python. In the future, simulation functionality will be provided by the Compiler, so all of these issues will be addressed. Until then, keep these in mind.
{% endhint %}

View File

@@ -4,10 +4,10 @@ In this tutorial, we will review the ways to perform direct table lookups in **C
## Direct table lookup
**Concrete** provides a `LookupTable` class for you to create your own tables and apply them in your circuits.
**Concrete** provides a `LookupTable` class to create your own tables and apply them in your circuits.
{% hint style="info" %}
`LookupTable`s can have any number of elements. Let's call them **N**. As long as the lookup variable is in range \[-**N**, **N**), table lookup is valid.
`LookupTable`s can have any number of elements. Let's call them **N**. As long as the lookup variable is in range \[-**N**, **N**), Table Lookup is valid.
If you go out of bounds of this range, you will get the following error:
@@ -92,7 +92,7 @@ assert circuit.encrypt_run_decrypt(3) == table[-3] == -1
assert circuit.encrypt_run_decrypt(4) == table[-4] == 2
```
## Direct multi table lookup
## Direct multi-table lookup
In case you want to apply a different lookup table to each element of a tensor, you can have a `LookupTable` of `LookupTable`s:
@@ -165,7 +165,7 @@ The function is first traced into:
![](../\_static/tutorials/table-lookup/1.initial.graph.png)
Then, **Concrete** fuses appropriate nodes:
**Concrete** then fuses appropriate nodes:
![](../\_static/tutorials/table-lookup/3.final.graph.png)

View File

@@ -1,6 +1,6 @@
# Tagging
When you have big circuits, keeping track of which node corresponds to which part of your code becomes very hard. Tagging system could simplify such situations:
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:
```python
def g(z):
@@ -20,7 +20,7 @@ def f(x):
return g(z + 3) * 2
```
when you compile `f` with inputset of `range(10)`, you get the following graph:
When you compile `f` with inputset of `range(10)`, you get the following graph:
```
%0 = x # EncryptedScalar<uint4> ∈ [0, 9]
@@ -49,8 +49,8 @@ Subgraphs:
return %2
```
and if you get an error, you'll precisely see where the error occurred (e.g., which layer of the neural network, if you tag layers).
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).
{% hint style="info" %}
In the future, we're planning to use tags for other features as well (e.g., to measure performance of tagged regions), so it's a good idea to start utilizing them for big circuits.
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.
{% endhint %}