From 239192ac6d03d67b2488cd6a193854953e74e661 Mon Sep 17 00:00:00 2001 From: jfrery Date: Thu, 16 May 2024 13:45:09 +0200 Subject: [PATCH] chore: remove deployment use case --- .../workflows/run_one_use_cases_example.yaml | 3 - .gitignore | 2 - Makefile | 4 +- .../breast_cancer/Dockerfile.client | 6 - .../deployment/breast_cancer/Dockerfile.train | 4 - .../deployment/breast_cancer/Makefile | 10 - .../deployment/breast_cancer/README.md | 23 - .../build_docker_client_image.py | 18 - .../deployment/breast_cancer/client.py | 110 ---- .../deployment/breast_cancer/client.sh | 2 - .../breast_cancer/client_requirements.txt | 3 - .../deployment/breast_cancer/train.py | 23 - .../breast_cancer/train_with_docker.sh | 6 - .../deployment/cifar/Dockerfile.client | 7 - .../deployment/cifar/Dockerfile.compile | 12 - use_case_examples/deployment/cifar/Makefile | 10 - use_case_examples/deployment/cifar/README.md | 25 - use_case_examples/deployment/cifar/client.py | 141 ----- use_case_examples/deployment/cifar/client.sh | 2 - .../deployment/cifar/client_requirements.txt | 3 - use_case_examples/deployment/cifar/compile.py | 91 --- .../deployment/cifar/compile_with_docker.py | 42 -- .../deployment/cifar/requirements.txt | 2 - use_case_examples/deployment/cifar/url.txt | 1 - .../sentiment_analysis/Dockerfile.client | 8 - .../sentiment_analysis/Dockerfile.train | 8 - .../deployment/sentiment_analysis/Makefile | 10 - .../deployment/sentiment_analysis/README.md | 25 - .../build_docker_client_image.py | 29 - .../deployment/sentiment_analysis/client.py | 122 ---- .../deployment/sentiment_analysis/client.sh | 2 - .../client_requirements.txt | 4 - .../sentiment_analysis/download_data.sh | 3 - .../sentiment_analysis/requirements.txt | 4 - .../deployment/sentiment_analysis/train.py | 206 ------- .../sentiment_analysis/train_requirements.txt | 1 - .../sentiment_analysis/train_with_docker.sh | 9 - .../sentiment_analysis/utility_functions.py | 34 -- .../deployment/server/Dockerfile.server | 8 - use_case_examples/deployment/server/README.md | 19 - .../deployment/server/deploy_to_aws.py | 528 ------------------ .../deployment/server/deploy_to_docker.py | 122 ---- use_case_examples/deployment/server/server.py | 91 --- .../deployment/server/server_requirements.txt | 3 - .../deployment/server/utils_server.py | 111 ---- 45 files changed, 1 insertion(+), 1896 deletions(-) delete mode 100644 use_case_examples/deployment/breast_cancer/Dockerfile.client delete mode 100644 use_case_examples/deployment/breast_cancer/Dockerfile.train delete mode 100644 use_case_examples/deployment/breast_cancer/Makefile delete mode 100644 use_case_examples/deployment/breast_cancer/README.md delete mode 100644 use_case_examples/deployment/breast_cancer/build_docker_client_image.py delete mode 100644 use_case_examples/deployment/breast_cancer/client.py delete mode 100644 use_case_examples/deployment/breast_cancer/client.sh delete mode 100644 use_case_examples/deployment/breast_cancer/client_requirements.txt delete mode 100644 use_case_examples/deployment/breast_cancer/train.py delete mode 100755 use_case_examples/deployment/breast_cancer/train_with_docker.sh delete mode 100644 use_case_examples/deployment/cifar/Dockerfile.client delete mode 100644 use_case_examples/deployment/cifar/Dockerfile.compile delete mode 100644 use_case_examples/deployment/cifar/Makefile delete mode 100644 use_case_examples/deployment/cifar/README.md delete mode 100644 use_case_examples/deployment/cifar/client.py delete mode 100644 use_case_examples/deployment/cifar/client.sh delete mode 100644 use_case_examples/deployment/cifar/client_requirements.txt delete mode 100644 use_case_examples/deployment/cifar/compile.py delete mode 100644 use_case_examples/deployment/cifar/compile_with_docker.py delete mode 100644 use_case_examples/deployment/cifar/requirements.txt delete mode 100644 use_case_examples/deployment/cifar/url.txt delete mode 100644 use_case_examples/deployment/sentiment_analysis/Dockerfile.client delete mode 100644 use_case_examples/deployment/sentiment_analysis/Dockerfile.train delete mode 100644 use_case_examples/deployment/sentiment_analysis/Makefile delete mode 100644 use_case_examples/deployment/sentiment_analysis/README.md delete mode 100644 use_case_examples/deployment/sentiment_analysis/build_docker_client_image.py delete mode 100644 use_case_examples/deployment/sentiment_analysis/client.py delete mode 100644 use_case_examples/deployment/sentiment_analysis/client.sh delete mode 100644 use_case_examples/deployment/sentiment_analysis/client_requirements.txt delete mode 100755 use_case_examples/deployment/sentiment_analysis/download_data.sh delete mode 100644 use_case_examples/deployment/sentiment_analysis/requirements.txt delete mode 100644 use_case_examples/deployment/sentiment_analysis/train.py delete mode 100644 use_case_examples/deployment/sentiment_analysis/train_requirements.txt delete mode 100755 use_case_examples/deployment/sentiment_analysis/train_with_docker.sh delete mode 100644 use_case_examples/deployment/sentiment_analysis/utility_functions.py delete mode 100644 use_case_examples/deployment/server/Dockerfile.server delete mode 100644 use_case_examples/deployment/server/README.md delete mode 100644 use_case_examples/deployment/server/deploy_to_aws.py delete mode 100644 use_case_examples/deployment/server/deploy_to_docker.py delete mode 100644 use_case_examples/deployment/server/server.py delete mode 100644 use_case_examples/deployment/server/server_requirements.txt delete mode 100644 use_case_examples/deployment/server/utils_server.py diff --git a/.github/workflows/run_one_use_cases_example.yaml b/.github/workflows/run_one_use_cases_example.yaml index 0d3989164c..454265f8db 100644 --- a/.github/workflows/run_one_use_cases_example.yaml +++ b/.github/workflows/run_one_use_cases_example.yaml @@ -12,9 +12,6 @@ on: - cifar/cifar_brevitas_finetuning - cifar/cifar_brevitas_training - credit_scoring - - deployment/breast_cancer - - deployment/cifar - - deployment/sentiment_analysis - disease_prediction - federated_learning - hybrid_model diff --git a/.gitignore b/.gitignore index 578c07780d..0efbfb1b48 100644 --- a/.gitignore +++ b/.gitignore @@ -148,8 +148,6 @@ progress.json use_case_examples/*/local_datasets/ use_case_examples/titanic/titanic_submission_fhe.csv use_case_examples/titanic/titanic_submission_xgb_clear.csv -use_case_examples/cifar_deployment/dev/ -use_case_examples/deployment/dev/ use_case_examples/llm/gpt2* use_case_examples/cifar_brevitas_training/.datasets/ diff --git a/Makefile b/Makefile index fd9c074313..bad8155004 100644 --- a/Makefile +++ b/Makefile @@ -450,11 +450,9 @@ finalize_nb: # A warning in a package unrelated to the project made pytest fail with notebooks # Run notebook tests without warnings as sources are already tested with warnings treated as errors # We need to disable xdist with -n0 to make sure to not have IPython port race conditions -# The deployment notebook is currently skipped until the AMI is fixed -# FIXME: https://github.com/zama-ai/concrete-ml-internal/issues/4064 .PHONY: pytest_nb # Launch notebook tests pytest_nb: - NOTEBOOKS=$$(find docs -name "*.ipynb" ! -name "*Deployment*" | grep -v _build | grep -v .ipynb_checkpoints || true) && \ + NOTEBOOKS=$$(find docs -name "*.ipynb" | grep -v _build | grep -v .ipynb_checkpoints || true) && \ if [[ "$${NOTEBOOKS}" != "" ]]; then \ echo "$${NOTEBOOKS}" | xargs poetry run pytest -svv \ --capture=tee-sys \ diff --git a/use_case_examples/deployment/breast_cancer/Dockerfile.client b/use_case_examples/deployment/breast_cancer/Dockerfile.client deleted file mode 100644 index 9d6315331f..0000000000 --- a/use_case_examples/deployment/breast_cancer/Dockerfile.client +++ /dev/null @@ -1,6 +0,0 @@ -FROM zamafhe/concrete-ml -WORKDIR /project -COPY client_requirements.txt . -RUN python -m pip install -r client_requirements.txt -COPY client.py . -ENTRYPOINT /bin/bash diff --git a/use_case_examples/deployment/breast_cancer/Dockerfile.train b/use_case_examples/deployment/breast_cancer/Dockerfile.train deleted file mode 100644 index 7cdb94d8b6..0000000000 --- a/use_case_examples/deployment/breast_cancer/Dockerfile.train +++ /dev/null @@ -1,4 +0,0 @@ -FROM zamafhe/concrete-ml -WORKDIR /project -COPY ./train.py ./train.py -ENTRYPOINT python train.py diff --git a/use_case_examples/deployment/breast_cancer/Makefile b/use_case_examples/deployment/breast_cancer/Makefile deleted file mode 100644 index 2be876d58a..0000000000 --- a/use_case_examples/deployment/breast_cancer/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -run_example: three - -one: - @./train_with_docker.sh - -two: one - @python -m concrete.ml.deployment.deploy_to_docker --only-build - -three: two - @python build_docker_client_image.py diff --git a/use_case_examples/deployment/breast_cancer/README.md b/use_case_examples/deployment/breast_cancer/README.md deleted file mode 100644 index f6ca46a941..0000000000 --- a/use_case_examples/deployment/breast_cancer/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Deployment - -In this folder we show how to deploy a simple Concrete ML model that does breast cancer classification, either through Docker or Amazon Web Services. - -## Get started - -To run this example on AWS you will also need to have the AWS CLI properly setup on your system. -To do so please refer to [AWS documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html). -One can also run this example locally using Docker, or just by running the scripts locally. - -1. To train your model you can use `train.py`, or `train_with_docker.sh` to use Docker (recommended way). - This will train a model and [serialize the FHE circuit](../../../docs/guides/client_server.md) in a new folder called `./dev`. -1. Once that's done you can use the script provided in Concrete ML in `use_case_examples/deployment/server/`, either use `deploy_to_aws.py` or `deploy_to_docker.py` according to your need. - -- `python use_case_examples/deployment/server/deploy_to_docker.py --path-to-model ./dev` -- `python use_case_examples/deployment/server/deploy_to_aws.py --path-to-model ./dev` - this will create and run a Docker container or an AWS EC2 instance. - -3. Once that's done you can launch the `build_docker_client_image.py` script to build a client Docker image. -1. You can then run the client by using the `client.sh` script. This will run the container in interactive mode. - To interact with the server you can launch the `client.py` script using `URL="" python client.py` where `` is the content of the `url.txt` file (default is `0.0.0.0`, ip to use when running server in Docker on localhost). - -And here it is you deployed a Concrete ML model and ran an inference using Fully Homormophic Encryption. diff --git a/use_case_examples/deployment/breast_cancer/build_docker_client_image.py b/use_case_examples/deployment/breast_cancer/build_docker_client_image.py deleted file mode 100644 index 961be1dce5..0000000000 --- a/use_case_examples/deployment/breast_cancer/build_docker_client_image.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import subprocess -from pathlib import Path - - -def main(): - path_of_script = Path(__file__).parent.resolve() - # Build image - os.chdir(path_of_script) - command = ( - f'docker build --tag cml_client_breast_cancer --file "{path_of_script}/Dockerfile.client" .' - ) - print(command) - subprocess.check_output(command, shell=True) - - -if __name__ == "__main__": - main() diff --git a/use_case_examples/deployment/breast_cancer/client.py b/use_case_examples/deployment/breast_cancer/client.py deleted file mode 100644 index fa2c79af5d..0000000000 --- a/use_case_examples/deployment/breast_cancer/client.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Client script. - -This script does the following: - - Query crypto-parameters and pre/post-processing parameters - - Quantize the inputs using the parameters - - Encrypt data using the crypto-parameters - - Send the encrypted data to the server (async using grequests) - - Collect the data and decrypt it - - De-quantize the decrypted results -""" - -import io -import os -from pathlib import Path - -import grequests -import numpy -import requests -from sklearn.datasets import load_breast_cancer -from tqdm import tqdm - -from concrete.ml.deployment import FHEModelClient - -URL = os.environ.get("URL", f"http://localhost:5000") -STATUS_OK = 200 -ROOT = Path(__file__).parent / "client" -ROOT.mkdir(exist_ok=True) - -if __name__ == "__main__": - # Get the necessary data for the client - # client.zip - zip_response = requests.get(f"{URL}/get_client") - assert zip_response.status_code == STATUS_OK - with open(ROOT / "client.zip", "wb") as file: - file.write(zip_response.content) - - # Get the data to infer - X, y = load_breast_cancer(return_X_y=True) - assert isinstance(X, numpy.ndarray) - assert isinstance(y, numpy.ndarray) - X = X[-10:] - y = y[-10:] - - assert isinstance(X, numpy.ndarray) - assert isinstance(y, numpy.ndarray) - - # Create the client - client = FHEModelClient(path_dir=str(ROOT.resolve()), key_dir=str((ROOT / "keys").resolve())) - - # The client first need to create the private and evaluation keys. - client.generate_private_and_evaluation_keys() - - # Get the serialized evaluation keys - serialized_evaluation_keys = client.get_serialized_evaluation_keys() - assert isinstance(serialized_evaluation_keys, bytes) - - # Evaluation keys can be quite large files but only have to be shared once with the server. - - # Check the size of the evaluation keys (in MB) - print(f"Evaluation keys size: {len(serialized_evaluation_keys) / (10**6):.2f} MB") - - # Send this evaluation key to the server (this has to be done only once) - # send_evaluation_key_to_server(serialized_evaluation_keys) - - # Now we have everything for the client to interact with the server - - # We create a loop to send the input to the server and receive the encrypted prediction - execution_time = [] - encrypted_input = None - clear_input = None - - # Update all base64 queries encodings with UploadFile - response = requests.post( - f"{URL}/add_key", files={"key": io.BytesIO(initial_bytes=serialized_evaluation_keys)} - ) - assert response.status_code == STATUS_OK - uid = response.json()["uid"] - - inferences = [] - # Launch the queries - for i in tqdm(range(len(X))): - clear_input = X[[i], :] - - assert isinstance(clear_input, numpy.ndarray) - encrypted_input = client.quantize_encrypt_serialize(clear_input) - assert isinstance(encrypted_input, bytes) - - inferences.append( - grequests.post( - f"{URL}/compute", - files={ - "model_input": io.BytesIO(encrypted_input), - }, - data={ - "uid": uid, - }, - ) - ) - - # Unpack the results - decrypted_predictions = [] - for result in grequests.map(inferences): - if result is None: - raise ValueError("Result is None, probably due to a crash on the server side.") - assert result.status_code == STATUS_OK - - encrypted_result = result.content - decrypted_prediction = client.deserialize_decrypt_dequantize(encrypted_result)[0] - decrypted_predictions.append(decrypted_prediction) - print(decrypted_predictions) diff --git a/use_case_examples/deployment/breast_cancer/client.sh b/use_case_examples/deployment/breast_cancer/client.sh deleted file mode 100644 index 205e927c42..0000000000 --- a/use_case_examples/deployment/breast_cancer/client.sh +++ /dev/null @@ -1,2 +0,0 @@ -#! /bin/env bash -docker run -it --network=host --entrypoint="/bin/bash" cml_client_breast_cancer diff --git a/use_case_examples/deployment/breast_cancer/client_requirements.txt b/use_case_examples/deployment/breast_cancer/client_requirements.txt deleted file mode 100644 index fc58e57cf6..0000000000 --- a/use_case_examples/deployment/breast_cancer/client_requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -grequests -requests -tqdm diff --git a/use_case_examples/deployment/breast_cancer/train.py b/use_case_examples/deployment/breast_cancer/train.py deleted file mode 100644 index 494354a2dd..0000000000 --- a/use_case_examples/deployment/breast_cancer/train.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy -from sklearn.datasets import load_breast_cancer - -from concrete.ml.deployment import FHEModelClient, FHEModelDev, FHEModelServer -from concrete.ml.sklearn import XGBClassifier - -if __name__ == "__main__": - # First get some data and train a model. - X, y = load_breast_cancer(return_X_y=True) - - assert isinstance(X, numpy.ndarray) - assert isinstance(y, numpy.ndarray) - - # Split X into X_model_owner and X_client - X_train = X[:-10] - y_train = y[:-10] - - # Train the model and compile it - model = XGBClassifier(n_bits=2, n_estimators=8, max_depth=3) - model.fit(X_train, y_train) - model.compile(X_train) - dev = FHEModelDev("./dev", model) - dev.save() diff --git a/use_case_examples/deployment/breast_cancer/train_with_docker.sh b/use_case_examples/deployment/breast_cancer/train_with_docker.sh deleted file mode 100755 index 11a545d810..0000000000 --- a/use_case_examples/deployment/breast_cancer/train_with_docker.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /bin/env bash - -docker build --tag train --file Dockerfile.train . && \ - docker run --name train_container train && \ - docker cp train_container:/project/dev . && \ - docker rm "$(docker ps -a --filter name=train_container -q)" diff --git a/use_case_examples/deployment/cifar/Dockerfile.client b/use_case_examples/deployment/cifar/Dockerfile.client deleted file mode 100644 index 53ae6763ab..0000000000 --- a/use_case_examples/deployment/cifar/Dockerfile.client +++ /dev/null @@ -1,7 +0,0 @@ -FROM zamafhe/concrete-ml -WORKDIR /project -COPY client.py . -COPY client_requirements.txt . -RUN python -m pip install -r client_requirements.txt -RUN python -m pip install torchvision==0.14.1 --no-deps -ENTRYPOINT /bin/bash diff --git a/use_case_examples/deployment/cifar/Dockerfile.compile b/use_case_examples/deployment/cifar/Dockerfile.compile deleted file mode 100644 index dc29700f97..0000000000 --- a/use_case_examples/deployment/cifar/Dockerfile.compile +++ /dev/null @@ -1,12 +0,0 @@ -FROM zamafhe/concrete-ml -WORKDIR /project -COPY requirements.txt requirements.txt -#RUN python -m pip install torchvision==0.14.1 --no-deps -RUN python -m pip install -r requirements.txt -RUN python -m pip install requests - -COPY models/ models/ -COPY experiments/ experiments/ -COPY compile.py compile.py - -ENTRYPOINT python compile.py diff --git a/use_case_examples/deployment/cifar/Makefile b/use_case_examples/deployment/cifar/Makefile deleted file mode 100644 index 1bb0742ae3..0000000000 --- a/use_case_examples/deployment/cifar/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -run_example: three - -one: - @python compile_with_docker.py - -two: one - @python -m concrete.ml.deployment.deploy_to_docker --only-build - -three: two - @docker build --tag cifar_client -f Dockerfile.client . diff --git a/use_case_examples/deployment/cifar/README.md b/use_case_examples/deployment/cifar/README.md deleted file mode 100644 index 929d64193f..0000000000 --- a/use_case_examples/deployment/cifar/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Deployment - -In this folder we show how to deploy a Concrete ML model that classifies images from CIFAR-10, either through Docker or Amazon Web Services. -We use the model showcased in [CIFAR QAT training from scratch](../../cifar/cifar_brevitas_training/README.md). - -## Get started - -To run this example on AWS you will also need to have the AWS CLI properly setup on your system. -To do so please refer to [AWS documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html). -One can also run this example locally using Docker, or just by running the scripts locally. - -Deployment this model on your personal machine is not recommended as running a VGG in FHE is computationally intensive. It is recommended to run this on a `m6i.metal` instance from AWS. - -1. To compile your model you can use `compile.py`, or `compile_with_docker.py` to use Docker. This will compile the model to an FHE circuit and [serialize it](../../../docs/guides/client_server.md). This will result in a new folder called `./dev`. -1. Once that's done you can use the script provided in Concrete ML in `use_case_examples/deployment/server/`, either use `deploy_to_aws.py` or `deploy_to_docker.py` according to your need. - -- `python use_case_examples/deployment/server/deploy_to_docker.py` -- `python use_case_examples/deployment/server/deploy_to_aws.py --instance-type m6i.metal` - this will create and run a Docker container or an AWS EC2 instance. - -3. Once that's done you can launch the `build_docker_client_image.py` script to build a client Docker image. -1. You can then run the client by using the `client.sh` script. This will run the container in interactive mode. - To interact with the server you can launch the `client.py` script using `URL="" python client.py` where `` is the content of the `url.txt` file. - -And here it is you deployed a deep-learning model and ran an inference using Fully Homormophic Encryption. diff --git a/use_case_examples/deployment/cifar/client.py b/use_case_examples/deployment/cifar/client.py deleted file mode 100644 index a35d669bbb..0000000000 --- a/use_case_examples/deployment/cifar/client.py +++ /dev/null @@ -1,141 +0,0 @@ -"""Client script. - -This script does the following: - - Query crypto-parameters and pre/post-processing parameters (client.zip) - - Quantize the inputs using the parameters - - Encrypt data using the crypto-parameters - - Send the encrypted data to the server (async using grequests) - - Collect the data and decrypt it - - De-quantize the decrypted results -""" - -import io -import os -import sys -from pathlib import Path - -import grequests -import numpy -import requests -import torch -import torchvision -import torchvision.transforms as transforms - -from concrete.ml.deployment import FHEModelClient - -PORT = os.environ.get("PORT", "5000") -IP = os.environ.get("IP", "localhost") -URL = os.environ.get("URL", f"http://{IP}:{PORT}") -NUM_SAMPLES = int(os.environ.get("NUM_SAMPLES", 1)) -STATUS_OK = 200 - - -def main(): - # Load data - IMAGE_TRANSFORM = transforms.Compose( - [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - - try: - train_set = torchvision.datasets.CIFAR10( - root=".data/", - train=True, - download=False, - transform=IMAGE_TRANSFORM, - target_transform=None, - ) - except: - train_set = torchvision.datasets.CIFAR10( - root=".data/", - train=True, - download=True, - transform=IMAGE_TRANSFORM, - target_transform=None, - ) - - num_samples = 1000 - train_sub_set = torch.stack( - [train_set[index][0] for index in range(min(num_samples, len(train_set)))] - ) - - # Get the necessary data for the client - # client.zip - zip_response = requests.get(f"{URL}/get_client") - assert zip_response.status_code == STATUS_OK - with open("./client.zip", "wb") as file: - file.write(zip_response.content) - - # Get the data to infer - X = train_sub_set[:1] - - # Create the client - client = FHEModelClient(path_dir="./", key_dir="./keys") - - # The client first need to create the private and evaluation keys. - client.generate_private_and_evaluation_keys() - - # Get the serialized evaluation keys - serialized_evaluation_keys = client.get_serialized_evaluation_keys() - assert isinstance(serialized_evaluation_keys, bytes) - - # Evaluation keys can be quite large files but only have to be shared once with the server. - - # Check the size of the evaluation keys (in MB) - print(f"Evaluation keys size: {sys.getsizeof(serialized_evaluation_keys) / 1024 / 1024:.2f} MB") - - # Update all base64 queries encodings with UploadFile - response = requests.post( - f"{URL}/add_key", - files={"key": io.BytesIO(initial_bytes=serialized_evaluation_keys)}, - ) - assert response.status_code == STATUS_OK - uid = response.json()["uid"] - - inferences = [] - # Launch the queries - clear_input = X[[0], :].numpy() - print("Input shape:", clear_input.shape) - - assert isinstance(clear_input, numpy.ndarray) - print("Quantize/Encrypt") - encrypted_input = client.quantize_encrypt_serialize(clear_input) - assert isinstance(encrypted_input, bytes) - - print(f"Encrypted input size: {sys.getsizeof(encrypted_input) / 1024 / 1024:.2f} MB") - - print("Posting query") - inferences.append( - grequests.post( - f"{URL}/compute", - files={ - "model_input": io.BytesIO(encrypted_input), - }, - data={ - "uid": uid, - }, - ) - ) - - del encrypted_input - del serialized_evaluation_keys - - print("Posted!") - - # Unpack the results - decrypted_predictions = [] - for result in grequests.map(inferences): - if result is None: - raise ValueError( - "Result is None, probably because the server crashed due to lack of available memory." - ) - assert result.status_code == STATUS_OK - print("OK!") - - encrypted_result = result.content - decrypted_prediction = client.deserialize_decrypt_dequantize(encrypted_result)[0] - decrypted_predictions.append(decrypted_prediction) - print(decrypted_predictions) - - -if __name__ == "__main__": - main() diff --git a/use_case_examples/deployment/cifar/client.sh b/use_case_examples/deployment/cifar/client.sh deleted file mode 100644 index 6c7192d987..0000000000 --- a/use_case_examples/deployment/cifar/client.sh +++ /dev/null @@ -1,2 +0,0 @@ -#! /bin/env bash -docker run -it --network=host --entrypoint="/bin/bash" cml_client_cifar_10_8_bit diff --git a/use_case_examples/deployment/cifar/client_requirements.txt b/use_case_examples/deployment/cifar/client_requirements.txt deleted file mode 100644 index fc58e57cf6..0000000000 --- a/use_case_examples/deployment/cifar/client_requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -grequests -requests -tqdm diff --git a/use_case_examples/deployment/cifar/compile.py b/use_case_examples/deployment/cifar/compile.py deleted file mode 100644 index dc7579d41b..0000000000 --- a/use_case_examples/deployment/cifar/compile.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Load torch model, compiles it to FHE and exports it""" - -import sys -import time -from pathlib import Path - -import torch -import torchvision -import torchvision.transforms as transforms -from concrete.fhe import Configuration -from models import cnv_2w2a - -from concrete.ml.deployment import FHEModelDev -from concrete.ml.torch.compile import compile_brevitas_qat_model - - -def main(): - # Load model - # model = CNV(num_classes=10, weight_bit_width=2, act_bit_width=2, in_bit_width=3, in_ch=3) - # loaded = torch.load(Path(__file__).parent / "8_bit_model.pt") - # model.load_state_dict(loaded["model_state_dict"]) - - # Instantiate the model - model = cnv_2w2a(pre_trained=False) - model.eval() - # Load the saved parameters using the available checkpoint - checkpoint = torch.load( - Path(__file__).parent / "experiments/CNV_2W2A_2W2A_20221114_131345/checkpoints/best.tar", - map_location=torch.device("cpu"), - ) - model.load_state_dict(checkpoint["state_dict"], strict=False) - - IMAGE_TRANSFORM = transforms.Compose( - [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))] - ) - - # Load data - try: - train_set = torchvision.datasets.CIFAR10( - root=".data/", - train=True, - download=False, - transform=IMAGE_TRANSFORM, - target_transform=None, - ) - except: - train_set = torchvision.datasets.CIFAR10( - root=".data/", - train=True, - download=True, - transform=IMAGE_TRANSFORM, - target_transform=None, - ) - - num_samples = 1000 - train_sub_set = torch.stack( - [train_set[index][0] for index in range(min(num_samples, len(train_set)))] - ) - - compilation_onnx_path = "compilation_model.onnx" - print("Compiling the model ...") - start_compile = time.time() - - # Compile the quantized model - quantized_numpy_module = compile_brevitas_qat_model( - torch_model=model, - torch_inputset=train_sub_set, - p_error=0.05, - output_onnx_file=compilation_onnx_path, - n_bits=8, - ) - end_compile = time.time() - print(f"Compilation finished in {end_compile - start_compile:.2f} seconds") - - # Key generation - print("Generating keys ...") - start_keygen = time.time() - quantized_numpy_module.fhe_circuit.keygen() - end_keygen = time.time() - print(f"Keygen finished in {end_keygen - start_keygen:.2f} seconds") - - print("size_of_inputs", quantized_numpy_module.fhe_circuit.size_of_inputs) - print("bootstrap_keys", quantized_numpy_module.fhe_circuit.size_of_bootstrap_keys) - print("keyswitches", quantized_numpy_module.fhe_circuit.size_of_keyswitch_keys) - - dev = FHEModelDev(path_dir="./dev", model=quantized_numpy_module) - dev.save() - - -if __name__ == "__main__": - main() diff --git a/use_case_examples/deployment/cifar/compile_with_docker.py b/use_case_examples/deployment/cifar/compile_with_docker.py deleted file mode 100644 index 899a261d66..0000000000 --- a/use_case_examples/deployment/cifar/compile_with_docker.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import shutil -import subprocess -from pathlib import Path - - -def main(): - path_of_script = Path(__file__).parent.resolve() - files = [ - "models/__init__.py", - "models/cnv_2w2a.ini", - "models/common.py", - "models/model.py", - "models/tensor_norm.py", - "experiments/CNV_2W2A_2W2A_20221114_131345/checkpoints/best.tar", - ] - - # Copy files - for file_name in files: - source = Path(path_of_script / f"../../cifar/cifar_brevitas_training/{file_name}").resolve() - target = Path(path_of_script / file_name).resolve() - if not target.exists(): - print(f"{source} -> {target}") - target.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(src=source, dst=target) - - # Build image - os.chdir(path_of_script) - command = f'docker build --tag compile_cifar --file "{path_of_script}/Dockerfile.compile" . && \ -docker run --name compile_cifar compile_cifar && \ -docker cp compile_cifar:/project/dev . && \ -docker rm "$(docker ps -a --filter name=compile_cifar -q)"' - subprocess.check_output(command, shell=True) - - # Remove files - for file_name in files: - target = Path(path_of_script / file_name).resolve() - target.unlink() - - -if __name__ == "__main__": - main() diff --git a/use_case_examples/deployment/cifar/requirements.txt b/use_case_examples/deployment/cifar/requirements.txt deleted file mode 100644 index d5fa2d7c94..0000000000 --- a/use_case_examples/deployment/cifar/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -torchvision==0.14.1 -Pillow diff --git a/use_case_examples/deployment/cifar/url.txt b/use_case_examples/deployment/cifar/url.txt deleted file mode 100644 index 900c82c8dc..0000000000 --- a/use_case_examples/deployment/cifar/url.txt +++ /dev/null @@ -1 +0,0 @@ -http://13.38.23.143:5000 \ No newline at end of file diff --git a/use_case_examples/deployment/sentiment_analysis/Dockerfile.client b/use_case_examples/deployment/sentiment_analysis/Dockerfile.client deleted file mode 100644 index 7bce3a75ec..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/Dockerfile.client +++ /dev/null @@ -1,8 +0,0 @@ -FROM zamafhe/concrete-ml -WORKDIR /project -COPY client.py . -COPY utils.py . -COPY client_requirements.txt . -COPY ./hf_cache ./hf_cache -RUN python -m pip install -r client_requirements.txt -ENTRYPOINT /bin/bash diff --git a/use_case_examples/deployment/sentiment_analysis/Dockerfile.train b/use_case_examples/deployment/sentiment_analysis/Dockerfile.train deleted file mode 100644 index 931ff1e845..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/Dockerfile.train +++ /dev/null @@ -1,8 +0,0 @@ -FROM zamafhe/concrete-ml -WORKDIR /project -COPY ./Tweets.csv ./Tweets.csv -COPY ./train_requirements.txt ./train_requirements.txt -RUN python -m pip install -r train_requirements.txt -COPY ./train.py ./train.py -COPY ./utils.py ./utils.py -ENTRYPOINT python train.py diff --git a/use_case_examples/deployment/sentiment_analysis/Makefile b/use_case_examples/deployment/sentiment_analysis/Makefile deleted file mode 100644 index 1bb0742ae3..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -run_example: three - -one: - @python compile_with_docker.py - -two: one - @python -m concrete.ml.deployment.deploy_to_docker --only-build - -three: two - @docker build --tag cifar_client -f Dockerfile.client . diff --git a/use_case_examples/deployment/sentiment_analysis/README.md b/use_case_examples/deployment/sentiment_analysis/README.md deleted file mode 100644 index c7e33f1e63..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Deployment - -In this folder we show how to deploy a Concrete ML model that does sentiment analysis, either through Docker or Amazon Web Services. -This is based on the sentiment analysis use case example where a XGBoost model is trained on top of a Transformer model. - -## Get started - -To run this example on AWS you will also need to have the AWS CLI properly setup on your system. -To do so please refer to [AWS documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html). -One can also run this example locally using Docker, or just by running the scripts locally. - -1. To train your model you can use `train.py`, or `train_with_docker.sh` to use Docker (recommended). This operation might take some time. - This will train a model and [serialize the FHE circuit](../../../docs/guides/client_server.md). - This will result in a new folder called `./dev`. -1. Once that's done you can use the script provided in Concrete ML in `src/concrete/ml/deployment/`, either use `deploy_to_aws.py` or `deploy_to_docker.py` according to your need. - -- `python use_case_examples/deployment/server/deploy_to_docker.py` -- `python use_case_examples/deployment/server/deploy_to_aws.py` - this will create and run a Docker container or an AWS EC2 instance. - -3. Once that's done you can launch the `build_docker_client_image.sh` script to build a client Docker image. -1. You can then run the client by using the `client.sh` script. This will run the container in interactive mode. - To interact with the server you can launch the `client.py` script using `URL="" python client.py` where `` is the content of the `url.txt` file. - -And here it is you deployed a Concrete ML model and ran an inference using Fully Homormophic Encryption. diff --git a/use_case_examples/deployment/sentiment_analysis/build_docker_client_image.py b/use_case_examples/deployment/sentiment_analysis/build_docker_client_image.py deleted file mode 100644 index 2f2e0941f2..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/build_docker_client_image.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import subprocess -from pathlib import Path - - -def main(): - path_of_script = Path(__file__).parent.resolve() - os.environ["TRANSFORMERS_CACHE"] = str((path_of_script / "hf_cache").resolve()) - from transformers import AutoModelForSequenceClassification, AutoTokenizer - - # Load the tokenizer (converts text to tokens) - tokenizer = AutoTokenizer.from_pretrained("cardiffnlp/twitter-roberta-base-sentiment-latest") - - # Load the pre-trained model - transformer_model = AutoModelForSequenceClassification.from_pretrained( - "cardiffnlp/twitter-roberta-base-sentiment-latest" - ) - del tokenizer - del transformer_model - - # Build image - os.chdir(path_of_script) - command = f'docker build --tag cml_client_sentiment_analysis --file "{path_of_script}/Dockerfile.client" .' - print(command) - subprocess.check_output(command, shell=True) - - -if __name__ == "__main__": - main() diff --git a/use_case_examples/deployment/sentiment_analysis/client.py b/use_case_examples/deployment/sentiment_analysis/client.py deleted file mode 100644 index 0d2fd1697d..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/client.py +++ /dev/null @@ -1,122 +0,0 @@ -#! /bin/env python -"""Client script. - -This script does the following: - - Query crypto-parameters and pre/post-processing parameters - - Quantize the inputs using the parameters - - Encrypt data using the crypto-parameters - - Send the encrypted data to the server (async using grequests) - - Collect the data and decrypt it - - De-quantize the decrypted results -""" -import io -import os -import time - -os.environ["TRANSFORMERS_CACHE"] = "./hf_cache" -import os -import sys -import time - -import grequests -import numpy -import requests -from transformers import AutoModelForSequenceClassification, AutoTokenizer -from utility_functions import text_to_tensor - -from concrete.ml.deployment import FHEModelClient - -URL = os.environ.get("URL", f"http://localhost:5000") -STATUS_OK = 200 - -CLASS_INDEX_TO_NAME = {0: "negative", 1: "neutral", 2: "positive"} - - -def main(): - # Load the tokenizer (converts text to tokens) - tokenizer = AutoTokenizer.from_pretrained("cardiffnlp/twitter-roberta-base-sentiment-latest") - - # Load the pre-trained model - transformer_model = AutoModelForSequenceClassification.from_pretrained( - "cardiffnlp/twitter-roberta-base-sentiment-latest" - ) - # Get the necessary data for the client - # client.zip - zip_response = requests.get(f"{URL}/get_client") - assert zip_response.status_code == STATUS_OK - with open("./client.zip", "wb") as file: - file.write(zip_response.content) - - # Create the client - client = FHEModelClient(path_dir="./", key_dir="./keys") - - # The client first need to create the private and evaluation keys. - client.generate_private_and_evaluation_keys() - - # Get the serialized evaluation keys - serialized_evaluation_keys = client.get_serialized_evaluation_keys() - assert isinstance(serialized_evaluation_keys, bytes) - # Evaluation keys can be quite large files but only have to be shared once with the server. - # Check the size of the evaluation keys (in MB) - print(f"Evaluation keys size: {sys.getsizeof(serialized_evaluation_keys) / 1024 / 1024:.2f} MB") - - response = requests.post( - f"{URL}/add_key", - files={"key": io.BytesIO(initial_bytes=serialized_evaluation_keys)}, - ) - del serialized_evaluation_keys - assert response.status_code == STATUS_OK - uid = response.json()["uid"] - - while True: - inputs = input("Text to classify: ") - # Launch the queries - print("extracting feature vector") - clear_input = text_to_tensor( - [inputs], transformer_model=transformer_model, tokenizer=tokenizer, device="cpu" - ) - print("Input shape:", clear_input.shape) - - start = time.time() - assert isinstance(clear_input, numpy.ndarray) - print("Quantize/Encrypt") - encrypted_input = client.quantize_encrypt_serialize(clear_input) - assert isinstance(encrypted_input, bytes) - print(f"Encrypted input size: {sys.getsizeof(encrypted_input) / 1024 / 1024:.2f} MB") - - print("Posting query ...") - inferences = [ - grequests.post( - f"{URL}/compute", - files={ - "model_input": io.BytesIO(encrypted_input), - }, - data={ - "uid": uid, - }, - ) - ] - del encrypted_input - print("Posted!") - - # Unpack the results - result = grequests.map(inferences)[0] - if result is None: - raise ValueError("Result is None, probably due to a crash on the server side.") - assert result.status_code == STATUS_OK - print("OK!") - - encrypted_result = result.content - decrypted_prediction = client.deserialize_decrypt_dequantize(encrypted_result) - end = time.time() - - predicted_class = numpy.argmax(decrypted_prediction) - print( - f"This tweet is {CLASS_INDEX_TO_NAME[predicted_class]}!\n" - f"Probabilites={decrypted_prediction.tolist()}\n" - f"It took {end - start} seconds to run." - ) - - -if __name__ == "__main__": - main() diff --git a/use_case_examples/deployment/sentiment_analysis/client.sh b/use_case_examples/deployment/sentiment_analysis/client.sh deleted file mode 100644 index c8e372d191..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/client.sh +++ /dev/null @@ -1,2 +0,0 @@ -#! /bin/env bash -docker run -it --network=host --entrypoint="/bin/bash" cml_client_sentiment_analysis diff --git a/use_case_examples/deployment/sentiment_analysis/client_requirements.txt b/use_case_examples/deployment/sentiment_analysis/client_requirements.txt deleted file mode 100644 index ec1e735775..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/client_requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -grequests -requests -tqdm -transformers \ No newline at end of file diff --git a/use_case_examples/deployment/sentiment_analysis/download_data.sh b/use_case_examples/deployment/sentiment_analysis/download_data.sh deleted file mode 100755 index 1124de3db3..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/download_data.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -wget -O Tweets.csv https://huggingface.co/datasets/osanseviero/twitter-airline-sentiment/resolve/main/Tweets.csv diff --git a/use_case_examples/deployment/sentiment_analysis/requirements.txt b/use_case_examples/deployment/sentiment_analysis/requirements.txt deleted file mode 100644 index 8af277bc18..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -concrete-ml -jupyter -pandas -transformers diff --git a/use_case_examples/deployment/sentiment_analysis/train.py b/use_case_examples/deployment/sentiment_analysis/train.py deleted file mode 100644 index d396001d3a..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/train.py +++ /dev/null @@ -1,206 +0,0 @@ -"""Copy-pasted from use_case_examples/sentiment_analysis_with_transformer""" - -import os - -os.environ["TRANSFORMERS_CACHE"] = "./hf_cache" - -import time - -import numpy -import onnx -import pandas as pd -import torch -from sklearn.metrics import average_precision_score -from sklearn.model_selection import GridSearchCV, train_test_split -from transformers import AutoModelForSequenceClassification, AutoTokenizer -from utility_functions import text_to_tensor - -from concrete.ml.sklearn import XGBClassifier - - -def train(dev_folder="./dev"): - # Download the data-sets - if not os.path.isfile("Tweets.csv"): - raise ValueError( - "Please launch the `download_data.sh` script in order to get the data-sets." - ) - - train = pd.read_csv("Tweets.csv", index_col=0) - - text_X = train["text"] - y = train["airline_sentiment"] - y = y.replace(["negative", "neutral", "positive"], [0, 1, 2]) - - pos_ratio = y.value_counts()[2] / y.value_counts().sum() - neg_ratio = y.value_counts()[0] / y.value_counts().sum() - neutral_ratio = y.value_counts()[1] / y.value_counts().sum() - - print(f"Proportion of positive examples: {round(pos_ratio * 100, 2)}%") - print(f"Proportion of negative examples: {round(neg_ratio * 100, 2)}%") - print(f"Proportion of neutral examples: {round(neutral_ratio * 100, 2)}%") - - # Split in train test - text_X_train, text_X_test, y_train, y_test = train_test_split( - text_X, y, test_size=0.1, random_state=42 - ) - - # # ### 2. A transformer approach to text representation - # # - # # [**Transformers**](https://en.wikipedia.org/wiki/Transformer_(machine_learning_model)) are neural networks that are often trained to predict the next words to appear in a text (this is commonly called self-supervised learning). - # # - # # They are powerful tools for all kind of Natural Language Processing tasks but supporting a transformer model in FHE might not always be ideal as they are quite big models. However, we can still leverage their hidden representation for any text and feed it to a more FHE-friendly machine learning model (in this notebook we will use XGBoost) for classification. - # # - # # Here we will use the transformer model from the amazing [**Huggingface**](https://huggingface.co/) repository. - - # Add MPS (for macOS with Apple Silicon or AMD GPUs) support when error is fixed. For now, we - # observe a decrease in torch's top1 accuracy when using MPS devices - # FIXME: https://github.com/zama-ai/concrete-ml-internal/issues/3953 - device = "cuda" if torch.cuda.is_available() else "cpu" - - # # Load the tokenizer (converts text to tokens) - tokenizer = AutoTokenizer.from_pretrained("cardiffnlp/twitter-roberta-base-sentiment-latest") - - # # Load the pre-trained model - transformer_model = AutoModelForSequenceClassification.from_pretrained( - "cardiffnlp/twitter-roberta-base-sentiment-latest" - ) - - # Vectorize the text using the transformer - list_text_X_train = text_X_train.tolist() - list_text_X_test = text_X_test.tolist() - - X_train_transformer = text_to_tensor(list_text_X_train, transformer_model, tokenizer, device) - X_test_transformer = text_to_tensor(list_text_X_test, transformer_model, tokenizer, device) - # Now we have a representation for each tweet, we can train a model on these. - - # Build our model - model = XGBClassifier() - - # A gridsearch to find the best parameters - parameters = { - "n_bits": [2, 3], - "max_depth": [1], - "n_estimators": [10, 30, 50], - "n_jobs": [-1], - } - - grid_search = GridSearchCV(model, parameters, cv=3, n_jobs=1, scoring="accuracy") - grid_search.fit(X_train_transformer, y_train) - - # Check the accuracy of the best model - print(f"Best score: {grid_search.best_score_}") - - # Check best hyper-parameters - print(f"Best parameters: {grid_search.best_params_}") - - # Extract best model - best_model = grid_search.best_estimator_ - assert isinstance(best_model, XGBClassifier) - - # Compute the metrics for each class - - y_proba = best_model.predict_proba(X_test_transformer) - - # Compute the accuracy - y_pred = numpy.argmax(y_proba, axis=1) - accuracy_transformer_xgboost = numpy.mean(y_pred == y_test) - print(f"Accuracy: {accuracy_transformer_xgboost:.4f}") - - y_pred_positive = y_proba[:, 2] - y_pred_negative = y_proba[:, 0] - y_pred_neutral = y_proba[:, 1] - - ap_positive_transformer_xgboost = average_precision_score((y_test == 2), y_pred_positive) - ap_negative_transformer_xgboost = average_precision_score((y_test == 0), y_pred_negative) - ap_neutral_transformer_xgboost = average_precision_score((y_test == 1), y_pred_neutral) - - print(f"Average precision score for positive class: " f"{ap_positive_transformer_xgboost:.4f}") - print(f"Average precision score for negative class: " f"{ap_negative_transformer_xgboost:.4f}") - print(f"Average precision score for neutral class: " f"{ap_neutral_transformer_xgboost:.4f}") - - # Our FHE-friendly XGBoost model does 38% better than the XGBoost model built over TF-IDF representation of the text. Note that here we are still not using FHE and only evaluating the model. - # Interestingly, using XGBoost over the transformer representation of the text matches the performance of the transformer model alone. - - # Get probabilities predictions in clear - y_pred_test = best_model.predict_proba(X_test_transformer) - - # See what are the top predictions based on the probabilities in y_pred_test - print("5 most positive tweets (class 2):") - for i in range(5): - print(text_X_test.iloc[y_pred_test[:, 2].argsort()[-1 - i]]) - - print("-" * 100) - - print("5 most negative tweets (class 0):") - for i in range(5): - print(text_X_test.iloc[y_pred_test[:, 0].argsort()[-1 - i]]) - - # Now we can see where the model is wrong - y_pred_test_0 = y_pred_test[y_test == 0] - text_X_test_0 = text_X_test[y_test == 0] - - print("5 most positive (predicted) tweets that are actually negative (ground truth class 0):") - for i in range(5): - print(text_X_test_0.iloc[y_pred_test_0[:, 2].argsort()[-1 - i]]) - - print("-" * 100) - - y_pred_test_2 = y_pred_test[y_test == 2] - text_X_test_2 = text_X_test[y_test == 2] - print("5 most negative (predicted) tweets that are actually positive (ground truth class 2):") - for i in range(5): - print(text_X_test_2.iloc[y_pred_test_2[:, 0].argsort()[-1 - i]]) - - # Interestingly, these misclassifications are not obvious and some actually look rather like mislabeled. Also, it seems that the model is having a hard time to find ironic tweets. - # - # Now we have our model trained which has some great accuracy. We can have it predict over the encrypted representation. - - # ### Sentiment Analysis of the Tweet with Fully Homomorphic Encryption - # - # Now that we have our model ready for FHE inference and our data ready for encryption we can use the model in a privacy preserving manner with FHE. - - # Compile the model to get the FHE inference engine - # (this may take a few minutes depending on the selected model) - start = time.perf_counter() - best_model.compile(X_train_transformer) - end = time.perf_counter() - print(f"Compilation time: {end - start:.4f} seconds") - - # Write a custom example and predict in FHE - tested_tweet = ["AirFrance is awesome, almost as much as Zama!"] - X_tested_tweet = text_to_tensor(tested_tweet, transformer_model, tokenizer, device) - clear_proba = best_model.predict_proba(X_tested_tweet) - - # Now we predict with FHE over a single tweet and print the time it takes - start = time.perf_counter() - decrypted_proba = best_model.predict_proba(X_tested_tweet, fhe="execute") - end = time.perf_counter() - fhe_exec_time = end - start - print(f"FHE inference time: {fhe_exec_time:.4f} seconds") - - print(f"Probabilities from the FHE inference: {decrypted_proba}") - print(f"Probabilities from the clear model: {clear_proba}") - - # Export the final model such that we can reuse it in a client/server environment - - # Export the model to ONNX - onnx.save(best_model.onnx_model_, "server_model.onnx") # pylint: disable=protected-access - - # Export some data to be used for compilation - X_train_numpy = X_train_transformer[:100] - - # Merge the two arrays in a pandas dataframe - X_test_numpy_df = pd.DataFrame(X_train_numpy) - - # to csv - X_test_numpy_df.to_csv("samples_for_compilation.csv") - - # Save the model to be pushed to a server later - from concrete.ml.deployment import FHEModelDev - - fhe_api = FHEModelDev(dev_folder, best_model) - fhe_api.save() - - -if __name__ == "__main__": - train() diff --git a/use_case_examples/deployment/sentiment_analysis/train_requirements.txt b/use_case_examples/deployment/sentiment_analysis/train_requirements.txt deleted file mode 100644 index 747b7aa97a..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/train_requirements.txt +++ /dev/null @@ -1 +0,0 @@ -transformers \ No newline at end of file diff --git a/use_case_examples/deployment/sentiment_analysis/train_with_docker.sh b/use_case_examples/deployment/sentiment_analysis/train_with_docker.sh deleted file mode 100755 index 0f3e2354ed..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/train_with_docker.sh +++ /dev/null @@ -1,9 +0,0 @@ -#! /bin/env bash - -bash download_data.sh && \ - docker build --tag train_sentiment_analysis --file Dockerfile.train . && \ - docker run --name train_sentiment_analysis_container train_sentiment_analysis && \ - docker cp train_sentiment_analysis_container:/project/dev . && - docker cp train_sentiment_analysis_container:/project/hf_cache . - -docker rm "$(docker ps -a --filter name=train_sentiment_analysis_container -q)" diff --git a/use_case_examples/deployment/sentiment_analysis/utility_functions.py b/use_case_examples/deployment/sentiment_analysis/utility_functions.py deleted file mode 100644 index 017159bd93..0000000000 --- a/use_case_examples/deployment/sentiment_analysis/utility_functions.py +++ /dev/null @@ -1,34 +0,0 @@ -import numpy -import tqdm -from transformers import AutoModelForSequenceClassification, AutoTokenizer - - -# Function that transforms a list of texts to their representation -# learned by the transformer. -def text_to_tensor( - list_text_X_train: list, - transformer_model: AutoModelForSequenceClassification, - tokenizer: AutoTokenizer, - device: str, -) -> numpy.ndarray: - # Tokenize each text in the list one by one - tokenized_text_X_train_split = [] - for text_x_train in list_text_X_train: - tokenized_text_X_train_split.append(tokenizer.encode(text_x_train, return_tensors="pt")) - - # Send the model to the device - transformer_model = transformer_model.to(device) - output_hidden_states_list = [] - - for tokenized_x in tqdm.tqdm(tokenized_text_X_train_split): - # Pass the tokens through the transformer model and get the hidden states - # Only keep the last hidden layer state for now - output_hidden_states = transformer_model(tokenized_x.to(device), output_hidden_states=True)[ - 1 - ][-1] - # Average over the tokens axis to get a representation at the text level. - output_hidden_states = output_hidden_states.mean(dim=1) - output_hidden_states = output_hidden_states.detach().cpu().numpy() - output_hidden_states_list.append(output_hidden_states) - - return numpy.concatenate(output_hidden_states_list, axis=0) diff --git a/use_case_examples/deployment/server/Dockerfile.server b/use_case_examples/deployment/server/Dockerfile.server deleted file mode 100644 index b422ad71eb..0000000000 --- a/use_case_examples/deployment/server/Dockerfile.server +++ /dev/null @@ -1,8 +0,0 @@ -FROM zamafhe/concrete-ml:latest -WORKDIR /project -COPY dev dev -COPY server_requirements.txt server_requirements.txt -COPY server.py server.py -RUN python -m pip install -r ./server_requirements.txt -EXPOSE 5000 -ENTRYPOINT python server.py diff --git a/use_case_examples/deployment/server/README.md b/use_case_examples/deployment/server/README.md deleted file mode 100644 index 17beaa694b..0000000000 --- a/use_case_examples/deployment/server/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Deployment - -In this folder we explain how to deploy Concrete ML models. -We show-case how to do this on 3 examples: - -- Breast cancer classification using a simple XGBoost model -- Sentiment analysis by running a XGBoost model on top of a Transformer model -- CIFAR-10 classification using a VGG model split in two parts. - -You can run these example locally using Docker, or on AWS if you have your credentials set up. - -For all of them the workflow is the same: -0\. Optional: Train the model - -1. Compile the model to an FHE circuit -1. Deploy to AWS, Docker or localhost -1. Run the inference using the client (locally or in Docker) - -The script to deploy the model compiled to an FHE circuit is the same for all. The main difference between them is the client. Each use-case needs its own client. diff --git a/use_case_examples/deployment/server/deploy_to_aws.py b/use_case_examples/deployment/server/deploy_to_aws.py deleted file mode 100644 index fee46a4721..0000000000 --- a/use_case_examples/deployment/server/deploy_to_aws.py +++ /dev/null @@ -1,528 +0,0 @@ -"""Methods to deploy a client/server to AWS. - -It takes as input a folder with: - - client.zip - - server.zip - - processing.json - -It spawns a AWS EC2 instance with proper security groups. -Then SSHs to it to rsync the files and update Python dependencies. -It then launches the server. -""" - -import argparse -import json -import subprocess -import time -import uuid -import zipfile -from contextlib import closing -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, Optional - -import boto3 - -from .utils_server import filter_logs, wait_for_connection_to_be_available - -DATE_FORMAT: str = "%Y_%m_%d_%H_%M_%S" -# More up to date public Concrete ML AWS AMI -DEFAULT_CML_AMI_ID: str = "ami-0d7427e883fa00ff3" - - -class AWSInstance: - """AWSInstance. - - Context manager for AWS instance that supports ssh and http over one port. - """ - - instance_metadata: Dict[str, Any] - - def __init__( - self, - instance_type: str = "c5.large", - open_port=5000, - instance_name: Optional[str] = None, - verbose: bool = False, - terminate_on_shutdown: bool = True, - region_name: Optional[str] = None, - ami_id: str = DEFAULT_CML_AMI_ID, - ): - metadata = create_instance( - instance_type=instance_type, - open_port=open_port, - instance_name=instance_name, - verbose=verbose, - region_name=region_name, - ami_id=ami_id, - ) - self.instance_metadata = metadata - self.terminate_on_shutdown = terminate_on_shutdown - self.region_name = region_name - - def __enter__( - self, - ): - """Return instance_metadata and streams. - - Returns: - Dict[str, Any] - - ip - - private_key - - instance_id - - key_path - - ip_address - """ - return self.instance_metadata - - def __exit__(self, exc_type, exc_value, exc_traceback): - """Terminates the instance. - - Arguments: - exc_type: exception type - exc_value: exception value - exc_traceback: exception traceback - """ - if self.terminate_on_shutdown: - terminate_instance(self.instance_metadata["instance_id"], region_name=self.region_name) - # We need to wait for instance termination to delete the security group - wait_instance_termination( - self.instance_metadata["instance_id"], region_name=self.region_name - ) - delete_security_group( - self.instance_metadata["security_group_id"], region_name=self.region_name - ) - - -def create_instance( - instance_type: str = "c5.large", - open_port=5000, - instance_name: Optional[str] = None, - verbose: bool = False, - region_name: Optional[str] = None, - ami_id=DEFAULT_CML_AMI_ID, -) -> Dict[str, Any]: - """Create a EC2 instance. - - Arguments: - instance_type (str): the type of AWS EC2 instance. - open_port (int): the port to open. - instance_name (Optional[str]): the name to use for AWS created objects - verbose (bool): show logs or not - region_name (Optional[str]): AWS region - ami_id (str): AMI to use - - Returns: - Dict[str, Any]: some information about the newly created instance. - - ip - - private_key - - instance_id - - key_path - - ip_address - - port - """ - open_port = int(open_port) - - # Create client/resource objects - with closing(boto3.client("ec2", region_name=region_name)) as client: - resources = boto3.resource("ec2", region_name=region_name) - str_now = datetime.now().strftime(DATE_FORMAT) - name = ( - f"deploy-cml-{str_now}-{uuid.uuid4()}" - if instance_name is None - else f"{instance_name}-{str_now}" - ) - - # Get VPC - vpc_id: str = client.describe_vpcs().get("Vpcs", [{}])[0].get("VpcId", "") - # OPTION 1: get fist vpc available - if vpc_id: - vpc = resources.Vpc(vpc_id) - # OPTION 2: create VPC (not possible if too many VPCs) - else: # pragma:no cover - vpc = resources.create_vpc(CidrBlock="0.0.0.0/0") - vpc.wait_until_available() - - # Get subnet - subnets = list(vpc.subnets.all()) - # OPTION 1: create subnet - if not subnets: # pragma: no cover - subnet = vpc.create_subnet(CidrBlock=vpc.cidr_block) - # OPTION 2: get first subnet - else: - subnet = subnets[0] - - # Create security group - security_group_id = client.create_security_group( - GroupName=name, Description=f"Deploy Concrete ML {str_now}", VpcId=vpc_id - )["GroupId"] - if verbose: - print(f"Security Group Created {security_group_id} in vpc {vpc_id}.") - - client.authorize_security_group_ingress( - GroupId=security_group_id, - IpPermissions=[ - # Server port - { - "IpProtocol": "tcp", - "FromPort": open_port, - "ToPort": open_port, - "IpRanges": [{"CidrIp": "0.0.0.0/0"}], - }, - # SSH port - { - "IpProtocol": "tcp", - "FromPort": 22, - "ToPort": 22, - "IpRanges": [{"CidrIp": "0.0.0.0/0"}], - }, - ], - ) - - # Create key-pair - keypair_name = f"{name}-keypair" - private_key: str = client.create_key_pair(KeyName=keypair_name)["KeyMaterial"] - - # Keep the key if we want to ssh to check what happened on the instance - key_folder = (Path(__file__).parent / "ssh_keys").resolve() - key_folder.mkdir(exist_ok=True) - key_path = key_folder / f"{keypair_name}.pem" - with open(key_path, "w", encoding="utf-8") as file: - file.write(private_key) - key_path.chmod(0o400) - - # Create instance - instances = resources.create_instances( - # Concrete ML official AMI ID to make sure to have everything needed - ImageId=ami_id, - InstanceType=instance_type, # Instance type - DryRun=False, - InstanceInitiatedShutdownBehavior="terminate", - # Associate keypair to instance - KeyName=keypair_name, - # Some tags - TagSpecifications=[ - { - "ResourceType": "instance", - "Tags": [ - {"Key": "Repository", "Value": "concrete-ml"}, - {"Key": "Name", "Value": name}, - ], - }, - ], - # Number of instances - MaxCount=1, - MinCount=1, - NetworkInterfaces=[ - { - "AssociatePublicIpAddress": True, - "DeviceIndex": 0, - "SubnetId": subnet.id, - "Groups": [security_group_id], - } - ], - ) - - instance = instances[0] - instance.terminate_on_exception = False - - before_time = time.time() - instance.wait_until_running() - instance.reload() # Needed to update information like public_ip_address - if verbose: - print(f"Instance took {time.time() - before_time} seconds to start running") - - # Get information about instance - ip_address: str = instance.public_ip_address - assert ip_address is not None - - metadata: Dict[str, Any] = { - "ip": ip_address, - "private_key": private_key, - "instance_id": instance.id, - "key_path": key_path, - "ip_address": ip_address, - "port": open_port, - "instance_name": name, - "security_group_id": security_group_id, - } - - return metadata - - -def deploy_to_aws( - instance_metadata: Dict[str, Any], - path_to_model: Path, - number_of_ssh_retries: int = -1, - wait_bar: bool = False, - verbose: bool = False, -): - """Deploy a model to a EC2 AWS instance. - - Arguments: - instance_metadata (Dict[str, Any]): the metadata of AWS EC2 instance - created using AWSInstance or create_instance - path_to_model (Path): the path to the serialized model - number_of_ssh_retries (int): the number of ssh retries (-1 is no limit) - wait_bar (bool): whether to show a wait bar when waiting for ssh connection to be available - verbose (bool): whether to show a logs - - Returns: - instance_metadata (Dict[str, Any]) - - Raises: - RuntimeError: if launching the server crashed - """ - - port = instance_metadata["port"] - ip_address: str = instance_metadata["ip_address"] - key_path: Path = instance_metadata["key_path"] - instance_metadata["URL"] = f"http://{ip_address}:{port}" - hostname = "ubuntu" - - with open("connect_to_instance.sh", "w", encoding="utf-8") as file: - file.write( - f"""#! /bin/env bash -ssh -i {key_path.resolve()} {hostname}@{ip_address}""" - ) - - with open("terminate_instance.sh", "w", encoding="utf-8") as file: - file.write( - f"""#! /bin/env bash -aws ec2 terminate-instances --instance-ids {instance_metadata['instance_id']} -aws ec2 delete-security-group --group-id {instance_metadata['security_group_id']}""" - ) - - if verbose: - print("Waiting for SSH connection to be available...") - - # Connect to instance - wait_for_connection_to_be_available( - hostname=hostname, - ip_address=ip_address, - path_to_private_key=key_path, - timeout=1, - max_retries=number_of_ssh_retries, - wait_bar=wait_bar, - ) - - if verbose: - print("SSH connection available.") - - path_to_server_file = Path(__file__).parent / "server.py" - path_to_server_requirements = Path(__file__).parent / "server_requirements.txt" - - if verbose: - print("upload files...") - - # Rsync needed files - subprocess.check_output( - f"rsync -Pav -e 'ssh -i {key_path.resolve()} " - '-o "StrictHostKeyChecking=no" -o " IdentitiesOnly=yes"\' ' - f"{path_to_model.resolve()} {path_to_server_file.resolve()} " - f"{path_to_server_requirements.resolve()} {hostname}@{ip_address}:~", - shell=True, - ) - - if verbose: - print("upload finished.") - - # Load versions for checking - with zipfile.ZipFile(path_to_model.resolve().joinpath("client.zip")) as client_zip: - with client_zip.open("versions.json", mode="r") as file: - versions = json.load(file) - - python_version = ".".join(versions["python"].split(".")[0:2]) - concrete_python_version = versions["concrete-python"] - concrete_ml_version = versions["concrete-ml"] - - # Launch commands - commands = [ - # Needed because of the way the AMI is setup - f"sudo chmod -R 777 /home/{hostname}/venv", - f"sudo apt install -y python{python_version} python{python_version}-distutils make cmake", - f"virtualenv deployment_venv --python=python{python_version}", - # The venv is not activated by default - "source deployment_venv/bin/activate", - # Install server requirements - "python -m pip install -r server_requirements.txt", - # We need to relax the constraint on the version for internal testing - f"python -m pip install concrete-ml=={concrete_ml_version}" - " || python -m pip install concrete-ml", - # We still need to force concrete-python version to be exactly the same as the file - f"python -m pip install concrete-python=={concrete_python_version} || :", - # Launch server - f'PORT={port} PATH_TO_MODEL="./{path_to_model.name}" python ./server.py', - ] - - # + f"-o RemoteCommand=\"tmux new -A -s {instance_metadata['instance_name']}\" " - # Join commands - ssh_command = ( - f"ssh -i {key_path.resolve()} " - + "-o StrictHostKeyChecking=no " - + "-o IdentitiesOnly=yes " - + "-o RequestTTY=yes " - + f"{hostname}@{ip_address} " - ) - launch_command = ( - ssh_command - + f'"tmux new-session -d -s {instance_metadata["instance_name"]} ' - + "'" - + " && ".join(commands) - + " || sleep 10" - + "'" - + '"' - ) - monitoring_command = ( - ssh_command + f"tmux capture-pane -pt {instance_metadata['instance_name']}:0.0" - ) - - # Launch - subprocess.check_output(launch_command, shell=True, stderr=subprocess.STDOUT) - - last_tmux_logs = "" - - while True: - try: - tmux_logs = subprocess.check_output( - monitoring_command, - shell=True, - text=True, - encoding="utf-8", - stderr=subprocess.DEVNULL, - ) - except subprocess.CalledProcessError as exception: # pragma: no cover - raise RuntimeError( - "Something crashed when launching the server.\n" f" Last logs:\n{last_tmux_logs}" - ) from exception - - if any( - error_message in tmux_logs - for error_message in ["can't find session:", "no server running on"] - ): # pragma: no cover - raise RuntimeError( - "Something crashed when launching the server.\n" f" Last logs:\n{last_tmux_logs}" - ) - - if verbose: - # This could be improved - to_print = filter_logs(current_logs=tmux_logs, previous_logs=last_tmux_logs).strip() - if to_print: - print(to_print) - - # Monitor and return correct - if "0.0.0.0" in tmux_logs: - break - - last_tmux_logs = tmux_logs - time.sleep(1) # Wait one second - return instance_metadata - - -def wait_instance_termination(instance_id: str, region_name: Optional[str] = None): - """Wait for AWS EC2 instance termination. - - Arguments: - instance_id (str): the id of the AWS EC2 instance to terminate. - region_name (Optional[str]): AWS region (Optional) - """ - with closing(boto3.client("ec2", region_name=region_name)) as client: - waiter = client.get_waiter("instance_terminated") - waiter.wait(InstanceIds=[instance_id]) - - -def terminate_instance(instance_id: str, region_name: Optional[str] = None): - """Terminate a AWS EC2 instance. - - Arguments: - instance_id (str): the id of the AWS EC2 instance to terminate. - region_name (Optional[str]): AWS region (Optional) - """ - with closing(boto3.client("ec2", region_name=region_name)) as client: - client.terminate_instances(InstanceIds=[instance_id]) - - -def delete_security_group(security_group_id: str, region_name: Optional[str] = None): - """Terminate a AWS EC2 instance. - - Arguments: - security_group_id (str): the id of the AWS EC2 instance to terminate. - region_name (Optional[str]): AWS region (Optional) - """ - with closing(boto3.client("ec2", region_name=region_name)) as client: - client.delete_security_group(GroupId=security_group_id) - - -def main( - path_to_model: Path, - port: int = 5000, - instance_type: str = "c5.large", - instance_name: Optional[str] = None, - verbose: bool = False, - wait_bar: bool = False, - terminate_on_shutdown: bool = True, -): # pragma: no cover - """Deploy a model. - - Arguments: - path_to_model (Path): path to serialized model to serve. - port (int): port to use. - instance_type (str): type of AWS EC2 instance to use. - instance_name (Optional[str]): the name to use for AWS created objects - verbose (bool): show logs or not - wait_bar (bool): show progress bar when waiting for ssh connection - terminate_on_shutdown (bool): terminate instance when script is over - """ - - with AWSInstance( - instance_type=instance_type, - open_port=port, - instance_name=instance_name, - verbose=verbose, - terminate_on_shutdown=terminate_on_shutdown, - ) as instance_metadata: - instance_metadata = deploy_to_aws( - instance_metadata=instance_metadata, - number_of_ssh_retries=-1, - path_to_model=path_to_model, - verbose=verbose, - wait_bar=wait_bar, - ) - url = f"http://{instance_metadata['ip_address']}:{port}" - print(url + "\r\n", end="", sep="") - with open("url.txt", mode="w", encoding="utf-8") as file: - file.write(url) - - print(f"Server running at {url} .\nNow waiting indefinitely.\r\n", end="", sep="") - - while True: - time.sleep(1) - - -if __name__ == "__main__": # pragma: no cover - parser = argparse.ArgumentParser() - parser.add_argument("--path-to-model", dest="path_to_model", type=Path, default=Path("./dev")) - parser.add_argument("--port", dest="port", type=str, default="5000") - parser.add_argument("--instance-type", dest="instance_type", type=str, default="c5.large") - parser.add_argument("--instance-name", dest="instance_name", type=str, default="cml-deploy") - parser.add_argument("--verbose", dest="verbose", type=lambda elt: bool(int(elt)), default=False) - parser.add_argument( - "--terminate-on-shutdown", - dest="terminate_on_shutdown", - type=lambda elt: bool(int(elt)), - default=True, - ) - parser.add_argument( - "--wait-bar", dest="wait_bar", type=lambda elt: bool(int(elt)), default=False - ) - args = parser.parse_args() - - main( - path_to_model=args.path_to_model, - port=args.port, - instance_type=args.instance_type, - instance_name=args.instance_name, - verbose=args.verbose, - wait_bar=args.wait_bar, - terminate_on_shutdown=args.terminate_on_shutdown, - ) diff --git a/use_case_examples/deployment/server/deploy_to_docker.py b/use_case_examples/deployment/server/deploy_to_docker.py deleted file mode 100644 index 881a3b9bcd..0000000000 --- a/use_case_examples/deployment/server/deploy_to_docker.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Methods to deploy a server using Docker. - -It takes as input a folder with: - - client.zip - - server.zip - - processing.json - -It builds a Docker image and spawns a Docker container that runs the server. - -This module is untested as it would require to first build the release Docker image. -FIXME: https://github.com/zama-ai/concrete-ml-internal/issues/3347 -""" - -import argparse -import os -import shutil -import subprocess -import sys -from pathlib import Path -from tempfile import TemporaryDirectory - -DATE_FORMAT: str = "%Y_%m_%d_%H_%M_%S" - - -def delete_image(image_name: str): - """Delete a Docker image. - - Arguments: - image_name (str): to name of the image to delete. - """ - to_delete = subprocess.check_output( - f"docker ps -a --filter name={image_name} -q", shell=True - ).decode("utf-8") - if to_delete: - subprocess.check_output(f"docker rmi {to_delete}", shell=True) - - -def stop_container(image_name: str): - """Kill all containers that use a given image. - - Arguments: - image_name (str): name of Docker image for which to stop Docker containers. - """ - to_delete = subprocess.check_output( - f"docker ps -q --filter ancestor={image_name}", shell=True - ).decode("utf-8") - if to_delete: - subprocess.check_output(f"docker kill {to_delete}", shell=True) - - -def build_docker_image(path_to_model: Path, image_name: str): - """Build server Docker image. - - Arguments: - path_to_model (Path): path to serialized model to serve. - image_name (str): name to give to the image. - """ - delete_image(image_name) - - path_of_script = Path(__file__).parent.resolve() - - cwd = os.getcwd() - with TemporaryDirectory() as directory: - temp_dir = Path(directory) - - files = ["server.py", "server_requirements.txt"] - # Copy files - for file_name in files: - source = path_of_script / file_name - target = temp_dir / file_name - shutil.copyfile(src=source, dst=target) - shutil.copytree(path_to_model, temp_dir / "dev") - - # Build image - os.chdir(temp_dir) - command = ( - f'docker build --tag {image_name}:latest --file "{path_of_script}/Dockerfile.server" .' - ) - subprocess.check_output(command, shell=True) - os.chdir(cwd) - - -def main(path_to_model: Path, image_name: str): - """Deploy function. - - - Builds Docker image. - - Runs Docker server. - - Stop container and delete image. - - Arguments: - path_to_model (Path): path to model to server - image_name (str): name of the Docker image - """ - - build_docker_image(path_to_model, image_name) - - if args.only_build: - return - - # Run newly created Docker server - try: - with open("./url.txt", mode="w", encoding="utf-8") as file: - file.write("http://localhost:5000") - subprocess.check_output(f"docker run -p 5000:5000 {image_name}", shell=True) - except KeyboardInterrupt: - message = "Terminate container? (y/n) " - shutdown_instance = input(message).lower() - while shutdown_instance not in {"no", "n", "yes", "y"}: - shutdown_instance = input(message).lower() - if shutdown_instance in {"y", "yes"}: - stop_container(image_name=image_name) - delete_image(image_name=image_name) - sys.exit(0) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--path-to-model", dest="path_to_model", type=Path, default=Path("./dev")) - parser.add_argument("--image-name", dest="image_name", type=Path, default="server") - parser.add_argument("--only-build", dest="only_build", action="store_true") - args = parser.parse_args() - main(path_to_model=args.path_to_model, image_name=args.image_name) diff --git a/use_case_examples/deployment/server/server.py b/use_case_examples/deployment/server/server.py deleted file mode 100644 index ab98cedf9a..0000000000 --- a/use_case_examples/deployment/server/server.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Deployment server. - -Routes: - - Get client.zip - - Add a key - - Compute -""" - -import io -import os -import uuid -from pathlib import Path -from typing import Dict - -import uvicorn -from fastapi import FastAPI, Form, HTTPException, UploadFile -from fastapi.responses import FileResponse, StreamingResponse - -# No relative import here because when not used in the package itself -from concrete.ml.deployment import FHEModelServer - -if __name__ == "__main__": - app = FastAPI(debug=False) - - FILE_FOLDER = Path(__file__).parent - - KEY_PATH = Path(os.environ.get("KEY_PATH", FILE_FOLDER / Path("server_keys"))) - CLIENT_SERVER_PATH = Path(os.environ.get("PATH_TO_MODEL", FILE_FOLDER / Path("dev"))) - PORT = os.environ.get("PORT", "5000") - - fhe = FHEModelServer(str(CLIENT_SERVER_PATH.resolve())) - - KEYS: Dict[str, bytes] = {} - - PATH_TO_CLIENT = (CLIENT_SERVER_PATH / "client.zip").resolve() - PATH_TO_SERVER = (CLIENT_SERVER_PATH / "server.zip").resolve() - - assert PATH_TO_CLIENT.exists() - assert PATH_TO_SERVER.exists() - - @app.get("/get_client") - def get_client(): - """Get client. - - Returns: - FileResponse: client.zip - - Raises: - HTTPException: if the file can't be find locally - """ - path_to_client = (CLIENT_SERVER_PATH / "client.zip").resolve() - if not path_to_client.exists(): - raise HTTPException(status_code=500, detail="Could not find client.") - return FileResponse(path_to_client, media_type="application/zip") - - @app.post("/add_key") - async def add_key(key: UploadFile): - """Add public key. - - Arguments: - key (UploadFile): public key - - Returns: - Dict[str, str] - - uid: uid a personal uid - """ - uid = str(uuid.uuid4()) - KEYS[uid] = await key.read() - return {"uid": uid} - - @app.post("/compute") - async def compute(model_input: UploadFile, uid: str = Form()): # noqa: B008 - """Compute the circuit over encrypted input. - - Arguments: - model_input (UploadFile): input of the circuit - uid (str): uid of the public key to use - - Returns: - StreamingResponse: the result of the circuit - """ - key = KEYS[uid] - encrypted_results = fhe.run( - serialized_encrypted_quantized_data=await model_input.read(), - serialized_evaluation_keys=key, - ) - return StreamingResponse( - io.BytesIO(encrypted_results), - ) - - uvicorn.run(app, host="0.0.0.0", port=int(PORT)) diff --git a/use_case_examples/deployment/server/server_requirements.txt b/use_case_examples/deployment/server/server_requirements.txt deleted file mode 100644 index 1363737a1a..0000000000 --- a/use_case_examples/deployment/server/server_requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -fastapi -uvicorn -python-multipart diff --git a/use_case_examples/deployment/server/utils_server.py b/use_case_examples/deployment/server/utils_server.py deleted file mode 100644 index 30d2410290..0000000000 --- a/use_case_examples/deployment/server/utils_server.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Utils. - -- Check if connection possible -- Wait for connection to be available (with timeout) -""" - -import subprocess -import time -from pathlib import Path - -from tqdm import tqdm - - -def filter_logs(previous_logs: str, current_logs: str) -> str: - """Filter logs based on previous logs. - - Arguments: - previous_logs (str): previous logs - current_logs (str): current logs - - Returns: - str: filtered logs - """ - current_splitted = current_logs.split("\n") - previous_splitted = set(previous_logs.split("\n")) - - for current_index, current_line in enumerate(current_splitted): - if current_line not in previous_splitted: - return "\n".join(current_splitted[current_index:]) - return "" - - -def wait_for_connection_to_be_available( - hostname: str, - ip_address: str, - path_to_private_key: Path, - timeout: int = 1, - wait_time: int = 1, - max_retries: int = 20, - wait_bar: bool = False, -): - """Wait for connection to be available. - - Arguments: - hostname (str): host name - ip_address (str): ip address - path_to_private_key (Path): path to private key - timeout (int): ssh timeout option - wait_time (int): time to wait between retries - max_retries (int): number of retries, if < 0 unlimited retries - wait_bar (bool): tqdm progress bar of retries - - Raises: - TimeoutError: if it wasn't able connect to ssh with the given constraints - """ - with tqdm(disable=not wait_bar) as pbar: - # We can't cover infinite retry without risking an infinite loop - if max_retries < 0: # pragma: no cover - while True: - if is_connection_available( - hostname=hostname, - ip_address=ip_address, - timeout=timeout, - path_to_private_key=path_to_private_key, - ): - return - time.sleep(wait_time) - pbar.update(1) - else: - for _ in range(max_retries): - if is_connection_available( - hostname=hostname, - ip_address=ip_address, - timeout=timeout, - path_to_private_key=path_to_private_key, - ): - return - time.sleep(wait_time) - pbar.update(1) - - raise TimeoutError( - "Timeout reached while trying to check for connection " - f"availability on {hostname}@{ip_address}" - ) - - -def is_connection_available( - hostname: str, ip_address: str, path_to_private_key: Path, timeout: int = 1 -): - """Check if ssh connection is available. - - Arguments: - hostname (str): host name - ip_address (str): ip address - path_to_private_key (Path): path to private key - timeout: ssh timeout option - - Returns: - bool: True if connection succeeded - """ - - command = ( - f"ssh -i {path_to_private_key.resolve()} " - + f"-q -o ConnectTimeout={timeout} -o BatchMode=yes -o " - + f"\"StrictHostKeyChecking=no\" {hostname}@{ip_address} 'exit 0'" - ) - try: - subprocess.check_output(command, shell=True) - return True - except subprocess.CalledProcessError: - return False