docs(frontend/python): overhaul python frontend docs

This commit is contained in:
aquint-zama
2023-03-13 09:17:59 +01:00
committed by Alex Quint
parent 8a672a0c59
commit fdf6f41a89
69 changed files with 1000 additions and 1219 deletions

View File

@@ -1,152 +1,18 @@
<p align="center">
<!-- product name logo -->
<img width=600 src="https://user-images.githubusercontent.com/5758427/193612313-6b1124c7-8e3e-4e23-8b8c-57fd43b17d4f.png">
</p>
# Python Frontend
<p align="center">
<!-- Version badge using shields.io -->
<a href="https://github.com/zama-ai/concrete-numpy/releases">
<img src="https://img.shields.io/github/v/release/zama-ai/concrete-numpy?style=flat-square">
</a>
<!-- Link to docs badge using shields.io -->
<a href="https://docs.zama.ai/concrete-numpy/">
<img src="https://img.shields.io/badge/read-documentation-yellow?style=flat-square">
</a>
<!-- Community forum badge using shields.io -->
<a href="https://community.zama.ai/c/concrete-numpy">
<img src="https://img.shields.io/badge/community%20forum-online-brightgreen?style=flat-square">
</a>
<!-- Open source badge using shields.io -->
<a href="https://docs.zama.ai/concrete-numpy/developer/contributing">
<img src="https://img.shields.io/badge/we're%20open%20source-contributing.md-blue?style=flat-square">
</a>
<!-- Twitter badge using shields.io -->
<a href="https://twitter.com/zama_fhe">
<img src="https://img.shields.io/badge/follow-zama_fhe-blue?logo=twitter&style=flat-square">
</a>
</p>
**Concrete Numpy** is an open-source library which simplifies the use of fully homomorphic encryption (FHE) in Python.
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 the privacy of the 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.
## Main features
- Ability to compile Python functions (that may use NumPy within) to their FHE equivalents, to operate on encrypted data
- Support for [large collection of operators](https://docs.zama.ai/concrete-numpy/getting-started/compatibility)
- Partial support for floating points
- Support for table lookups on integers
- Support for integration with Client / Server architectures
## Installation
| OS / HW | Available on Docker | Available on PyPI |
|:------------------------------------:|:-------------------:|:-----------------:|
| Linux | Yes | Yes |
| Windows | Yes | Coming soon |
| Windows Subsystem for Linux | Yes | Yes |
| macOS (Intel) | Yes | Yes |
| macOS (Apple Silicon, ie M1, M2 etc) | Yes (Rosetta) | Coming soon |
The preferred way to install Concrete Numpy is through PyPI:
```shell
pip install concrete-numpy
```
You can get the concrete-numpy docker image by pulling the latest docker image:
```shell
docker pull zamafhe/concrete-numpy:v0.10.0
```
You can find more detailed installation instructions in [installing.md](docs/getting-started/installing.md)
## Getting started
```python
import concrete.numpy as cnp
def add(x, y):
return x + y
compiler = cnp.Compiler(add, {"x": "encrypted", "y": "encrypted"})
inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1), (3, 2), (6, 1), (1, 7), (4, 5), (5, 4)]
print(f"Compiling...")
circuit = compiler.compile(inputset)
print(f"Generating keys...")
circuit.keygen()
examples = [(3, 4), (1, 2), (7, 7), (0, 0)]
for example in examples:
encrypted_example = circuit.encrypt(*example)
encrypted_result = circuit.run(encrypted_example)
result = circuit.decrypt(encrypted_result)
print(f"Evaluation of {' + '.join(map(str, example))} homomorphically = {result}")
```
or if you have a simple function that you can decorate, and you don't care about explicit steps of key generation, encryption, evaluation and decryption:
```python
import concrete.numpy as cnp
@cnp.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)]
print(f"Compiling...")
circuit = add.compile(inputset)
examples = [(3, 4), (1, 2), (7, 7), (0, 0)]
for example in examples:
result = circuit.encrypt_run_decrypt(*example)
print(f"Evaluation of {' + '.join(map(str, example))} homomorphically = {result}")
```
## Documentation
Full, comprehensive documentation is available at [https://docs.zama.ai/concrete-numpy](https://docs.zama.ai/concrete-numpy).
## Target users
Concrete Numpy is a generic library that supports a variety of use cases. Because of this flexibility,
it doesn't provide primitives for specific use cases.
If you have a specific use case, or a specific field of computation, you may want to build abstractions on top of Concrete Numpy.
One such example is [Concrete ML](https://github.com/zama-ai/concrete-ml), which is built on top of Concrete Numpy to simplify Machine Learning oriented use cases.
## Tutorials
Various tutorials are proposed in the documentation to help you start writing homomorphic programs:
- How to use Concrete Numpy with [Decorators](https://docs.zama.ai/concrete-numpy/tutorials/decorator)
- Partial support of [Floating Points](https://docs.zama.ai/concrete-numpy/tutorials/floating_points)
- How to perform [Table Lookup](https://docs.zama.ai/concrete-numpy/tutorials/table_lookup)
More generally, if you have built awesome projects using Concrete Numpy, feel free to let us know, and we'll link to it!
## Setting up for local development
## Setup for development
```shell
# clone the repository
git clone https://github.com/zama-ai/concrete-open-source.git
cd concrete-open-source
git clone https://github.com/zama-ai/concrete.git
cd concrete
# create virtual environment
cd frontends/concrete-python
make venv
# activate virtual environment
source .vevn/bin/activate
source .venv/bin/activate
# build the compiler bindings
cd ../../compilers/concrete-compiler/compiler
@@ -160,17 +26,3 @@ echo "export COMPILER_BUILD_DIRECTORY=$(pwd)/build" >> ~/.bashrc
cd ../../../frontends/concrete-python
make pytest
```
Building python bindings requires some python packages to be installed, hence virtual environment is created and activated before building compiler bindings.
Also, you don't have to follow these steps exactly. As long as the compiler is built with CMake in release mode and build directory is exported as the environment variable `COMPILER_BUILD_DIRECTORY`, it'll be okay.
## Need support?
<a target="_blank" href="https://community.zama.ai">
<img src="https://user-images.githubusercontent.com/5758427/191792238-b132e413-05f9-4fee-bee3-1371f3d81c28.png">
</a>
## License
This software is distributed under the BSD-3-Clause-Clear license. If you have any questions, please contact us at hello@zama.ai.

View File

@@ -368,7 +368,7 @@ def is_single_common_ancestor(
# - [...] = Node of which single common ancestor is searched
# - {[...]} = Both Candidate Node and Node of which single common ancestor is searched
#
# Consider the folowing graph:
# Consider the following graph:
#
# (3) (x) (2)
# \ / \ /
@@ -393,7 +393,7 @@ def is_single_common_ancestor(
# which means there is path leading to the addition node and that path doesn't include
# the multiplication node, so we conclude multiplication node is not a single common ancestor
#
# Now, consider the folowing graph:
# Now, consider the following graph:
#
# (3) {x} (2)
# \ / \ /
@@ -419,7 +419,7 @@ def is_single_common_ancestor(
# In this subgraph, every node except the candidate node
# will keep all of their non-constant predecessors,
# which means all of their non-constant predecessors originated
# from the `candidate`, so it's a single common anscestor.
# from the `candidate`, so it's a single common ancestor.
#
# When you think about it, this implementation makes a lot of sense for our purposes
# It basically determines if `nodes` "solely" depend on the `candidate`,
@@ -532,7 +532,7 @@ def convert_subgraph_to_subgraph_node(
Args:
graph (Graph):
orginal graph
original graph
all_nodes (Dict[Node, None]):
all nodes in the subgraph

View File

@@ -1,24 +0,0 @@
# What is Concrete Numpy?
[<mark style="background-color:yellow;">⭐️ Star the repo on Github</mark>](https://github.com/zama-ai/concrete-numpy) <mark style="background-color:yellow;">| 🗣</mark> [<mark style="background-color:yellow;">Community support forum</mark>](https://community.zama.ai/c/concrete-numpy) <mark style="background-color:yellow;">| 📁</mark> [<mark style="background-color:yellow;">Contribute to the project</mark>](dev/contributing.md)
<figure><img src="_static/zama_home_docs.png" alt=""><figcaption></figcaption></figure>
**Concrete-Numpy** is an open-source library 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.
## 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,
* **How to** helps you perform specific tasks,
* and **Developer** explains the inner workings of the library and everything related to contributing to the project.
## Looking for support? Ask our team!
* 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**

View File

@@ -1,40 +0,0 @@
# Table of contents
* [What is Concrete Numpy?](README.md)
## Getting Started
* [Installation](getting-started/installing.md)
* [Quick Start](getting-started/quick\_start.md)
* [Compatibility](getting-started/compatibility.md)
* [Exactness](getting-started/exactness.md)
* [Performance](getting-started/performance.md)
## Tutorials
* [Decorator](tutorial/decorator.md)
* [Formatting](tutorial/formatting.md)
* [Tagging](tutorial/tagging.md)
* [Extensions](tutorial/extensions.md)
* [Table Lookups](tutorial/table\_lookups.md)
* [Rounded Table Lookups](tutorial/rounded\_table\_lookups.md)
* [Floating Points](tutorial/floating\_points.md)
* [Simulation](tutorial/simulation.md)
* [Direct Circuits](tutorial/direct\_circuits.md)
* [Key Value Database](tutorial/key\_value\_database.md)
## How To
* [Configure](howto/configure.md)
* [Debug](howto/debug.md)
* [Deploy](howto/deploy.md)
## Developer
* [Project Setup](dev/project\_setup.md)
* [Docker Setup](dev/docker.md)
* [Contribute](dev/contributing.md)
* [Terminology and Structure](dev/terminology\_and\_structure.md)
* [Compilation](dev/compilation.md)
* [Fusing](dev/fusing.md)
* [MLIR](dev/mlir.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

View File

@@ -1 +0,0 @@
../tests/conftest.py

View File

@@ -1,148 +0,0 @@
# 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.
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.
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.
The final step is to transform the computation graph to equivalent `MLIR` code. How this is done will be explained in detail in its own chapter.
Once the MLIR is generated, we send it to the **Concrete-Compiler**, and it completes the compilation process.
## Tracing
Given 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.
![](../\_static/compilation-pipeline/two\_x\_plus\_three.png)
(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 `Tracer`s, 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. `Tracer`s make use of the operator overloading feature of Python to achieve their goal:
```
def f(x, y):
return x + 2 * y
x = Tracer(computation=Input("x"))
y = Tracer(computation=Input("y"))
resulting_tracer = f(x, y)
```
`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, and the rule for that is if a node has an encrypted predecessor, it is encrypted as well.
## Topological transforms
The goal of topological transforms is to make more functions compilable.
With the current version of **Concrete-Numpy**, 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.
### Fusing.
We have allocated a whole new chapter to explaining fusing. You can find it after this chapter.
## 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.
Let's say we have an encrypted input that is always between `0` and `10`. We should assign the type `Encrypted<uint4>` to the node of this input as `Encrypted<uint4>` 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.
### 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, 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.
Here is an example, given this computation graph where `x` is encrypted:
![](../\_static/compilation-pipeline/two\_x\_plus\_three.png)
and this inputset:
```
[2, 3, 1]
```
Evaluation Result of `2`:
* `x`: 2
* `2`: 2
* `*`: 4
* `3`: 3
* `+`: 7
New Bounds:
* `x`: \[**2**, **2**]
* `2`: \[**2**, **2**]
* `*`: \[**4**, **4**]
* `3`: \[**3**, **3**]
* `+`: \[**7**, **7**]
Evaluation Result of `3`:
* `x`: 3
* `2`: 2
* `*`: 6
* `3`: 3
* `+`: 9
New Bounds:
* `x`: \[2, **3**]
* `2`: \[2, 2]
* `*`: \[4, **6**]
* `3`: \[3, 3]
* `+`: \[7, **9**]
Evaluation Result of `1`:
* `x`: 1
* `2`: 2
* `*`: 2
* `3`: 3
* `+`: 5
New Bounds:
* `x`: \[**1**, 3]
* `2`: \[2, 2]
* `*`: \[**2**, 6]
* `3`: \[3, 3]
* `+`: \[**5**, 9]
Assigned Data Types:
* `x`: Encrypted<**uint2**>
* `2`: Clear<**uint2**>
* `*`: Encrypted<**uint3**>
* `3`: Clear<**uint2**>
* `+`: Encrypted<**uint4**>
## MLIR conversion
The actual compilation will be done by the **Concrete-Compiler**, which is expecting an MLIR input. The MLIR conversion goes from a computation graph to its MLIR equivalent. You can read more about it [here](mlir.md).

View File

@@ -1,99 +0,0 @@
# Contribute
{% hint style="info" %}
There are two ways to contribute to **Concrete-Numpy** or to **Concrete** tools in general:
* 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!
{% endhint %}
Now, let's go over some other important items that you need to know.
## Creating a new branch
We are using a consistent branch naming scheme, and you are expected to follow it as well. Here is the format:
```shell
git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description
```
...and here are some examples:
```shell
git checkout -b feat/direct-tlu
git checkout -b fix/tracing-indexing
```
## Before committing
### Conformance.
Each commit to **Concrete-Numpy** should conform to the standards decided by the team. Conformance can be checked using the following command:
```shell
make pcc
```
### Testing.
On top of conformance, all tests must pass with 100% code coverage across the codebase:
```shell
make pytest
```
{% hint style="info" %}
There may be cases where covering 100% of the code is not possible (e.g., exceptions that cannot be triggered in normal execution circumstances). In those cases, you may be allowed to disable coverage for some specific lines. This should be the exception rather than the rule. Reviewers may ask why some lines are not covered and, if it appears they can be covered, then the PR won't be accepted in that state.
{% endhint %}
## Committing
We are using a consistent commit naming scheme, and you are expected to follow it as well. Again, here is the accepted format:
```shell
make show_scope
```
...and some examples:
```shell
git commit -m "feat: implement bounds checking"
git commit -m "feat(debugging): add an helper function to print intermediate representation"
git commit -m "fix(tracing): fix a bug that crashed pytorch tracer"
```
To learn more about conventional commits, check [this](https://www.conventionalcommits.org/en/v1.0.0/) page.
## Before creating a pull request
{% hint style="info" %}
We remind you that only official contributors can send pull requests. To become an official contributor, please email hello@zama.ai.
{% endhint %}
You should rebase on top of the `main` branch before you create your pull request. We don't allow merge commits, so rebasing on `main` before pushing gives you the best chance of avoiding rewriting parts of your PR later if conflicts arise with other PRs being merged. After you commit your changes to your new branch, you can use the following commands to rebase:
```shell
# fetch the list of active remote branches
git fetch --all --prune
# checkout to main
git checkout main
# pull the latest changes to main (--ff-only is there to prevent accidental commits to main)
git pull --ff-only
# checkout back to your branch
git checkout $YOUR_BRANCH
# rebase on top of main branch
git rebase main
# If there are conflicts during the rebase, resolve them
# and continue the rebase with the following command
git rebase --continue
# push the latest version of the local branch to remote
git push --force
```
You can learn more about rebasing [here](https://git-scm.com/docs/git-rebase).

View File

@@ -1,45 +0,0 @@
# Docker Setup
## Installation
Before you start this section, go ahead and install Docker. You can follow [this](https://docs.docker.com/engine/install/) official guide if you need help.
## X forwarding
### Linux.
You can use this xhost command:
```shell
xhost +localhost
```
### macOS.
To use X forwarding on macOS:
* Install XQuartz
* Open XQuartz.app application, make sure in the application parameters that `authorize network connections` are set (currently in the Security settings)
* Open a new terminal within XQuartz.app and type:
```shell
xhost +127.0.0.1
```
X server should be all set for Docker in the regular terminal.
## Building
You can use the dedicated target in the makefile to build the docker image:
```shell
make docker_build
```
## Starting
You can use the dedicated target in the makefile to start the docker session:
```shell
make docker_start
```

View File

@@ -1,37 +0,0 @@
# Fusing
Fusing is the act of combining multiple nodes into a single node, which is converted to a table lookup.
## How is it done?
Code related to fusing is in the `concrete/numpy/compilation/utils.py` file. Fusing can be performed using the `fuse` function.
Within `fuse`:
1. We loop until there are no more subgraphs to fuse.
2. <mark style="background-color:yellow;">Within each iteration:</mark>
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.10. We substitute the `subgraph` node to the original graph.
## Limitations
With the current implementation, we cannot fuse subgraphs that depend on multiple encrypted values where those values doesn't have a common lca (e.g., `np.round(np.sin(x) + np.cos(y))`).
{% hint style="info" %}
[KolmogorovArnold representation theorem](https://en.wikipedia.org/wiki/Kolmogorov%E2%80%93Arnold\_representation\_theorem) states that every multivariate continuous function can be represented as a superposition of continuous functions of one variable. Therefore, the case above could be handled in future versions of **Concrete-Numpy**.
{% endhint %}

View File

@@ -1,17 +0,0 @@
# MLIR
The MLIR project is a sub-project of the LLVM project. It's designed to simplify building domain-specific compilers such as our **Concrete-Compiler**.
**Concrete-Compiler** accepts MLIR as an input and emits compiled assembly code for a target architecture.
**Concrete-Numpy** performs the MLIR generation from the computation graph. Code related to this conversion is in the `concrete/numpy/mlir` folder.
The conversion can be performed using the `convert` method of the `GraphConverter` class.
Within the `convert` method of `GraphConverter`:
* MLIR compatibility of the graph is checked;
* bit width constraints are checked;
* negative lookup tables are offset;
* the computation graph is traversed and each node is converted to their corresponding MLIR representation using the `NodeConverter` class;
* and string representation of the resulting MLIR is returned.

View File

@@ -1,91 +0,0 @@
# Project Setup
{% hint style="info" %}
It is **strongly** recommended to use the development tool Docker. However, you are able to set the project up on a bare Linux or macOS as long as you have the required dependencies. You can see the required dependencies in `Dockerfile.dev` under `docker` directory.
{% endhint %}
## Installing `Python`
**Concrete-Numpy** is a `Python` library, so `Python` should be installed to develop it. `v3.8` and `v3.9` are, currently, the only supported versions.
You probably have Python already, but in case you don't, or in case you have an unsupported version, you can google `how to install python 3.8` and follow one of the results.
## Installing `Poetry`
`Poetry` is our package manager. It drastically simplifies dependency and environment management.
You can follow [this](https://python-poetry.org/docs/#installation) official guide to install it.
## Installing `make`
`make` is used to launch various commands such as formatting and testing.
On Linux, you can install `make` using the package manager of your distribution.
On macOS, you can install `gmake` via brew:
```shell
brew install make
```
{% hint style="info" %}
In the following sections, be sure to use the proper `make` tool for your system (i.e., `make`, `gmake`, etc).
{% endhint %}
## Cloning the repository
Now, it's time to get the source code of **Concrete-Numpy**.
Clone the git repository from GitHub using the protocol of your choice (ssh or https).
## Setting up the environment
Virtual environments are utilized to keep the project isolated from other `Python` projects in the system.
To create a new virtual environment and install dependencies, use the command:
```shell
make setup_env
```
## Activating the environment
To activate the newly created environment, use:
```shell
source .venv/bin/activate
```
## Syncing the environment
From time to time, new dependencies will be added to the project and old ones will be removed.mThe command below will make sure the project has the proper environment, so run it regularly.
```shell
make sync_env
```
## Troubleshooting
### In native setups.
If you are having issues in a native setup, you can try to re-create your environment like this:
```shell
deactivate
rm -rf .venv
make setup_env
source .venv/bin/activate
```
If the problem persists, you should consider using Docker. If you are working on a platform specific feature and Docker is not an option, you should create an issue so that we can take a look at your problem.
### In docker setups.
If you are having issues in a docker setup, you can try to re-build the docker image:
```shell
make docker_rebuild
make docker_start
```
If the problem persists, you should contact us for help.

View File

@@ -1,17 +0,0 @@
# Release process
## Release candidate cycle
Throughout the quarter, many release candidatess are relesed. Those candidates are released in a private package repository. At the end of the quarter, we take the latest release candidate, and release it in PyPI without `rcX` tag.
## Release flow
* Checkout to the commit that you want to include in the release (everything before this commit and this commit will be in the release)
* Run `make release`
* Wait for CI to complete
* Checkout to `chore/version` branch
* Run `VERSION=a.b.c-rcX make set_version` with appropriate version
* Push the branch to origin
* Create a PR to merge it to main
* Wait for CI to finish and get approval in the meantime
* Merge the version update to main

View File

@@ -1,26 +0,0 @@
# Terminology and Structure
## 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 - the technique that takes a Python function from the user and generates the corresponding computation graph in an easy-to-read format.
* bounds - before a computation graph is converted to MLIR, we need to know which node will output which type (e.g., uint3 vs euint5). Computation graphs with different inputs must remember the minimum and maximum values 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 and has methods, everything from printing to evaluation.
## Module structure
In this section, we will briefly discuss the module structure of **Concrete-Numpy**. You are encouraged to check individual `.py` files to learn more.
* Concrete
* Numpy
* dtypes - data type specifications
* values - value specifications (i.e., data type + shape + encryption status)
* representation - representation of computation
* tracing - tracing of Python functions
* extensions - custom functionality which is not available in NumPy (e.g., direct table lookups)
* MLIR - MLIR conversion
* compilation - compilation from a Python function to a circuit, client/server architecture
* ONNX
* convolution - custom convolution operations that follow the behavior of ONNX

View File

@@ -1,177 +0,0 @@
# Compatibility
## Supported operations
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.
{% endhint %}
### Supported Python operators.
* [\_\_abs\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_abs\_\_)
* [\_\_add\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_add\_\_)
* [\_\_and\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_and\_\_)
* [\_\_eq\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_eq\_\_)
* [\_\_floordiv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_floordiv\_\_)
* [\_\_ge\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_ge\_\_)
* [\_\_getitem\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_getitem\_\_)
* [\_\_gt\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_gt\_\_)
* [\_\_invert\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_invert\_\_)
* [\_\_le\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_le\_\_)
* [\_\_lshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_lshift\_\_)
* [\_\_lt\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_lt\_\_)
* [\_\_matmul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_matmul\_\_)
* [\_\_mod\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_mod\_\_)
* [\_\_mul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_mul\_\_)
* [\_\_ne\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_ne\_\_)
* [\_\_neg\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_neg\_\_)
* [\_\_or\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_or\_\_)
* [\_\_pos\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_pos\_\_)
* [\_\_pow\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_pow\_\_)
* [\_\_radd\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_radd\_\_)
* [\_\_rand\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rand\_\_)
* [\_\_rfloordiv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rfloordiv\_\_)
* [\_\_rlshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rlshift\_\_)
* [\_\_rmatmul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rmatmul\_\_)
* [\_\_rmod\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rmod\_\_)
* [\_\_rmul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rmul\_\_)
* [\_\_ror\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_ror\_\_)
* [\_\_round\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_round\_\_)
* [\_\_rpow\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rpow\_\_)
* [\_\_rrshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rrshift\_\_)
* [\_\_rshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rshift\_\_)
* [\_\_rsub\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rsub\_\_)
* [\_\_rtruediv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rtruediv\_\_)
* [\_\_rxor\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rxor\_\_)
* [\_\_sub\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_sub\_\_)
* [\_\_truediv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_truediv\_\_)
* [\_\_xor\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_xor\_\_)
### Supported NumPy functions.
* [np.absolute](https://numpy.org/doc/stable/reference/generated/numpy.absolute.html)
* [np.add](https://numpy.org/doc/stable/reference/generated/numpy.add.html)
* [np.arccos](https://numpy.org/doc/stable/reference/generated/numpy.arccos.html)
* [np.arccosh](https://numpy.org/doc/stable/reference/generated/numpy.arccosh.html)
* [np.arcsin](https://numpy.org/doc/stable/reference/generated/numpy.arcsin.html)
* [np.arcsinh](https://numpy.org/doc/stable/reference/generated/numpy.arcsinh.html)
* [np.arctan](https://numpy.org/doc/stable/reference/generated/numpy.arctan.html)
* [np.arctan2](https://numpy.org/doc/stable/reference/generated/numpy.arctan2.html)
* [np.arctanh](https://numpy.org/doc/stable/reference/generated/numpy.arctanh.html)
* [np.around](https://numpy.org/doc/stable/reference/generated/numpy.around.html)
* [np.bitwise\_and](https://numpy.org/doc/stable/reference/generated/numpy.bitwise\_and.html)
* [np.bitwise\_or](https://numpy.org/doc/stable/reference/generated/numpy.bitwise\_or.html)
* [np.bitwise\_xor](https://numpy.org/doc/stable/reference/generated/numpy.bitwise\_xor.html)
* [np.broadcast\_to](https://numpy.org/doc/stable/reference/generated/numpy.broadcast\_to.html)
* [np.cbrt](https://numpy.org/doc/stable/reference/generated/numpy.cbrt.html)
* [np.ceil](https://numpy.org/doc/stable/reference/generated/numpy.ceil.html)
* [np.clip](https://numpy.org/doc/stable/reference/generated/numpy.clip.html)
* [np.concatenate](https://numpy.org/doc/stable/reference/generated/numpy.concatenate.html)
* [np.copysign](https://numpy.org/doc/stable/reference/generated/numpy.copysign.html)
* [np.cos](https://numpy.org/doc/stable/reference/generated/numpy.cos.html)
* [np.cosh](https://numpy.org/doc/stable/reference/generated/numpy.cosh.html)
* [np.deg2rad](https://numpy.org/doc/stable/reference/generated/numpy.deg2rad.html)
* [np.degrees](https://numpy.org/doc/stable/reference/generated/numpy.degrees.html)
* [np.dot](https://numpy.org/doc/stable/reference/generated/numpy.dot.html)
* [np.equal](https://numpy.org/doc/stable/reference/generated/numpy.equal.html)
* [np.exp](https://numpy.org/doc/stable/reference/generated/numpy.exp.html)
* [np.exp2](https://numpy.org/doc/stable/reference/generated/numpy.exp2.html)
* [np.expand\_dims](https://numpy.org/doc/stable/reference/generated/numpy.expand\_dims.html)
* [np.expm1](https://numpy.org/doc/stable/reference/generated/numpy.expm1.html)
* [np.fabs](https://numpy.org/doc/stable/reference/generated/numpy.fabs.html)
* [np.float\_power](https://numpy.org/doc/stable/reference/generated/numpy.float\_power.html)
* [np.floor](https://numpy.org/doc/stable/reference/generated/numpy.floor.html)
* [np.floor\_divide](https://numpy.org/doc/stable/reference/generated/numpy.floor\_divide.html)
* [np.fmax](https://numpy.org/doc/stable/reference/generated/numpy.fmax.html)
* [np.fmin](https://numpy.org/doc/stable/reference/generated/numpy.fmin.html)
* [np.fmod](https://numpy.org/doc/stable/reference/generated/numpy.fmod.html)
* [np.gcd](https://numpy.org/doc/stable/reference/generated/numpy.gcd.html)
* [np.greater](https://numpy.org/doc/stable/reference/generated/numpy.greater.html)
* [np.greater\_equal](https://numpy.org/doc/stable/reference/generated/numpy.greater\_equal.html)
* [np.heaviside](https://numpy.org/doc/stable/reference/generated/numpy.heaviside.html)
* [np.hypot](https://numpy.org/doc/stable/reference/generated/numpy.hypot.html)
* [np.invert](https://numpy.org/doc/stable/reference/generated/numpy.invert.html)
* [np.isfinite](https://numpy.org/doc/stable/reference/generated/numpy.isfinite.html)
* [np.isinf](https://numpy.org/doc/stable/reference/generated/numpy.isinf.html)
* [np.isnan](https://numpy.org/doc/stable/reference/generated/numpy.isnan.html)
* [np.lcm](https://numpy.org/doc/stable/reference/generated/numpy.lcm.html)
* [np.ldexp](https://numpy.org/doc/stable/reference/generated/numpy.ldexp.html)
* [np.left\_shift](https://numpy.org/doc/stable/reference/generated/numpy.left\_shift.html)
* [np.less](https://numpy.org/doc/stable/reference/generated/numpy.less.html)
* [np.less\_equal](https://numpy.org/doc/stable/reference/generated/numpy.less\_equal.html)
* [np.log](https://numpy.org/doc/stable/reference/generated/numpy.log.html)
* [np.log10](https://numpy.org/doc/stable/reference/generated/numpy.log10.html)
* [np.log1p](https://numpy.org/doc/stable/reference/generated/numpy.log1p.html)
* [np.log2](https://numpy.org/doc/stable/reference/generated/numpy.log2.html)
* [np.logaddexp](https://numpy.org/doc/stable/reference/generated/numpy.logaddexp.html)
* [np.logaddexp2](https://numpy.org/doc/stable/reference/generated/numpy.logaddexp2.html)
* [np.logical\_and](https://numpy.org/doc/stable/reference/generated/numpy.logical\_and.html)
* [np.logical\_not](https://numpy.org/doc/stable/reference/generated/numpy.logical\_not.html)
* [np.logical\_or](https://numpy.org/doc/stable/reference/generated/numpy.logical\_or.html)
* [np.logical\_xor](https://numpy.org/doc/stable/reference/generated/numpy.logical\_xor.html)
* [np.matmul](https://numpy.org/doc/stable/reference/generated/numpy.matmul.html)
* [np.maximum](https://numpy.org/doc/stable/reference/generated/numpy.maximum.html)
* [np.minimum](https://numpy.org/doc/stable/reference/generated/numpy.minimum.html)
* [np.multiply](https://numpy.org/doc/stable/reference/generated/numpy.multiply.html)
* [np.negative](https://numpy.org/doc/stable/reference/generated/numpy.negative.html)
* [np.nextafter](https://numpy.org/doc/stable/reference/generated/numpy.nextafter.html)
* [np.not\_equal](https://numpy.org/doc/stable/reference/generated/numpy.not\_equal.html)
* [np.ones\_like](https://numpy.org/doc/stable/reference/generated/numpy.ones\_like.html)
* [np.positive](https://numpy.org/doc/stable/reference/generated/numpy.positive.html)
* [np.power](https://numpy.org/doc/stable/reference/generated/numpy.power.html)
* [np.rad2deg](https://numpy.org/doc/stable/reference/generated/numpy.rad2deg.html)
* [np.radians](https://numpy.org/doc/stable/reference/generated/numpy.radians.html)
* [np.reciprocal](https://numpy.org/doc/stable/reference/generated/numpy.reciprocal.html)
* [np.remainder](https://numpy.org/doc/stable/reference/generated/numpy.remainder.html)
* [np.reshape](https://numpy.org/doc/stable/reference/generated/numpy.reshape.html)
* [np.right\_shift](https://numpy.org/doc/stable/reference/generated/numpy.right\_shift.html)
* [np.rint](https://numpy.org/doc/stable/reference/generated/numpy.rint.html)
* [np.round\_](https://numpy.org/doc/stable/reference/generated/numpy.round\_.html)
* [np.sign](https://numpy.org/doc/stable/reference/generated/numpy.sign.html)
* [np.signbit](https://numpy.org/doc/stable/reference/generated/numpy.signbit.html)
* [np.sin](https://numpy.org/doc/stable/reference/generated/numpy.sin.html)
* [np.sinh](https://numpy.org/doc/stable/reference/generated/numpy.sinh.html)
* [np.spacing](https://numpy.org/doc/stable/reference/generated/numpy.spacing.html)
* [np.sqrt](https://numpy.org/doc/stable/reference/generated/numpy.sqrt.html)
* [np.square](https://numpy.org/doc/stable/reference/generated/numpy.square.html)
* [np.subtract](https://numpy.org/doc/stable/reference/generated/numpy.subtract.html)
* [np.sum](https://numpy.org/doc/stable/reference/generated/numpy.sum.html)
* [np.tan](https://numpy.org/doc/stable/reference/generated/numpy.tan.html)
* [np.tanh](https://numpy.org/doc/stable/reference/generated/numpy.tanh.html)
* [np.transpose](https://numpy.org/doc/stable/reference/generated/numpy.transpose.html)
* [np.true\_divide](https://numpy.org/doc/stable/reference/generated/numpy.true\_divide.html)
* [np.trunc](https://numpy.org/doc/stable/reference/generated/numpy.trunc.html)
* [np.where](https://numpy.org/doc/stable/reference/generated/numpy.where.html)
* [np.zeros\_like](https://numpy.org/doc/stable/reference/generated/numpy.zeros\_like.html)
### Supported `ndarray` methods.
* [np.ndarray.astype](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.astype.html)
* [np.ndarray.clip](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.clip.html)
* [np.ndarray.dot](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.dot.html)
* [np.ndarray.flatten](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flatten.html)
* [np.ndarray.reshape](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.reshape.html)
* [np.ndarray.transpose](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.transpose.html)
### Supported `ndarray` properties.
* [np.ndarray.shape](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.shape.html)
* [np.ndarray.ndim](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.ndim.html)
* [np.ndarray.size](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.size.html)
* [np.ndarray.T](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.T.html)
## Limitations
### 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":`).
### 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)`).
### 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.

View File

@@ -1,27 +0,0 @@
# Exactness
One of the most common operations in **Concrete-Numpy** 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.
Let's say you have the table:
```python
[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 sometimes get `9` or `25`. Sometimes even `4` or `36` if you have a high probability of error.
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)`).
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.
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! For example, in some machine learning use cases, off-by-one or off-by-two errors doesn't affect the result much, in such cases `p_error` could be set to increase performance without losing accuracy.
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.
{% endhint %}

View File

@@ -1,46 +0,0 @@
# Installation
**Concrete Python** is natively supported on Linux and macOS for Python 3.8 onwards.
## Using PyPI
You can install **Concrete Python** from PyPI:
```shell
pip install -U pip wheel setuptools
pip install concrete-python
```
{% hint style="warning" %}
Apple Silicon is not supported for the time being. We're working on bringing support for it, which should arrive soon.
{% endhint %}
## Using Docker
You can also get the **Concrete Python** docker image:
```shell
docker pull zamafhe/concrete-python:v1.0.0
```
### Starting a Jupyter server.
By default, the entry point of the **Concrete Python** docker image is a jupyter server that you can access from your browser:
```shell
docker run --rm -it -p 8888:8888 zamafhe/concrete-python:v1.0.0
```
To save notebooks on host, you can use a local volume:
```shell
docker run --rm -it -p 8888:8888 -v /path/to/notebooks:/data zamafhe/concrete-python:v1.0.0
```
### Starting a Bash session.
Alternatively, you can launch a Bash session:
```shell
docker run --rm -it zamafhe/concrete-python:v1.0.0 /bin/bash
```

View File

@@ -1,104 +0,0 @@
# Performance
The most important operation in Concrete-Numpy is the table lookup operation. All operations except addition, subtraction, multiplication with non-encrypted values, and a few operations built with those primitive operations (e.g. matmul, conv) are converted to table lookups under the hood:
```python
import concrete.numpy as cnp
@cnp.compiler({"x": "encrypted"})
def f(x):
return x ** 2
inputset = range(2 ** 4)
circuit = f.compile(inputset)
```
is exactly the same as
```python
import concrete.numpy as cnp
table = cnp.LookupTable([x ** 2 for x in range(2 ** 4)])
@cnp.compiler({"x": "encrypted"})
def f(x):
return table[x]
inputset = range(2 ** 4)
circuit = f.compile(inputset)
```
Table lookups are very flexible, and they allow Concrete Numpy to support many operations, but they are expensive! 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.
The exact cost depend on many variables (machine configuration, error probability, etc.), but you can develop some intuition for single threaded CPU execution performance using:
```python
import time
import concrete.numpy as cnp
import numpy as np
WARMUP = 3
SAMPLES = 8
BITWIDTHS = range(1, 15)
CONFIGURATION = cnp.Configuration(
enable_unsafe_features=True,
use_insecure_key_cache=True,
insecure_key_cache_location=".keys",
)
timings = {}
for n in BITWIDTHS:
@cnp.compiler({"x": "encrypted"})
def base(x):
return x
table = cnp.LookupTable([np.sqrt(x).round().astype(np.int64) for x in range(2 ** n)])
@cnp.compiler({"x": "encrypted"})
def tlu(x):
return table[x]
inputset = [0, 2**n - 1]
base_circuit = base.compile(inputset, CONFIGURATION)
tlu_circuit = tlu.compile(inputset, CONFIGURATION)
print()
print(f"Generating keys for n={n}...")
base_circuit.keygen()
tlu_circuit.keygen()
timings[n] = []
for i in range(SAMPLES + WARMUP):
sample = np.random.randint(0, 2 ** n)
encrypted_sample = base_circuit.encrypt(sample)
start = time.time()
encrypted_result = base_circuit.run(encrypted_sample)
end = time.time()
assert base_circuit.decrypt(encrypted_result) == sample
base_time = end - start
encrypted_sample = tlu_circuit.encrypt(sample)
start = time.time()
encrypted_result = tlu_circuit.run(encrypted_sample)
end = time.time()
assert tlu_circuit.decrypt(encrypted_result) == np.sqrt(sample).round().astype(np.int64)
tlu_time = end - start
if i >= WARMUP:
timings[n].append(tlu_time - base_time)
print(f"Sample #{i - WARMUP + 1} took {timings[n][-1] * 1000:.3f}ms")
print()
for n, times in timings.items():
print(f"{n}-bits -> {np.mean(times) * 1000:.3f}ms")
```
{% hint style="info" %}
Concrete Numpy automatically parallelize execution if TLUs are applied to tensors.
{% endhint %}

View File

@@ -1,90 +0,0 @@
# 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-Numpy `Circuit`, which you can use to perform homomorphic evaluation.
Here is the full example that we will walk through:
```python
import concrete.numpy as cnp
def add(x, y):
return x + y
compiler = cnp.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)
```
## Importing the library
Everything you need to perform homomorphic evaluation is included in a single module:
<!--pytest-codeblocks:skip-->
```python
import concrete.numpy as cnp
```
## Defining the function to compile
In this example, we will compile a simple addition function:
<!--pytest-codeblocks:skip-->
```python
def add(x, y):
return 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:
<!--pytest-codeblocks:skip-->
```python
compiler = cnp.Compiler(add, {"x": "encrypted", "y": "clear"})
```
## 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 an iterable, yielding tuples of the same length as the number of arguments of the function being compiled:
<!--pytest-codeblocks:skip-->
```python
inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)]
```
{% hint style="warning" %}
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.
{% endhint %}
## 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:
<!--pytest-codeblocks:skip-->
```python
circuit = compiler.compile(inputset)
```
## Performing homomorphic evaluation
You can use the `encrypt_run_decrypt` method of a `Circuit` class to perform homomorphic evaluation:
<!--pytest-codeblocks:skip-->
```python
homomorphic_evaluation = circuit.encrypt_run_decrypt(4, 4)
```
{% hint style="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)))`.
{% endhint %}

View File

@@ -1,101 +0,0 @@
# Configure
The behavior of **Concrete-Numpy** can be customized using `Configuration`s:
```python
import concrete.numpy as cnp
import numpy as np
configuration = cnp.Configuration(p_error=0.01, loop_parallelize=True)
@cnp.compiler({"x": "encrypted"})
def f(x):
return x + 42
inputset = range(10)
circuit = f.compile(inputset, configuration=configuration)
```
Alternatively, you can overwrite individual options as kwargs to `compile` method:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.compiler({"x": "encrypted"})
def f(x):
return x + 42
inputset = range(10)
circuit = f.compile(inputset, p_error=0.01, loop_parallelize=True)
```
Or you can combine both:
```python
import concrete.numpy as cnp
import numpy as np
configuration = cnp.Configuration(p_error=0.01)
@cnp.compiler({"x": "encrypted"})
def f(x):
return x + 42
inputset = range(10)
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.
{% 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.
* **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.
* **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,265 +0,0 @@
# 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.
## Debug Artifacts
**Concrete-Numpy** 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.
```python
def f(x):
return np.sin(x)
```
This function fails to compile because **Concrete-Numpy** 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:
#### environment.txt
This file contains information about your setup (i.e., your operating system and python version).
```
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
```
#### requirements.txt
This file contains information about python packages and their versions installed on your system.
```
alabaster==0.7.12
appdirs==1.4.4
argon2-cffi==21.1.0
...
wheel==0.37.0
widgetsnbextension==3.5.1
wrapt==1.12.1
```
#### function.txt
This file contains information about the function you tried to compile.
```
def f(x):
return np.sin(x)
```
#### parameters.txt
This file contains information about the encryption status of the parameters of the function you tried to compile.
```
x :: encrypted
```
#### 1.initial.graph.txt
This file contains the textual representation of the initial computation graph right after tracing.
```
%0 = x # EncryptedScalar<uint3>
%1 = sin(%0) # EncryptedScalar<float64>
return %1
```
#### 1.initial.graph.png
This file contains the visual representation of the initial computation graph right after tracing.
![](../\_static/tutorials/artifacts/auto/1.initial.graph.png)
#### 2.final.graph.txt
This file contains the textual representation of the final computation graph right before MLIR conversion.
```
%0 = x # EncryptedScalar<uint3>
%1 = sin(%0) # EncryptedScalar<float64>
return %1
```
#### 2.final.graph.png
This file contains the visual representation of the final computation graph right before MLIR conversion.
![](../\_static/tutorials/artifacts/auto/2.final.graph.png)
#### traceback.txt
This file contains information about the error you received.
```
Traceback (most recent call last):
File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compilation/compiler.py", line 320, in compile
mlir = GraphConverter.convert(self.graph)
File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/mlir/graph_converter.py", line 298, in convert
GraphConverter._check_graph_convertibility(graph)
File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/mlir/graph_converter.py", line 175, in _check_graph_convertibility
raise RuntimeError(message)
RuntimeError: Function you are trying to compile cannot be converted to MLIR
%0 = x # EncryptedScalar<uint4>
%1 = sin(%0) # EncryptedScalar<float64>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported
return %1
```
### Manual export.
Manual exports are mostly used for visualization. Nonetheless, they can be very useful for demonstrations. Here is how to perform one:
```python
import concrete.numpy as cnp
import numpy as np
artifacts = cnp.DebugArtifacts("/tmp/custom/export/path")
@cnp.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()
```
If you go to the `/tmp/custom/export/path` directory, you'll see the following files:
#### 1.initial.graph.txt
This file contains the textual representation of the initial computation graph right after tracing.
```
%0 = 127 # ClearScalar<uint7>
%1 = 50 # ClearScalar<uint6>
%2 = 1 # ClearScalar<uint1>
%3 = x # EncryptedScalar<uint1>
%4 = sin(%3) # EncryptedScalar<float64>
%5 = add(%4, %2) # EncryptedScalar<float64>
%6 = multiply(%1, %5) # EncryptedScalar<float64>
%7 = astype(%6, dtype=int_) # EncryptedScalar<uint1>
%8 = subtract(%0, %7) # EncryptedScalar<uint1>
return %8
```
#### 1.initial.graph.png
This file contains the visual representation of the initial computation graph right after tracing.
![](../\_static/tutorials/artifacts/manual/1.initial.graph.png)
#### 2.after-float-fuse-0.graph.txt
This file contains the textual representation of the intermediate computation graph after fusing.
```
%0 = 127 # ClearScalar<uint7>
%1 = x # EncryptedScalar<uint1>
%2 = subgraph(%1) # EncryptedScalar<uint1>
%3 = subtract(%0, %2) # EncryptedScalar<uint1>
return %3
Subgraphs:
%2 = subgraph(%1):
%0 = 50 # ClearScalar<uint6>
%1 = 1 # ClearScalar<uint1>
%2 = input # EncryptedScalar<uint1>
%3 = sin(%2) # EncryptedScalar<float64>
%4 = add(%3, %1) # EncryptedScalar<float64>
%5 = multiply(%0, %4) # EncryptedScalar<float64>
%6 = astype(%5, dtype=int_) # EncryptedScalar<uint1>
return %6
```
#### 2.after-fusing.graph.png
This file contains the visual representation of the intermediate computation graph after fusing.
![](../\_static/tutorials/artifacts/manual/2.after-fusing.graph.png)
#### 3.final.graph.txt
This file contains the textual representation of the final computation graph right before MLIR conversion.
```
%0 = 127 # ClearScalar<uint7>
%1 = x # EncryptedScalar<uint3>
%2 = subgraph(%1) # EncryptedScalar<uint7>
%3 = subtract(%0, %2) # EncryptedScalar<uint7>
return %3
Subgraphs:
%2 = subgraph(%1):
%0 = 50 # ClearScalar<uint6>
%1 = 1 # ClearScalar<uint1>
%2 = input # EncryptedScalar<uint1>
%3 = sin(%2) # EncryptedScalar<float64>
%4 = add(%3, %1) # EncryptedScalar<float64>
%5 = multiply(%0, %4) # EncryptedScalar<float64>
%6 = astype(%5, dtype=int_) # EncryptedScalar<uint1>
return %6
```
#### 3.final.graph.png
This file contains the visual representation of the final computation graph right before MLIR conversion.
![](../\_static/tutorials/artifacts/manual/3.final.graph.png)
#### bounds.txt
This file contains information about the bounds of the final computation graph of the function you are compiling using the inputset you provide.
```
%0 :: [127, 127]
%1 :: [0, 7]
%2 :: [2, 95]
%3 :: [32, 125]
```
#### mlir.txt
This file contains information about the MLIR of the function you compiled using the inputset you provided.
```
module {
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>
}
}
```
## Asking the community
You can seek help with your issue by asking a question directly in the [community forum](https://community.zama.ai/).
## Submitting an issue
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:
* 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
In case of a feature request:
* try to give a minimal example of the desired behavior
* try to explain your use case

View File

@@ -1,132 +0,0 @@
# 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-Numpy**.
## Development of the circuit
You can develop your circuit like we've discussed in the previous chapters. Here is a simple example:
<!--pytest-codeblocks:skip-->
```python
import concrete.numpy as cnp
@cnp.compiler({"x": "encrypted"})
def function(x):
return x + 42
inputset = range(10)
circuit = function.compile(inputset)
```
Once you have your circuit, you can save everything the server needs like so:
<!--pytest-codeblocks:skip-->
```python
circuit.server.save("server.zip")
```
All you need to do now is to 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:
<!--pytest-codeblocks:skip-->
```python
import concrete.numpy as cnp
server = cnp.Server.load("server.zip")
```
At this point, 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:
<!--pytest-codeblocks:skip-->
```python
serialized_client_specs: str = server.client_specs.serialize()
```
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:
<!--pytest-codeblocks:skip-->
```python
client_specs = cnp.ClientSpecs.deserialize(serialized_client_specs)
client = cnp.Client(client_specs)
```
## Generating keys (on the client)
Once you have the `Client` object, you can perform key generation:
<!--pytest-codeblocks:skip-->
```python
client.keygen()
```
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:
<!--pytest-codeblocks:skip-->
```python
serialized_evaluation_keys: bytes = client.evaluation_keys.serialize()
```
After serialization, you can 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.
{% endhint %}
## 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:
<!--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.
## Performing computation (on the server)
Upon having the serialized evaluation keys and serialized arguments, you can deserialize them like so:
<!--pytest-codeblocks:skip-->
```python
deserialized_evaluation_keys = cnp.EvaluationKeys.deserialize(serialized_evaluation_keys)
deserialized_args = server.client_specs.deserialize_public_args(serialized_args)
```
And you can perform the computation as well:
<!--pytest-codeblocks:skip-->
```python
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.
## Decrypting the result (on the client)
Once you have received the public result of the computation from the server, you can deserialize it:
<!--pytest-codeblocks:skip-->
```python
deserialized_public_result = client.specs.deserialize_public_result(serialized_public_result)
```
Finally, you can decrypt the result like so:
<!--pytest-codeblocks:skip-->
```python
result = client.decrypt(deserialized_public_result)
assert result == 49
```

View File

@@ -1,21 +0,0 @@
Name Version License
Pillow 9.4.0 Historical Permission Notice and Disclaimer (HPND)
PyYAML 6.0 MIT License
concrete-compiler 0.24.0rc5 BSD-3
cycler 0.11.0 BSD License
fonttools 4.38.0 MIT License
kiwisolver 1.4.4 BSD License
matplotlib 3.5.3 Python Software Foundation License
networkx 2.6.3 BSD License
numpy 1.24.2 BSD License
nvidia-cublas-cu11 11.10.3.66 Other/Proprietary License
nvidia-cuda-nvrtc-cu11 11.7.99 Other/Proprietary License
nvidia-cuda-runtime-cu11 11.7.99 Other/Proprietary License
nvidia-cudnn-cu11 8.5.0.96 Other/Proprietary License
packaging 23.0 Apache Software License; BSD License
pyparsing 3.0.9 MIT License
python-dateutil 2.8.2 Apache Software License; BSD License
scipy 1.10.1 BSD License
six 1.16.0 MIT License
torch 1.13.1 BSD License
typing-extensions 3.10.0.2 Python Software Foundation License

View File

@@ -1,20 +0,0 @@
# 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:
```python
import concrete.numpy as cnp
@cnp.compiler({"x": "encrypted"})
def f(x):
return x + 42
inputset = range(10)
circuit = f.compile(inputset)
assert circuit.encrypt_run_decrypt(10) == f(10)
```
{% hint style="info" %}
Think of this decorator as a way to add the `compile` method to the function object without changing its name elsewhere.
{% endhint %}

View File

@@ -1,83 +0,0 @@
# 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.
{% 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:
```python
import concrete.numpy as cnp
@cnp.circuit({"x": "encrypted"})
def circuit(x: cnp.uint8):
return x + 42
assert circuit.encrypt_run_decrypt(10) == 52
```
There are a few differences between direct circuits and traditional circuits though:
- 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: cnp.uint8`, you'll not get the negative value as the result will be `cnp.uint8` as well)
- You need to use cnp types in `.astype(...)` calls (e.g., `np.sqrt(x).astype(cnp.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#cnpunivariatefunction) extension (e.g., `cnp.univariate(function, outputs=cnp.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!
Let's go over a more complicated example to see how direct circuits behave:
```python
import concrete.numpy as cnp
import numpy as np
def square(value):
return value ** 2
@cnp.circuit({"x": "encrypted", "y": "encrypted"})
def circuit(x: cnp.uint8, y: cnp.int2):
a = x + 10
b = y + 10
c = np.sqrt(a).round().astype(cnp.uint4)
d = cnp.univariate(square, outputs=cnp.uint8)(b)
return d - c
print(circuit)
```
prints
```
%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
```
And 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
%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
```
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.

View File

@@ -1,153 +0,0 @@
# Extensions
**Concrete-Numpy** tries to support **NumPy** as much as possible, but due to some technical limitations, not everything can be supported. On top of that, there are some things **NumPy** lack, which are useful. In some of these situations, we provide extensions in **Concrete-Numpy** to improve your experience.
## cnp.zero()
Allows you to create encrypted scalar zero:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.compiler({"x": "encrypted"})
def f(x):
z = cnp.zero()
return x + z
inputset = range(10)
circuit = f.compile(inputset)
for x in range(10):
assert circuit.encrypt_run_decrypt(x) == x
```
## cnp.zeros(shape)
Allows you to create encrypted tensor of zeros:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.compiler({"x": "encrypted"})
def f(x):
z = cnp.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]]))
```
## cnp.one()
Allows you to create encrypted scalar one:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.compiler({"x": "encrypted"})
def f(x):
z = cnp.one()
return x + z
inputset = range(10)
circuit = f.compile(inputset)
for x in range(10):
assert circuit.encrypt_run_decrypt(x) == x + 1
```
## cnp.ones(shape)
Allows you to create encrypted tensor of ones:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.compiler({"x": "encrypted"})
def f(x):
z = cnp.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)
```
## cnp.univariate(function)
Allows you to wrap any univariate function into a single table lookup:
```python
import concrete.numpy as cnp
import numpy as np
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)
@cnp.compiler({"x": "encrypted"})
def f(x):
return cnp.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))
```
{% hint style="danger" %}
The wrapped function shouldn't have any side effects, and it should be deterministic.
{% endhint %}
## coonx.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 concrete.numpy as cnp
import concrete.onnx as connx
import numpy as np
weight = np.array([[2, 1], [3, 2]]).reshape(1, 1, 2, 2)
@cnp.compiler({"x": "encrypted"})
def f(x):
return connx.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))
```
{% hint style="danger" %}
Only 2D convolutions with one groups and without padding are supported for the time being.
{% endhint %}

View File

@@ -1,79 +0,0 @@
# Floating Points
**Concrete-Numpy** partly supports floating points:
* They cannot be inputs
* They cannot be outputs
* They can be intermediate values under certain constraints
## 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.
As long as your floating point operations comply with those constraints, **Concrete-Numpy** automatically converts your operations to a table lookup operation:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.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)
```
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-Numpy** detects this and fuses all of those 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:
<!--pytest-codeblocks:skip-->
```python
import concrete.numpy as cnp
import numpy as np
@cnp.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)
```
... results in:
```
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
```
The reason for this is that `d` no longer depends solely on `x`, it depends on `y` as well. **Concrete-Numpy** cannot fuse these operations, so it raises an exception instead.

View File

@@ -1,19 +0,0 @@
# Formatting
You can convert your compiled circuit into its textual representation by converting it to string:
<!--pytest-codeblocks:skip-->
```python
str(circuit)
```
If you just want to see the output on your terminal, you can directly print it as well:
<!--pytest-codeblocks:skip-->
```python
print(circuit)
```
{% hint style="warning" %}
Formatting is just for debugging. It's not possible to serialize the circuit back from its textual representation. See [How to Deploy](../howto/deploy.md) if that's your goal.
{% endhint %}

View File

@@ -1,3 +0,0 @@
# Key Value Database
This is an interactive tutorial written as a Jupyter Notebook, which you can find [here](https://github.com/zama-ai/concrete-numpy/blob/main/docs/tutorial/key_value_database.ipynb).

View File

@@ -1,183 +0,0 @@
# 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.
{% 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.
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.
Imagine you have an 8-bit value, but you want to have a 5-bit table lookup, you can call `cnp.round_bit_pattern(input, lsbs_to_remove=3)` and use the value you get in the table lookup.
In Python, evaluation will work like the following:
```
0b_0000_0000 => 0b_0000_0000
0b_0000_0001 => 0b_0000_0000
0b_0000_0010 => 0b_0000_0000
0b_0000_0011 => 0b_0000_0000
0b_0000_0100 => 0b_0000_1000
0b_0000_0101 => 0b_0000_1000
0b_0000_0110 => 0b_0000_1000
0b_0000_0111 => 0b_0000_1000
0b_1010_0000 => 0b_1010_0000
0b_1010_0001 => 0b_1010_0000
0b_1010_0010 => 0b_1010_0000
0b_1010_0011 => 0b_1010_0000
0b_1010_0100 => 0b_1010_1000
0b_1010_0101 => 0b_1010_1000
0b_1010_0110 => 0b_1010_1000
0b_1010_0111 => 0b_1010_1000
0b_1010_1000 => 0b_1010_1000
0b_1010_1001 => 0b_1010_1000
0b_1010_1010 => 0b_1010_1000
0b_1010_1011 => 0b_1010_1000
0b_1010_1100 => 0b_1011_0000
0b_1010_1101 => 0b_1011_0000
0b_1010_1110 => 0b_1011_0000
0b_1010_1111 => 0b_1011_0000
0b_1011_1000 => 0b_1011_1000
0b_1011_1001 => 0b_1011_1000
0b_1011_1010 => 0b_1011_1000
0b_1011_1011 => 0b_1011_1000
0b_1011_1100 => 0b_1100_0000
0b_1011_1101 => 0b_1100_0000
0b_1011_1110 => 0b_1100_0000
0b_1011_1111 => 0b_1100_0000
```
and during homomorphic execution, it'll be converted like this:
```
0b_0000_0000 => 0b_00000
0b_0000_0001 => 0b_00000
0b_0000_0010 => 0b_00000
0b_0000_0011 => 0b_00000
0b_0000_0100 => 0b_00001
0b_0000_0101 => 0b_00001
0b_0000_0110 => 0b_00001
0b_0000_0111 => 0b_00001
0b_1010_0000 => 0b_10100
0b_1010_0001 => 0b_10100
0b_1010_0010 => 0b_10100
0b_1010_0011 => 0b_10100
0b_1010_0100 => 0b_10101
0b_1010_0101 => 0b_10101
0b_1010_0110 => 0b_10101
0b_1010_0111 => 0b_10101
0b_1010_1000 => 0b_10101
0b_1010_1001 => 0b_10101
0b_1010_1010 => 0b_10101
0b_1010_1011 => 0b_10101
0b_1010_1100 => 0b_10110
0b_1010_1101 => 0b_10110
0b_1010_1110 => 0b_10110
0b_1010_1111 => 0b_10110
0b_1011_1000 => 0b_10111
0b_1011_1001 => 0b_10111
0b_1011_1010 => 0b_10111
0b_1011_1011 => 0b_10111
0b_1011_1100 => 0b_11000
0b_1011_1101 => 0b_11000
0b_1011_1110 => 0b_11000
0b_1011_1111 => 0b_11000
```
and then 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:
```python
import matplotlib.pyplot as plt
def relu(x):
return x if x >= 0 else 0
xs = range(-100_000, 100_000)
ys = [relu(x) for x in xs]
plt.plot(xs, ys)
plt.show()
```
![](../_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:
```python
import concrete.numpy as cnp
import matplotlib.pyplot as plt
import numpy as np
def relu(x):
return x if x >= 0 else 0
@cnp.compiler({"x": "encrypted"})
def f(x):
x = cnp.round_bit_pattern(x, lsbs_to_remove=10)
return cnp.univariate(relu)(x)
inputset = [-100_000, (100_000 - 1)]
circuit = f.compile(inputset)
xs = range(-100_000, 100_000)
ys = [circuit.simulate(x) for x in xs]
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:
![](../_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:
![](../_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.
```python
import concrete.numpy as cnp
import matplotlib.pyplot as plt
import numpy as np
rounder = cnp.AutoRounder(target_msbs=6)
def relu(x):
return x if x >= 0 else 0
@cnp.compiler({"x": "encrypted"})
def f(x):
x = cnp.round_bit_pattern(x, lsbs_to_remove=rounder)
return cnp.univariate(relu)(x)
inputset = [-100_000, (100_000 - 1)]
cnp.AutoRounder.adjust(f, inputset) # alternatively, you can use `auto_adjust_rounders=True` below
circuit = f.compile(inputset)
xs = range(-100_000, 100_000)
ys = [circuit.simulate(x) for x in xs]
plt.plot(xs, ys)
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 `cnp.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:
![](../_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:
![](../_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.
{% endhint %}

View File

@@ -1,37 +0,0 @@
# 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)).
Considering these, simulation is introduced:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.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)
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())
```
prints
```
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[1, 4, 9, 16, 16, 36, 49, 64, 81, 100]
```
{% 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.
{% endhint %}

View File

@@ -1,178 +0,0 @@
# Table Lookups
In this tutorial, we will review the ways to perform direct table lookups in **Concrete-Numpy**.
## Direct table lookup
**Concrete-Numpy** provides a `LookupTable` class for you 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.
If you go out of bounds of this range, you will get the following error:
```
IndexError: index 10 is out of bounds for axis 0 with size 6
```
{% endhint %}
{% hint style="info" %}
The number of elements in the lookup table doesn't affect performance in any way.
{% endhint %}
### With scalars.
You can create the lookup table using a list of integers and apply it using indexing:
```python
import concrete.numpy as cnp
table = cnp.LookupTable([2, -1, 3, 0])
@cnp.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
```
### With tensors.
When you apply the table lookup to a tensor, you apply the scalar table lookup to each element of the tensor:
```python
import concrete.numpy as cnp
import numpy as np
table = cnp.LookupTable([2, -1, 3, 0])
@cnp.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]]
```
### 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:
```python
import concrete.numpy as cnp
table = cnp.LookupTable([2, -1, 3, 0])
@cnp.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
```
## 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:
```python
import concrete.numpy as cnp
import numpy as np
squared = cnp.LookupTable([i ** 2 for i in range(4)])
cubed = cnp.LookupTable([i ** 3 for i in range(4)])
table = cnp.LookupTable([
[squared, cubed],
[squared, cubed],
[squared, cubed],
])
@cnp.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]]
```
In this example, we applied a `squared` table to the first column and a `cubed` table to the second one.
## Fused table lookup
**Concrete-Numpy** tries to fuse some operations into table lookups automatically, so you don't need to create the lookup tables manually:
```python
import concrete.numpy as cnp
import numpy as np
@cnp.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)
```
{% hint style="info" %}
All lookup tables need to be from integers to integers. So, without `.astype(np.int64)`, **Concrete-Numpy** will not be able to fuse.
{% endhint %}
The function is first traced into:
![](../\_static/tutorials/table-lookup/1.initial.graph.png)
Then, **Concrete-Numpy** fuses appropriate nodes:
![](../\_static/tutorials/table-lookup/3.final.graph.png)
{% hint style="info" %}
Fusing makes the code more readable and easier to modify, so try to utilize it over manual `LookupTable`s as much as possible.
{% endhint %}

View File

@@ -1,56 +0,0 @@
# 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:
```python
def g(z):
with cnp.tag("def"):
a = 120 - z
b = a // 4
return b
def f(x):
with cnp.tag("abc"):
x = x * 2
with cnp.tag("foo"):
y = x + 42
z = np.sqrt(y).astype(np.int64)
return g(z + 3) * 2
```
when you compile `f` with inputset of `range(10)`, you get the following graph:
```
%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
```
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).
{% 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.
{% endhint %}