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

@@ -2,7 +2,7 @@
name: New Operator
about: Organise the support of a new operator or notion in the framework.
labels: feature
title: Support of **please-fill** in Concrete Numpy
title: Support of **please-fill** in Concrete
---
## Umbrella

View File

@@ -12,7 +12,7 @@ jobs:
- name: Check first line
uses: gsactions/commit-message-checker@v1
with:
pattern: '^(feat|fix|test|bench|doc|chore|refactor|perf)\((compiler|backend|frontend|optimizer|tools|ci).*\): '
pattern: '^(feat|fix|test|bench|docs|chore|refactor|perf)\((compiler|backend|frontend|optimizer|tools|ci|common).*\): '
flags: 'gs'
error: 'Your first line has to contain a commit type and scope like "feat(my_feature): msg".'
excludeDescription: 'true' # optional: this excludes the description body of a pull request

165
README.md
View File

@@ -1,21 +1,158 @@
# Concrete
<p align="center">
<!-- product name logo -->
<img width=600 src="https://user-images.githubusercontent.com/5758427/177340641-f152edb7-1957-49a3-86ab-246774701aab.png">
</p>
The `concrete` project is a set of crates that implements Zama's variant of
[TFHE](https://eprint.iacr.org/2018/421.pdf) and make it easy to use. In a nutshell,
[fully homomorphic encryption (FHE)](https://en.wikipedia.org/wiki/Homomorphic_encryption), allows
you to perform computations over encrypted data, allowing you to implement Zero Trust services.
<p align="center">
<!-- Version badge using shields.io -->
<a href="https://github.com/zama-ai/concrete/releases">
<img src="https://img.shields.io/github/v/release/zama-ai/concrete?style=flat-square">
</a>
<!-- Link to docs badge using shields.io -->
<a href="https://docs.zama.ai/concrete/">
<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">
<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/developer/contributing">
<img src="https://img.shields.io/badge/we're%20open%20source-contributing.md-blue?style=flat-square">
</a>
<!-- Follow on 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 is based on the
[Learning With Errors (LWE)](https://cims.nyu.edu/~regev/papers/lwesurvey.pdf) and the
[Ring Learning With Errors (RLWE)](https://eprint.iacr.org/2012/230.pdf) problems, which are well
studied cryptographic hardness assumptions believed to be secure even against quantum computers.
**Concrete** is an open-source framework which simplifies the use of fully homomorphic encryption (FHE).
FHE is a powerful cryptographic tool, which allows computation to be performed directly on encrypted data without needing to decrypt it first. With FHE, you can build services that preserve privacy for all users. FHE is also great against data breaches as everything is done on encrypted data. Even if the server is compromised, in the end no sensitive data is leaked.
Since writing FHE program is hard, concrete framework contains a TFHE Compiler based on LLVM to make this process easier for developers.
## 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 | No |
| Windows Subsystem for Linux | Yes | Yes |
| macOS (Intel) | Yes | Yes |
| macOS (Apple Silicon) | Yes | Yes |
The preferred way to install Concrete is through PyPI:
```shell
pip install concrete-python
```
You can get the concrete-python docker image by pulling the latest docker image:
```shell
docker pull zamafhe/concrete-python:v1.0.0
```
You can find more detailed installation instructions in [installing.md](docs/getting-started/installing.md)
## Getting started
```python
from concrete import fhe
def add(x, y):
return x + y
compiler = fhe.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
from concrete import fhe
@fhe.circuit({"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](https://docs.zama.ai/concrete).
## Target users
Concrete 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.
One such example is [Concrete ML](https://github.com/zama-ai/concrete-ml), which is built on top of Concrete 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 with [Decorators](https://docs.zama.ai/concrete/tutorials/decorator)
- Partial support of [Floating Points](https://docs.zama.ai/concrete/tutorials/floating_points)
- How to perform [Table Lookup](https://docs.zama.ai/concrete/tutorials/table_lookup)
More generally, if you have built awesome projects using Concrete, feel free to let us know and we'll link to it!
## Project layout
The `concrete` project is a set of several modules which are high-level frontends, compilers, backends and side tools.
- The `frontends` directory contains a `python` frontend.
- The `compilers` directory contains the `concrete-compiler` and `concrete-optimizer` modules. The `concrete-compiler` is a compiler that synthetize a FHE computation dag expressed as a [MLIR](https://mlir.llvm.org/) dialect, compile to a set of artifacts, and provide tools to manipulate those artifacts at runtime. The `concrete-optimizer` is a specific module used by the compiler to find the best, secure and accurate set of crypto parameters for a given dag.
- The `backends` directory contains implementations of cryptographic primitives on different computation unit, used by the `concrete-compiler` runtime. The `concrete-cpu` module provides CPU implementation, while `concrete-cuda` module provides GPU implementation using the CUDA platform.
`concrete` project is a set of several modules which are high-level frontends, compilers, backends and side tools.
- `frontends` directory contains a `python` frontend.
- `compilers` directory contains the `concrete-compiler` and `concrete-optimizer` modules. `concrete-compiler` is a compiler that:
- synthetize a FHE computation dag expressed as a [MLIR](https://mlir.llvm.org/) dialect
- compile to a set of artifacts
- and provide tools to manipulate those artifacts at runtime.
`concrete-optimizer` is a specific module used by the compiler to find the best, secure and accurate set of cryptographic parameters for a given dag.
- The `backends` directory contains implementations of cryptographic primitives on different computation unit, used by `concrete-compiler` runtime. `concrete-cpu` module provides CPU implementation, while `concrete-cuda` module provides GPU implementation using the CUDA platform.
- The `tools` directory contains side tools used by the rest of the project.
## 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.

65
UPGRADING.md Normal file
View File

@@ -0,0 +1,65 @@
# Upgrading Guide
## From `Concrete Numpy v0.x` To `Concrete v1`
### The PyPI package `concrete-numpy` is now called `concrete-python`.
### The module `concrete.numpy` is now called `concrete.fhe` and we advise you to use:
```python
from concrete import fhe
```
instead of the previous:
```python
import concrete.numpy as cnp
```
### The module `concrete.onnx` is merged into `concrete.fhe` so we advise you to use:
```python
from concrete import fhe
fhe.conv(...)
fhe.maxpool(...)
```
instead of the previous:
```python
from concrete.onnx import connx
connx.conv(...)
connx.maxpool(...)
```
### Virtual configuration option is removed. Simulation is still supported using the new `simulate` method on circuits:
```python
from concrete import fhe
@fhe.compiler({"x": "encrypted"})
def f(x):
return x + 42
inputset = range(10)
circuit = f.compile(inputset)
assert circuit.simulate(1) == 43
```
instead of the previous:
```python
import concrete.numpy as cnp
@cnp.compiler({"x": "encrypted"})
def f(x):
return x + 42
inputset = range(10)
circuit = f.compile(inputset, enable_unsafe_features=True, virtual=True)
assert circuit.encrypt_run_decrypt(1) == 43
```

36
docs/README.md Normal file
View File

@@ -0,0 +1,36 @@
# What is Concrete?
[<mark style="background-color:yellow;">⭐️ Star the repo on Github</mark>](https://github.com/zama-ai/concrete) <mark style="background-color:yellow;">| 🗣</mark> [<mark style="background-color:yellow;">Community support forum</mark>](https://community.zama.ai/c/concrete) <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** is an open-source framework which simplifies the use of fully homomorphic encryption (FHE).
FHE is a powerful cryptographic tool, which allows computation to be performed directly on encrypted data without needing to decrypt it first. With FHE, you can build services that preserve privacy for all users. FHE is also great against data breaches as everything is done on encrypted data. Even if the server is compromised, in the end no sensitive data is leaked.
Since writing FHE program is hard, concrete framework contains a TFHE Compiler based on LLVM to make this process easier for developers.
## Organization of this documentation
This documentation is split into several sections:
* **Getting Started** gives you the basics,
* **Tutorials** gives you some essential examples on various features of the library,
* **How to** helps you perform specific tasks,
* **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**
## How is it different from Concrete Numpy?
Concrete Numpy was the former name of Concrete Compiler Python's frontend. Starting from v1, Concrete Compiler is now open sourced and the package name is updated from `concrete-numpy` to `concrete-python` (as `concrete` is already booked for a non FHE-related project).
Users from Concrete-Numpy could safely update to Concrete with few changes explained in the [upgrading document](https://github.com/zama-ai/concrete/blob/main/UPGRADING.md).
## How is it different from the previous Concrete (v0.x)?
Before v1.0, Concrete was a set of Rust libraries implementing Zama's variant of TFHE. Starting with v1, Concrete is now Zama's TFHE compiler framework only. Rust library could be found under [TFHE-rs](https://github.com/zama-ai/tfhe-rs) project.

View File

@@ -1,6 +1,6 @@
# Table of contents
* [What is Concrete Numpy?](README.md)
* [What is Concrete?](README.md)
## Getting Started
@@ -31,10 +31,7 @@
## 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)

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 376 KiB

1
docs/conftest.py Symbolic link
View File

@@ -0,0 +1 @@
../frontends/concrete-python/tests/conftest.py

View File

@@ -6,9 +6,7 @@ The next step in the compilation is transforming the computation graph. There ar
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.
The final step is to transform the computation graph to equivalent `MLIR` code. Once the MLIR is generated, our compiler backend compiles it down to native binaries.
## Tracing
@@ -49,7 +47,7 @@ Tracing is also responsible for indicating whether the values in the node would
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.
With the current version of **Concrete**, floating-point inputs and floating-point outputs are not supported. However, if the floating-point operations are intermediate operations, they can sometimes be fused into a single table lookup from integer to integer, thanks to some specific transforms.
Let's take a closer look at the transforms we can currently perform.
@@ -61,7 +59,7 @@ We have allocated a whole new chapter to explaining fusing. You can find it afte
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`.
Let's say we have an encrypted input that is always between `0` and `10`. We should assign the type `EncryptedScalar<uint4>` to the node of this input as `EncryptedScalar<uint4>` is the minimal encrypted integer that supports all values between `0` and `10`.
If there were negative values in the range, we could have used `intX` instead of `uintX`.
@@ -137,12 +135,8 @@ New Bounds:
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).
* `x`: EncryptedScalar<**uint2**>
* `2`: ClearScalar<**uint2**>
* `*`: EncryptedScalar<**uint3**>
* `3`: ClearScalar<**uint2**>
* `+`: EncryptedScalar<**uint4**>

6
docs/dev/contributing.md Normal file
View File

@@ -0,0 +1,6 @@
# Contribute
There are two ways to contribute to **Concrete**:
* 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!

33
docs/dev/fusing.md Normal file
View File

@@ -0,0 +1,33 @@
# 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 `frontends/concrete-python/concrete/fhe/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.9. 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 don't have a common lca (e.g., `np.round(np.sin(x) + np.cos(y))`).

View File

@@ -2,7 +2,7 @@
## 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.
Throughout the quarter, many release candidates are released. 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

View File

@@ -0,0 +1,24 @@
# 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: a technique that takes a python function from the user and generates a corresponding computation graph
* bounds: before computation graphs are converted to MLIR, we need to know which value should have which type (e.g., uint3 vs int5). we use inputsets to do that. we simulate the graph with the inputs in the inputset to remember the minimum and the maximum value for each node, which is what we call bounds, and use bounds to determine the appropriate type for each node.
* circuit: the result of compilation. a circuit is made of the client and server components. it has methods for everything from printing to evaluation.
## Module structure
In this section, we will briefly discuss the module structure of **Concrete-Python**. You are encouraged to check individual `.py` files to learn more.
* concrete
* fhe
* dtypes: data type specifications (e.g., int4, uint5, float32)
* values: value specifications (i.e., data type + shape + encryption status)
* representation: representation of computation (e.g., computation graphs, nodes)
* tracing: tracing of python functions
* extensions: custom functionality (see [Extensions](../tutorial/extensions.md))
* mlir: computation graph to mlir conversion
* compilation: configuration, compiler, artifacts, circuit, client/server, and anything else related to compilation

View File

@@ -1,6 +1,6 @@
# 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.
One of the most common operations in **Concrete** is `Table Lookups` (TLUs). TLUs are performed with an FHE operation called `Programmable Bootstrapping` (PBS). PBSes have a certain probability of error, which, when triggered, result in inaccurate results.
Let's say you have the table:
@@ -8,7 +8,7 @@ Let's say you have the table:
[0, 1, 4, 9, 16, 25, 36, 49, 64]
```
And you performed a table lookup using `4`. The result you should get is `16`, but because of the possibility of error, you can sometimes get `9` or `25`. Sometimes even `4` or `36` if you have a high probability of error.
And you performed a table lookup using `4`. The result you should get is `16`, but because of the possibility of error, you can get any other value in the table.
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.
@@ -18,10 +18,10 @@ However, if you set `global_p_error` to `0.01`, the whole circuit will have 1% p
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.
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!
See [How to Configure](../howto/configure.md) to learn how you can set a custom `p_error` and/or `global_p_error`.
{% hint style="info" %}
Configuring either of those variables would affect computation time (compilation, keys generation, circuit execution) and space requirements (size of the keys on disk and in memory). Lower error probability would result in longer computation time and larger space requirements.
Configuring either of those variables 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

@@ -0,0 +1,21 @@
# Installation
**Concrete** is natively supported on Linux and macOS from Python 3.8 to 3.10 inclusive, but if you have Docker in your platform, you can use the docker image to use **Concrete**.
## Using PyPI
You can install **Concrete** from PyPI:
```shell
pip install -U pip wheel setuptools
pip install concrete-python
```
## Using Docker
You can also get the **Concrete** docker image:
```shell
docker pull zamafhe/concrete-python:v1.0.0
docker run --rm -it zamafhe/concrete-python:latest /bin/bash
```

View File

@@ -0,0 +1,35 @@
# Performance
One of the most common operations in **Concrete** is `Table Lookups` (TLUs). All operations except addition, subtraction, multiplication with non-encrypted values, tensor manipulation operations, and a few operations built with those primitive operations (e.g. matmul, conv) are converted to table lookups under the hood:
```python
from concrete import fhe
@fhe.compiler({"x": "encrypted"})
def f(x):
return x ** 2
inputset = range(2 ** 4)
circuit = f.compile(inputset)
```
is exactly the same as
```python
from concrete import fhe
table = fhe.LookupTable([x ** 2 for x in range(2 ** 4)])
@fhe.compiler({"x": "encrypted"})
def f(x):
return table[x]
inputset = range(2 ** 4)
circuit = f.compile(inputset)
```
Table lookups are very flexible! They allow Concrete to support many operations, but they are expensive. The exact cost depends on many variables (hardware used, error probability, etc.) but they are always much more expensive compared to other operations. Therefore, you should try to avoid them as much as possible. In most cases, it's not possible to avoid them completely, but you might remove the number of TLUs or replace some of them with other primitive operations.
{% hint style="info" %}
Concrete automatically parallelize TLUs if they are applied to tensors.
{% endhint %}

View File

@@ -1,16 +1,16 @@
# 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.
To compute on encrypted data, you first need to define the function that you want to compute, then compile it into a Concrete `Circuit`, which you can use to perform homomorphic evaluation.
Here is the full example that we will walk through:
```python
import concrete.numpy as cnp
from concrete import fhe
def add(x, y):
return x + y
compiler = cnp.Compiler(add, {"x": "encrypted", "y": "clear"})
compiler = fhe.Compiler(add, {"x": "encrypted", "y": "clear"})
inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)]
circuit = compiler.compile(inputset)
@@ -30,7 +30,7 @@ Everything you need to perform homomorphic evaluation is included in a single mo
<!--pytest-codeblocks:skip-->
```python
import concrete.numpy as cnp
from concrete import fhe
```
## Defining the function to compile
@@ -49,7 +49,7 @@ To compile the function, you need to create a `Compiler` by specifying the funct
<!--pytest-codeblocks:skip-->
```python
compiler = cnp.Compiler(add, {"x": "encrypted", "y": "clear"})
compiler = fhe.Compiler(add, {"x": "encrypted", "y": "clear"})
```
## Defining an inputset

View File

@@ -1,14 +1,14 @@
# Configure
The behavior of **Concrete-Numpy** can be customized using `Configuration`s:
The behavior of **Concrete** can be customized using `Configuration`s:
```python
import concrete.numpy as cnp
from concrete import fhe
import numpy as np
configuration = cnp.Configuration(p_error=0.01, loop_parallelize=True)
configuration = fhe.Configuration(p_error=0.01, dataflow_parallelize=True)
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return x + 42
@@ -19,26 +19,26 @@ circuit = f.compile(inputset, configuration=configuration)
Alternatively, you can overwrite individual options as kwargs to `compile` method:
```python
import concrete.numpy as cnp
from concrete import fhe
import numpy as np
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return x + 42
inputset = range(10)
circuit = f.compile(inputset, p_error=0.01, loop_parallelize=True)
circuit = f.compile(inputset, p_error=0.01, dataflow_parallelize=True)
```
Or you can combine both:
```python
import concrete.numpy as cnp
from concrete import fhe
import numpy as np
configuration = cnp.Configuration(p_error=0.01)
configuration = fhe.Configuration(p_error=0.01)
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return x + 42
@@ -66,18 +66,21 @@ Additional kwarg to `compile` function have higher precedence. So if you set an
* **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
* **auto\_adjust\_rounders**: bool = False
* Whether to adjust rounders automatically.
* **p_error**: Optional[float] = None
* **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.
* **global\_p\_error**: Optional[float] = None
* Global error probability for the whole circuit. If set, the whole circuit will have the probability of non-exact result smaller than the set value. See [Exactness](../getting-started/exactness.md) to learn more.
* **single_precision**: bool = True
* Whether to use single precision for the whole circuit.
* **jit**: bool = False
* Whether to use JIT compilation.

305
docs/howto/debug.md Normal file
View File

@@ -0,0 +1,305 @@
# 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** 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** 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.
```
astroid==2.15.0
attrs==22.2.0
auditwheel==5.3.0
...
wheel==0.40.0
wrapt==1.15.0
zipp==3.15.0
```
#### 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
```
#### 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
```
#### traceback.txt
This file contains information about the error you received.
```
Traceback (most recent call last):
File "/path/to/your/script.py", line 9, in <module>
circuit = f.compile(inputset)
File "/usr/local/lib/python3.10/site-packages/concrete/fhe/compilation/decorators.py", line 159, in compile
return self.compiler.compile(inputset, configuration, artifacts, **kwargs)
File "/usr/local/lib/python3.10/site-packages/concrete/fhe/compilation/compiler.py", line 437, in compile
mlir = GraphConverter.convert(self.graph)
File "/usr/local/lib/python3.10/site-packages/concrete/fhe/mlir/graph_converter.py", line 677, in convert
GraphConverter._check_graph_convertibility(graph)
File "/usr/local/lib/python3.10/site-packages/concrete/fhe/mlir/graph_converter.py", line 240, in _check_graph_convertibility
raise RuntimeError(message)
RuntimeError: Function you are trying to compile cannot be converted to MLIR
%0 = x # EncryptedScalar<uint3> ∈ [3, 5]
%1 = sin(%0) # EncryptedScalar<float64> ∈ [-0.958924, 0.14112]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported
/path/to/your/script.py:6
return %1
```
### Manual export.
Manual exports are mostly used for visualization. Nonetheless, they can be very useful for demonstrations. Here is how to perform one:
```python
from concrete import fhe
import numpy as np
artifacts = fhe.DebugArtifacts("/tmp/custom/export/path")
@fhe.compiler({"x": "encrypted"})
def f(x):
return 127 - (50 * (np.sin(x) + 1)).astype(np.int64)
inputset = range(2 ** 3)
circuit = f.compile(inputset, artifacts=artifacts)
artifacts.export()
```
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 = x # EncryptedScalar<uint1>
%1 = sin(%0) # EncryptedScalar<float64>
%2 = 1 # ClearScalar<uint1>
%3 = add(%1, %2) # EncryptedScalar<float64>
%4 = 50 # ClearScalar<uint6>
%5 = multiply(%4, %3) # EncryptedScalar<float64>
%6 = astype(%5, dtype=int_) # EncryptedScalar<uint1>
%7 = 127 # ClearScalar<uint7>
%8 = subtract(%7, %6) # EncryptedScalar<uint1>
return %8
```
#### 2.after-fusing.graph.txt
This file contains the textual representation of the intermediate computation graph after fusing.
```
%0 = x # EncryptedScalar<uint1>
%1 = subgraph(%0) # EncryptedScalar<uint1>
%2 = 127 # ClearScalar<uint7>
%3 = subtract(%2, %1) # EncryptedScalar<uint1>
return %3
Subgraphs:
%1 = subgraph(%0):
%0 = input # EncryptedScalar<uint1>
%1 = sin(%0) # EncryptedScalar<float64>
%2 = 1 # ClearScalar<uint1>
%3 = add(%1, %2) # EncryptedScalar<float64>
%4 = 50 # ClearScalar<uint6>
%5 = multiply(%4, %3) # EncryptedScalar<float64>
%6 = astype(%5, dtype=int_) # EncryptedScalar<uint1>
return %6
```
#### 3.final.graph.txt
This file contains the textual representation of the final computation graph right before MLIR conversion.
```
%0 = x # EncryptedScalar<uint3> ∈ [0, 7]
%1 = subgraph(%0) # EncryptedScalar<uint7> ∈ [2, 95]
%2 = 127 # ClearScalar<uint7> ∈ [127, 127]
%3 = subtract(%2, %1) # EncryptedScalar<uint7> ∈ [32, 125]
return %3
Subgraphs:
%1 = subgraph(%0):
%0 = input # EncryptedScalar<uint1>
%1 = sin(%0) # EncryptedScalar<float64>
%2 = 1 # ClearScalar<uint1>
%3 = add(%1, %2) # EncryptedScalar<float64>
%4 = 50 # ClearScalar<uint6>
%5 = multiply(%4, %3) # EncryptedScalar<float64>
%6 = astype(%5, dtype=int_) # EncryptedScalar<uint1>
return %6
```
#### mlir.txt
This file contains information about the MLIR of the function you compiled using the inputset you provided.
```
module {
func.func @main(%arg0: !FHE.eint<7>) -> !FHE.eint<7> {
%c127_i8 = arith.constant 127 : i8
%cst = arith.constant dense<"..."> : tensor<128xi64>
%0 = "FHE.apply_lookup_table"(%arg0, %cst) : (!FHE.eint<7>, tensor<128xi64>) -> !FHE.eint<7>
%1 = "FHE.sub_int_eint"(%c127_i8, %0) : (i8, !FHE.eint<7>) -> !FHE.eint<7>
return %1 : !FHE.eint<7>
}
}
```
#### client_parameters.json
This file contains information about the client parameters chosen by **Concrete**.
```
{
"bootstrapKeys": [
{
"baseLog": 22,
"glweDimension": 1,
"inputLweDimension": 908,
"inputSecretKeyID": 1,
"level": 1,
"outputSecretKeyID": 0,
"polynomialSize": 8192,
"variance": 4.70197740328915e-38
}
],
"functionName": "main",
"inputs": [
{
"encryption": {
"encoding": {
"isSigned": false,
"precision": 7
},
"secretKeyID": 0,
"variance": 4.70197740328915e-38
},
"shape": {
"dimensions": [],
"sign": false,
"size": 0,
"width": 7
}
}
],
"keyswitchKeys": [
{
"baseLog": 3,
"inputSecretKeyID": 0,
"level": 6,
"outputSecretKeyID": 1,
"variance": 1.7944329123150665e-13
}
],
"outputs": [
{
"encryption": {
"encoding": {
"isSigned": false,
"precision": 7
},
"secretKeyID": 0,
"variance": 4.70197740328915e-38
},
"shape": {
"dimensions": [],
"sign": false,
"size": 0,
"width": 7
}
}
],
"packingKeyswitchKeys": [],
"secretKeys": [
{
"dimension": 8192
},
{
"dimension": 908
}
]
}
```
## 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,6 +1,6 @@
# Deploy
After developing your circuit, you may want to deploy it. However, sharing the details of your circuit with every client might not be desirable. Further, you might want to perform the computation in dedicated servers. In this case, you can use the `Client` and `Server` features of **Concrete-Numpy**.
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**.
## Development of the circuit
@@ -8,9 +8,9 @@ You can develop your circuit like we've discussed in the previous chapters. Here
<!--pytest-codeblocks:skip-->
```python
import concrete.numpy as cnp
from concrete import fhe
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def function(x):
return x + 42
@@ -33,9 +33,9 @@ You can load the `server.zip` you get from the development machine as follows:
<!--pytest-codeblocks:skip-->
```python
import concrete.numpy as cnp
from concrete import fhe
server = cnp.Server.load("server.zip")
server = fhe.Server.load("server.zip")
```
At this point, you will need to wait for requests from clients. The first likely request is for `ClientSpecs`.
@@ -55,8 +55,8 @@ After getting the serialized `ClientSpecs` from a server, you can create the cli
<!--pytest-codeblocks:skip-->
```python
client_specs = cnp.ClientSpecs.deserialize(serialized_client_specs)
client = cnp.Client(client_specs)
client_specs = fhe.ClientSpecs.deserialize(serialized_client_specs)
client = fhe.Client(client_specs)
```
## Generating keys (on the client)
@@ -100,7 +100,7 @@ Upon having the serialized evaluation keys and serialized arguments, you can des
<!--pytest-codeblocks:skip-->
```python
deserialized_evaluation_keys = cnp.EvaluationKeys.deserialize(serialized_evaluation_keys)
deserialized_evaluation_keys = fhe.EvaluationKeys.deserialize(serialized_evaluation_keys)
deserialized_args = server.client_specs.deserialize_public_args(serialized_args)
```

View File

@@ -3,9 +3,9 @@
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
from concrete import fhe
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return x + 42

View File

@@ -7,10 +7,10 @@ Direct circuits are still experimental, and it's very easy to shoot yourself in
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
from concrete import fhe
@cnp.circuit({"x": "encrypted"})
def circuit(x: cnp.uint8):
@fhe.circuit({"x": "encrypted"})
def circuit(x: fhe.uint8):
return x + 42
assert circuit.encrypt_run_decrypt(10) == 52
@@ -18,27 +18,27 @@ 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 remember that resulting dtype for each operation will be determined by its inputs. This can lead to some unexpected results if you're not careful (e.g., if you do `-x` where `x: fhe.uint8`, you'll not get the negative value as the result will be `fhe.uint8` as well)
- You need to use fhe types in `.astype(...)` calls (e.g., `np.sqrt(x).astype(fhe.uint4)`). This is because there are no inputset evaluation, so cannot determine the bit-width of the output.
- You need to specify the resulting data type in [univariate](./extensions.md#fheunivariatefunction) extension (e.g., `fhe.univariate(function, outputs=fhe.uint4)(x)`), because of the same reason as above.
- You need to be careful with overflows. With inputset evaluation, you'll get bigger bit-widths but no overflows, with direct definition, you're responsible to ensure there aren't any overflows!
Let's go over a more complicated example to see how direct circuits behave:
```python
import concrete.numpy as cnp
from concrete import fhe
import numpy as np
def square(value):
return value ** 2
@cnp.circuit({"x": "encrypted", "y": "encrypted"})
def circuit(x: cnp.uint8, y: cnp.int2):
@fhe.circuit({"x": "encrypted", "y": "encrypted"})
def circuit(x: fhe.uint8, y: fhe.int2):
a = x + 10
b = y + 10
c = np.sqrt(a).round().astype(cnp.uint4)
d = cnp.univariate(square, outputs=cnp.uint8)(b)
c = np.sqrt(a).round().astype(fhe.uint4)
d = fhe.univariate(square, outputs=fhe.uint8)(b)
return d - c

205
docs/tutorial/extensions.md Normal file
View File

@@ -0,0 +1,205 @@
# Extensions
**Concrete** tries to support native Python and NumPy operations as much as possible, but not everything is available in Python or NumPy. So, we provide some extensions ourselves to improve your experience.
## fhe.univariate(function)
Allows you to wrap any univariate function into a single table lookup:
```python
import numpy as np
from concrete import fhe
def complex_univariate_function(x):
def per_element(element):
result = 0
for i in range(element):
result += i
return result
return np.vectorize(per_element)(x)
@fhe.compiler({"x": "encrypted"})
def f(x):
return fhe.univariate(complex_univariate_function)(x)
inputset = [np.random.randint(0, 5, size=(3, 2)) for _ in range(10)]
circuit = f.compile(inputset)
sample = np.array([
[0, 4],
[2, 1],
[3, 0],
])
assert np.array_equal(circuit.encrypt_run_decrypt(sample), complex_univariate_function(sample))
```
{% hint style="danger" %}
The wrapped function shouldn't have any side effects, and it should be deterministic. Otherwise, the outcome is undefined.
{% endhint %}
## fhe.conv(...)
Allows you to perform convolution operation, with the same semantic of [onnx.Conv](https://github.com/onnx/onnx/blob/main/docs/Operators.md#conv):
```python
import numpy as np
from concrete import fhe
weight = np.array([[2, 1], [3, 2]]).reshape(1, 1, 2, 2)
@fhe.compiler({"x": "encrypted"})
def f(x):
return fhe.conv(x, weight, strides=(2, 2), dilations=(1, 1), group=1)
inputset = [np.random.randint(0, 4, size=(1, 1, 4, 4)) for _ in range(10)]
circuit = f.compile(inputset)
sample = np.array(
[
[3, 2, 1, 0],
[3, 2, 1, 0],
[3, 2, 1, 0],
[3, 2, 1, 0],
]
).reshape(1, 1, 4, 4)
assert np.array_equal(circuit.encrypt_run_decrypt(sample), f(sample))
```
{% hint style="danger" %}
Only 2D convolutions without padding and with one groups are supported for the time being.
{% endhint %}
## fhe.maxpool(...)
Allows you to perform maxpool operation, with the same semantic of [onnx.MaxPool](https://github.com/onnx/onnx/blob/main/docs/Operators.md#maxpool):
```python
import numpy as np
from concrete import fhe
@fhe.compiler({"x": "encrypted"})
def f(x):
return fhe.maxpool(x, kernel_shape=(2, 2), strides=(2, 2), dilations=(1, 1))
inputset = [np.random.randint(0, 4, size=(1, 1, 4, 4)) for _ in range(10)]
circuit = f.compile(inputset)
sample = np.array(
[
[3, 2, 1, 0],
[3, 2, 1, 0],
[3, 2, 1, 0],
[3, 2, 1, 0],
]
).reshape(1, 1, 4, 4)
assert np.array_equal(circuit.encrypt_run_decrypt(sample), f(sample))
```
{% hint style="danger" %}
Only 2D maxpooling without padding up to 15-bits is supported for the time being.
{% endhint %}
## fhe.array(...)
Allows you to create encrypted arrays:
```python
import numpy as np
from concrete import fhe
@fhe.compiler({"x": "encrypted", "y": "encrypted"})
def f(x, y):
return fhe.array([x, y])
inputset = [(3, 2), (7, 0), (0, 7), (4, 2)]
circuit = f.compile(inputset)
sample = (3, 4)
assert np.array_equal(circuit.encrypt_run_decrypt(*sample), f(*sample))
```
{% hint style="danger" %}
Only scalars can be used to create arrays for the time being.
{% endhint %}
## fhe.zero()
Allows you to create encrypted scalar zero:
```python
from concrete import fhe
import numpy as np
@fhe.compiler({"x": "encrypted"})
def f(x):
z = fhe.zero()
return x + z
inputset = range(10)
circuit = f.compile(inputset)
for x in range(10):
assert circuit.encrypt_run_decrypt(x) == x
```
## fhe.zeros(shape)
Allows you to create encrypted tensor of zeros:
```python
from concrete import fhe
import numpy as np
@fhe.compiler({"x": "encrypted"})
def f(x):
z = fhe.zeros((2, 3))
return x + z
inputset = range(10)
circuit = f.compile(inputset)
for x in range(10):
assert np.array_equal(circuit.encrypt_run_decrypt(x), np.array([[x, x, x], [x, x, x]]))
```
## fhe.one()
Allows you to create encrypted scalar one:
```python
from concrete import fhe
import numpy as np
@fhe.compiler({"x": "encrypted"})
def f(x):
z = fhe.one()
return x + z
inputset = range(10)
circuit = f.compile(inputset)
for x in range(10):
assert circuit.encrypt_run_decrypt(x) == x + 1
```
## fhe.ones(shape)
Allows you to create encrypted tensor of ones:
```python
from concrete import fhe
import numpy as np
@fhe.compiler({"x": "encrypted"})
def f(x):
z = fhe.ones((2, 3))
return x + z
inputset = range(10)
circuit = f.compile(inputset)
for x in range(10):
assert np.array_equal(circuit.encrypt_run_decrypt(x), np.array([[x, x, x], [x, x, x]]) + 1)
```

View File

@@ -1,6 +1,6 @@
# Floating Points
**Concrete-Numpy** partly supports floating points:
**Concrete** partly supports floating points:
* They cannot be inputs
* They cannot be outputs
@@ -10,13 +10,13 @@
**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:
As long as your floating point operations comply with those constraints, **Concrete** automatically converts your operations to a table lookup operation:
```python
import concrete.numpy as cnp
from concrete import fhe
import numpy as np
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
a = x + 1.5
b = np.sin(x)
@@ -31,16 +31,16 @@ 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`.
In the example above, `a`, `b`, and `c` are all floating point intermediates. However, they are just used to calculate `d`, which is an integer and value of `d` dependent upon `x` , which is another integer. **Concrete** detects this and fuses all of those operations into a single table lookup from `x` to `d`.
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
from concrete import fhe
import numpy as np
@cnp.compiler({"x": "encrypted", "y": "encrypted"})
@fhe.compiler({"x": "encrypted", "y": "encrypted"})
def f(x, y):
a = x + 1.5
b = np.sin(y)
@@ -76,4 +76,4 @@ RuntimeError: Function you are trying to compile cannot be converted to MLIR
return %7
```
The reason for this is that `d` no longer depends solely on `x`, it depends on `y` as well. **Concrete-Numpy** cannot fuse these operations, so it raises an exception instead.
The reason for this is that `d` no longer depends solely on `x`, it depends on `y` as well. **Concrete** cannot fuse these operations, so it raises an exception instead.

View File

@@ -15,5 +15,5 @@ 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.
Formatting is just for debugging. It's not possible to create the circuit back from its textual representation. See [How to Deploy](../howto/deploy.md) if that's your goal.
{% endhint %}

View File

@@ -1,14 +1,14 @@
# Rounded Table Lookups
{% hint style="warning" %}
Rounded table lookups are not compilable yet. API is stable and will not change so it's documented, but you might not be able to run the code samples in this document.
Rounded table lookups are not 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.
Imagine you have an 8-bit value, but you want to have a 5-bit table lookup, you can call `fhe.round_bit_pattern(input, lsbs_to_remove=3)` and use the value you get in the table lookup.
In Python, evaluation will work like the following:
```
@@ -110,17 +110,17 @@ plt.show()
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
from concrete import fhe
import matplotlib.pyplot as plt
import numpy as np
def relu(x):
return x if x >= 0 else 0
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
x = cnp.round_bit_pattern(x, lsbs_to_remove=10)
return cnp.univariate(relu)(x)
x = fhe.round_bit_pattern(x, lsbs_to_remove=10)
return fhe.univariate(relu)(x)
inputset = [-100_000, (100_000 - 1)]
circuit = f.compile(inputset)
@@ -143,22 +143,22 @@ which is close enough to original ReLU for some cases. If your application is mo
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
from concrete import fhe
import matplotlib.pyplot as plt
import numpy as np
rounder = cnp.AutoRounder(target_msbs=6)
rounder = fhe.AutoRounder(target_msbs=6)
def relu(x):
return x if x >= 0 else 0
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
x = cnp.round_bit_pattern(x, lsbs_to_remove=rounder)
return cnp.univariate(relu)(x)
x = fhe.round_bit_pattern(x, lsbs_to_remove=rounder)
return fhe.univariate(relu)(x)
inputset = [-100_000, (100_000 - 1)]
cnp.AutoRounder.adjust(f, inputset) # alternatively, you can use `auto_adjust_rounders=True` below
fhe.AutoRounder.adjust(f, inputset) # alternatively, you can use `auto_adjust_rounders=True` below
circuit = f.compile(inputset)
xs = range(-100_000, 100_000)
@@ -168,7 +168,7 @@ 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.
`AutoRounder`s allow you to set how many of the most significant bits to keep, but they need to be adjusted using an inputset to determine how many of the least significant bits to remove. This can be done manually using `fhe.AutoRounder.adjust(function, inputset)`, or by setting `auto_adjust_rounders` to `True` during compilation.
In the example above, `6` of the most significant bits are kept to get:

View File

@@ -6,10 +6,10 @@ You could call the function you're trying to compile directly of course, but it
Considering these, simulation is introduced:
```python
import concrete.numpy as cnp
from concrete import fhe
import numpy as np
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return (x + 1) ** 2

View File

@@ -1,10 +1,10 @@
# Table Lookups
In this tutorial, we will review the ways to perform direct table lookups in **Concrete-Numpy**.
In this tutorial, we will review the ways to perform direct table lookups in **Concrete**.
## Direct table lookup
**Concrete-Numpy** provides a `LookupTable` class for you to create your own tables and apply them in your circuits.
**Concrete** 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.
@@ -16,20 +16,16 @@ 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
from concrete import fhe
table = cnp.LookupTable([2, -1, 3, 0])
table = fhe.LookupTable([2, -1, 3, 0])
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return table[x]
@@ -47,12 +43,12 @@ assert circuit.encrypt_run_decrypt(3) == table[3] == 0
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
from concrete import fhe
import numpy as np
table = cnp.LookupTable([2, -1, 3, 0])
table = fhe.LookupTable([2, -1, 3, 0])
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return table[x]
@@ -79,11 +75,11 @@ for i in range(2):
`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
from concrete import fhe
table = cnp.LookupTable([2, -1, 3, 0])
table = fhe.LookupTable([2, -1, 3, 0])
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return table[-x]
@@ -101,19 +97,19 @@ assert circuit.encrypt_run_decrypt(4) == table[-4] == 2
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
from concrete import fhe
import numpy as np
squared = cnp.LookupTable([i ** 2 for i in range(4)])
cubed = cnp.LookupTable([i ** 3 for i in range(4)])
squared = fhe.LookupTable([i ** 2 for i in range(4)])
cubed = fhe.LookupTable([i ** 3 for i in range(4)])
table = cnp.LookupTable([
table = fhe.LookupTable([
[squared, cubed],
[squared, cubed],
[squared, cubed],
])
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return table[x]
@@ -144,13 +140,13 @@ In this example, we applied a `squared` table to the first column and a `cubed`
## 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:
**Concrete** 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
from concrete import fhe
import numpy as np
@cnp.compiler({"x": "encrypted"})
@fhe.compiler({"x": "encrypted"})
def f(x):
return (42 * np.sin(x)).astype(np.int64) // 10
@@ -162,14 +158,14 @@ for x in range(8):
```
{% 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.
All lookup tables need to be from integers to integers. So, without `.astype(np.int64)`, **Concrete** 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:
Then, **Concrete** fuses appropriate nodes:
![](../\_static/tutorials/table-lookup/3.final.graph.png)

View File

@@ -1,19 +1,19 @@
# Tagging
When you have big circuits, keeping track of which node corresponds to which part of your code becomes very hard. Tagging system could simplify such situations:
When you have big circuits, keeping track of which node corresponds to which part of your code becomes very hard. Tagging system could simplify such situations:
```python
def g(z):
with cnp.tag("def"):
with fhe.tag("def"):
a = 120 - z
b = a // 4
return b
def f(x):
with cnp.tag("abc"):
with fhe.tag("abc"):
x = x * 2
with cnp.tag("foo"):
with fhe.tag("foo"):
y = x + 42
z = np.sqrt(y).astype(np.int64)

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**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

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

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,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,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,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,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,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 %}