diff --git a/Makefile b/Makefile index de76ba7ea..6f73b877f 100644 --- a/Makefile +++ b/Makefile @@ -267,11 +267,11 @@ todo: .PHONY: supported_functions # Update docs with supported functions supported_functions: - poetry run python script/doc_utils/gen_supported_ufuncs.py docs/basics/numpy_support.md + poetry run python script/doc_utils/gen_supported_ufuncs.py docs/basics/compatibility.md .PHONY: check_supported_functions # Check supported functions (for the doc) check_supported_functions: - poetry run python script/doc_utils/gen_supported_ufuncs.py docs/basics/numpy_support.md --check + poetry run python script/doc_utils/gen_supported_ufuncs.py docs/basics/compatibility.md --check .PHONY: licenses # Generate the list of licenses of dependencies licenses: diff --git a/README.md b/README.md index 919bc8875..a09642079 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,66 @@ -# Concrete Numpy +
+

Concrete Numpy

+
-An open-source set of tools which aims to simplify the use of fully homomorphic encryption (FHE) for data scientists. +

+ + + + + + + + + + + + + + + + + + + + + + + + +

-With Concrete Numpy, data scientists can implement machine learning models using a subset of numpy that compiles to FHE. They will be able to train models with popular machine learning libraries and then convert the prediction functions of these models to FHE with concrete-numpy. +## Quick tour - +- [Introduction](#Introduction) +- [Installation](#Installation) +- [Getting started](#getting-started) +- [License](#license) -- [concrete-numpy](#concrete-numpy) - - [Links](#links) - - [For end users](#for-end-users) - - [Installation](#Installation) - - [A simple example](#a-simple-example-numpy-addition-in-fhe) - - [For developers](#for-developers) - - [Project setup](#project-setup) - - [Documenting](#documenting) - - [Developing](#developing) - - [Contributing](#contributing) - - [License](#license) +## Introduction - -# Links +**Concrete Numpy** is an open-source library which simplifies the use of fully homomorphic encryption (FHE). -- [documentation](https://docs.zama.ai/concrete-numpy/main/) -- [community website](https://community.zama.ai/c/concrete-numpy/7) -- [machine learning examples](https://docs.zama.ai/concrete-numpy/main/user/advanced_examples/index.html) -# For end users +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. ## Installation -The preferred way to use concrete-numpy is through docker. You can get the concrete-numpy docker image by pulling the latest docker image: +The preferred way to install concrete-numpy is through PyPI. To install Concrete Numpy from PyPi, run the following: -`docker pull zamafhe/concrete-numpy:latest` +```shell +pip install concrete-numpy +``` -To install Concrete Numpy from PyPi, run the following: +You can get the concrete-numpy docker image by pulling the latest docker image: -`pip install concrete-numpy` +```shell +docker pull zamafhe/concrete-numpy:v0.6.0 +``` -You can find more detailed installation instructions in [installing.md](docs/user/basics/installing.md) +You can find more detailed installation instructions in [installing.md](docs/basics/installing.md) - -## A simple example +## Getting started ```python import concrete.numpy as cnp @@ -59,7 +80,7 @@ for example in examples: print(f"Evaluation of {' + '.join(map(str, example))} homomorphically = {result}") ``` -if you have a function object that you cannot decorate, you can use the explicit compiler API instead +if you have a function object that you cannot decorate, you can use the explicit `Compiler` API instead ```python import concrete.numpy as cnp @@ -79,26 +100,6 @@ for example in examples: print(f"Evaluation of {' + '.join(map(str, example))} homomorphically = {result}") ``` -# For developers - -### Project setup - -Installation steps are described in [project_setup.md](docs/dev/howto/project_setup.md). -Information about how to use Docker for development are available in [docker.md](docs/dev/howto/docker.md). - -### Documenting - -Some information about how to build the documentation of `concrete-numpy` are available in [documenting.md](docs/dev/howto/documenting.md). Notably, our documentation is pushed to [https://docs.zama.ai/concrete-numpy/](https://docs.zama.ai/concrete-numpy/). - -### Developing - -Some information about our terminology and the infrastructure of `concrete-numpy` are available in [terminology_and_structure.md](docs/dev/explanation/terminology_and_structure.md). An in-depth look at what is done in `concrete-numpy` is available in [compilation.md](docs/dev/explanation/compilation.md). - -### Contributing - -Information about how to contribute are available in [contributing.md](docs/dev/howto/contributing.md). - - -# License +## License This software is distributed under the BSD-3-Clause-Clear license. If you have any questions, please contact us at hello@zama.ai. diff --git a/concrete/numpy/compilation/server.py b/concrete/numpy/compilation/server.py index f9f7ca1cd..12dfcb9fa 100644 --- a/concrete/numpy/compilation/server.py +++ b/concrete/numpy/compilation/server.py @@ -27,7 +27,7 @@ from .specs import ClientSpecs class Server: """ - Client class, which can be used to perform homomorphic computation. + Server class, which can be used to perform homomorphic computation. """ client_specs: ClientSpecs diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..17e8c5985 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,26 @@ +# Introduction + +[⭐️ Star the repo on Github](https://github.com/zama-ai/concrete-numpy) | 🗣 [Community support forum](https://community.zama.ai/c/concrete-numpy) | 📁 [Contribute to the project](dev/contributing.md) + +## Welcome to Concrete Numpy! + +![](\_static/zama\_home\_docs.jpeg) + +**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** section to give you the basics +* **Tutorials** section to give you some essential examples on various features of the library +* **How to** section to help you perform specific tasks +* **Developer** section to explain the inner workings of the library and everything related to contributing to the project + +## Looking for support? Join our community! + +![](\_static/zama\_support\_docs.jpeg) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 0514c2811..45577f40b 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,39 +1,33 @@ -# User Guide +# Table of contents -* [What is Concrete Numpy?](intro.md) +* [Introduction](README.md) ## Getting Started * [Installation](basics/installing.md) -* [Compiling and Executing your first Numpy Function](basics/compiling_and_executing.md) -* [List of supported Numpy operations](basics/numpy_support.md) +* [Quick start](basics/quick\_start.md) +* [Compatibility](basics/compatibility.md) ## Tutorials -* [Table lookup](tutorial/table_lookup.md) -* [Working with floating points](tutorial/working_with_floating_points.md) -* [Indexing](tutorial/indexing.md) -* [Compilation artifacts](tutorial/compilation_artifacts.md) +* [Decorator](tutorial/decorator.md) +* [Extensions](tutorial/extensions.md) +* [Table Lookups](tutorial/table\_lookup.md) +* [Floating Points](tutorial/floating\_points.md) +* [Format and Draw](tutorial/formatting\_and\_drawing.md) ## How To -* [Printing and drawing](howto/printing_and_drawing.md) -* [Reduce needed precision](howto/reduce_needed_precision.md) -* [Debug](howto/debug_support_submit_issues.md) - -## Explanations - -* [What is FHE?](explanation/what_is_fhe.md) -* [Framework limits](explanation/fhe_and_framework_limits.md) -* [Future features](explanation/future_features.md) +* [Configure](howto/configure.md) +* [Debug](howto/debug.md) +* [Deploy](howto/deploy.md) ## Developer -* [Setup the project](dev/project_setup.md) -* [Use Docker](dev/docker.md) -* [Create a release](dev/releasing.md) +* [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) -* [Terminology and structure](dev/terminology_and_structure.md) -* [Float fusing](dev/float-fusing.md) +* [Fusing](dev/fusing.md) * [MLIR](dev/mlir.md) diff --git a/docs/_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png b/docs/_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png deleted file mode 100644 index 2ec2a408e..000000000 Binary files a/docs/_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png and /dev/null differ diff --git a/docs/_static/compilation-pipeline/frontend_flow.svg b/docs/_static/compilation-pipeline/frontend_flow.svg deleted file mode 100644 index 6b03dec9e..000000000 --- a/docs/_static/compilation-pipeline/frontend_flow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
Input Program
NumPy function
Input Program...
Tracing
Tracing
Data
Data
Algorithm/Function/Transform
Algorithm/Function/Transform
Operator DAG:
"Base Graph"
Operator DAG:...
Topological transform
Topological transform
Operator DAG:
"Candidate Graph"
Operator DAG:...
Constraints check
Constraints check
Input/Intermediate/Output values: 7b unsigned int
Constants: 7+1 = 8b signless int
Input/Intermediate/Output values: 7b unsigned int...
UI/UX
UI/UX
Error Message + Debug Infos
Error Message + Debug Infos
Bounds Measurement
Bounds Measurement
Dataset + Evaluation
Dataset + Evaluation
Operator DAG + Width:
"Compilable Graph"
Operator DAG + Width:...
MLIR Lowering
MLIR Lowering
MLIR
MLIR
Compiler "Backend"
Compiler "Backend"
Input/Intermediate/Output values: unsigned int
Constants: signless int
Input/Intermediate/Output values: unsigned int...
Error Message + Debug Infos
Error Message + Debug Infos
Executable FHE program
Executable FHE program
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/compilation-pipeline/two_x_plus_three.png b/docs/_static/compilation-pipeline/two_x_plus_three.png index 0ccae9c4e..61dc8ad49 100644 Binary files a/docs/_static/compilation-pipeline/two_x_plus_three.png and b/docs/_static/compilation-pipeline/two_x_plus_three.png differ diff --git a/docs/_static/tutorials/artifacts/auto/1.initial.graph.png b/docs/_static/tutorials/artifacts/auto/1.initial.graph.png index 50495286b..462f2093c 100644 Binary files a/docs/_static/tutorials/artifacts/auto/1.initial.graph.png and b/docs/_static/tutorials/artifacts/auto/1.initial.graph.png differ diff --git a/docs/_static/tutorials/artifacts/auto/2.final.graph.png b/docs/_static/tutorials/artifacts/auto/2.final.graph.png index 50495286b..462f2093c 100644 Binary files a/docs/_static/tutorials/artifacts/auto/2.final.graph.png and b/docs/_static/tutorials/artifacts/auto/2.final.graph.png differ diff --git a/docs/_static/tutorials/artifacts/manual/1.initial.graph.png b/docs/_static/tutorials/artifacts/manual/1.initial.graph.png index 5ab218394..ef26ca811 100644 Binary files a/docs/_static/tutorials/artifacts/manual/1.initial.graph.png and b/docs/_static/tutorials/artifacts/manual/1.initial.graph.png differ diff --git a/docs/_static/tutorials/artifacts/manual/2.after-float-fuse-0.graph.png b/docs/_static/tutorials/artifacts/manual/2.after-float-fuse-0.graph.png deleted file mode 100644 index 497bb9020..000000000 Binary files a/docs/_static/tutorials/artifacts/manual/2.after-float-fuse-0.graph.png and /dev/null differ diff --git a/docs/_static/tutorials/artifacts/manual/2.after-fusing.graph.png b/docs/_static/tutorials/artifacts/manual/2.after-fusing.graph.png new file mode 100644 index 000000000..cbb9c3f45 Binary files /dev/null and b/docs/_static/tutorials/artifacts/manual/2.after-fusing.graph.png differ diff --git a/docs/_static/tutorials/artifacts/manual/3.final.graph.png b/docs/_static/tutorials/artifacts/manual/3.final.graph.png index 497bb9020..cbb9c3f45 100644 Binary files a/docs/_static/tutorials/artifacts/manual/3.final.graph.png and b/docs/_static/tutorials/artifacts/manual/3.final.graph.png differ diff --git a/docs/_static/tutorials/table-lookup/1.initial.graph.png b/docs/_static/tutorials/table-lookup/1.initial.graph.png index c6dc84e8c..0849bc65d 100644 Binary files a/docs/_static/tutorials/table-lookup/1.initial.graph.png and b/docs/_static/tutorials/table-lookup/1.initial.graph.png differ diff --git a/docs/_static/tutorials/table-lookup/3.final.graph.png b/docs/_static/tutorials/table-lookup/3.final.graph.png index 73b5ab86d..cb5756bc1 100644 Binary files a/docs/_static/tutorials/table-lookup/3.final.graph.png and b/docs/_static/tutorials/table-lookup/3.final.graph.png differ diff --git a/docs/_static/zama_home_docs.jpeg b/docs/_static/zama_home_docs.jpeg new file mode 100644 index 000000000..e51821042 Binary files /dev/null and b/docs/_static/zama_home_docs.jpeg differ diff --git a/docs/_static/zama_support_docs.jpeg b/docs/_static/zama_support_docs.jpeg new file mode 100644 index 000000000..a1ab1e56e Binary files /dev/null and b/docs/_static/zama_support_docs.jpeg differ diff --git a/docs/basics/compatibility.md b/docs/basics/compatibility.md new file mode 100644 index 000000000..cae688cd3 --- /dev/null +++ b/docs/basics/compatibility.md @@ -0,0 +1,192 @@ +# Compatibility + +## Supported operations + +Here are the operations you can use inside the function you are compiling. + +{% hint style="info" %} +Some of these operations are not supported between two encrypted values. A detailed error will be raised if you try to do something that is not supported. +{% endhint %} + +### Supported Python operators + +* [\_\_abs\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_abs\_\_) +* [\_\_add\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_add\_\_) +* [\_\_and\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_and\_\_) +* [\_\_eq\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_eq\_\_) +* [\_\_floordiv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_floordiv\_\_) +* [\_\_ge\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_ge\_\_) +* [\_\_getitem\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_getitem\_\_) +* [\_\_gt\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_gt\_\_) +* [\_\_invert\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_invert\_\_) +* [\_\_le\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_le\_\_) +* [\_\_lshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_lshift\_\_) +* [\_\_lt\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_lt\_\_) +* [\_\_matmul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_matmul\_\_) +* [\_\_mod\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_mod\_\_) +* [\_\_mul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_mul\_\_) +* [\_\_ne\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_ne\_\_) +* [\_\_neg\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_neg\_\_) +* [\_\_or\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_or\_\_) +* [\_\_pos\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_pos\_\_) +* [\_\_pow\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_pow\_\_) +* [\_\_radd\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_radd\_\_) +* [\_\_rand\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rand\_\_) +* [\_\_rfloordiv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rfloordiv\_\_) +* [\_\_rlshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rlshift\_\_) +* [\_\_rmatmul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rmatmul\_\_) +* [\_\_rmod\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rmod\_\_) +* [\_\_rmul\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rmul\_\_) +* [\_\_ror\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_ror\_\_) +* [\_\_round\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_round\_\_) +* [\_\_rpow\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rpow\_\_) +* [\_\_rrshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rrshift\_\_) +* [\_\_rshift\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rshift\_\_) +* [\_\_rsub\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rsub\_\_) +* [\_\_rtruediv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rtruediv\_\_) +* [\_\_rxor\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_rxor\_\_) +* [\_\_sub\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_sub\_\_) +* [\_\_truediv\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_truediv\_\_) +* [\_\_xor\_\_](https://docs.python.org/3/reference/datamodel.html#object.\_\_xor\_\_) + +### Supported NumPy functions + + + +* [np.absolute](https://numpy.org/doc/stable/reference/generated/numpy.absolute.html) +* [np.add](https://numpy.org/doc/stable/reference/generated/numpy.add.html) +* [np.arccos](https://numpy.org/doc/stable/reference/generated/numpy.arccos.html) +* [np.arccosh](https://numpy.org/doc/stable/reference/generated/numpy.arccosh.html) +* [np.arcsin](https://numpy.org/doc/stable/reference/generated/numpy.arcsin.html) +* [np.arcsinh](https://numpy.org/doc/stable/reference/generated/numpy.arcsinh.html) +* [np.arctan](https://numpy.org/doc/stable/reference/generated/numpy.arctan.html) +* [np.arctan2](https://numpy.org/doc/stable/reference/generated/numpy.arctan2.html) +* [np.arctanh](https://numpy.org/doc/stable/reference/generated/numpy.arctanh.html) +* [np.around](https://numpy.org/doc/stable/reference/generated/numpy.around.html) +* [np.bitwise\_and](https://numpy.org/doc/stable/reference/generated/numpy.bitwise\_and.html) +* [np.bitwise\_or](https://numpy.org/doc/stable/reference/generated/numpy.bitwise\_or.html) +* [np.bitwise\_xor](https://numpy.org/doc/stable/reference/generated/numpy.bitwise\_xor.html) +* [np.cbrt](https://numpy.org/doc/stable/reference/generated/numpy.cbrt.html) +* [np.ceil](https://numpy.org/doc/stable/reference/generated/numpy.ceil.html) +* [np.clip](https://numpy.org/doc/stable/reference/generated/numpy.clip.html) +* [np.concatenate](https://numpy.org/doc/stable/reference/generated/numpy.concatenate.html) +* [np.copysign](https://numpy.org/doc/stable/reference/generated/numpy.copysign.html) +* [np.cos](https://numpy.org/doc/stable/reference/generated/numpy.cos.html) +* [np.cosh](https://numpy.org/doc/stable/reference/generated/numpy.cosh.html) +* [np.deg2rad](https://numpy.org/doc/stable/reference/generated/numpy.deg2rad.html) +* [np.degrees](https://numpy.org/doc/stable/reference/generated/numpy.degrees.html) +* [np.dot](https://numpy.org/doc/stable/reference/generated/numpy.dot.html) +* [np.equal](https://numpy.org/doc/stable/reference/generated/numpy.equal.html) +* [np.exp](https://numpy.org/doc/stable/reference/generated/numpy.exp.html) +* [np.exp2](https://numpy.org/doc/stable/reference/generated/numpy.exp2.html) +* [np.expm1](https://numpy.org/doc/stable/reference/generated/numpy.expm1.html) +* [np.fabs](https://numpy.org/doc/stable/reference/generated/numpy.fabs.html) +* [np.float\_power](https://numpy.org/doc/stable/reference/generated/numpy.float\_power.html) +* [np.floor](https://numpy.org/doc/stable/reference/generated/numpy.floor.html) +* [np.floor\_divide](https://numpy.org/doc/stable/reference/generated/numpy.floor\_divide.html) +* [np.fmax](https://numpy.org/doc/stable/reference/generated/numpy.fmax.html) +* [np.fmin](https://numpy.org/doc/stable/reference/generated/numpy.fmin.html) +* [np.fmod](https://numpy.org/doc/stable/reference/generated/numpy.fmod.html) +* [np.gcd](https://numpy.org/doc/stable/reference/generated/numpy.gcd.html) +* [np.greater](https://numpy.org/doc/stable/reference/generated/numpy.greater.html) +* [np.greater\_equal](https://numpy.org/doc/stable/reference/generated/numpy.greater\_equal.html) +* [np.heaviside](https://numpy.org/doc/stable/reference/generated/numpy.heaviside.html) +* [np.hypot](https://numpy.org/doc/stable/reference/generated/numpy.hypot.html) +* [np.invert](https://numpy.org/doc/stable/reference/generated/numpy.invert.html) +* [np.isfinite](https://numpy.org/doc/stable/reference/generated/numpy.isfinite.html) +* [np.isinf](https://numpy.org/doc/stable/reference/generated/numpy.isinf.html) +* [np.isnan](https://numpy.org/doc/stable/reference/generated/numpy.isnan.html) +* [np.lcm](https://numpy.org/doc/stable/reference/generated/numpy.lcm.html) +* [np.ldexp](https://numpy.org/doc/stable/reference/generated/numpy.ldexp.html) +* [np.left\_shift](https://numpy.org/doc/stable/reference/generated/numpy.left\_shift.html) +* [np.less](https://numpy.org/doc/stable/reference/generated/numpy.less.html) +* [np.less\_equal](https://numpy.org/doc/stable/reference/generated/numpy.less\_equal.html) +* [np.log](https://numpy.org/doc/stable/reference/generated/numpy.log.html) +* [np.log10](https://numpy.org/doc/stable/reference/generated/numpy.log10.html) +* [np.log1p](https://numpy.org/doc/stable/reference/generated/numpy.log1p.html) +* [np.log2](https://numpy.org/doc/stable/reference/generated/numpy.log2.html) +* [np.logaddexp](https://numpy.org/doc/stable/reference/generated/numpy.logaddexp.html) +* [np.logaddexp2](https://numpy.org/doc/stable/reference/generated/numpy.logaddexp2.html) +* [np.logical\_and](https://numpy.org/doc/stable/reference/generated/numpy.logical\_and.html) +* [np.logical\_not](https://numpy.org/doc/stable/reference/generated/numpy.logical\_not.html) +* [np.logical\_or](https://numpy.org/doc/stable/reference/generated/numpy.logical\_or.html) +* [np.logical\_xor](https://numpy.org/doc/stable/reference/generated/numpy.logical\_xor.html) +* [np.matmul](https://numpy.org/doc/stable/reference/generated/numpy.matmul.html) +* [np.maximum](https://numpy.org/doc/stable/reference/generated/numpy.maximum.html) +* [np.minimum](https://numpy.org/doc/stable/reference/generated/numpy.minimum.html) +* [np.multiply](https://numpy.org/doc/stable/reference/generated/numpy.multiply.html) +* [np.negative](https://numpy.org/doc/stable/reference/generated/numpy.negative.html) +* [np.nextafter](https://numpy.org/doc/stable/reference/generated/numpy.nextafter.html) +* [np.not\_equal](https://numpy.org/doc/stable/reference/generated/numpy.not\_equal.html) +* [np.ones\_like](https://numpy.org/doc/stable/reference/generated/numpy.ones\_like.html) +* [np.positive](https://numpy.org/doc/stable/reference/generated/numpy.positive.html) +* [np.power](https://numpy.org/doc/stable/reference/generated/numpy.power.html) +* [np.rad2deg](https://numpy.org/doc/stable/reference/generated/numpy.rad2deg.html) +* [np.radians](https://numpy.org/doc/stable/reference/generated/numpy.radians.html) +* [np.reciprocal](https://numpy.org/doc/stable/reference/generated/numpy.reciprocal.html) +* [np.remainder](https://numpy.org/doc/stable/reference/generated/numpy.remainder.html) +* [np.reshape](https://numpy.org/doc/stable/reference/generated/numpy.reshape.html) +* [np.right\_shift](https://numpy.org/doc/stable/reference/generated/numpy.right\_shift.html) +* [np.rint](https://numpy.org/doc/stable/reference/generated/numpy.rint.html) +* [np.round\_](https://numpy.org/doc/stable/reference/generated/numpy.round\_.html) +* [np.sign](https://numpy.org/doc/stable/reference/generated/numpy.sign.html) +* [np.signbit](https://numpy.org/doc/stable/reference/generated/numpy.signbit.html) +* [np.sin](https://numpy.org/doc/stable/reference/generated/numpy.sin.html) +* [np.sinh](https://numpy.org/doc/stable/reference/generated/numpy.sinh.html) +* [np.spacing](https://numpy.org/doc/stable/reference/generated/numpy.spacing.html) +* [np.sqrt](https://numpy.org/doc/stable/reference/generated/numpy.sqrt.html) +* [np.square](https://numpy.org/doc/stable/reference/generated/numpy.square.html) +* [np.subtract](https://numpy.org/doc/stable/reference/generated/numpy.subtract.html) +* [np.sum](https://numpy.org/doc/stable/reference/generated/numpy.sum.html) +* [np.tan](https://numpy.org/doc/stable/reference/generated/numpy.tan.html) +* [np.tanh](https://numpy.org/doc/stable/reference/generated/numpy.tanh.html) +* [np.transpose](https://numpy.org/doc/stable/reference/generated/numpy.transpose.html) +* [np.true\_divide](https://numpy.org/doc/stable/reference/generated/numpy.true\_divide.html) +* [np.trunc](https://numpy.org/doc/stable/reference/generated/numpy.trunc.html) +* [np.where](https://numpy.org/doc/stable/reference/generated/numpy.where.html) +* [np.zeros\_like](https://numpy.org/doc/stable/reference/generated/numpy.zeros\_like.html) + + +### Supported `ndarray` methods + +* [np.ndarray.astype](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.astype.html) +* [np.ndarray.clip](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.clip.html) +* [np.ndarray.dot](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.dot.html) +* [np.ndarray.flatten](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flatten.html) +* [np.ndarray.reshape](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.reshape.html) +* [np.ndarray.transpose](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.transpose.html) + +### Supported `ndarray` properties + +* [np.ndarray.shape](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.shape.html) +* [np.ndarray.ndim](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.ndim.html) +* [np.ndarray.size](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.size.html) +* [np.ndarray.T](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.T.html) + +## Limitations + +### Operational constraints + +Some Python control flow statements are not supported. For example, you cannot have an `if` statement or a `while` statement for which the condition depends on an encrypted value. However, such statements are supported with constant values (e.g., `for i in range(SOME_CONSTANT)`, `if os.environ.get("SOME_FEATURE") == "ON":`). + +Another constraint is that you cannot have floating-point inputs or floating-point outputs. You can have floating-point intermediate values as long as they can be converted to an integer Table Lookup (e.g., `(60 * np.sin(x)).astype(np.int64)`). + +### Bit width constraints + +There is a limit on the bit width of encrypted values. We are constantly working on increasing this bit width. If you go above this limit, you will get an error. + +### Computation constraints + +One of the most common operations in **Concrete Numpy** is `Table Lookups` (TLUs). TLUs are performed with an FHE operation called `Programmable Bootstrapping` (PBS). PBSes have a certain probability of error, which, when triggered, result in inaccurate results. + +Let's say you have the table: + +```python +[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`. + +{% hint style="info" %} +The probability of this error can be configured through the `p_error` configuration option, which has the default value of `0.000063342483999973` (i.e., probability of success is `99.993`%). Keep in mind that changing it could affect compilation and key generation times. +{% endhint %} diff --git a/docs/basics/compiling_and_executing.md b/docs/basics/compiling_and_executing.md deleted file mode 100644 index fae2c7adf..000000000 --- a/docs/basics/compiling_and_executing.md +++ /dev/null @@ -1,103 +0,0 @@ -# Compiling and Executing your first function - -## Importing necessary components - -Everything you need to compile and execute homomorphic functions is included in a single module. You can import it like so: - -```python -import concrete.numpy as cnp -``` - -## Defining a function to compile - -You need to have a python function that follows the [limits](../explanation/fhe\_and\_framework_limits.md) of **Concrete Numpy**. Here is a simple example: - - -```python -def f(x, y): - return x + y -``` - -## Compiling the function - -To compile the function, you need to identify the inputs that it is expecting. In the example function above, `x` and `y` could be scalars or tensors (though, for now, only dot between tensors are supported), they can be encrypted or clear, they can be signed or unsigned, they can have different bit-widths. So, we need to know what they are beforehand. We can do that like so: - - -```python -x = "encrypted" -y = "encrypted" -``` - -In this configuration, both `x` and `y` will be encrypted values. - -We also need an inputset. It is to determine the bit-widths of the intermediate results. It should be an iterable yielding tuples in the same order as the inputs of the function to compile. There should be at least 10 inputs in the input set to avoid warnings (except for functions with less than 10 possible inputs). The warning is there because the bigger the input set, the better the bounds will be. - - -```python -inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1), (3, 2), (6, 1), (1, 7), (4, 5), (5, 4)] -``` - -Finally, we can compile our function to its homomorphic equivalent. - - -```python -compiler = cnp.Compiler(f, {"x": x, "y": y}) -circuit = compiler.compile(inputset) - -# You can print the compiled circuit: -print(circuit) - -# Outputs - -# %0 = x # EncryptedScalar -# %1 = y # EncryptedScalar -# %2 = add(%0, %1) # EncryptedScalar -# return %2 - -# Or draw it -circuit.draw(show=True) -``` - -Here is the graph from the previous code block drawn with `draw`: - -![Drawn graph of previous code block](../\_static/basics/compiling\_and\_executing\_example\_graph.png) - -## Performing homomorphic evaluation - -You can use `.encrypt_run_decrypt(...)` method of `Circuit` to perform fully homomorphic evaluation. Here are some examples: - - -```python -circuit.encrypt_run_decrypt(3, 4) -# 7 -circuit.encrypt_run_decrypt(1, 2) -# 3 -circuit.encrypt_run_decrypt(7, 7) -# 14 -circuit.encrypt_run_decrypt(0, 0) -# 0 -``` - -{% hint style="warning" %} -Be careful about the inputs, though. If you were to run with values outside the range of the inputset, the result might not be correct. -{% endhint %} - -While `.encrypt_run_decrypt(...)` is a good start for prototyping examples, more advanced usages require control over the different steps that are happening behind the scene, mainly key generation, encryption, execution, and decryption. The different steps can of course be called separately as in the example below: - - -```python -# generate keys required for encrypted computation -circuit.keygen() -# this will encrypt arguments that require encryption and pack all arguments -# as well as public materials (public keys) -public_args = circuit.encrypt(3, 4) -# this will run the encrypted computation using public materials and inputs provided -encrypted_result = circuit.run(public_args) -# the execution returns the encrypted result which can later be decrypted -decrypted_result = circuit.decrypt(encrypted_result) -``` - -## Further reading - -* [Working With Floating Points Tutorial](../tutorial/working\_with\_floating\_points.md) -* [Table Lookup Tutorial](../tutorial/table\_lookup.md) diff --git a/docs/basics/installing.md b/docs/basics/installing.md index d8140e9d2..6910d6577 100644 --- a/docs/basics/installing.md +++ b/docs/basics/installing.md @@ -1,62 +1,55 @@ -# Installing +# Installation -## Python package +**Concrete Numpy** is natively supported on Linux and macOS for Python 3.8 and 3.9, but if you have Docker support in your platform, you can use the docker image to use **Concrete Numpy**. -To install **Concrete Numpy** from PyPi, run the following: +## Using PyPI + +You can install **Concrete Numpy** from PyPI: ```shell pip install concrete-numpy ``` -{% hint style='info' %} -Note that **concrete-numpy** has `pygraphviz` as an optional dependency to draw graphs. +{% hint style="warning" %} +Apple silicon users must use docker installation (explained below) as there is no ARM version of some of our dependencies for the time being. {% endhint %} -{% hint style='info' %} -`pygraphviz` requires `graphviz` packages being installed on your OS, see https://pygraphviz.github.io/documentation/stable/install.html -{% endhint %} - -{% hint style='tip' %} -`graphviz` packages are binary packages that won't automatically be installed by pip. -Do check https://pygraphviz.github.io/documentation/stable/install.html for instructions on how to install `graphviz` for `pygraphviz`. -{% endhint %} - -You can install the extra python dependencies for drawing with: +You can install the extra python dependencies for drawing circuits: ```shell pip install concrete-numpy[full] -# you may need to force reinstallation -pip install --force-reinstall concrete-numpy[full] ``` -## Docker image +{% hint style="info" %} +**Concrete Numpy** depends on `pygraphviz` for drawing, which requires `graphviz` packages to be installed on your system (see [pygraphviz installation documentation](https://pygraphviz.github.io/documentation/stable/install.html)). +{% endhint %} -You can also get the **concrete-numpy** docker image by either pulling the latest docker image or a specific version: +## Using Docker + +You can also get the **Concrete Numpy** docker image: ```shell -docker pull zamafhe/concrete-numpy:latest -# or -docker pull zamafhe/concrete-numpy:v0.2.0 +docker pull zamafhe/concrete-numpy:v0.6.0 ``` -The image can be used with docker volumes, [see the docker documentation here](https://docs.docker.com/storage/volumes/). +### Starting Jupyter server -You can then use this image with the following command: +By default, the entry point of the **Concrete Numpy** docker image is a jupyter server that you can access from your browser: ```shell -docker run --rm -it -p 8888:8888 zamafhe/concrete-numpy:v0.2.0 +docker run --rm -it -p 8888:8888 zamafhe/concrete-numpy:latest ``` -or with local volume to save notebooks on host: - -``` -docker run --rm -it -p 8888:8888 -v /host/path:/data zamafhe/concrete-numpy:v0.2.0 -``` - -This will launch a **Concrete Numpy** enabled jupyter server in the docker, that you can access from your browser. - -Alternatively, you can just open a shell in the docker with or without volumes: +To save notebooks on host, you can use a local volume: ```shell -docker run --rm -it zamafhe/concrete-numpy:v0.2.0 /bin/bash +docker run --rm -it -p 8888:8888 -v /path/to/notebooks:/data zamafhe/concrete-numpy:latest +``` + +### Starting Bash session + +Alternatively, you can launch a Bash session: + +```shell +docker run --rm -it zamafhe/concrete-numpy:latest /bin/bash ``` diff --git a/docs/basics/numpy_support.md b/docs/basics/numpy_support.md deleted file mode 100644 index 422518699..000000000 --- a/docs/basics/numpy_support.md +++ /dev/null @@ -1,148 +0,0 @@ -# Numpy Support - -In this section, we list the operations which are supported currently in **Concrete Numpy**. Please have a look to numpy [documentation](https://numpy.org/doc/stable/user/index.html) to know what these operations are about. - - - -List of supported functions: -- absolute -- add -- arccos -- arccosh -- arcsin -- arcsinh -- arctan -- arctan2 -- arctanh -- around -- bitwise_and -- bitwise_or -- bitwise_xor -- cbrt -- ceil -- clip -- concatenate -- copysign -- cos -- cosh -- deg2rad -- degrees -- dot -- equal -- exp -- exp2 -- expm1 -- fabs -- float_power -- floor -- floor_divide -- fmax -- fmin -- fmod -- gcd -- greater -- greater_equal -- heaviside -- hypot -- invert -- isfinite -- isinf -- isnan -- lcm -- ldexp -- left_shift -- less -- less_equal -- log -- log10 -- log1p -- log2 -- logaddexp -- logaddexp2 -- logical_and -- logical_not -- logical_or -- logical_xor -- matmul -- maximum -- minimum -- multiply -- negative -- nextafter -- not_equal -- ones_like -- positive -- power -- rad2deg -- radians -- reciprocal -- remainder -- reshape -- right_shift -- rint -- round_ -- sign -- signbit -- sin -- sinh -- spacing -- sqrt -- square -- subtract -- sum -- tan -- tanh -- transpose -- true_divide -- trunc -- where -- zeros_like - - -# Shapes - -Our encrypted tensors have shapes just like numpy arrays. -We determine the shapes of the inputs from the inputset, and we infer the shapes of the intermediate values from the function that is being compiled. - -You can access the shape of a tensor by accessing the `shape` property, just like in numpy. -Here is an example: - -```python -def function_to_compile(x): - return x.reshape((x.shape[0], -1)) -``` - -One important aspect of our library is that, scalars are tensors of shape `()`. -This is transparent to you, as a user, but it's something to keep in mind, especialy if you are accessing the `shape` property in the functions that you are compiling. -This schema is used by numpy and pytorch as well. - -## Indexing - -Indexing is described in [this section](../tutorial/indexing.md). - -## Other machine-learning-related operators - -We support (sometimes, with limits) some other operators: - -- dot: one of the operators must be non-encrypted -- clip: the minimum and maximum values must be constant -- transpose -- ravel -- reshape: the shapes must be constant -- flatten -- matmul: one of the two matrices must be non-encrypted. Only 2D matrix multiplication is supported for now - -## Operators which are not numpy-restricted - -The framework also gives support for: - -- shifts, i.e., `x op y` for `op` in `[<<, >>, ]`: if one of `x` or `y` is a constant -- boolean test operations, i.e., `x op y` for `op` in `[<, <=, ==, !=, >, >=]`: if one of `x` or `y` is a constant -- boolean operators, i.e., `x op y` for `op` in `[&, ^, |]`: if one of `x` or `y` is a constant -- powers, i.e., `x ** y`: if one of `x` or `y` is a constant -- modulo, i.e., `x % y`: if one of `x` or `y` is a constant -- invert, i.e., `~x` -- true div, i.e., `x / y`: if one of `x` or `y` is a constant -- floor div, i.e., `x // y`: if one of `x` or `y` is a constant - -There is support for astype as well, e.g. `x.astype(numpy.int64)`. This allows to control which data type to use for computations. In the context of FHE going back to integers may allow to fuse floating point operations together, see [this tutorial](../tutorial/working_with_floating_points.md) to see how to work with floating point values. diff --git a/docs/basics/quick_start.md b/docs/basics/quick_start.md new file mode 100644 index 000000000..49ac7a497 --- /dev/null +++ b/docs/basics/quick_start.md @@ -0,0 +1,86 @@ +# Quick Start + +To compute on encrypted data, you first need to define the function that you want to compute, then compile it into a Concrete Numpy `Circuit`, which you can use to perform homomorphic evaluation. + +Here is the full example that we will walk through: + +```python +import concrete.numpy as cnp + +def add(x, y): + return x + y + +compiler = cnp.Compiler(add, {"x": "encrypted", "y": "clear"}) + +inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] +circuit = compiler.compile(inputset) + +x = 4 +y = 4 + +clear_evaluation = add(x, y) +homomorphic_evaluation = circuit.encrypt_run_decrypt(x, y) + +print(x, "+", y, "=", clear_evaluation, "=", homomorphic_evaluation) +``` + +## Importing the library + +Everything you need to perform homomorphic evaluation is included in a single module: + + +```python +import concrete.numpy as cnp +``` + +## Defining the function to compile + +In this example, we will compile a simple addition function: + + +```python +def add(x, y): + return x + y +``` + +## Creating a compiler + +To compile the function, you need to create a `Compiler` by specifying the function to compile and encryption status of its inputs: + + +```python +compiler = cnp.Compiler(add, {"x": "encrypted", "y": "clear"}) +``` + +## Defining an inputset + +An inputset is a collection representing the typical inputs to the function. It is used to determine the bit widths and shapes of the variables within the function. + +It should be an iterable, yielding tuples of the same length as the number of arguments of the function being compiled: + + +```python +inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] +``` + +## Compiling the function + +You can use the `compile` method of a `Compiler` class with an inputset to perform the compilation and get the resulting circuit back: + + +```python +circuit = compiler.compile(inputset) +``` + +## Performing homomorphic evaluation + +You can use the `encrypt_run_decrypt` method of a `Circuit` class to perform homomorphic evaluation: + + +```python +homomorphic_evaluation = circuit.encrypt_run_decrypt(4, 4) +``` + +{% hint style="info" %} +`circuit.encrypt_run_decrypt(*args)` is just a convenient way to do everything at once. It is implemented as `circuit.decrypt(circuit.run(circuit.encrypt(*args)))`. +{% endhint %} diff --git a/docs/dev/compilation.md b/docs/dev/compilation.md index c94aeb601..b2ce940bf 100644 --- a/docs/dev/compilation.md +++ b/docs/dev/compilation.md @@ -1,45 +1,14 @@ -# Compilation Pipeline In Depth +# Compilation -## What is **concrete-numpy**? - -**concrete-numpy** is a convenient python package, made on top of **Concrete compiler** and **Concrete library**, for developing homomorphic applications. One of its essential functionalities is to transform Python functions to their `MLIR` equivalent. Unfortunately, not all python functions can be converted due to the limits of current product (we are in the alpha stage), or sometimes due to inherent restrictions of FHE itself. However, you can already build interesting and impressing use cases, and more will be available in further versions of the framework. - -## How can I use it? - -```python -# Import necessary Concrete components -import concrete.numpy as cnp - -# Define the function to homomorphize -def f(x, y): - return (2 * x) + y - -# Create a Compiler -compiler = cnp.Compiler(f, {"x": "encrypted", "y": "encrypted"}) - -# Compile to a Circuit using an inputset -inputset = [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)] -circuit = compiler.compile(inputset) - -# Make homomorphic inference -circuit.encrypt_run_decrypt(1, 0) -``` - -## Overview of the numpy compilation process - -The compilation journey begins with tracing to get an easy to understand and manipulate representation of the function. We call this representation `Computation Graph` which is basically a Directed Acyclic Graph (DAG) containing nodes representing the computations done in the function. Working with graphs is good because they have been studied extensively over the years and there are a lot of algorithms to manipulate them. Internally, we use [networkx](https://networkx.org) which is an excellent graph library for Python. +The compilation journey begins with tracing to get an easy-to-manipulate representation of the function. We call this representation a `Computation Graph`, which is basically a Directed Acyclic Graph (DAG) containing nodes representing the computations done in the function. Working with graphs is good because they have been studied extensively over the years and there are a lot of algorithms to manipulate them. Internally, we use [networkx](https://networkx.org), which is an excellent graph library for Python. The next step in the compilation is transforming the computation graph. There are many transformations we perform, and they will be discussed in their own sections. In any case, the result of transformations is just another computation graph. -After transformations are applied, we need to determine the bounds (i.e., the minimum and the maximum values) of each intermediate node. This is required because FHE currently allows a limited precision for computations. Bound measurement is our way to know what is the needed precision for the function. +After transformations are applied, we need to determine the bounds (i.e., the minimum and the maximum values) of each intermediate node. This is required because FHE currently allows 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 prepared, the rest of the stack, which you can learn more about [here](http://docs.zama.ai/), takes over and completes the compilation process. - -Here is the visual representation of the pipeline: - -![Frontend Flow](../_static/compilation-pipeline/frontend_flow.svg) +Once the MLIR is generated, we send it to the **Concrete Compiler**, and it completes the compilation process. ## Tracing @@ -52,13 +21,11 @@ def f(x): the goal of tracing is to create the following computation graph without needing any change from the user. -![](../_static/compilation-pipeline/two_x_plus_three.png) +![](../\_static/compilation-pipeline/two\_x\_plus\_three.png) (Note that the edge labels are for non-commutative operations. To give an example, a subtraction node represents `(predecessor with edge label 0) - (predecessor with edge label 1)`) -To do this, we make use of Tracers, which are objects that record the operation performed during their creation. We create a `Tracer` for each argument of the function and call the function with those tracers. Tracers make use of operator overloading feature of Python to achieve their goal. - -Here is an example: +To do this, we make use of `Tracer`s, which are objects that record the operation performed during their creation. We create a `Tracer` for each argument of the function and call the function with those tracers. `Tracer`s make use of the operator overloading feature of Python to achieve their goal: ``` def f(x, y): @@ -70,11 +37,11 @@ y = Tracer(computation=Input("y")) resulting_tracer = f(x, y) ``` -`2 * y` will be performed first, and `*` is overloaded for `Tracer` to return another tracer: `Tracer(computation=Multiply(Constant(2), self.computation))` which is equal to: `Tracer(computation=Multiply(Constant(2), Input("y")))` +`2 * y` will be performed first, and `*` is overloaded for `Tracer` to return another tracer: `Tracer(computation=Multiply(Constant(2), self.computation))`, which is equal to `Tracer(computation=Multiply(Constant(2), Input("y")))` -`x + (2 * y)` will be performed next, and `+` is overloaded for `Tracer` to return another tracer: `Tracer(computation=Add(self.computation, (2 * y).computation))` which is equal to: `Tracer(computation=Add(Input("x"), Multiply(Constant(2), Input("y")))` +`x + (2 * y)` will be performed next, and `+` is overloaded for `Tracer` to return another tracer: `Tracer(computation=Add(self.computation, (2 * y).computation))`, which is equal to `Tracer(computation=Add(Input("x"), Multiply(Constant(2), Input("y")))` -In the end, we will have output Tracers that can be used to create the computation graph. The implementation is a bit more complex than this, but the idea is the same. +In the end, we will have output tracers that can be used to create the computation graph. The implementation is a bit more complex than this, but the idea is the same. Tracing is also responsible for indicating whether the values in the node would be encrypted or not, and the rule for that is if a node has an encrypted predecessor, it is encrypted as well. @@ -86,33 +53,33 @@ With the current version of **Concrete Numpy**, floating point inputs and floati Let's take a closer look at the transforms we can currently perform. -### Fusing floating point operations +### Fusing. -We have allocated a whole new chapter to explaining float fusing. You can find it [here](float-fusing.md). +We have allocated a whole new chapter to explaining fusing. You can find it after this chapter. ## Bounds measurement Given a computation graph, the goal of the bound measurement step is to assign the minimal data type to each node in the graph. -Let's say we have an encrypted input that is always between `0` and `10`, we should assign the type `Encrypted` to node of this input as `Encrypted` is the minimal encrypted integer that supports all the 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 `Encrypted` to the node of this input as `Encrypted` is the minimal encrypted integer that supports all values between `0` and `10`. If there were negative values in the range, we could have used `intX` instead of `uintX`. -Bounds measurement is necessary because FHE supports limited precision, and we don't want unexpected behaviour during evaluation of the compiled functions. +Bounds measurement is necessary because FHE supports limited precision, and we don't want unexpected behaviour while evaluating the compiled functions. Let's take a closer look at how we perform bounds measurement. -### Inputset evaluation +### Inputset evaluation. This is a simple approach that requires an inputset to be provided by the user. -The inputset is not to be confused with the dataset which is classical in ML, as it doesn't require labels. Rather, it is a set of values which are typical inputs of the function. +The inputset is not to be confused with the dataset, which is classical in ML, as it doesn't require labels. Rather, it is a set of values which are typical inputs of the function. -The idea is to evaluate each input in the inputset and record the result of each operation in the computation graph. Then we compare the evaluation results with the current minimum/maximum values of each node and update the minimum/maximum accordingly. After the entire inputset is evaluated, we assign a data type to each node using the minimum and the maximum value it contains. +The idea is to evaluate each input in the inputset and record the result of each operation in the computation graph. Then we compare the evaluation results with the current minimum/maximum values of each node and update the minimum/maximum accordingly. After the entire inputset is evaluated, we assign a data type to each node using the minimum and the maximum values it contains. Here is an example, given this computation graph where `x` is encrypted: -![](../_static/compilation-pipeline/two_x_plus_three.png) +![](../\_static/compilation-pipeline/two\_x\_plus\_three.png) and this inputset: @@ -178,156 +145,4 @@ Assigned Data Types: ## 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) - -## Example walkthrough #1 - -### Function to homomorphize - -``` -def f(x): - return (2 * x) + 3 -``` - -### Parameters - -``` -x = "encrypted" -``` - -#### Corresponding computation graph - -![](../_static/compilation-pipeline/two_x_plus_three.png) - -### Topological transforms - -#### Fusing floating point operations - -This transform isn't applied since the computation doesn't involve any floating point operations. - -### Bounds measurement using \[2, 3, 1] as inputset (same settings as above) - -Data Types: - -* `x`: Encrypted<**uint2**> -* `2`: Clear<**uint2**> -* `*`: Encrypted<**uint3**> -* `3`: Clear<**uint2**> -* `+`: Encrypted<**uint4**> - -### MLIR lowering - -``` -module { - func @main(%arg0: !FHE.eint<4>) -> !FHE.eint<4> { - %c3_i5 = constant 3 : i5 - %c2_i5 = constant 2 : i5 - %0 = "FHE.mul_eint_int"(%arg0, %c2_i5) : (!FHE.eint<4>, i5) -> !FHE.eint<4> - %1 = "FHE.add_eint_int"(%0, %c3_i5) : (!FHE.eint<4>, i5) -> !FHE.eint<4> - return %1 : !FHE.eint<4> - } -} -``` - -## Example walkthrough #2 - -### Function to homomorphize - -``` -def f(x, y): - return (42 - x) + (y * 2) -``` - -### Parameters - -``` -x = "encrypted" -y = "encrypted" -``` - -#### Corresponding computation graph - -![](../_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png) - -### Topological transforms - -#### Fusing floating point operations - -This transform isn't applied since the computation doesn't involve any floating point operations. - -### Bounds measurement using \[(6, 0), (5, 1), (3, 0), (4, 1)] as inputset - -Evaluation Result of `(6, 0)`: - -* `42`: 42 -* `x`: 6 -* `y`: 0 -* `2`: 2 -* `-`: 36 -* `*`: 0 -* `+`: 36 - -Evaluation Result of `(5, 1)`: - -* `42`: 42 -* `x`: 5 -* `y`: 1 -* `2`: 2 -* `-`: 37 -* `*`: 2 -* `+`: 39 - -Evaluation Result of `(3, 0)`: - -* `42`: 42 -* `x`: 3 -* `y`: 0 -* `2`: 2 -* `-`: 39 -* `*`: 0 -* `+`: 39 - -Evaluation Result of `(4, 1)`: - -* `42`: 42 -* `x`: 4 -* `y`: 1 -* `2`: 2 -* `-`: 38 -* `*`: 2 -* `+`: 40 - -Bounds: - -* `42`: \[42, 42] -* `x`: \[3, 6] -* `y`: \[0, 1] -* `2`: \[2, 2] -* `-`: \[36, 39] -* `*`: \[0, 2] -* `+`: \[36, 40] - -Data Types: - -* `42`: Clear<**uint6**> -* `x`: Encrypted<**uint3**> -* `y`: Encrypted<**uint1**> -* `2`: Clear<**uint2**> -* `-`: Encrypted<**uint6**> -* `*`: Encrypted<**uint2**> -* `+`: Encrypted<**uint6**> - -### MLIR lowering - -``` -module { - func @main(%arg0: !FHE.eint<6>, %arg1: !FHE.eint<6>) -> !FHE.eint<6> { - %c42_i7 = constant 42 : i7 - %c2_i7 = constant 2 : i7 - %0 = "FHE.sub_int_eint"(%c42_i7, %arg0) : (i7, !FHE.eint<6>) -> !FHE.eint<6> - %1 = "FHE.mul_eint_int"(%arg1, %c2_i7) : (!FHE.eint<6>, i7) -> !FHE.eint<6> - %2 = "FHE.add_eint"(%0, %1) : (!FHE.eint<6>, !FHE.eint<6>) -> !FHE.eint<6> - return %2 : !FHE.eint<6> - } -} -``` +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). diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index 092f3cf62..08c1d272a 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -1,64 +1,60 @@ +# Contribute -# Contributing +{% hint style="info" %} +There are two ways to contribute to **Concrete Numpy** or to **Concrete** tools in general: -{% 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, so please make sure to get in touch before you do! +* 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, so please make sure to get in touch before you do! {% endhint %} -Let's go over some other important things that you need to be careful about. +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 and some examples. +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_$issue_id +git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description ``` -e.g. +...and some examples: ```shell -git checkout -b feat/explicit-tlu_11 -git checkout -b fix/tracing_indexing_42 +git checkout -b feat/direct-tlu +git checkout -b fix/tracing-indexing ``` ## Before committing -### Conformance +### Conformance. -Each commit to **concrete-numpy** should conform to the standards decided by the team. Conformance can be checked using the following command. +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 ``` -### pytest +### Testing. -Of course, tests must pass as well. +On top of conformance, all tests must pass with 100% code coverage across the codebase: ```shell make pytest ``` -### Coverage +{% 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 %} -The last requirement is to make sure you get 100 percent code coverage. The `make pytest` command checks that by default and will fail with a coverage report at the end should some lines of your code not be executed during testing. +## Committing -If your coverage is below 100 percent, you should write more tests and then create the pull request. If you ignore this warning and create the PR, GitHub actions will fail and your PR will not be merged anyway. - -There may be cases where covering you code is not possible (exception 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 and reviewers will 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. - -## Commiting - -We are using a consistent commit naming scheme, and you are expected to follow it as well (the CI will make sure you do). The accepted format can be printed to your terminal by running: +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 ``` -e.g. +...and some examples: ```shell git commit -m "feat: implement bounds checking" @@ -66,15 +62,15 @@ git commit -m "feat(debugging): add an helper function to draw intermediate repr 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. Just a reminder that commit messages are checked in the comformance step, and rejected if they don't follow the rules. +To learn more about conventional commits, check [this](https://www.conventionalcommits.org/en/v1.0.0/) page. -## Before creating pull request +## Before creating a pull request -{% hint style='tip' %} -We remind that only official contributors can send pull requests. To become such an official contributor, please email hello@zama.ai. +{% 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 `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 having to rewrite parts of your PR later if some conflicts arise with other PRs being merged. After you commit your changes to your new branch, you can use the following commands to rebase: +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 diff --git a/docs/dev/docker.md b/docs/dev/docker.md index ea2fb5b7e..ee8303875 100644 --- a/docs/dev/docker.md +++ b/docs/dev/docker.md @@ -1,54 +1,45 @@ -# Docker +# Docker Setup -## Setting up docker and X forwarding +## Installation -Before you start this section, go ahead and install docker. You can follow [this](https://docs.docker.com/engine/install/) official guide for that. +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. -### Linux +## X forwarding + +### Linux. + +You can use xhost command: ```shell xhost +localhost ``` -### Mac OS +### macOS. -To be able to use X forwarding on Mac OS: -- 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: +To use X forwarding on Mac OS: + +* 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 ``` -and now, the X server should be all set in docker (in the regular terminal). +X server should be all set for Docker in the regular terminal. -### Windows +## Building -Install Xming and use Xlaunch: -- Multiple Windows, Display number: 0 -- `Start no client` -- **IMPORTANT**: Check `No Access Control` -- You can save this configuration to re-launch easily, then click finish. - -## Logging in and building the image - -Docker image of **Concrete-Numpy** is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. - -Upon joining to the team, you need to log in using the following command: +You can use the dedicated target in the Makefile to build the docker image: ```shell -docker login ghcr.io +make docker_build ``` -This command will ask for a username and a password. For username, just enter your GitHub username. For password, you should create a personal access token from [here](https://github.com/settings/tokens) selecting `read:packages` permission. Just paste the generated access token as your password, and you are good to go. +## Starting -Once you do that, you can get inside the docker environment using the following command: +You can use the dedicated target in the Makefile to start the docker session: ```shell -make docker_build_and_start -# or equivalently but shorter -make docker_bas +make docker_start ``` - -After you finish your work, you can leave the docker by using the `exit` command or by pressing `CTRL + D`. diff --git a/docs/dev/float-fusing.md b/docs/dev/float-fusing.md deleted file mode 100644 index 769b91c05..000000000 --- a/docs/dev/float-fusing.md +++ /dev/null @@ -1,86 +0,0 @@ -# Fusing Floating Point Operations - -## Why is it needed? - -The current compiler stack only supports integers with 7 bits or less. But it's not uncommon to have numpy code using floating point numbers. - -We added fusing floating point operations to make tracing numpy functions somewhat user friendly to allow in-line quantization in the numpy code e.g.: - - -```python -import numpy - -def quantized_sin(x): - # from a 7 bit unsigned integer x, compute z in the [0; 2 * pi] range - z = 2 * numpy.pi * x * (1 / 127) - # quantize over 6 bits and offset to be >= 0, round and convert to integers in range [0; 63] - quantized_sin = numpy.rint(31 * numpy.sin(z) + 31).astype(numpy.int64) - # output quantized_sin and a further offset result - return quantized_sin, quantized_sin + 32 -``` - -This function `quantized_sin` is not strictly supported as is by the compiler as there are floating point intermediate values. However, when looking at the function globally we can see we have a single integer input and a single integer output. As we know the input range we can compute a table to represent the whole computation for each input value, which can later be lowered to a PBS in the FHE world. - -Any computation where there is a single variable integer input and a single integer output can be replaced by an equivalent table lookup. - -The `quantized_sin` graph of operations: - -![](../_static/float_fusing_example/before.png) - -The float subgraph that was detected: - -![](../_static/float_fusing_example/subgraph.png) - -The simplified graph of operations with the float subgraph condensed in a `GenericFunction` node: - -![](../_static/float_fusing_example/after.png) - -## How is it done in **Concrete Numpy**? - -The first step consists in detecting where we go from floating point computation back to integers. This allows the identification of the potential terminal node of the float subgraph we are going to fuse. - -From the terminal node, we go back up through the nodes until we find nodes that go from integers to floats. If we find a single node then we have a fusable subgraph that we replace by an equivalent GenericFunction node and stop the search for fusable subgraphs for the terminal node being considered. If we find more than one such node we try to find a single common ancestor that would go from integers to floats. We repeat the process as long as there are potential ancestors nodes, stopping if we find a suitable float subgraph with a single integer input and a single integer output. - -Here is an example benefiting from the expanded search: - - -```python -def fusable_with_bigger_search(x, y): - """fusable with bigger search""" - x = x + 1 - x_1 = x.astype(numpy.int64) - x_1 = x_1 + 1.5 - x_2 = x.astype(numpy.int64) - x_2 = x_2 + 3.4 - add = x_1 + x_2 - add_int = add.astype(numpy.int64) - return add_int + y -``` - -The `fusable_with_bigger_search` graph of operations: - -![](../_static/float_fusing_example/before_bigger_search.png) - -The float subgraph that was detected: - -![](../_static/float_fusing_example/subgraph_bigger_search.png) - -The simplified graph of operations with the float subgraph condensed in a `GenericFunction` node: - -![](../_static/float_fusing_example/after_bigger_search.png) - -An example of a non fusable computation with that technique is: - - -```python -import numpy - -def non_fusable(x, y): - x_1 = x + 1.5 # x_1 is now float - y_1 = y + 3.4 # y_1 is now float - add = x_1 + y_1 - add_int = add.astype(numpy.int64) - return add_int -``` - -From `add_int` you will find two `Add` nodes going from int to float (`x_1` and `y_1`) which we cannot represent with a single input table look-up. Kolmogorov–Arnold representation theorem states that every multivariate continuous function can be represented as a superposition of continuous functions of one variable ([from Wikipedia](https://en.wikipedia.org/wiki/Kolmogorov%E2%80%93Arnold\_representation\_theorem)), so the above case could be handled in future versions of **Concrete** tools. diff --git a/docs/dev/fusing.md b/docs/dev/fusing.md new file mode 100644 index 000000000..c7f249191 --- /dev/null +++ b/docs/dev/fusing.md @@ -0,0 +1,37 @@ +# 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. Within each iteration: +3. We find a subgraph to fuse. + + 3.1. We search for a terminal node that is appropriate for fusing. + + 3.2. We crawl backwards to find the closest integer nodes to this node. + + 3.3. If there is a single node as such, we return the subgraph from this node to the terminal node. + + 3.4. Otherwise, we try to find the lowest common ancestor (lca) of this list of nodes. + + 3.5. If lca doesn't exist, we say this particular terminal node is not fusable, and we go back to search for another subgraph. + + 3.6. Otherwise, we use this lca as the input of the subgraph and continue with `subgraph` node creation below. +4. We convert the subgraph into a `subgraph` node + + 4.1. We check fusability status of the nodes of the subgraph in this step. +5. 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" %} +[Kolmogorov–Arnold 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 %} diff --git a/docs/dev/mlir.md b/docs/dev/mlir.md index 10234fbdd..50344b5e9 100644 --- a/docs/dev/mlir.md +++ b/docs/dev/mlir.md @@ -1,17 +1,17 @@ # MLIR -The MLIR project is a sub-project of the LLVM project. It's designed to simplify building domain-specific compilers such as ours: Concrete Compiler. +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 input and emits compiled assembly code for the target architecture. +**Concrete Compiler** accepts MLIR as an input and emits compiled assembly code for a target architecture. -Concrete NumPy does the MLIR generation from the computation graph. Code related to this conversion is in `concrete/numpy/mlir` folder. +**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 `convert` method of `GraphConverter` class. +The conversion can be performed using the `convert` method of the `GraphConverter` class. -Within `convert` method of `GraphConverter`: +Within the `convert` method of `GraphConverter`: -* MLIR compatibility of the graph is checked -* Bit-width constraints are checked -* Negative lookup tables are offsetted -* Computation graph is traversed and each node is converted to their corresponding MLIR representation using `NodeConverter` class -* String representation of resulting MLIR is returned +* 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. diff --git a/docs/dev/project_setup.md b/docs/dev/project_setup.md index 53466f0a9..976fdf1e5 100644 --- a/docs/dev/project_setup.md +++ b/docs/dev/project_setup.md @@ -1,171 +1,93 @@ # Project Setup -{% hint style='info' %} -It is strongly recommended to use the development docker (see the [docker](./docker.md) guide). However you can setup the project on bare macOS and Linux provided you install the required dependencies (check Dockerfile.env for the required binary packages like make). - -The project targets Python 3.8 through 3.9 inclusive. +{% hint style="info" %} +It is **strongly** recommended to use the development tool Docker. Though, you can 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 +## Installing `Python` -**concrete-numpy** is a `Python` library, so `Python` should be installed to develop **concrete-numpy**. `v3.8` and `v3.9` are the only supported versions. +**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 can follow [this](https://realpython.com/installing-python/) guide to install it (alternatively you can google `how to install python 3.8 (or 3.9)`). +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 +## 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. -{% hint style='danger' %} -As there is no `concrete-compiler` package for Windows, only the dev dependencies can be installed. This requires poetry >= 1.2. +## Installing `make` -At the time of writing (January 2022), there is only an alpha version of poetry 1.2 that you can install. In the meantime we recommend following [this link to setup the docker environment](./docker.md) on Windows. -{% endhint %} +`make` is used to launch various commands such as formatting and testing. -## Installing make +On Linux, you can install `make` using the package manager of your distribution. -The dev tools use `make` to launch the various commands. - -On Linux you can install `make` from your distribution's preferred package manager. - -On Mac OS you can install a more recent version of `make` via brew: +On macOS, you can install `gmake` via brew: ```shell -# check for gmake -which gmake -# If you don't have it, it will error out, install gmake brew install make -# recheck, now you should have gmake -which gmake ``` -It is possible to install `gmake` as `make`, check this [StackOverflow post](https://stackoverflow.com/questions/38901894/how-can-i-install-a-newer-version-of-make-on-mac-os) for more info. - -On Windows check [this GitHub gist](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make). - -{% hint style='tip' %} -In the following sections, be sure to use the proper `make` tool for your system: `make`, `gmake`, or other. +{% 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 repository +## Cloning the repository -Now, it's time to get the source code of **concrete-numpy**. +Now, it's time to get the source code of **Concrete Numpy**. -Clone the code repository using the link for your favourite communication protocol (ssh or https). +Clone the git repository from GitHub using the protocol of your choice (ssh or https). -## Setting up environment on your host OS +## Setting up the environment -We are going to make use of virtual environments. This helps to keep the project isolated from other `Python` projects in the system. The following commands will create a new virtual environment under the project directory and install dependencies to it. +Virtual environments are utilized to keep the project isolated from other `Python` projects in the system. -{% hint style='danger' %} -The following command will not work on Windows if you don't have poetry >= 1.2. As poetry 1.2 is still in alpha we recommend following [this link to setup the docker environment](./docker.md) instead. -{% endhint %} +To create a new virtual environment and install dependencies, use the command: ```shell -cd concrete-numpy make setup_env ``` ## Activating the environment -Finally, all we need to do is to activate the newly created environment using the following command. - -### macOS or Linux +To activate the newly created environment, use: ```shell source .venv/bin/activate ``` -### Windows +## Syncing the environment + +From time to time, new dependencies will be added to the project and old ones will be removed. + +The command below will make sure the project has the proper environment, so run it regularly. ```shell -source .venv/Scripts/activate +make sync_env ``` -## Setting up environment on docker +## Troubleshooting -The docker automatically creates and sources a venv in ~/dev_venv/ +### In native setups. -The venv persists thanks to volumes. We also create a volume for ~/.cache to speed up later reinstallations. You can check which docker volumes exist with: - -```shell -docker volume ls -``` - -You can still run all `make` commands inside the docker (to update the venv, for example). Be mindful of the current venv being used (the name in parentheses at the beginning of your command prompt). - -```shell -# Here we have dev_venv sourced -(dev_venv) dev_user@8e299b32283c:/src$ make setup_env -``` - -## Leaving the environment - -After your work is done, you can simply run the following command to leave the environment. +If you are having issues in a native setup, you can try to re-create your environment like this: ```shell deactivate -``` - -## Syncing environment with the latest changes - -From time to time, new dependencies will be added to project or the old ones will be removed. The command below will make sure the project has the proper environment. So run it regularly! - -```shell -make sync_env -``` - -## Troubleshooting your environment - -### In your OS - -If you are having issues, consider using the dev docker exclusively (unless you are working on OS specific bug fixes or features). - -Here are the steps you can take on your OS to try and fix issues: - -```shell -# Try to install the env normally -make setup_env - -# If you are still having issues, sync the environment -make sync_env - -# If you are still having issues on your OS delete the venv: rm -rf .venv - -# And re-run the env setup make setup_env +source .venv/bin/activate ``` -At this point you should consider using docker as nobody will have the exact same setup as you, unless you need to develop on your OS directly, in which case you can ask us for help but may not get a solution right away. +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 +### In docker setups. -Here are the steps you can take in your docker to try and fix issues: +If you are having issues in a docker setup, you can try to re-build the docker image: ```shell -# Try to install the env normally -make setup_env - -# If you are still having issues, sync the environment -make sync_env - -# If you are still having issues in docker delete the venv: -rm -rf ~/dev_venv/* - -# Disconnect from the docker -exit - -# And relaunch, the venv will be reinstalled -make docker_start - -# If you are still out of luck, force a rebuild which will also delete the volumes make docker_rebuild - -# And start the docker which will reinstall the venv make docker_start ``` -If the problem persists at this point, you should consider asking for help. We're here and ready to assist! +If the problem persists, you should contact us for help. diff --git a/docs/dev/releasing.md b/docs/dev/releasing.md index 5f09d17a1..f92efa8d7 100644 --- a/docs/dev/releasing.md +++ b/docs/dev/releasing.md @@ -1,9 +1,17 @@ -# Creating A Release On GitHub +# Release process -## Release Candidate cycle +## Release candidate cycle -Before settling for a final release, we go through a Release Candidate (RC) cycle. The idea is that once the code base and documentations look ready for a release you create an RC Release by opening an issue with the release template [here](https://github.com/zama-ai/concrete-numpy-internal/issues/new?assignees=&labels=&template=release.md), starting with version `vX.Y.Zrc1` and then with versions `vX.Y.Zrc2`, `vX.Y.Zrc3`... +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. -## Proper release +## Release flow -Once the last RC is deemed ready, open an issue with the release template using the last RC version from which you remove the `rc?` part (i.e. `v12.67.19` if your last RC version was `v12.67.19-rc4`) on [github](https://github.com/zama-ai/concrete-numpy-internal/issues/new?assignees=&labels=&template=release.md). +* Checkout to the commit that you want to include in the release (everything before this commit and this commit will be in the release) +* Run `make release` +* Wait for CI to complete +* Checkout to `chore/version` branch +* Run `VERSION=a.b.c-rcX make set_version` with appropriate version +* Push the branch to origin +* Create a PR to merge it to main +* Wait for CI to finish and get approval in the meantime +* Merge the version update to main diff --git a/docs/dev/terminology_and_structure.md b/docs/dev/terminology_and_structure.md index 71b64f0a8..5f102a840 100644 --- a/docs/dev/terminology_and_structure.md +++ b/docs/dev/terminology_and_structure.md @@ -2,31 +2,25 @@ ## Terminology -In this section we will go over some terms that we use throughout the project. +Some terms used throughout the project include: -- intermediate representation - - a data structure to represent a computation - - basically a computation graph in which nodes are either inputs, constants, or operations on other nodes -- tracing - - it is the technique to take a python function from a user and generate intermediate representation corresponding to it in a painless way for the user -- bounds - - before intermediate representation is converted to MLIR, we need to know which node will output which type (e.g., uint3 vs uint5) - - there are several ways to do this but the simplest one is to evaluate the intermediate representation with some combinations of inputs and remember the maximum and the minimum values for each node, which is what we call bounds, and bounds can be used to determine the appropriate type for each node -- circuit - - it is the result of compilation - - it is made of the computation graph and the compiler engine - - it has methods for printing, visualizing, and evaluating +* 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 and drawing to evaluation. ## Module structure -In this section, we will discuss the module structure of **concrete-numpy** briefly. You are encouraged to check individual `.py` files to learn more! +In this section, we will discuss the module structure of **Concrete Numpy** briefly. 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., conv2d) - - mlir: mlir conversion - - compilation: compilation from python functions to circuits +* 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 diff --git a/docs/explanation/fhe_and_framework_limits.md b/docs/explanation/fhe_and_framework_limits.md deleted file mode 100644 index b7f02880d..000000000 --- a/docs/explanation/fhe_and_framework_limits.md +++ /dev/null @@ -1,33 +0,0 @@ -# FHE and **Concrete Numpy** Limits - -## FHE limits - -FHE used to be an impossible thing to imagine, twenty years ago. Then, with advances due to [Craig Gentry](https://crypto.stanford.edu/craig/), this became a dream come true. And, even more recently, with several generations of new scheme, FHE became practical. - -### Speed - -However, one still has to consider that FHE is slow, as compared to the vanilla implementations. With the different HW pluggins that can be added to **Concrete**, an important speed factor can be achieved. - -### Multiplying by constants - -In the scheme used in **Concrete Numpy**, namely [TFHE](https://tfhe.github.io/tfhe/), multiplications by constants is only defined for integer constants. Notably, one can't multiply by floats. As float multiplication is very usual in the data science (think of weights of dense layers, for example), this could be a problem, but quantization is at our rescue. See [Quantization](https://docs.preprod.zama.ai/concrete-ml/main/user/explanation/quantization.html) section of Concrete ML documentation for more details. - -### Achieving computations of not-linear functions - -For most FHE scheme but TFHE, the application of a non-linear function is complicated and slow, if not impossible. Typically, this is a blocker, since activation functions _are_ non-linear. However, in the **Concrete Numpy** package, we use an operation called _programmable bootstrapping_ (described in this [white paper](https://whitepaper.zama.ai)), which allows to apply any table lookup: by quantizing the non-linear function, any function can thus be replaced. - -## Limits of this project - -Since this is an early version of the product, not everything is done, to say the least. What we wanted to tackle first was the cryptographic complexities. This is why we concentrated on the cryptographic part, and let some engineering problems for later. - -### Currently executing locally - -As of today, the execution of the FHE program is done locally. Notably, in the current version, there is no client (on which we encrypt the private data, or decrypt the returned result) or server (on which the computation is done completely over encrypted data), but a single host. As explained in [this section](future_features.md), this limit will be removed in the next version, such that **Concrete Numpy** can be used in production. - -### Currently slow - -As we explained, we wanted to focus first on cryptographic challenges. Performance has been postponed, and will be tackled in the next release. - -### Currently restricted to 7 bits computations - -For the moment, we can only perform computations with 7 bits or less. Furthermore, the exactness of computations is only ensured for 6 bits or less; for 7 bits, the computations are exact with a probability close to 90%. Of course, we are working on increasing this limit, and making the probability of a wrong computation as close to 0% as possible. Don't hesitate to look at [Quantization](https://docs.preprod.zama.ai/concrete-ml/main/user/explanation/quantization.html) section of Concrete ML documentation to know how to use smaller integers. diff --git a/docs/explanation/future_features.md b/docs/explanation/future_features.md deleted file mode 100644 index f75f54bb1..000000000 --- a/docs/explanation/future_features.md +++ /dev/null @@ -1,12 +0,0 @@ -# Future Features - -As explained in [this section](fhe_and_framework_limits.md#limits-of-this-project), the **Concrete Numpy** package -is currently in its first version, and is sometimes constrained in term of functionalities. However, the good -news is that we are going to release new versions regularly, and more functionality will be added progressively. - -In this page, we briefly list what the plans for next versions of **Concrete Numpy** are: -- **better performance**: further versions will contain improved versions of the **Concrete Library**, with faster -execution; also, the **Concrete Compiler** will be improved, to have faster local execution (with multi-threading -for example) and faster production execution (with distribution over a set of machines or use of hardware accelerations) -- **more complete benchmarks**: we will have an extended benchmark, containing lots of functions that you may want to compile; then, we will measure the framework progress by tracking the number of successfully compiled functions over time. Also, this public benchmark will be a way for other competing frameworks or technologies to compare fairly with us, in terms of functionality or performance -- **serialization**: we are going to add several utils, to serialize ciphertexts or keys diff --git a/docs/explanation/what_is_fhe.md b/docs/explanation/what_is_fhe.md deleted file mode 100644 index 0936b393b..000000000 --- a/docs/explanation/what_is_fhe.md +++ /dev/null @@ -1,12 +0,0 @@ -# What is FHE? - -Fully Homomorphic Encryption (FHE for short) is a technology that enables computing on encrypted data directly, without having to decrypt it. -Users would encrypt their data using their own secret key, then send it to your servers for processing. Your servers would process the encrypted data blindly, producing a result which itself is encrypted, and that only the user can decrypt with their secret key. - -From the user's perspective, nothing changes (but the fact that her data is never in clear on the server), they are still sending data to your service and getting a response. But you now no longer need to worry about securing your user data, as it is now encrypted both in transit and during processing, i.e., it is encrypted end-to-end. - -You can learn more about FHE using the following links: -- [quick overview](https://6min.zama.ai/) -- [monthly technical FHE.org meetup](https://www.meetup.com/fhe-org/) -- [videos and resources](http://fhe.org/) -- [Homomorphic Encryption 101](https://medium.com/zama-ai/homomorphic-encryption-101-c1524fb76013) diff --git a/docs/howto/configure.md b/docs/howto/configure.md new file mode 100644 index 000000000..f167cec41 --- /dev/null +++ b/docs/howto/configure.md @@ -0,0 +1,92 @@ +# Configure + +Behavior of **Concrete Numpy** can be customized using `Configuration`s: + +```python +import concrete.numpy as cnp +import numpy as np + +configuration = cnp.Configuration(p_error=0.01, loop_parallelize=True) + +@cnp.compiler({"x": "encrypted"}) +def f(x): + return x + 42 + +inputset = range(10) +circuit = f.compile(inputset, configuration=configuration) +``` + +Alternatively, you can overwrite individual options as kwargs to `compile` method: + +```python +import concrete.numpy as cnp +import numpy as np + +@cnp.compiler({"x": "encrypted"}) +def f(x): + return x + 42 + +inputset = range(10) +circuit = f.compile(inputset, p_error=0.01, loop_parallelize=True) +``` + +Or you can combine both: + +```python +import concrete.numpy as cnp +import numpy as np + +configuration = cnp.Configuration(p_error=0.01) + +@cnp.compiler({"x": "encrypted"}) +def f(x): + return x + 42 + +inputset = range(10) +circuit = f.compile(inputset, configuration=configuration, loop_parallelize=True) +``` + +{% hint style="info" %} +Additional kwarg to `compile` function have higher precedence. So if you set an option in both `configuration` and in `compile` method, the value in the `compile` method will be used. +{% endhint %} + +## Options + +* **show_graph**: bool = False + * Whether to print computation graph during compilation. + +* **show_mlir**: bool = False + * Whether to print MLIR during compilation. + +* **verbose**: bool = False + * Whether to print computation graph and MLIR during compilation. + +* **dump_artifacts_on_unexpected_failures**: bool = True + * Whether to export debugging artifacts automatically on compilation failures. + +* **p_error**: float = 0.000063342483999973 + * Error probability for table lookups. + +* **jit**: bool = False + * Whether to use JIT compilation. + +* **loop_parallelize**: bool = True + * Whether to enable loop parallelization in the compiler. + +* **dataflow_parallelize**: bool = False + * Whether to enable dataflow parallelization in the compiler. + +* **auto_parallelize**: bool = False + * Whether to enable auto parallelization in the compiler. + +* **enable_unsafe_features**: bool = False + * Whether to enable unsage features. + +* **virtual**: bool = False _(Unsafe)_ + * Whether to create a virtual circuit. + +* **use_insecure_key_cache**: bool = False _(Unsafe)_ + * Whether to use the insecure key cache. + +* **insecure_key_cache_location**: Optional[Union[Path, str]] = None + * Location of insecure key cache. diff --git a/docs/howto/debug.md b/docs/howto/debug.md new file mode 100644 index 000000000..411dadcdb --- /dev/null +++ b/docs/howto/debug.md @@ -0,0 +1,266 @@ +# 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 textual representation of the initial computation graph right after tracing. + +``` +%0 = x # EncryptedScalar +%1 = sin(%0) # EncryptedScalar +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 textual representation of the final computation graph right before MLIR conversion. + +``` +%0 = x # EncryptedScalar +%1 = sin(%0) # EncryptedScalar +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, virtual=self.configuration.virtual) + 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( +RuntimeError: Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedScalar +%1 = sin(%0) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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 textual representation of the initial computation graph right after tracing. + +``` +%0 = 127 # ClearScalar +%1 = 50 # ClearScalar +%2 = 1 # ClearScalar +%3 = x # EncryptedScalar +%4 = sin(%3) # EncryptedScalar +%5 = add(%4, %2) # EncryptedScalar +%6 = multiply(%1, %5) # EncryptedScalar +%7 = astype(%6, dtype=int_) # EncryptedScalar +%8 = subtract(%0, %7) # EncryptedScalar +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 textual representation of the intermediate computation graph after fusing. + +``` +%0 = 127 # ClearScalar +%1 = x # EncryptedScalar +%2 = subgraph(%1) # EncryptedScalar +%3 = subtract(%0, %2) # EncryptedScalar +return %3 + +Subgraphs: + + %2 = subgraph(%1): + + %0 = 50 # ClearScalar + %1 = 1 # ClearScalar + %2 = input # EncryptedScalar + %3 = sin(%2) # EncryptedScalar + %4 = add(%3, %1) # EncryptedScalar + %5 = multiply(%0, %4) # EncryptedScalar + %6 = astype(%5, dtype=int_) # EncryptedScalar + 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 +%1 = x # EncryptedScalar +%2 = subgraph(%1) # EncryptedScalar +%3 = subtract(%0, %2) # EncryptedScalar +return %3 + +Subgraphs: + + %2 = subgraph(%1): + + %0 = 50 # ClearScalar + %1 = 1 # ClearScalar + %2 = input # EncryptedScalar + %3 = sin(%2) # EncryptedScalar + %4 = add(%3, %1) # EncryptedScalar + %5 = multiply(%0, %4) # EncryptedScalar + %6 = astype(%5, dtype=int_) # EncryptedScalar + 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 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 diff --git a/docs/howto/debug_support_submit_issues.md b/docs/howto/debug_support_submit_issues.md deleted file mode 100644 index 026122c77..000000000 --- a/docs/howto/debug_support_submit_issues.md +++ /dev/null @@ -1,76 +0,0 @@ -# Debugging / Support / Submitting Issues - -This version of **Concrete Numpy** is a first version of the product, meaning that it is not completely finished, contains several bugs (would they be known or unknown at this time), and will improve over time with feedback from early users. - -Here are some ways to debug your problems. If nothing seems conclusive, you can still report the issue, as explained in a later section of this page. - -## Is it a bug by the framework or by the user? - -If ever your numpy program fails, it may be because: - -* of bugs due to **Concrete** -* of bugs due to the user, notably who would have a bug without even considering FHE (does the function you want to compile run well with numpy?), or who would not use the framework as expected or not consider the limits of the framework. - -For the latter kind of bugs, we encourage the user to have a look at: - -* the error message received -* the documentation of the product -* the known limits of the product (such as the reduced set of supported operations at this time, or the limited precision of the computations). - -Once you have tried to see if the bug was not your own, it is time to go further. - -## Is the inputset sufficiently representative? - -A bug may happen if ever the inputset, which is internally used by the compilation core to set bit widths of some intermediate data, is not sufficiently representative. If ever, with all the inputs in the inputset, it appears that an intermediate data can be represented an `n`-bit integer, but for a particular computation, this same intermediate data needs a bit more bits to be represented, the FHE execution for this computation will result in a wrong output (as typically in integer overflows in classical programs). - -So, in general, when a bug appears, it may be a good idea to enlarge the inputset, and try to have random-looking inputs in this latter, following distribution of inputs used with the function. - -## Having a reproducible bug - -Once you're sure it is a bug, it would be nice to try to: - -* make it highly reproducible: e.g., by reducing as much the randomness as possible; e.g., if you can find an input which fails, there is no reason to let the input random -* reduce it to the smallest possible bug: it is easier to investigate bugs which are small, so when you have an issue, please try to reduce to a smaller issue, notably with less lines of code, smaller parameters, less complex function to compile, faster scripts etc. - -## Asking the community - -You can directly ask the developers and community about your issue on our Discourse server (link on the right of the top menu). - -Hopefully, it is just a misunderstanding or a small mistake on your side, that we can help you fix easily. And, the good point with your feedback is that, once we have heard the problem or misunderstanding, we can make the documentation even clearer (by adding to the FAQ). - -## Having a look to the compilation artifacts - -When things are more complicated, or if you want to have a look by yourself, you may want to start with the compilation reports, which are called artifacts. This is as simple as described in [here](../tutorial/compilation\_artifacts.md) - -The artifact system will create a directory, containing: - -* **environment.txt:** information about your system -* **requirements.txt:** information about your python dependencies -* **function.txt:** source code of the function you are compiling -* **parameters.txt:** parameters you specified for compilation -* **1.initial.graph.txt:** textual representation of the initial computation graph right after tracing -* **1.initial.graph.png:** visual representation of the initial computation graph right after tracing -* ... -* **X.description.graph.txt:** textual representation of the Xth computation graph after topological transforms -* **X.description.graph.png:** visual representation of the Xth computation graph after topological transforms -* ... -* **N.final.graph.txt:** textual representation of the final computation graph right before MLIR conversion -* **N.final.graph.png:** visual representation of the final computation graph right before MLIR conversion -* **bounds.txt:** ranges of data in the different steps of the computation for the final graph that is being compiled -* **mlir.txt**: resulting MLIR code that is sent to the compiler (if compilation succeeded) -* **traceback.txt**: information about the error you encountered (if compilation failed) - -Attaching the artifact with your issue or Slack message may help people to have a look at the core of the problem. The more precise your bug, the more likely we can reproduce and fix it. - -To simplify our work and let us reproduce your bug easily, we need all the information we can get. So, in addition to your python script, the following information would be very useful: - -* compilation artifacts -* reproducibility rate you see on your side -* any insight you might have on the bug -* any workaround you have been able to find - -Remember, **Concrete Numpy** is a project where we are open to contributions, more information at [Contributing](../developer/contributing.md). - -## Submitting an issue - -In case you have a bug, which is reproducible, that you have reduced to a small piece of code, we have our issue tracker (link on the right of the top menu). Remember that a well-described short issue is an issue which is more likely to be studied and fixed. The more issues we receive, the better the product will be. diff --git a/docs/howto/deploy.md b/docs/howto/deploy.md new file mode 100644 index 000000000..792220979 --- /dev/null +++ b/docs/howto/deploy.md @@ -0,0 +1,132 @@ +# Deploy + +After developing your circuit, you may want to deploy it. Sharing the details of your circuit with every client might not be desirable. Further, you might want to perform the computation in dedicated servers. In this case, you can use the `Client` and `Server` features of **Concrete Numpy**. + +## Development of the circuit + +You can develop your circuit like we've discussed in the previous chapters. Here is a simple example: + + +```python +import concrete.numpy as cnp + +@cnp.compiler({"x": "encrypted"}) +def function(x): + return x + 42 + +inputset = range(10) +circuit = function.compile(inputset) +``` + +Once you have your circuit, you can save everything the server needs like so: + + +```python +circuit.server.save("server.zip") +``` + +All you need to do now is to send `server.zip` to your computation server. + +## Setting up a server + +You can load the `server.zip` you get from the development machine like so: + + +```python +import concrete.numpy as cnp + +server = cnp.Server.load("server.zip") +``` + +At this point, you will need to wait for requests from clients. The first likely request is for `ClientSpecs`. + +Clients need `ClientSpecs` to generate keys and request computation. You can serialize `ClientSpecs` like so: + + +```python +serialized_client_specs: str = server.client_specs.serialize() +``` + +Then, you can send it to the clients requesting it. + +## Setting up clients + +After getting the serialized `ClientSpecs` from a server, you can create the client object like so: + + +```python +client_specs = cnp.ClientSpecs.unserialize(serialized_client_specs) +client = cnp.Client(client_specs) +``` + +## Generating keys (on the client) + +Once you have the `Client` object, you can perform key generation like so: + + +```python +client.keygen() +``` + +This method generates encryption/decryption keys and evaluation keys. + +The server requires evaluation keys linked to the encryption keys that you just generated. You can serialize your evaluation keys like so: + + +```python +serialized_evaluation_keys: bytes = client.evaluation_keys.serialize() +``` + +After serialization, you can send the evaluation keys to the server. + +{% hint style="info" %} +Serialized evaluation keys are very big in size, so you may want to cache them on the server instead of sending them with each request. +{% endhint %} + +## Encrypting inputs (on the client) + +You are now ready to encrypt your inputs and request the server to perform the computation. You can do it like so: + + +```python +serialized_args: bytes = client.encrypt(7).serialize() +``` + +The only thing left to do is to send serialized args to the server. + +## Performing computation (on the server) + +Upon having the serialized evaluation keys and serialized arguments, you can unserialize them like so: + + +```python +unserialized_evaluation_keys = cnp.EvaluationKeys.unserialize(serialized_evaluation_keys) +unserialized_args = server.client_specs.unserialize_public_args(serialized_args) +``` + +And you can perform the computation like so: + + +```python +public_result = server.run(unserialized_args, unserialized_evaluation_keys) +serialized_public_result: bytes = public_result.serialize() +``` + +Finally, you can send the serialized public result back to the client, so they can decrypt it and get the result of the computation. + +## Decrypting the result (on the client) + +Once you have received the public result of the computation from the server, you can unserialize it like so: + + +```python +unserialized_public_result = client.specs.unserialize_public_result(serialized_public_result) +``` + +Finally, you can decrypt the result like so: + + +```python +result = client.decrypt(unserialized_public_result) +assert result == 49 +``` diff --git a/docs/howto/printing_and_drawing.md b/docs/howto/printing_and_drawing.md deleted file mode 100644 index 83168fa09..000000000 --- a/docs/howto/printing_and_drawing.md +++ /dev/null @@ -1,63 +0,0 @@ -# Printing and drawing - -Sometimes, it can be useful to print or draw fhe circuits, we provide methods to just do that. Please read [Compiling and Executing](../basics/compiling_and_executing.md) before reading further to see how you can compile your function into an fhe circuit. - -## Printing - -To print your circuit, you can do the following: - - -```python -print(circuit) -``` - -## Drawing - -{% hint style="danger" %} -The draw function requires the installation of the package's extra dependencies. - -The drawing package required is `pygraphviz` which needs `graphviz` packages installed on your OS, see [https://pygraphviz.github.io/documentation/stable/install.html](https://pygraphviz.github.io/documentation/stable/install.html) - -To install the required drawing packages once you have `graphviz` installed run: - -`pip install concrete-numpy[full]` - -You may need to force reinstallation - -`pip install --force-reinstall concrete-numpy[full]` -{% endhint %} - -To draw your circuit, you can do the following: - - -```python -drawing = circuit.draw() -``` - -This method will draw the circuit on a temporary PNG file and return the path to this file. - -To show the drawing, you can use the following code in a jupyter notebook. - - -```python -from PIL import Image -drawing = Image.open(circuit.draw()) -drawing.show() -drawing.close() -``` - -Additionally, you can use the `show` option of the `draw` method to show the drawing with matplotlib. Beware that this will clear the matplotlib plots you have. - - -```python -circuit.draw(show=True) -``` - -Lastly, you can save the drawing to a specific path like this: - - -```python -destination = "/tmp/path/of/your/choice.png" -drawing = circuit.draw(save_to=destination) -assert drawing == destination -``` diff --git a/docs/howto/reduce_needed_precision.md b/docs/howto/reduce_needed_precision.md deleted file mode 100644 index 3bd32d738..000000000 --- a/docs/howto/reduce_needed_precision.md +++ /dev/null @@ -1,90 +0,0 @@ -# Computation With Quantized Functions - -With our current technology, we cannot represent integers with more than 7 bits. -We are actively working on supporting larger integers, so it should get better in the future. - -## What happens when you have larger values? - -You get a compilation error. Here is an example: - - -```python -import concrete.numpy as cnp - -@cnp.compiler({"x": "encrypted"}) -def f(x): - return 42 * x - -circuit = f.compile(range(2 ** 3)) -``` - -results in - -``` -Traceback (most recent call last): - File "/home/default/Documents/Projects/Zama/hdk/dist/demo.py", line 9, in - circuit = compiler.get_compiled_fhe_circuit() - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/np_fhe_compiler.py", line 274, in get_compiled_fhe_circuit - return compile_op_graph_to_fhe_circuit( - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 676, in compile_op_graph_to_fhe_circuit - result = run_compilation_function_with_error_management( - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 141, in run_compilation_function_with_error_management - return compilation_function() - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 674, in compilation_function - return _compile_op_graph_to_fhe_circuit_internal(op_graph, show_mlir, compilation_artifacts) - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 626, in _compile_op_graph_to_fhe_circuit_internal - prepare_op_graph_for_mlir(op_graph) - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 603, in prepare_op_graph_for_mlir - update_bit_width_for_mlir(op_graph) - File "/home/default/Documents/Projects/Zama/hdk/concrete/common/mlir/utils.py", line 204, in update_bit_width_for_mlir - raise RuntimeError( -RuntimeError: max_bit_width of some nodes is too high for the current version of the compiler (maximum must be 8) which is not compatible with: - -%0 = x # EncryptedScalar -%1 = 42 # ClearScalar -%2 = mul(%0, %1) # EncryptedScalar -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 9 bits is not supported for the time being -return %2 -``` - -when you try to run. - -## Why can some computation work with less precision? - -### The input data uses more bits than required - -For some tasks, like classification for example, the output prediction often carries much less information than the input data used to make that prediction. - -For example the MNIST classification task consists in taking an image, a 28x28 array containing uint8 values, representing a handwritten digit and predicting whether it belongs to one of 10 classes: the digits from 0 to 9. The output is a one-hot vector which indicates the class a particular sample belongs to. - -The input contains 28x28x8 = 6272 bits of information. In practice you could still obtain good results on MNIST by thresholding the pixels to {0, 1} and training a model for this new Binarized MNIST task. This means that in a real use case where you actually need to do digits recognition, you could binarize your input on the fly, replacing each pixel by either 0 or 1. Doing so, you use 1 bit per pixel and now only have 784 bits of input data. It also means that if you are doing some accumulation (adding pixel values together), you are going to need accumulators that are smaller (adding 0s and 1s requires less space than adding values ranging from 0 to 255 included). - -This shows how adapting your data can allow you to use models that may require smaller data types (i.e. use less precision) to perform their computations. - -### Model accuracy when quantizing for FHE - -Quantization and binarization increase inference speed, reduce model byte-size and are required to run computation in FHE. However, quantization and, especially, binarization, induce a loss in the accuracy of the model since it's representation power is diminished. Choosing quantization parameters carefully can alleviate the accuracy loss all the while allowing compilation to FHE. - -The end result has a granularity/imprecision linked to the data types used and for the Quantized Logistic Regression to the lattice used to evaluate the logistic model. - -## Limitations for FHE friendly neural network - -Recent quantization literature often takes a few shortcuts to reach performance similar to those achieved by floating point models. A common one is that the input is left in floating point. This is also true for the first and last layers which have more impact on the resulting model accuracy than hidden layers. - -But, in **Concrete Numpy**, the inputs, weights and the accumulator must remain on a maximum of 7 bits. - -Thus, in **Concrete Numpy**, we also quantize the input data and network output activations in the same way as the rest of the network: everything is quantized to a specific number of bits. It turns out, that the number of bits used for the input or the output of any activation function is crucial to comply with the constraint on accumulator width. - -The core operation in neural networks is essentially matrix multiplications (matmul). This operation must be done such that the maximum value of its result requires at most 7 bits of precision. - -For example, if you quantize your input and weights with $$ n_{\mathsf{weights}} $$, $$ n_{\mathsf{inputs}} $$ bits of precision, one can compute the maximum dimensionality of the input and weights before the matmul **can** exceed the 7 bits as such: - -$$ \Omega = \mathsf{floor} \left( \frac{2^{n_{\mathsf{max}}} - 1}{(2^{n_{\mathsf{weights}}} - 1)(2^{n_{\mathsf{inputs}}} - 1)} \right) $$ - -where $$ n_{\mathsf{max}} = 7 $$ is the maximum precision allowed. For example, if we set $$ n_{\mathsf{weights}} = 2$$ and $$ n_{\mathsf{inputs}} = 2$$ with $$ n_{\mathsf{max}} = 7$$ then we have the $$ \Omega = 14 $$ different inputs/weights allowed in the matmul. - -Above $$ \Omega $$ dimensions in the input and weights, the risk of overflow increases quickly. It may happen that for some distributions of weights and values the computation does not overflow, but the risk increases rapidly with the number of dimensions. - -Currently, **Concrete Numpy** pre-computes the number of bits needed for the computation depending on the input set calibration data and does not allow the overflow[^1] to happen. - -[^1]: [Integer overflow](https://en.wikipedia.org/wiki/Integer_overflow) diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 195c0003d..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Concrete Numpy's documentation -================================== - - -.. toctree:: - :maxdepth: 2 - - User Guide - API <_apidoc/modules.rst> - Developer diff --git a/docs/intro.md b/docs/intro.md deleted file mode 100644 index bac0d78dc..000000000 --- a/docs/intro.md +++ /dev/null @@ -1,31 +0,0 @@ -# What is **Concrete Numpy**? - -## Introduction - -**Concrete Numpy**, or **Concrete** for short, is an open-source set of tools which aims to simplify the use of fully homomorphic encryption (FHE) for data scientists. - -FHE is a powerful cryptographic tool, which allows servers to perform computations directly on encrypted data without needing to decrypt first. With FHE, privacy is at the center, and you can build services which ensure full privacy of the user and are the perfect equivalent of their unsecure counterpart. - -FHE is also a killer feature regarding data breaches: as anything done on the server is done over encrypted data, even if the server is compromised, there is in the end no leak of useful data. - -With **Concrete Numpy**, data scientists can implement machine learning models using a [subset of numpy](basics/numpy_support.md) that compile to FHE. They will be able to train models with popular machine learning libraries and then convert the prediction functions of these models, that they write in numpy, to FHE. - -**Concrete Numpy** is made of several parts: -- an entry API, which is the main function of the so-called **Concrete frontend**, which takes programs made from a subset of numpy, and converts them to an FHE program -- the **Concrete compiler**, which is called by the frontend, which allows you to turn an MLIR program into an FHE program, on the top of **Concrete Library**, which contains the core cryptographic APIs for computing with FHE; - -In a further release, **Concrete Numpy** will be divided into a **Concrete Framework** package, containing the compiler, the core lib and the frontend(s), and in a **Concrete ML**, which will contain ML tools, made on top of the **Concrete Framework**. Names of these packages are succeptible to change. - -## Organization of the documentation - -Basically, we have divided our documentation into several parts: -- one about basic elements, notably a description of the installation, that you are currently reading -- one dedicated to _users_ of **Concrete Numpy**, with tutorials, how-tos and deeper explanations -- one detailing the APIs of the different functions of the frontend, directly done by parsing its source code -- and finally, one dedicated to _developers_ of **Concrete Numpy**, who could be internal or external contributors to the framework - -## A work in progress - -{% hint style='info' %} -Concrete is a work in progress, and is currently limited to a certain number of operators and features. In the future, there will be improvements as described in this [section](explanation/future_features.md). -{% endhint %} diff --git a/docs/tutorial/compilation_artifacts.md b/docs/tutorial/compilation_artifacts.md deleted file mode 100644 index 8b3a975d3..000000000 --- a/docs/tutorial/compilation_artifacts.md +++ /dev/null @@ -1,247 +0,0 @@ -# Compilation artifacts - -In this tutorial, we are going to go over the artifact system, which is designed to inspect/debug the compilation process easily. - -## Automatic export - -In case of compilation failures, artifacts are exported automatically to `.artifacts` directory under the working directory. Let's intentionally create a compilation failure and show what kinds of things are exported. - - -```python -def f(x): - return np.sin(x) -``` - -This function fails to compile because **Concrete Numpy** doesn't support floating point outputs. When you try to compile it (you might want to check [this](../basics/compiling_and_executing.md) to see how you can do that), an exception will be raised and the artifacts will be exported automatically. - -### 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 are trying to compile. - -``` -def f(x): - return np.sin(x) -``` - -### parameters.txt - -This file contains information about the parameters of the function you are trying to compile. - -``` -x :: EncryptedScalar> -``` - -### 1.initial.graph.txt - -This file contains textual representation of the initial computation graph right after tracing. - -``` -%0 = x # EncryptedScalar -%1 = sin(%0) # EncryptedScalar -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 textual representation of the final computation graph right before MLIR conversion. - -``` -%0 = x # EncryptedScalar -%1 = sin(%0) # EncryptedScalar -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 got. - -``` -Traceback (most recent call last): - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 141, in run_compilation_function_with_error_management - return compilation_function() - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 769, in compilation_function - return _compile_numpy_function_internal( - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 722, in _compile_numpy_function_internal - fhe_circuit = _compile_op_graph_to_fhe_circuit_internal( - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 626, in _compile_op_graph_to_fhe_circuit_internal - prepare_op_graph_for_mlir(op_graph) - File "/home/default/Documents/Projects/Zama/hdk/concrete/numpy/compile.py", line 597, in prepare_op_graph_for_mlir - raise RuntimeError( -RuntimeError: function you are trying to compile isn't supported for MLIR lowering - -%0 = x # EncryptedScalar -%1 = sin(%0) # EncryptedScalar -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer outputs 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 do it: - -```python -import concrete.numpy as cnp -import numpy as np -import pathlib - -artifacts = cnp.DebugArtifacts("/tmp/custom/export/path") - -@cnp.compiler({"x": "encrypted"}) -def f(x): - return 127 - (50 * (np.sin(x) + 1)).astype(np.int64) - -f.compile(range(2 ** 3), artifacts=artifacts) - -artifacts.export() -``` - -### 1.initial.graph.txt - -This file contains textual representation of the initial computation graph right after tracing. - -``` -%0 = 127 # ClearScalar -%1 = 50 # ClearScalar -%2 = 1 # ClearScalar -%3 = x # EncryptedScalar -%4 = sin(%3) # EncryptedScalar -%5 = add(%4, %2) # EncryptedScalar -%6 = mul(%5, %1) # EncryptedScalar -%7 = astype(%6, dtype=uint32) # EncryptedScalar -%8 = sub(%0, %7) # EncryptedScalar -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 textual representation of the intermediate computation graph after fusing. - -``` -%0 = 127 # ClearScalar -%1 = x # EncryptedScalar -%2 = subgraph(%1) # EncryptedScalar -%3 = sub(%0, %2) # EncryptedScalar -return %3 - -Subgraphs: - - %2 = subgraph(%1): - - %0 = 50 # ClearScalar - %1 = 1 # ClearScalar - %2 = float_subgraph_input # EncryptedScalar - %3 = sin(%2) # EncryptedScalar - %4 = add(%3, %1) # EncryptedScalar - %5 = mul(%4, %0) # EncryptedScalar - %6 = astype(%5, dtype=uint32) # EncryptedScalar - return %6 -``` - -### 2.after-float-fuse-0.graph.png - -This file contains the visual representation of the intermediate computation graph after fusing. - -![](../_static/tutorials/artifacts/manual/2.after-float-fuse-0.graph.png) - -### 3.final.graph.txt - -This file contains textual representation of the final computation graph right before MLIR conversion. - -``` -%0 = 127 # ClearScalar -%1 = x # EncryptedScalar -%2 = subgraph(%1) # EncryptedScalar -%3 = sub(%0, %2) # EncryptedScalar -return %3 - -Subgraphs: - - %2 = subgraph(%1): - - %0 = 50 # ClearScalar - %1 = 1 # ClearScalar - %2 = float_subgraph_input # EncryptedScalar - %3 = sin(%2) # EncryptedScalar - %4 = add(%3, %1) # EncryptedScalar - %5 = mul(%4, %0) # EncryptedScalar - %6 = astype(%5, dtype=uint32) # EncryptedScalar - 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 trying to compile using the input set you provide. - -``` -%0 :: [127, 127] -%1 :: [0, 7] -%2 :: [2, 95] -%3 :: [32, 125] -``` - -You can learn what bounds are [here](../../dev/explanation/terminology_and_structure.md). - -### mlir.txt - -This file contains information about the MLIR of the function you are trying to compile using the input set you provide. - -``` -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> - } -} -``` - -You can learn more about MLIR [here](../../dev/explanation/mlir.md). diff --git a/docs/tutorial/decorator.md b/docs/tutorial/decorator.md new file mode 100644 index 000000000..c46ca92ab --- /dev/null +++ b/docs/tutorial/decorator.md @@ -0,0 +1,20 @@ +# Decorator + +If you are trying to compile a regular function, you can use the decorator interface instead of the explicit `Compiler` interface to simplify your code: + +```python +import concrete.numpy as cnp + +@cnp.compiler({"x": "encrypted"}) +def f(x): + return x + 42 + +inputset = range(10) +circuit = f.compile(inputset) + +assert circuit.encrypt_run_decrypt(10) == f(10) +``` + +{% hint style="info" %} +Think of this decorator as a way to add the `compile` method to the function object without changing its name elsewhere. +{% endhint %} diff --git a/docs/tutorial/extensions.md b/docs/tutorial/extensions.md new file mode 100644 index 000000000..154b1f15b --- /dev/null +++ b/docs/tutorial/extensions.md @@ -0,0 +1,153 @@ +# 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 extesions 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 %} diff --git a/docs/tutorial/floating_points.md b/docs/tutorial/floating_points.md new file mode 100644 index 000000000..de27c1fbd --- /dev/null +++ b/docs/tutorial/floating_points.md @@ -0,0 +1,79 @@ +# Floating Points + +**Concrete Numpy** partly supports floating points: + +* They cannot be inputs +* They cannot be outputs +* They can be intermediate values under certain constraints + +## As intermediate values + +**Concrete Compile**, which is used for compiling the circuit, doesn't support floating points at all. However, it supports table lookups. They take an integer and map it to another integer. It does not care how the lookup table is calculated. Further, the constraints of this operation are such that there should be a single integer input and it should result in a single integer output. + +As long as your floating point operations comply with those constraints, **Concrete Numpy** automatically converts your operations to a table lookup operation: + +```python +import concrete.numpy as cnp +import numpy as np + +@cnp.compiler({"x": "encrypted"}) +def f(x): + a = x + 1.5 + b = np.sin(x) + c = np.around(a + b) + d = c.astype(np.int64) + return d + +inputset = range(8) +circuit = f.compile(inputset) + +for x in range(8): + assert circuit.encrypt_run_decrypt(x) == f(x) +``` + +In the example above, `a`, `b`, and `c` are all floating point intermediates. However, they are just used to calculate `d`, which is an integer and value of `d` dependent upon `x` , which is another integer. **Concrete Numpy** detects this and fuses all of those operations into a single table lookup from `x` to `d`. + +This approach works for a variety of use cases, but it comes up short for some: + + +```python +import concrete.numpy as cnp +import numpy as np + +@cnp.compiler({"x": "encrypted", "y": "encrypted"}) +def f(x, y): + a = x + 1.5 + b = np.sin(y) + c = np.around(a + b) + d = c.astype(np.int64) + return d + +inputset = [(1, 2), (3, 0), (2, 2), (1, 3)] +circuit = f.compile(inputset) + +for x in range(8): + assert circuit.encrypt_run_decrypt(x) == f(x) +``` + +results in + +``` +RuntimeError: Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedScalar +%1 = 1.5 # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported +%2 = y # EncryptedScalar +%3 = add(%0, %1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%4 = sin(%2) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%5 = add(%3, %4) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%6 = around(%5) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%7 = astype(%6, dtype=int_) # EncryptedScalar +return %7 +``` + +The reason for that is that `d` no longer depends solely on `x`, it depends on `y` as well. Thus, **Concrete Numpy** cannot fuse these operations, so it raises an exception. diff --git a/docs/tutorial/formatting_and_drawing.md b/docs/tutorial/formatting_and_drawing.md new file mode 100644 index 000000000..22f20928c --- /dev/null +++ b/docs/tutorial/formatting_and_drawing.md @@ -0,0 +1,64 @@ +# Format and Draw + +Sometimes, it can be useful to format or draw circuits. We provide methods to just do that. + +## Formatting + +You can convert your compiled circuit into its textual representation by converting it to string: + + +```python +str(circuit) +``` + +If you just want to see the output on your terminal, you can directly print it as well: + + +```python +print(circuit) +``` + +## Drawing + +{% hint style="danger" %} +Drawing functionality requires the installation of the package with a full feature set. See the Installation section to learn how to do that. +{% endhint %} + +You can use the `draw` method of your compiled circuit to draw it: + + +```python +drawing = circuit.draw() +``` + +This method will draw the circuit on a temporary PNG file and return the path to this file. + +You can show the drawing in a Jupyter notebook, like this: + + +```python +from PIL import Image +drawing = Image.open(circuit.draw()) +drawing.show() +drawing.close() +``` + +Or, you can use the `show` option of the `draw` method to show the drawing with matplotlib. + + +```python +circuit.draw(show=True) +``` + +{% hint style="danger" %} +Beware that this will clear the matplotlib plots you have. +{% endhint %} + +Lastly, you can save the drawing to a specific path: + + +```python +destination = "/tmp/path/of/your/choice.png" +drawing = circuit.draw(save_to=destination) +assert drawing == destination +``` diff --git a/docs/tutorial/indexing.md b/docs/tutorial/indexing.md deleted file mode 100644 index 7154bda91..000000000 --- a/docs/tutorial/indexing.md +++ /dev/null @@ -1,95 +0,0 @@ -# Indexing - -## Constant Indexing - -Constant indexing refers to the index being static (i.e., known during compilation). - -Here are some examples of constant indexing: - -### Extracting a single element - -```python -import concrete.numpy as cnp -import numpy as np - -@cnp.compiler({"x": "encrypted"}) -def f(x): - return x[1] - -inputset = [np.random.randint(0, 2 ** 3, size=(3,), dtype=np.uint8) for _ in range(10)] -circuit = f.compile(inputset) - -test_input = np.array([4, 2, 6], dtype=np.uint8) -expected_output = 2 - -assert np.array_equal(circuit.encrypt_run_decrypt(test_input), expected_output) -``` - -You can use negative indexing. - -```python -import concrete.numpy as cnp -import numpy as np - -@cnp.compiler({"x": "encrypted"}) -def f(x): - return x[-1] - -inputset = [np.random.randint(0, 2 ** 3, size=(3,), dtype=np.uint8) for _ in range(10)] -circuit = f.compile(inputset) - -test_input = np.array([4, 2, 6], dtype=np.uint8) -expected_output = 6 - -assert np.array_equal(circuit.encrypt_run_decrypt(test_input), expected_output) -``` - -You can use multidimensional indexing as well. - -```python -import concrete.numpy as cnp -import numpy as np - -@cnp.compiler({"x": "encrypted"}) -def f(x): - return x[-1, 1] - -inputset = [np.random.randint(0, 2 ** 3, size=(3, 2), dtype=np.uint8) for _ in range(10)] -circuit = f.compile(inputset) - -test_input = np.array([[4, 2], [1, 5], [7, 6]], dtype=np.uint8) -expected_output = 6 - -assert np.array_equal(circuit.encrypt_run_decrypt(test_input), expected_output) -``` - -### Extracting a slice - -```python -import concrete.numpy as cnp -import numpy as np - -@cnp.compiler({"x": "encrypted"}) -def f(x): - return x[1:4] - -inputset = [np.random.randint(0, 2 ** 3, size=(5,), dtype=np.uint8) for _ in range(10)] -circuit = f.compile(inputset) - -test_input = np.array([4, 2, 6, 1, 7], dtype=np.uint8) -expected_output = np.array([2, 6, 1], dtype=np.uint8) - -assert np.array_equal(circuit.encrypt_run_decrypt(test_input), expected_output) -``` - -You can use multidimensional slicing as well. - -{% hint style='tip' %} -There are certain limitations of slicing due to MLIR. So if you stumple into `RuntimeError: Compilation failed: Failed to lower to LLVM dialect`, know that we are aware of it, and we are trying to make such cases compilable. -{% endhint %} - -## Dynamic Indexing - -Dynamic indexing refers to the index being dynamic (i.e., can change during runtime). -Such indexing is especially useful for things like decision trees. -Unfortunately, we don't support dynamic indexing for the time being. diff --git a/docs/tutorial/table_lookup.md b/docs/tutorial/table_lookup.md index 6f634673b..8260819d0 100644 --- a/docs/tutorial/table_lookup.md +++ b/docs/tutorial/table_lookup.md @@ -1,87 +1,108 @@ -# Table lookup +# Table Lookups -In this tutorial, we are going to go over the ways to perform direct table lookups in **Concrete Numpy**. Please read [Compiling and Executing](../basics/compiling\_and\_executing.md) before reading further to see how you can compile the functions below. +In this tutorial, we will review the ways to perform direct table lookups in **Concrete Numpy**. ## Direct table lookup -**Concrete Numpy** provides a special class to allow direct table lookups. Here is how to use it: +**Concrete Numpy** provides a `LookupTable` class for you to create your own tables and apply them in your circuits. + +{% hint style="info" %} +`LookupTable`s can have any number of elements. Let's call them **N**. As long as the lookup variable is in range \[-**N**, **N**), table lookup is valid. + +If you go out of bounds of this range, you will get the following error: + +``` +IndexError: index 10 is out of bounds for axis 0 with size 6 +``` +{% endhint %} + +{% hint style="info" %} +The number of elements in the lookup table doesn't affect performance in any way. +{% endhint %} + +### With scalars. + +You can create the lookup table using a list of integers and apply it using indexing: ```python import concrete.numpy as cnp table = cnp.LookupTable([2, -1, 3, 0]) +@cnp.compiler({"x": "encrypted"}) def f(x): return table[x] + +inputset = range(4) +circuit = f.compile(inputset) + +assert circuit.encrypt_run_decrypt(0) == table[0] == 2 +assert circuit.encrypt_run_decrypt(1) == table[1] == -1 +assert circuit.encrypt_run_decrypt(2) == table[2] == 3 +assert circuit.encrypt_run_decrypt(3) == table[3] == 0 ``` -where +### With tensors. -* `x = "encrypted"` scalar +When you apply the table lookup to a tensor, you apply the scalar table lookup to each element of the tensor: -results in - - ```python -circuit.encrypt_run_decrypt(0) == 2 -circuit.encrypt_run_decrypt(1) == -1 -circuit.encrypt_run_decrypt(2) == 3 -circuit.encrypt_run_decrypt(3) == 0 +import concrete.numpy as cnp +import numpy as np + +table = cnp.LookupTable([2, -1, 3, 0]) + +@cnp.compiler({"x": "encrypted"}) +def f(x): + return table[x] + +inputset = [np.random.randint(0, 4, size=(2, 3)) for _ in range(10)] +circuit = f.compile(inputset) + +sample = [ + [0, 1, 3], + [2, 3, 1], +] +expected_output = [ + [2, -1, 0], + [3, 0, -1], +] +actual_output = circuit.encrypt_run_decrypt(np.array(sample)) + +for i in range(2): + for j in range(3): + assert actual_output[i][j] == expected_output[i][j] == table[sample[i][j]] ``` -Moreover, direct lookup tables can be used with tensors where the same table lookup is applied to each value in the tensor, so +### With negative values. -* `x = "encrypted"` tensor of shape `(2, 3)` - -results in - - -```python -input = np.array([[0, 1, 3], [2, 3, 1]], dtype=np.uint8) -circuit.encrypt_run_decrypt(input) == [[2, 1, 0], [3, 0, 1]] -``` - -Direct table lookups behaves like array indexing in python. Which means, if the lookup variable is negative, table is looked up from the back. +`LookupTable` mimics array indexing in Python, which means if the lookup variable is negative, the table is looked up from the back: ```python import concrete.numpy as cnp -table = cnp.LookupTable([2, 1, 3, 0]) +table = cnp.LookupTable([2, -1, 3, 0]) +@cnp.compiler({"x": "encrypted"}) def f(x): return table[-x] + +inputset = range(1, 5) +circuit = f.compile(inputset) + +assert circuit.encrypt_run_decrypt(1) == table[-1] == 0 +assert circuit.encrypt_run_decrypt(2) == table[-2] == 3 +assert circuit.encrypt_run_decrypt(3) == table[-3] == -1 +assert circuit.encrypt_run_decrypt(4) == table[-4] == 2 ``` -where - -* `x = "encrypted"` scalar - -results in - - -```python -circuit.encrypt_run_decrypt(0) == 2 -circuit.encrypt_run_decrypt(1) == 0 -circuit.encrypt_run_decrypt(2) == 3 -circuit.encrypt_run_decrypt(3) == 1 -circuit.encrypt_run_decrypt(4) == 2 -``` - -Lastly, a `LookupTable` can have any number of elements, let's call it **N**, as long as the lookup variable is in range \[-**N**, **N**). If you go out of bounds of this range, you will get the following error: - -``` -IndexError: index 10 is out of bounds for axis 0 with size 6 -``` - -Note that, number of elements in the lookup table doesn't affect the performance in any way. - ## Direct multi table lookup -Sometimes you may want to apply a different lookup table to each value in a tensor. That's where direct multi lookup table becomes handy. Here is how to use it: +In case you want to apply a different lookup table to each element of a tensor, you can have a `LookupTable` of `LookupTable`s: - ```python import concrete.numpy as cnp +import numpy as np squared = cnp.LookupTable([i ** 2 for i in range(4)]) cubed = cnp.LookupTable([i ** 3 for i in range(4)]) @@ -92,72 +113,66 @@ table = cnp.LookupTable([ [squared, cubed], ]) +@cnp.compiler({"x": "encrypted"}) def f(x): return table[x] + +inputset = [np.random.randint(0, 4, size=(3, 2)) for _ in range(10)] +circuit = f.compile(inputset) + +sample = [ + [0, 1], + [2, 3], + [3, 0], +] +expected_output = [ + [0, 1], + [4, 27], + [9, 0] +] +actual_output = circuit.encrypt_run_decrypt(np.array(sample)) + +for i in range(3): + for j in range(2): + if j == 0: + assert actual_output[i][j] == expected_output[i][j] == squared[sample[i][j]] + else: + assert actual_output[i][j] == expected_output[i][j] == cubed[sample[i][j]] ``` -where - -* `x = "encrypted"` tensor of shape `(3, 2)` - -results in - - -```python -input = np.array([[2, 3], [1, 2], [3, 0]], dtype=np.uint8) -circuit.encrypt_run_decrypt(input) == [[4, 27], [1, 8], [9, 0]] -``` - -Basically, we applied `squared` table to the first column and `cubed` to the second one. +In this example, we applied a `squared` table to the first column and a `cubed` table to the second one. ## Fused table lookup -Direct tables are tedious to prepare by hand. When possible, **Concrete Numpy** fuses the floating point operations into table lookups automatically. There are some limitations on fusing operations, which you can learn more about on the next tutorial, [Working With Floating Points](working\_with\_floating\_points.md). +**Concrete Numpy** tries to fuse some operations into table lookups automatically, so you don't need to create the lookup tables manually: -Here is an example function that results in fused table lookup: - - ```python +import concrete.numpy as cnp +import numpy as np + +@cnp.compiler({"x": "encrypted"}) def f(x): - return 127 - (50 * (np.sin(x) + 1)).astype(np.int64) # astype is to go back to integer world + return (42 * np.sin(x)).astype(np.int64) // 10 + +inputset = range(8) +circuit = f.compile(inputset) + +for x in range(8): + assert circuit.encrypt_run_decrypt(x) == f(x) ``` -where +{% hint style="info" %} +All lookup tables need to be from integers to integers. So, without `.astype(np.int64)`, **Concrete Numpy** will not be able to fuse. +{% endhint %} -* `x = "encrypted"` scalar - -results in - - -```python -circuit.encrypt_run_decrypt(0) == 77 -circuit.encrypt_run_decrypt(1) == 35 -circuit.encrypt_run_decrypt(2) == 32 -circuit.encrypt_run_decrypt(3) == 70 -circuit.encrypt_run_decrypt(4) == 115 -circuit.encrypt_run_decrypt(5) == 125 -circuit.encrypt_run_decrypt(6) == 91 -circuit.encrypt_run_decrypt(7) == 45 -``` - -Initially, the function is converted to this operation graph +The function is first traced into: ![](../\_static/tutorials/table-lookup/1.initial.graph.png) -and after floating point operations are fused, we get the following operation graph +Then, **Concrete Numpy** fuses appropriate nodes: ![](../\_static/tutorials/table-lookup/3.final.graph.png) -Internally, it uses the following lookup table - - -```python -table = cnp.LookupTable([50, 92, 95, 57, 12, 2, 36, 82]) -``` - -which is calculated by: - - -```python -[(50 * (np.sin(x) + 1)).astype(np.int64) for x in range(2 ** 3)] -``` +{% hint style="info" %} +Fusing makes the code more readable and easier to modify. So try to utilize it over manual `LookupTable`s as much as possible. +{% endhint %} diff --git a/docs/tutorial/working_with_floating_points.md b/docs/tutorial/working_with_floating_points.md deleted file mode 100644 index 749a396cc..000000000 --- a/docs/tutorial/working_with_floating_points.md +++ /dev/null @@ -1,36 +0,0 @@ -# Working with floating points - -## An example - -```python -import concrete.numpy as cnp -import numpy as np - -# Function using floating points values converted back to integers at the end -@cnp.compiler({"x": "encrypted"}) -def f(x): - return np.fabs(50 * (2 * np.sin(x) * np.cos(x))).astype(np.int64) - # astype is to go back to the integer world - -circuit = f.compile(range(64)) - -print(circuit.encrypt_run_decrypt(3) == f(3)) -print(circuit.encrypt_run_decrypt(0) == f(0)) -print(circuit.encrypt_run_decrypt(1) == f(1)) -print(circuit.encrypt_run_decrypt(10) == f(10)) -print(circuit.encrypt_run_decrypt(60) == f(60)) - -print("All good!") -``` - -You can look to [numpy supported functions](../howto/numpy\_support.md) for information about possible float operations. - -## Limitations - -Floating point support in **Concrete Numpy** is very limited for the time being. They can't appear on inputs, or they can't be outputs. However, they can be used in intermediate results. Unfortunately, there are limitations on that front as well. - -This biggest one is that, because floating point operations are fused into table lookups with a single unsigned integer input and single unsigned integer output, only univariate portion of code can be replaced with table lookups, which means multivariate portions cannot be compiled. - -To give a precise example, `100 - np.fabs(50 * (np.sin(x) + np.sin(y)))` cannot be compiled because the floating point part depends on both `x` and `y` (i.e., it cannot be rewritten in the form `100 - table[z]` for a `z` that could be computed easily from `x` and `y`). - -To dive into implementation details, you may refer to [Fusing Floating Point Operations](../../developer/float-fusing.md) document. diff --git a/pyproject.toml b/pyproject.toml index 142cdc058..e1e2280e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "concrete-numpy" version = "0.6.0-rc9" -description = "Concrete Numpy is a python package that contains the tools data scientists need to compile various numpy functions into their Fully Homomorphic Encryption (FHE) equivalents. Concrete Numpy goes on top of the Concrete Library and its Compiler." +description = "Concrete Numpy is an open-source library which simplifies the use of fully homomorphic encryption (FHE)." license = "BSD-3-Clause" authors = [ "Zama ", diff --git a/script/doc_utils/gen_supported_ufuncs.py b/script/doc_utils/gen_supported_ufuncs.py index ae1771146..fa2b0b820 100644 --- a/script/doc_utils/gen_supported_ufuncs.py +++ b/script/doc_utils/gen_supported_ufuncs.py @@ -7,7 +7,11 @@ from concrete.numpy.tracing import Tracer def main(file_to_update): """Update list of supported functions in file_to_update""" - supported_func = sorted(f.__name__ for f in Tracer.SUPPORTED_NUMPY_OPERATORS) + f_names = sorted(f.__name__.replace("_", "\\_") for f in Tracer.SUPPORTED_NUMPY_OPERATORS) + supported_func = [ + f"[np.{f}](https://numpy.org/doc/stable/reference/generated/numpy.{f}.html)" + for f in f_names + ] with open(file_to_update, "r", encoding="utf-8") as file: lines = file.readlines() @@ -36,9 +40,7 @@ def main(file_to_update): keep_line = True # Inject the supported functions - newlines.append("List of supported functions:\n") - - newlines.extend(f"- {f}\n" for f in supported_func) + newlines.extend(f"* {f}\n" for f in supported_func) newlines.append(line) else: