From aad659f160a6a73b4b067e04899a370afa6f035e Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 6 Mar 2024 15:33:27 +0300 Subject: [PATCH] feat(frontend-python): random inputset generator --- docs/getting-started/quick_start.md | 5 + docs/tutorial/extensions.md | 17 ++++ .../concrete-python/concrete/fhe/__init__.py | 1 + .../concrete/fhe/compilation/__init__.py | 1 + .../concrete/fhe/compilation/utils.py | 47 +++++++++- .../tests/compilation/test_utils.py | 93 +++++++++++++++++++ 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 frontends/concrete-python/tests/compilation/test_utils.py diff --git a/docs/getting-started/quick_start.md b/docs/getting-started/quick_start.md index 981ea8b2a..7d6f59fad 100644 --- a/docs/getting-started/quick_start.md +++ b/docs/getting-started/quick_start.md @@ -67,6 +67,11 @@ inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] All inputs in the inputset will be evaluated in the graph, which takes time. If you're experiencing long compilation times, consider providing a smaller inputset. {% endhint %} +{% hint style="warning" %} +There is a utility function called `fhe.inputset(...)` for easily creating random inputsets, see its +[documentation](../tutorial/extensions.md#fheinputset) to learn more! +{% endhint %} + ## Compiling the function You can use the `compile` method of a `Compiler` class with an inputset to perform the compilation and get the resulting circuit back: diff --git a/docs/tutorial/extensions.md b/docs/tutorial/extensions.md index db65da2aa..381ed6deb 100644 --- a/docs/tutorial/extensions.md +++ b/docs/tutorial/extensions.md @@ -547,3 +547,20 @@ is used instead, `x` will be assigned 2-bits as it should and `fhe.identity(x)` {% hint style="warning" %} Identity extension only works in `Native` encoding, which is usually selected when all table lookups in the circuit are below or equal to 8 bits. {% endhint %} + +## fhe.inputset(...) + +Used for creating a random inputset with the given specifications: + +```python +inputset = fhe.inputset(fhe.uint4, fhe.tensor[fhe.int3, 3, 2], lambda index: custom_value(index)) +assert isinstance(inputset, list) +assert all(isinstance(sample, tuple) and len(sample) == 3 for sample in inputset) +``` + +The result will have 100 inputs by default which can be customized using the size keyword argument: + +```python +inputset = fhe.inputset(fhe.uint4, fhe.uint4, size=10) +assert len(inputset) == 10 +``` diff --git a/frontends/concrete-python/concrete/fhe/__init__.py b/frontends/concrete-python/concrete/fhe/__init__.py index 940289374..0f336c6d1 100644 --- a/frontends/concrete-python/concrete/fhe/__init__.py +++ b/frontends/concrete-python/concrete/fhe/__init__.py @@ -27,6 +27,7 @@ from .compilation import ( ParameterSelectionStrategy, Server, Value, + inputset, ) from .compilation.decorators import circuit, compiler from .dtypes import Integer diff --git a/frontends/concrete-python/concrete/fhe/compilation/__init__.py b/frontends/concrete-python/concrete/fhe/compilation/__init__.py index 5f134c2a9..d73958617 100644 --- a/frontends/concrete-python/concrete/fhe/compilation/__init__.py +++ b/frontends/concrete-python/concrete/fhe/compilation/__init__.py @@ -22,4 +22,5 @@ from .configuration import ( from .keys import Keys from .server import Server from .specs import ClientSpecs +from .utils import inputset from .value import Value diff --git a/frontends/concrete-python/concrete/fhe/compilation/utils.py b/frontends/concrete-python/concrete/fhe/compilation/utils.py index f903b80f4..3c169436a 100644 --- a/frontends/concrete-python/concrete/fhe/compilation/utils.py +++ b/frontends/concrete-python/concrete/fhe/compilation/utils.py @@ -6,13 +6,14 @@ import json import os import re from copy import deepcopy -from typing import Dict, Iterable, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union import networkx as nx import numpy as np from ..dtypes import Float, Integer, SignedInteger, UnsignedInteger from ..representation import Graph, Node, Operation +from ..tracing import ScalarAnnotation from ..values import ValueDescription from .artifacts import DebugArtifacts from .specs import ClientSpecs @@ -20,6 +21,50 @@ from .specs import ClientSpecs # ruff: noqa: ERA001 +def inputset( + *inputs: Union[ScalarAnnotation, ValueDescription, Callable[[int], Any]], + size: int = 100, +) -> List[Tuple[Any, ...]]: + """ + Generate a random inputset. + + Args: + *inputs (Union[ScalarAnnotation, ValueDescription, Callable[[int], Any]]): + specification of each input + + size (int, default = 100): + size of the inputset + + Returns: + List[Tuple[Any, ...]]: + generated inputset + """ + + result: List[Tuple[Any, ...]] = [] + for i in range(size): + sample: List[Any] = [] + for specification in inputs: + is_value = isinstance(specification, ValueDescription) + is_scalar_annotation = isinstance(specification, type) and issubclass( + specification, ScalarAnnotation + ) + + if is_scalar_annotation or is_value: + dtype = specification.dtype # type: ignore + shape = () if is_scalar_annotation else specification.shape # type: ignore + + if isinstance(dtype, Integer): + sample.append(np.random.randint(dtype.min(), dtype.max() + 1, size=shape)) + else: + sample.append(np.random.rand(*shape)) + else: + assert not isinstance(specification, (ScalarAnnotation, ValueDescription)) + sample.append(specification(i)) + + result.append(tuple(sample)) + return result + + def validate_input_args( client_specs: ClientSpecs, *args: Optional[Union[int, np.ndarray, List]], diff --git a/frontends/concrete-python/tests/compilation/test_utils.py b/frontends/concrete-python/tests/compilation/test_utils.py new file mode 100644 index 000000000..ac8cf40dd --- /dev/null +++ b/frontends/concrete-python/tests/compilation/test_utils.py @@ -0,0 +1,93 @@ +""" +Tests of compilation utilities. +""" + +import numpy as np +import pytest + +from concrete import fhe + + +@pytest.mark.parametrize( + "inputs,ranges,shapes", + [ + ( + (fhe.uint3,), # type: ignore + ([0, 2**3],), + ((),), + ), + ( + (fhe.int3,), + ([-(2**2), 2**2],), # type: ignore + ((),), + ), + ( + (fhe.tensor[fhe.uint3, 3, 2],), # type: ignore + ([0, 2**3],), + ((3, 2),), + ), + ( + (fhe.tensor[fhe.int3, 3, 2],), # type: ignore + ([-(2**2), 2**2],), + ((3, 2),), + ), + ( + (fhe.uint3, fhe.uint4), # type: ignore + ([0, 2**3], [0, 2**4]), + ((), ()), + ), + ( + (fhe.int4, fhe.int3), # type: ignore + ([-(2**3), 2**3], [-(2**2), 2**2]), + ((), ()), + ), + ( + (fhe.tensor[fhe.uint6, 3, 2], fhe.tensor[fhe.int6, 5]), # type: ignore + ([0, 2**6], [-(2**5), 2**5]), + ((3, 2), (5,)), + ), + ( + (fhe.f32,), # type: ignore + ([0.0, 1.0],), + ((),), + ), + ( + (fhe.tensor[fhe.f32, 3, 2],), # type: ignore + ([0.0, 1.0],), + ((3, 2),), + ), + ( + (lambda _index: np.random.randint(10, 20),), + ([10, 20],), + ((),), + ), + ( + (lambda _index: np.random.randint(10, 20, size=(3, 2)),), + ([10, 20],), + ((3, 2),), + ), + ], +) +@pytest.mark.parametrize("size", [10, 100]) +def test_inputset(inputs, ranges, shapes, size): + """ + Test `inputset` utility. + """ + + inputset = fhe.inputset(*inputs, size=size) + + assert isinstance(inputset, list) + assert len(inputset) == size + + for sample in inputset: + assert isinstance(sample, tuple) + assert len(sample) == len(inputs) + for value, range_, shape in zip(sample, ranges, shapes): + assert isinstance(value, (int, float, np.ndarray)) + if isinstance(value, (int, float)): + assert shape == () + assert range_[0] <= value < range_[1] + else: + assert shape == value.shape + assert value.min() >= range_[0] + assert value.max() < range_[1]