mirror of
https://github.com/zama-ai/concrete.git
synced 2026-02-08 19:44:57 -05:00
chore: test slow compiler
- fix CI scripts for the current compiler releases - add keycache system
This commit is contained in:
77
.github/workflows/continuous-integration.yaml
vendored
77
.github/workflows/continuous-integration.yaml
vendored
@@ -50,9 +50,6 @@ jobs:
|
||||
force-rebuild-docker: ${{ env.FORCE_REBUILD_DOCKER }}
|
||||
report: ${{ steps.report.outputs.report || 'Did not run.' }}
|
||||
|
||||
env:
|
||||
WHEEL: concretefhe_compiler-0.1.0-cp38-cp38-manylinux_2_24_x86_64.whl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
with:
|
||||
@@ -99,6 +96,8 @@ jobs:
|
||||
- name: Set prefligh Docker image download compiler
|
||||
id: set_image
|
||||
if: ${{ fromJSON(env.BUILD_DOCKER) }}
|
||||
env:
|
||||
WHEEL_SPEC: cp38-cp38-manylinux_2_24_x86_64
|
||||
run: |
|
||||
PREFLIGHT_IMAGE_TAG=$(echo ${{ github.ref }} | sed -e 's/\//-/g')
|
||||
PREFLIGHT_IMAGE="${PREFLIGHT_IMAGE_BASE}-${PREFLIGHT_IMAGE_TAG}"
|
||||
@@ -111,17 +110,13 @@ jobs:
|
||||
./script/actions_utils/gh_dl_release.sh \
|
||||
--token ${{ secrets.BOT_TOKEN }} \
|
||||
--org-repo zama-ai/homomorphizer \
|
||||
--file "${WHEEL}" \
|
||||
--dest-file "pkg/${WHEEL}" \
|
||||
--file "${WHEEL_SPEC}" \
|
||||
--dest-dir "pkg" \
|
||||
--github-env "${GITHUB_ENV}" \
|
||||
--compiler-tag-output-file compiler-output-tag.txt
|
||||
|
||||
COMPILER_TAG=$(cat compiler-output-tag.txt)
|
||||
echo "::set-output name=compiler-tag::${COMPILER_TAG}"
|
||||
# Disabled buildx for now as we are seeing a lot of fails on layer pushes
|
||||
# - name: Set up Docker Buildx
|
||||
# if: ${{ fromJSON(env.BUILD_DOCKER) }}
|
||||
# id: buildx
|
||||
# uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ fromJSON(env.BUILD_DOCKER) }}
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
@@ -223,21 +218,6 @@ jobs:
|
||||
uses: actions/setup-python@f38219332975fe8f9c04cca981d674bf22aea1d3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
# - name: Cache Installation Files
|
||||
# uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353
|
||||
# with:
|
||||
# # Paths are Unix specific for now
|
||||
# path: |
|
||||
# ~/.cache/pip
|
||||
# ~/.cache/pypoetry
|
||||
# # Ignore line break in the evaluated double quoted string
|
||||
# key: "${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-${{ matrix.python-version }}-\
|
||||
# ${{ hashFiles('poetry.lock') }}"
|
||||
# restore-keys: |
|
||||
# ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-${{ matrix.python-version }}-
|
||||
# ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-
|
||||
# ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-
|
||||
# ${{ secrets.CACHE_VERSION }}-
|
||||
- name: Install dependencies
|
||||
id: install-deps
|
||||
run: |
|
||||
@@ -312,12 +292,17 @@ jobs:
|
||||
echo "Conformance failed, check logs"
|
||||
exit 1
|
||||
fi
|
||||
- name: Tar docs artifacts
|
||||
if: ${{ steps.conformance.outcome == 'success' && !cancelled() }}
|
||||
run: |
|
||||
cd docs/_build/html
|
||||
tar -cvf docs.tar *
|
||||
- name: Archive docs artifacts
|
||||
if: ${{ steps.conformance.outcome == 'success' && !cancelled() }}
|
||||
uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2
|
||||
with:
|
||||
name: html-docs
|
||||
path: docs/_build/html
|
||||
path: docs/_build/html/docs.tar
|
||||
- name: Upload changelog artifacts
|
||||
if: ${{ steps.changelog.outcome == 'success' && !cancelled() }}
|
||||
uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2
|
||||
@@ -329,10 +314,10 @@ jobs:
|
||||
if: ${{ steps.conformance.outcome == 'success' && !cancelled() }}
|
||||
run: |
|
||||
make pytest
|
||||
- name: Test CodeBlocks
|
||||
- name: PyTest CodeBlocks
|
||||
if: ${{ steps.conformance.outcome == 'success' && !cancelled() }}
|
||||
run: |
|
||||
make test_codeblocks
|
||||
make pytest_codeblocks
|
||||
- name: PyTest Notebooks
|
||||
if: ${{ github.event_name == 'schedule' && steps.conformance.outcome == 'success' && !cancelled() }}
|
||||
run: |
|
||||
@@ -346,12 +331,6 @@ jobs:
|
||||
if: ${{ always() && steps.pytest.outcome != 'skipped' && !cancelled() }}
|
||||
run: |
|
||||
./script/actions_utils/coverage.sh global-coverage-infos.json
|
||||
- name: Archive test coverage
|
||||
uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2
|
||||
if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }}
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage.html
|
||||
- name: Comment with coverage
|
||||
uses: marocchino/sticky-pull-request-comment@39c5b5dc7717447d0cba270cd115037d32d28443
|
||||
if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }}
|
||||
@@ -477,6 +456,12 @@ jobs:
|
||||
uses: actions/download-artifact@f023be2c48cc18debc3bacd34cb396e0295e2869
|
||||
with:
|
||||
name: html-docs
|
||||
- name: Untar docs artifacts
|
||||
id: untar
|
||||
if: ${{ fromJSON(steps.docs-push-infos.outputs.has-preprod) }}
|
||||
run: |
|
||||
tar -xvf docs.tar
|
||||
rm docs.tar
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@ea7b857d8a33dc2fb4ef5a724500044281b49a5e
|
||||
with:
|
||||
@@ -486,7 +471,7 @@ jobs:
|
||||
|
||||
- name: Publish Documentation to S3
|
||||
id: publish
|
||||
if: ${{ steps.download.outcome == 'success' && !cancelled() }}
|
||||
if: ${{ steps.untar.outcome == 'success' && !cancelled() }}
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ steps.docs-push-infos.outputs.aws-bucket }}
|
||||
SOURCE_DIR: '.'
|
||||
@@ -606,22 +591,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
# To be removed once poetry 1.2 is released to manage dependencies with groups
|
||||
# - name: Cache Installation Files
|
||||
# uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353
|
||||
# with:
|
||||
# # Paths are Unix specific for now
|
||||
# path: |
|
||||
# ~/.cache/pip
|
||||
# ~/.cache/pypoetry
|
||||
# # Use python 3.8 as it is the version available in ubuntu 20.04 and we develop with it
|
||||
# key: "$${{ secrets.CACHE_VERSION }}-{{ runner.os }}-build-3.8-\
|
||||
# ${{ hashFiles('poetry.lock') }}"
|
||||
# restore-keys: |
|
||||
# ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-3.8-
|
||||
# ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-
|
||||
# ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-
|
||||
# ${{ secrets.CACHE_VERSION }}-
|
||||
# See #570 To be updated to only install required dependencies group with poetry 1.2 and
|
||||
# remove graphviz installs which are only required for the actual package and not dev tools
|
||||
- name: Install dependencies
|
||||
@@ -716,6 +685,12 @@ jobs:
|
||||
with:
|
||||
name: html-docs
|
||||
path: ${{ env.ARTIFACTS_RAW_DIR }}/html_docs/
|
||||
- name: Untar docs artifacts
|
||||
if: ${{ success() && !cancelled() }}
|
||||
run: |
|
||||
cd ${{ env.ARTIFACTS_RAW_DIR }}/html_docs/
|
||||
tar -xvf docs.tar
|
||||
rm docs.tar
|
||||
- name: Download changelog
|
||||
if: ${{ success() && !cancelled() }}
|
||||
id: download-changelog
|
||||
|
||||
9
Makefile
9
Makefile
@@ -245,12 +245,19 @@ release_docker:
|
||||
upgrade_py_deps:
|
||||
./script/make_utils/upgrade_deps.sh
|
||||
|
||||
# Keeping this target as it proved useful before the package was stabilized
|
||||
# This is done by hand as pytest-codeblocks was failing with our native extensions.
|
||||
# See refused PR on the project here: https://github.com/nschloe/pytest-codeblocks/pull/58
|
||||
.PHONY: test_codeblocks # Test code block in the documentation
|
||||
# Test code blocks using a custom python script in the documentation
|
||||
.PHONY: test_codeblocks
|
||||
test_codeblocks:
|
||||
poetry run python ./script/make_utils/test_md_python_code.py --md_dir docs/
|
||||
|
||||
.PHONY: pytest_codeblocks # Test code blocks using pytest in the documentation
|
||||
pytest_codeblocks:
|
||||
poetry run pytest --codeblocks -svv -n $$(./script/make_utils/ncpus.sh) \
|
||||
--randomly-dont-reorganize docs/
|
||||
|
||||
# From https://stackoverflow.com/a/63523300 for the find command
|
||||
.PHONY: shell_lint # Lint all bash scripts
|
||||
shell_lint:
|
||||
|
||||
@@ -10,6 +10,7 @@ class CompilationConfiguration:
|
||||
treat_warnings_as_errors: bool
|
||||
enable_unsafe_features: bool
|
||||
random_inputset_samples: int
|
||||
use_insecure_key_cache: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -19,6 +20,7 @@ class CompilationConfiguration:
|
||||
treat_warnings_as_errors: bool = False,
|
||||
enable_unsafe_features: bool = False,
|
||||
random_inputset_samples: int = 30,
|
||||
use_insecure_key_cache: bool = False,
|
||||
):
|
||||
self.dump_artifacts_on_unexpected_failures = dump_artifacts_on_unexpected_failures
|
||||
self.enable_topological_optimizations = enable_topological_optimizations
|
||||
@@ -26,6 +28,7 @@ class CompilationConfiguration:
|
||||
self.treat_warnings_as_errors = treat_warnings_as_errors
|
||||
self.enable_unsafe_features = enable_unsafe_features
|
||||
self.random_inputset_samples = random_inputset_samples
|
||||
self.use_insecure_key_cache = use_insecure_key_cache
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return isinstance(other, CompilationConfiguration) and self.__dict__ == other.__dict__
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Utilities for MLIR conversion."""
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import networkx as nx
|
||||
|
||||
@@ -170,14 +170,23 @@ def _set_all_bit_width(op_graph: OPGraph, p: int):
|
||||
value.dtype.bit_width = p
|
||||
|
||||
|
||||
def update_bit_width_for_mlir(op_graph: OPGraph):
|
||||
"""Prepare bit_width of all nodes to be the same, set to the maximum value in the graph.
|
||||
def get_op_graph_max_bit_width_and_nodes_over_bit_width_limit(
|
||||
op_graph: OPGraph,
|
||||
) -> Tuple[int, Dict[IntermediateNode, List[str]]]:
|
||||
"""Get the maximum bit width of integer nodes in the given OPGraph.
|
||||
|
||||
Also returns a dictionary with nodes having an unsupported bit width.
|
||||
|
||||
Args:
|
||||
op_graph: graph to update bit_width for
|
||||
|
||||
Returns:
|
||||
Tuple[int, Dict[IntermediateNode, List[str]]]: a tuple containing the maximum bit width of
|
||||
integer values in the OPGraph as well as a dictionary with nodes and the list of issues
|
||||
that the nodes have, in this case having an unsupported bit width.
|
||||
"""
|
||||
max_bit_width = 0
|
||||
offending_nodes = {}
|
||||
offending_nodes: Dict[IntermediateNode, List[str]] = {}
|
||||
for node in op_graph.graph.nodes:
|
||||
for value_out in node.outputs:
|
||||
if value_is_clear_scalar_integer(value_out) or value_is_clear_tensor_integer(value_out):
|
||||
@@ -199,6 +208,19 @@ def update_bit_width_for_mlir(op_graph: OPGraph):
|
||||
f"{current_node_out_bit_width} bits is not supported for the time being"
|
||||
]
|
||||
|
||||
return max_bit_width, offending_nodes
|
||||
|
||||
|
||||
def update_bit_width_for_mlir(op_graph: OPGraph):
|
||||
"""Prepare bit_width of all nodes to be the same, set to the maximum value in the graph.
|
||||
|
||||
Args:
|
||||
op_graph: graph to update bit_width for
|
||||
"""
|
||||
max_bit_width, offending_nodes = get_op_graph_max_bit_width_and_nodes_over_bit_width_limit(
|
||||
op_graph
|
||||
)
|
||||
|
||||
if len(offending_nodes) != 0:
|
||||
raise RuntimeError(
|
||||
f"max_bit_width of some nodes is too high for the current version of "
|
||||
|
||||
@@ -32,6 +32,8 @@ from .np_dtypes_helpers import (
|
||||
from .np_inputset_helpers import _check_special_inputset_availability, _generate_random_inputset
|
||||
from .np_mlir_converter import NPMLIRConverter
|
||||
|
||||
_COMPILE_FHE_INSECURE_KEY_CACHE_DIR: Optional[str] = None
|
||||
|
||||
|
||||
def numpy_max_func(lhs: Any, rhs: Any) -> Any:
|
||||
"""Compute the maximum value between two values which can be numpy classes (e.g. ndarray).
|
||||
@@ -610,13 +612,18 @@ def prepare_op_graph_for_mlir(op_graph: OPGraph):
|
||||
|
||||
|
||||
def _compile_op_graph_to_fhe_circuit_internal(
|
||||
op_graph: OPGraph, show_mlir: bool, compilation_artifacts: CompilationArtifacts
|
||||
op_graph: OPGraph,
|
||||
show_mlir: bool,
|
||||
compilation_configuration: CompilationConfiguration,
|
||||
compilation_artifacts: CompilationArtifacts,
|
||||
) -> FHECircuit:
|
||||
"""Compile the OPGraph to an FHECircuit.
|
||||
|
||||
Args:
|
||||
op_graph (OPGraph): the OPGraph to compile.
|
||||
show_mlir (bool): determine whether we print the mlir string.
|
||||
compilation_configuration (CompilationConfiguration): Configuration object to use
|
||||
during compilation
|
||||
compilation_artifacts (CompilationArtifacts): Artifacts object to fill
|
||||
during compilation
|
||||
|
||||
@@ -636,9 +643,18 @@ def _compile_op_graph_to_fhe_circuit_internal(
|
||||
# Add MLIR representation as an artifact
|
||||
compilation_artifacts.add_final_operation_graph_mlir(mlir_result)
|
||||
|
||||
if (
|
||||
_COMPILE_FHE_INSECURE_KEY_CACHE_DIR is not None
|
||||
and not compilation_configuration.use_insecure_key_cache
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Unable to use insecure key cache {_COMPILE_FHE_INSECURE_KEY_CACHE_DIR} "
|
||||
f"as use_insecure_key_cache is not set to True in compilation_configuration"
|
||||
)
|
||||
|
||||
# Compile the MLIR representation
|
||||
engine = CompilerEngine()
|
||||
engine.compile_fhe(mlir_result)
|
||||
engine.compile_fhe(mlir_result, unsecure_key_set_cache_path=_COMPILE_FHE_INSECURE_KEY_CACHE_DIR)
|
||||
|
||||
return FHECircuit(op_graph, engine)
|
||||
|
||||
@@ -671,7 +687,9 @@ def compile_op_graph_to_fhe_circuit(
|
||||
)
|
||||
|
||||
def compilation_function():
|
||||
return _compile_op_graph_to_fhe_circuit_internal(op_graph, show_mlir, compilation_artifacts)
|
||||
return _compile_op_graph_to_fhe_circuit_internal(
|
||||
op_graph, show_mlir, compilation_configuration, compilation_artifacts
|
||||
)
|
||||
|
||||
result = run_compilation_function_with_error_management(
|
||||
compilation_function, compilation_configuration, compilation_artifacts
|
||||
@@ -720,7 +738,7 @@ def _compile_numpy_function_internal(
|
||||
)
|
||||
|
||||
fhe_circuit = _compile_op_graph_to_fhe_circuit_internal(
|
||||
op_graph, show_mlir, compilation_artifacts
|
||||
op_graph, show_mlir, compilation_configuration, compilation_artifacts
|
||||
)
|
||||
|
||||
return fhe_circuit
|
||||
|
||||
1
docs/conftest.py
Symbolic link
1
docs/conftest.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../tests/conftest.py
|
||||
@@ -6,7 +6,7 @@ The current compiler stack only supports integers with 7 bits or less. But it's
|
||||
|
||||
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-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
import numpy
|
||||
|
||||
@@ -43,7 +43,7 @@ From the terminal node, we go back up through the nodes until we find nodes that
|
||||
|
||||
Here is an example benefiting from the expanded search:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
def fusable_with_bigger_search(x, y):
|
||||
"""fusable with bigger search"""
|
||||
@@ -71,7 +71,7 @@ The simplified graph of operations with the float subgraph condensed in a `Gener
|
||||
|
||||
An example of a non fusable computation with that technique is:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
import numpy
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import concrete.numpy as hnp
|
||||
|
||||
You need to have a python function that follows the [limits](../explanation/fhe_and_framework_limits.md) of the **Concrete Framework**. Here is a simple example:
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
def f(x, y):
|
||||
return x + y
|
||||
@@ -22,7 +22,7 @@ def f(x, y):
|
||||
|
||||
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-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
x = "encrypted"
|
||||
y = "encrypted"
|
||||
@@ -32,14 +32,14 @@ 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-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```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-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
compiler = hnp.NPFHECompiler(
|
||||
f, {"x": x, "y": y},
|
||||
@@ -80,7 +80,7 @@ Here is the graph from the previous code block drawn with `draw_graph`:
|
||||
|
||||
You can use `.run(...)` method of `FHECircuit` returned by `hnp.compile_numpy_function(...)` to perform fully homomorphic evaluation. Here are some examples:
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
circuit.run(3, 4)
|
||||
# 7
|
||||
|
||||
@@ -33,7 +33,7 @@ Note that the architecture of the neural network passed to be compiled must resp
|
||||
|
||||
Once your model is trained you can simply call the `compile_torch_model` function to execute the compilation.
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
from concrete.torch.compile import compile_torch_model
|
||||
import numpy
|
||||
@@ -49,7 +49,7 @@ You can then call `quantized_numpy_module.forward_fhe.run()` to have the FHE inf
|
||||
|
||||
Now your model is ready to infer in FHE settings !
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
enc_x = numpy.array([numpy.random.randn(14)]).astype(numpy.uint8) # An example that is going to be encrypted, and used for homomorphic inference.
|
||||
fhe_prediction = quantized_numpy_module.forward_fhe.run(enc_x)
|
||||
@@ -57,7 +57,7 @@ fhe_prediction = quantized_numpy_module.forward_fhe.run(enc_x)
|
||||
|
||||
`fhe_prediction` contains the clear quantized output. The user can now dequantize the output to get the actual floating point prediction as follows:
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
clear_output = quantized_numpy_module.dequantize_output(
|
||||
numpy.array(fhe_prediction, dtype=numpy.float32)
|
||||
|
||||
@@ -11,7 +11,7 @@ Sometimes, it can be useful to print or draw fhe circuits, we provide methods to
|
||||
|
||||
To print your circuit, you can do the following:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
print(circuit)
|
||||
```
|
||||
@@ -34,7 +34,7 @@ You may need to force reinstallation
|
||||
|
||||
To draw your circuit, you can do the following:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
drawing = circuit.draw()
|
||||
```
|
||||
@@ -43,7 +43,7 @@ This method will draw the circuit on a temporary PNG file and return the path to
|
||||
|
||||
To show the drawing, you can use the following code in a jupyter notebook.
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
from PIL import Image
|
||||
drawing = Image.open(circuit.draw())
|
||||
@@ -53,14 +53,14 @@ 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-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
circuit.draw(show=True)
|
||||
```
|
||||
|
||||
Lastly, you can save the drawing to a specific path like this:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
destination = "/tmp/path/of/your/choice.png"
|
||||
drawing = circuit.draw(save_to=destination)
|
||||
|
||||
@@ -7,7 +7,7 @@ We are actively working on supporting larger integers, so it should get better i
|
||||
|
||||
You get a compilation error. Here is an example:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
import concrete.numpy as hnp
|
||||
|
||||
@@ -93,4 +93,4 @@ Above $ \Omega $ dimensions in the input and weights, the risk of overflow incre
|
||||
|
||||
Currently, Concrete Framework 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)
|
||||
[^1]: [Integer overflow](https://en.wikipedia.org/wiki/Integer_overflow)
|
||||
|
||||
@@ -47,7 +47,7 @@ Concrete Framework allows you to convert Numpy operations to their FHE counterpa
|
||||
|
||||
First we define a model:
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
from torch import nn
|
||||
import torch
|
||||
@@ -70,7 +70,7 @@ torch_model = LogisticRegression()
|
||||
```
|
||||
|
||||
We then convert this model to numpy only operations:
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
from concrete.torch import NumpyModule
|
||||
numpy_model = NumpyModule(torch_model)
|
||||
@@ -80,7 +80,7 @@ The `NumpyModule` allows us to runs inference as for a `nn.Module`. Here, the pr
|
||||
|
||||
We can then quantize the numpy module with `PostTrainingAffineQuantization` as follows:
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
from concrete.quantization import PostTrainingAffineQuantization
|
||||
numpy_input = numpy.random.uniform(-1, 1, size=(10,14)) # some input with 14 features to calibrate the quantization
|
||||
@@ -93,7 +93,7 @@ Here, the quantized model takes a quantized array and runs inference in the quan
|
||||
|
||||
We can then easily verify that all models give similar predictions. Obviously, the `n_bits` chosen may adversely affect the prediction of the `quantized_numpy_module`. You can try increasing this parameter to see the effect on your model but keep in mind that the compilation will require all the values of your network to be less than 7 bits of precision.
|
||||
|
||||
<!--python-test:cont-->
|
||||
<!--pytest-codeblocks:cont-->
|
||||
```python
|
||||
torch_model(torch.from_numpy(numpy_input).float())
|
||||
# tensor([[-0.0690],
|
||||
|
||||
@@ -6,7 +6,7 @@ In this tutorial, we are going to go over the artifact system, which is designed
|
||||
|
||||
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-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
def f(x):
|
||||
return np.sin(x)
|
||||
|
||||
@@ -21,7 +21,7 @@ where
|
||||
|
||||
results in
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
circuit.run(0) == 2
|
||||
circuit.run(1) == 1
|
||||
@@ -35,7 +35,7 @@ Moreover, direct lookup tables can be used with tensors where the same table loo
|
||||
|
||||
results in
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
input = np.array([[0, 1, 3], [2, 3, 1]], dtype=np.uint8)
|
||||
circuit.run(input) == [[2, 1, 0], [3, 0, 1]]
|
||||
@@ -45,7 +45,7 @@ circuit.run(input) == [[2, 1, 0], [3, 0, 1]]
|
||||
|
||||
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:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
import concrete.numpy as hnp
|
||||
|
||||
@@ -68,7 +68,7 @@ where
|
||||
|
||||
results in
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
input = np.array([[2, 3], [1, 2], [3, 0]], dtype=np.uint8)
|
||||
circuit.run(input) == [[4, 27], [1, 8], [9, 0]]
|
||||
@@ -82,7 +82,7 @@ Direct tables are tedious to prepare by hand. When possible, **Concrete Framewor
|
||||
|
||||
Here is an example function that results in fused table lookup:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
def f(x):
|
||||
return 127 - (50 * (np.sin(x) + 1)).astype(np.uint32) # astype is to go back to integer world
|
||||
@@ -94,7 +94,7 @@ where
|
||||
|
||||
results in
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
circuit.run(0) == 77
|
||||
circuit.run(1) == 35
|
||||
@@ -116,14 +116,14 @@ and after floating point operations are fused, we get the following operation gr
|
||||
|
||||
Internally, it uses the following lookup table
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
table = hnp.LookupTable([50, 92, 95, 57, 12, 2, 36, 82])
|
||||
```
|
||||
|
||||
which is calculated by:
|
||||
|
||||
<!--python-test:skip-->
|
||||
<!--pytest-codeblocks:skip-->
|
||||
```python
|
||||
[(50 * (np.sin(x) + 1)).astype(np.uint32) for x in range(2 ** 3)]
|
||||
```
|
||||
|
||||
17
poetry.lock
generated
17
poetry.lock
generated
@@ -1492,6 +1492,17 @@ toml = "*"
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-codeblocks"
|
||||
version = "0.12.2"
|
||||
description = "Test code blocks in your READMEs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=6"
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "3.0.0"
|
||||
@@ -2234,7 +2245,7 @@ full = ["pygraphviz"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.8,<3.11"
|
||||
content-hash = "767afd54d83cdd78c3ddf10a8972660d839b7baa6d69fee693acfda671be67ed"
|
||||
content-hash = "a65fc210c558f583f6bc0dc3c1ebe6f768ed51c38a074952b61cea66fc1b4181"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
@@ -3239,6 +3250,10 @@ pytest = [
|
||||
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
|
||||
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
|
||||
]
|
||||
pytest-codeblocks = [
|
||||
{file = "pytest-codeblocks-0.12.2.tar.gz", hash = "sha256:6554cb970bdc5933dd70397b9f10c9495dc10b4765f83e6abbe2e96839053492"},
|
||||
{file = "pytest_codeblocks-0.12.2-py3-none-any.whl", hash = "sha256:6be59c283c9a5226eb77ea4b066f913a1f7078828ace6cca26147b75d151b3fb"},
|
||||
]
|
||||
pytest-cov = [
|
||||
{file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
|
||||
{file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
|
||||
|
||||
@@ -51,6 +51,7 @@ sphinx-zama-theme = "2.0.8"
|
||||
scikit-learn = "1.0.1"
|
||||
pandas = "1.3.4"
|
||||
pip-audit = "^1.1.1"
|
||||
pytest-codeblocks = "^0.12.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
@@ -61,6 +62,7 @@ filterwarnings = [
|
||||
"error",
|
||||
"ignore:pandas not found, skipping conversion test.:ImportWarning",
|
||||
"ignore:scipy not found, skipping conversion test.:ImportWarning",
|
||||
"ignore:Matplotlib is currently using .*, which is a non-GUI backend, so cannot show the figure\\.:UserWarning"
|
||||
]
|
||||
|
||||
[tool.semantic_release]
|
||||
|
||||
@@ -29,9 +29,10 @@ TOKEN=
|
||||
ORG_REPO=
|
||||
# the name of your release asset file, e.g. build.tar.gz
|
||||
FILE=
|
||||
DEST_FILE=
|
||||
DEST_DIR=
|
||||
VERSION="latest"
|
||||
COMPILER_TAG_OUTPUT_FILE=debug.txt
|
||||
GITHUB_ENV_FILE=debug.txt
|
||||
|
||||
while [ -n "$1" ]
|
||||
do
|
||||
@@ -51,11 +52,16 @@ do
|
||||
VERSION="$1"
|
||||
;;
|
||||
|
||||
"--dest-file" )
|
||||
"--dest-dir" )
|
||||
shift
|
||||
DEST_FILE="$1"
|
||||
DEST_DIR="$1"
|
||||
;;
|
||||
|
||||
"--github-env")
|
||||
shift
|
||||
GITHUB_ENV_FILE="$1"
|
||||
;;
|
||||
|
||||
"--file" )
|
||||
shift
|
||||
FILE="$1"
|
||||
@@ -76,12 +82,11 @@ done
|
||||
|
||||
alias errcho='>&2 echo'
|
||||
|
||||
DEST_DIR=$(dirname "${DEST_FILE}")
|
||||
mkdir -p "${DEST_DIR}"
|
||||
|
||||
if [[ "${VERSION}" == "latest" ]]; then
|
||||
# Github should return the latest release first.
|
||||
jq_parser=".[0]"
|
||||
# Select first non draft version
|
||||
jq_parser='. | map(select(.draft == false))[0]'
|
||||
else
|
||||
jq_parser=". | map(select(.tag_name == \"${VERSION}\"))[0]"
|
||||
fi;
|
||||
@@ -98,6 +103,11 @@ asset_json=$(echo "${release_json}" | jq ".assets | map(select(.name | contains(
|
||||
echo "Asset json:"
|
||||
echo "${asset_json}"
|
||||
|
||||
asset_filename=$(echo "${asset_json}" | jq -rc '.name')
|
||||
echo "Asset filename:"
|
||||
echo "${asset_filename}"
|
||||
echo "WHEEL=${asset_filename}" >> "${GITHUB_ENV_FILE}"
|
||||
|
||||
release_tag=$(echo "${release_json}" | jq -rc '.tag_name')
|
||||
asset_id=$(echo "${asset_json}" | jq -rc '.id')
|
||||
|
||||
@@ -115,7 +125,7 @@ echo "Downloading..."
|
||||
|
||||
wget --auth-no-challenge --header='Accept:application/octet-stream' \
|
||||
"https://${TOKEN}:@api.github.com/repos/${ORG_REPO}/releases/assets/${asset_id}" \
|
||||
-O "${DEST_FILE}"
|
||||
-O "${DEST_DIR}/${asset_filename}"
|
||||
|
||||
err_code=$?
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ jq -rc '.[] | select(.metadata.container.tags[] | contains("latest"))')
|
||||
|
||||
RELEASE_JSON=$(curl -H "Authorization: token ${TOKEN}" \
|
||||
-H "Accept: application/vnd.github.v3.raw" \
|
||||
"${COMPILER_RELEASE_ENDPOINT_URL}" | jq '.[0]')
|
||||
"${COMPILER_RELEASE_ENDPOINT_URL}" | jq '. | map(select(.draft == false))[0]')
|
||||
|
||||
echo "Release json:"
|
||||
echo "${RELEASE_JSON}"
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Dict, List
|
||||
PYTHON_BLOCK_HINTS = ["py", "python", "python3"]
|
||||
BLOCK_STARTS = tuple(f"```{hint}" for hint in PYTHON_BLOCK_HINTS)
|
||||
BLOCK_END = "```"
|
||||
DIRECTIVE_COMMENT_PATTERN = "<!--python-test:(.*)-->"
|
||||
DIRECTIVE_COMMENT_PATTERN = "<!--pytest-codeblocks:(.*)-->"
|
||||
SKIP_DIRECTIVE = "skip"
|
||||
CONT_DIRECTIVE = "cont"
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import json
|
||||
import operator
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Iterable, Type
|
||||
from typing import Any, Callable, Dict, Iterable, Optional, Type
|
||||
|
||||
import networkx as nx
|
||||
import networkx.algorithms.isomorphism as iso
|
||||
@@ -14,6 +15,10 @@ import torch
|
||||
|
||||
from concrete.common.compilation import CompilationConfiguration
|
||||
from concrete.common.fhe_circuit import FHECircuit
|
||||
from concrete.common.mlir.utils import (
|
||||
ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB,
|
||||
get_op_graph_max_bit_width_and_nodes_over_bit_width_limit,
|
||||
)
|
||||
from concrete.common.representation.intermediate import (
|
||||
ALL_IR_NODES,
|
||||
Add,
|
||||
@@ -27,6 +32,7 @@ from concrete.common.representation.intermediate import (
|
||||
Mul,
|
||||
Sub,
|
||||
)
|
||||
from concrete.numpy import compile as compile_
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -40,6 +46,74 @@ def pytest_addoption(parser):
|
||||
help="To dump pytest-cov term report to a text file.",
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
"--keyring-dir",
|
||||
action="store",
|
||||
default=None,
|
||||
type=str,
|
||||
help="Specify the dir to use to store key cache",
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_KEYRING_PATH = Path.home().resolve() / ".cache/concretefhe_pytest"
|
||||
|
||||
|
||||
def get_keyring_dir_from_session_or_default(
|
||||
session: Optional[pytest.Session] = None,
|
||||
) -> Optional[Path]:
|
||||
"""Get keyring dir from test session."""
|
||||
if session is None:
|
||||
return DEFAULT_KEYRING_PATH
|
||||
|
||||
keyring_dir = session.config.getoption("--keyring-dir", default=None)
|
||||
if keyring_dir is not None:
|
||||
if keyring_dir.lower() == "disable":
|
||||
return None
|
||||
keyring_dir = Path(keyring_dir).expanduser().resolve()
|
||||
else:
|
||||
keyring_dir = DEFAULT_KEYRING_PATH
|
||||
return keyring_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_keyring_path():
|
||||
"""fixture to get test keyring dir"""
|
||||
return DEFAULT_KEYRING_PATH
|
||||
|
||||
|
||||
# This is only for doctests where we currently cannot make use of fixtures
|
||||
original_compilation_config_init = CompilationConfiguration.__init__
|
||||
|
||||
|
||||
def monkeypatched_compilation_configuration_init_for_codeblocks(self, *args, **kwargs):
|
||||
"""Monkeypatched compilation configuration init for codeblocks tests."""
|
||||
original_compilation_config_init(self, *args, **kwargs)
|
||||
self.dump_artifacts_on_unexpected_failures = False
|
||||
self.treat_warnings_as_errors = True
|
||||
self.use_insecure_key_cache = True
|
||||
|
||||
|
||||
def pytest_sessionstart(session: pytest.Session):
|
||||
"""Handle keyring for session and codeblocks CompilationConfiguration if needed."""
|
||||
if session.config.getoption("--codeblocks", default=False):
|
||||
# setattr to avoid mypy complaining
|
||||
# Disable the flake8 bug bear warning for the mypy fix
|
||||
setattr( # noqa: B010
|
||||
CompilationConfiguration,
|
||||
"__init__",
|
||||
monkeypatched_compilation_configuration_init_for_codeblocks,
|
||||
)
|
||||
|
||||
keyring_dir = get_keyring_dir_from_session_or_default(session)
|
||||
if keyring_dir is None:
|
||||
return
|
||||
keyring_dir.mkdir(parents=True, exist_ok=True)
|
||||
keyring_dir_as_str = str(keyring_dir)
|
||||
print(f"Using {keyring_dir_as_str} as key cache dir")
|
||||
compile_._COMPILE_FHE_INSECURE_KEY_CACHE_DIR = ( # pylint: disable=protected-access
|
||||
keyring_dir_as_str
|
||||
)
|
||||
|
||||
|
||||
def pytest_sessionfinish(session: pytest.Session, exitstatus): # pylint: disable=unused-argument
|
||||
"""Pytest callback when testing ends."""
|
||||
@@ -66,6 +140,12 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus): # pylint: disabl
|
||||
with open(global_coverage_file_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"exit_code": coverage_status, "content": coverage_txt}, f)
|
||||
|
||||
keyring_dir = get_keyring_dir_from_session_or_default(session)
|
||||
if keyring_dir is not None:
|
||||
# Remove incomplete keys
|
||||
for incomplete_keys in keyring_dir.glob("**/*incomplete*"):
|
||||
shutil.rmtree(incomplete_keys, ignore_errors=True)
|
||||
|
||||
|
||||
def _is_equivalent_to_binary_commutative(lhs: IntermediateNode, rhs: object) -> bool:
|
||||
"""is_equivalent_to for a binary and commutative operation."""
|
||||
@@ -270,6 +350,7 @@ def default_compilation_configuration():
|
||||
return CompilationConfiguration(
|
||||
dump_artifacts_on_unexpected_failures=False,
|
||||
treat_warnings_as_errors=True,
|
||||
use_insecure_key_cache=True, # This is for our tests only, never use that in prod
|
||||
)
|
||||
|
||||
|
||||
@@ -310,7 +391,20 @@ def check_is_good_execution_impl(
|
||||
return an error. One can set the expected probability of success of one execution and the
|
||||
number of tests, to finetune the probability of bad luck, ie that we run several times the
|
||||
check and always have a wrong result."""
|
||||
nb_tries = 5
|
||||
max_bit_width, _ = get_op_graph_max_bit_width_and_nodes_over_bit_width_limit(
|
||||
fhe_circuit.op_graph
|
||||
)
|
||||
|
||||
# Allow tests to pass if cells of the output result are good at least once over the nb_tries
|
||||
# Enabled only when we have a circuit that's using the maximum possible bit width
|
||||
allow_relaxed_tests_passing = max_bit_width == ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB
|
||||
|
||||
# Increased with compiler accuracy which dropped
|
||||
nb_tries = 10
|
||||
|
||||
# Prepare the bool array to record if cells were properly computed
|
||||
preprocessed_args = tuple(preprocess_input_func(val) for val in args)
|
||||
cells_were_properly_computed = numpy.zeros_like(function(*preprocessed_args), dtype=bool)
|
||||
|
||||
for i in range(1, nb_tries + 1):
|
||||
preprocessed_args = tuple(preprocess_input_func(val) for val in args)
|
||||
@@ -323,7 +417,20 @@ def check_is_good_execution_impl(
|
||||
print(f"Good computation after {i} tries")
|
||||
return
|
||||
|
||||
# Computation was bad, record the cells that were well computed
|
||||
cells_were_properly_computed = numpy.logical_or(
|
||||
cells_were_properly_computed, last_engine_result == last_function_result
|
||||
)
|
||||
|
||||
# Bad computation after nb_tries
|
||||
if allow_relaxed_tests_passing:
|
||||
if cells_were_properly_computed.all():
|
||||
print(
|
||||
"Computation was never good for all output cells at the same time, "
|
||||
"however each was evaluated properly at least once"
|
||||
)
|
||||
return
|
||||
|
||||
raise AssertionError(
|
||||
f"bad computation after {nb_tries} tries.\nLast engine result:\n{last_engine_result}\n"
|
||||
f"Last function result:\n{last_function_result}"
|
||||
|
||||
@@ -12,6 +12,7 @@ from concrete.common.debugging import draw_graph, format_operation_graph
|
||||
from concrete.common.extensions.multi_table import MultiLookupTable
|
||||
from concrete.common.extensions.table import LookupTable
|
||||
from concrete.common.values import ClearTensor, EncryptedScalar, EncryptedTensor
|
||||
from concrete.numpy import compile as compile_
|
||||
from concrete.numpy import tracing
|
||||
from concrete.numpy.compile import (
|
||||
FHECircuit,
|
||||
@@ -2148,3 +2149,30 @@ def test_compile_and_run_correctness_with_negative_results(
|
||||
|
||||
args = [random.randint(low, high) for (low, high) in input_ranges]
|
||||
assert check_equality_modulo(compiler_engine.run(*args), function(*args), modulus)
|
||||
|
||||
|
||||
def test_compile_improper_use_of_insecure_key_cache(default_keyring_path):
|
||||
"""Test the case where the key cache is used with wrong compilation configuration"""
|
||||
|
||||
def f(x):
|
||||
return x + 42
|
||||
|
||||
if compile_._COMPILE_FHE_INSECURE_KEY_CACHE_DIR is None: # pylint: disable=protected-access
|
||||
compile_._COMPILE_FHE_INSECURE_KEY_CACHE_DIR = str( # pylint: disable=protected-access
|
||||
default_keyring_path
|
||||
)
|
||||
|
||||
compilation_configuration = CompilationConfiguration()
|
||||
compilation_configuration.use_insecure_key_cache = False
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="Unable to use insecure key cache .* "
|
||||
"as use_insecure_key_cache is not set to True in compilation_configuration",
|
||||
):
|
||||
_ = compile_numpy_function(
|
||||
f,
|
||||
{"x": EncryptedScalar(Integer(64, False))},
|
||||
range(10),
|
||||
compilation_configuration,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user