binoua commited on
Commit
6e245b1
1 Parent(s): 2a9913b

chore: adding a test with DL

Browse files
README.md CHANGED
@@ -1,3 +1,47 @@
1
  ---
2
  license: apache-2.0
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
  ---
4
+
5
+ # Template for Concrete ML
6
+
7
+ Concrete ML is Zama's open-source privacy-preserving ML package, based on fully homomorphic encryption (FHE). We refer the reader to fhe.org or Zama's websites for more information on FHE.
8
+
9
+ This directory is used:
10
+ - by ML practicioners, to create Concrete ML FHE-friendly models, and make them available to HF users
11
+ - by companies, institutions or people to deploy those models over HF inference endpoints
12
+ - by developers, to use these entry points to make applications on privacy-preserving ML
13
+
14
+ ## Creating models and making them available on HF
15
+
16
+ This is quite easy. Fork this template (maybe use this experimental tool https://huggingface.co/spaces/huggingface-projects/repo_duplicator for that), and then:
17
+ - install everything with: `pip install -r requirements.txt`
18
+ - edit `creating_models.py`, and fill the part between "# BEGIN: insert your ML task here" and
19
+ "# END: insert your ML task here"
20
+ - run the python file: `python creating_models.py`
21
+
22
+ At the end, if the script is successful, you'll have your compiled model ready in `compiled_model`. Now you can commit and push your repository (with in particular `compiled_model`, `handler.py`, `play_with_endpoint.py` and `requirements.txt`, but you can include the other files as well).
23
+
24
+ We recommend you to tag your Concrete ML compiled repository with `Concrete ML FHE friendly` tag, such that people can find them easily.
25
+
26
+ ## Deploying a compiled model on HF inference endpoint
27
+
28
+ If you find an `Concrete ML FHE friendly` repository that you would like to deploy, it is very easy.
29
+ - click on 'Deploy' button in HF interface
30
+ - chose "Inference endpoints"
31
+ - chose the right model repository
32
+ - (the rest of the options are classical to HF end points; we refer you to their documentation for more information)
33
+ and then click on 'Create endpoint'
34
+
35
+ And now, your model should be deployed, after few secunds of installation.
36
+
37
+ ## Using HF entry points on privacy-preserving models
38
+
39
+ Now, this is the final step: using the entry point. You should:
40
+ - if your inference endpoint is private, set an environment variable HF_TOKEN with your HF token
41
+ - edit `play_with_endpoint.py`
42
+ - replace `API_URL` by your entry point URL
43
+ - replace the part between "# BEGIN: replace this part with your privacy-preserving application" and
44
+ "# END: replace this part with your privacy-preserving application" with your application
45
+
46
+ Finally, you'll be able to launch your application with `python play_with_endpoint.py`.
47
+
compiled_model/client.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c24aec589f5004924933388404ed735703dd743ae5fad0fc1c24f6ab413107ef
3
+ size 30194
compiled_model/server.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:de29e9e7c7b1ba38d25d811d29791d71deb9c14ddcdf4eea15a2526a62550578
3
+ size 9144
compiled_model/versions.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"concrete-python": "2.5.0rc1", "concrete-ml": "1.3.0", "python": "3.9.15"}
creating_models.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from concrete.ml.deployment import FHEModelDev
6
+ from concrete.ml.deployment import FHEModelClient
7
+
8
+
9
+ def compile_and_make_it_deployable(model_dev, X_train):
10
+
11
+ path_to_model = Path("compiled_model")
12
+
13
+ n_bits = 4
14
+
15
+ model_dev = compile_torch_model(model_dev, X_train, rounding_threshold_bits=6, p_error=0.1)
16
+
17
+ # Accuracy in simulation
18
+ accs = test_with_concrete(
19
+ model_dev,
20
+ test_dataloader,
21
+ use_sim=True,
22
+ )
23
+
24
+ print(f"Simulated FHE execution for {n_bits} bit model_devwork accuracy: {accs:.2f}")
25
+
26
+ # Saving the model
27
+ shutil.rmtree(path_to_model, ignore_errors=True)
28
+ fhemodel_dev = FHEModelDev(path_to_model, model_dev)
29
+ fhemodel_dev.save(via_mlir=True)
30
+
31
+
32
+ # BEGIN: insert your ML task here
33
+ # Typically
34
+ import time
35
+
36
+ import numpy as np
37
+ import torch
38
+ import torch.utils
39
+ from sklearn.datasets import load_digits
40
+ from sklearn.model_selection import train_test_split
41
+ from torch import nn
42
+ from torch.utils.data import DataLoader, TensorDataset
43
+ from tqdm import tqdm
44
+
45
+ from concrete.ml.torch.compile import compile_torch_model
46
+
47
+ X, y = load_digits(return_X_y=True)
48
+
49
+ # The sklearn Digits data-set, though it contains digit images, keeps these images in vectors
50
+ # so we need to reshape them to 2D first. The images are 8x8 px in size and monochrome
51
+ X = np.expand_dims(X.reshape((-1, 8, 8)), 1)
52
+
53
+ X_train, X_test, Y_train, Y_test = train_test_split(
54
+ X, y, test_size=0.25, shuffle=True, random_state=42
55
+ )
56
+
57
+
58
+ class TinyCNN(nn.Module):
59
+ """A very small CNN to classify the sklearn digits data-set."""
60
+
61
+ def __init__(self, n_classes) -> None:
62
+ """Construct the CNN with a configurable number of classes."""
63
+ super().__init__()
64
+
65
+ # This model_devwork has a total complexity of 1216 MAC
66
+ self.conv1 = nn.Conv2d(1, 8, 3, stride=1, padding=0)
67
+ self.conv2 = nn.Conv2d(8, 16, 3, stride=2, padding=0)
68
+ self.conv3 = nn.Conv2d(16, 32, 2, stride=1, padding=0)
69
+ self.fc1 = nn.Linear(32, n_classes)
70
+
71
+ def forward(self, x):
72
+ """Run inference on the tiny CNN, apply the decision layer on the reshaped conv output."""
73
+ x = self.conv1(x)
74
+ x = torch.relu(x)
75
+ x = self.conv2(x)
76
+ x = torch.relu(x)
77
+ x = self.conv3(x)
78
+ x = torch.relu(x)
79
+ x = x.flatten(1)
80
+ x = self.fc1(x)
81
+ return x
82
+
83
+
84
+ torch.manual_seed(42)
85
+
86
+
87
+ def train_one_epoch(model_dev, optimizer, train_loader):
88
+ # Cross Entropy loss for classification when not using a softmax layer in the model_devwork
89
+ loss = nn.CrossEntropyLoss()
90
+
91
+ model_dev.train()
92
+ avg_loss = 0
93
+ for data, target in train_loader:
94
+ optimizer.zero_grad()
95
+ output = model_dev(data)
96
+ loss_model_dev = loss(output, target.long())
97
+ loss_model_dev.backward()
98
+ optimizer.step()
99
+ avg_loss += loss_model_dev.item()
100
+
101
+ return avg_loss / len(train_loader)
102
+
103
+
104
+ def test_torch(model_dev, test_loader):
105
+ """Test the model_devwork: measure accuracy on the test set."""
106
+
107
+ # Freeze normalization layers
108
+ model_dev.eval()
109
+
110
+ all_y_pred = np.zeros((len(test_loader)), dtype=np.int64)
111
+ all_targets = np.zeros((len(test_loader)), dtype=np.int64)
112
+
113
+ # Iterate over the batches
114
+ idx = 0
115
+ for data, target in test_loader:
116
+ # Accumulate the ground truth labels
117
+ endidx = idx + target.shape[0]
118
+ all_targets[idx:endidx] = target.numpy()
119
+
120
+ # Run forward and get the predicted class id
121
+ output = model_dev(data).argmax(1).detach().numpy()
122
+ all_y_pred[idx:endidx] = output
123
+
124
+ idx += target.shape[0]
125
+
126
+ # Print out the accuracy as a percentage
127
+ n_correct = np.sum(all_targets == all_y_pred)
128
+ print(
129
+ f"Test accuracy for fp32 weights and activations: "
130
+ f"{n_correct / len(test_loader) * 100:.2f}%"
131
+ )
132
+
133
+
134
+ def test_with_concrete(quantized_module, test_loader, use_sim):
135
+ """Test a neural model_devwork that is quantized and compiled with Concrete ML."""
136
+
137
+ # Casting the inputs into int64 is recommended
138
+ all_y_pred = np.zeros((len(test_loader)), dtype=np.int64)
139
+ all_targets = np.zeros((len(test_loader)), dtype=np.int64)
140
+
141
+ # Iterate over the test batches and accumulate predictions and ground truth labels in a vector
142
+ idx = 0
143
+ for data, target in tqdm(test_loader):
144
+ data = data.numpy()
145
+ target = target.numpy()
146
+
147
+ fhe_mode = "simulate" if use_sim else "execute"
148
+
149
+ # Quantize the inputs and cast to appropriate data type
150
+ y_pred = quantized_module.forward(data, fhe=fhe_mode)
151
+
152
+ endidx = idx + target.shape[0]
153
+
154
+ # Accumulate the ground truth labels
155
+ all_targets[idx:endidx] = target
156
+
157
+ # Get the predicted class id and accumulate the predictions
158
+ y_pred = np.argmax(y_pred, axis=1)
159
+ all_y_pred[idx:endidx] = y_pred
160
+
161
+ # Update the index
162
+ idx += target.shape[0]
163
+
164
+ # Compute and report results
165
+ n_correct = np.sum(all_targets == all_y_pred)
166
+
167
+ return n_correct / len(test_loader)
168
+
169
+
170
+ # Create the tiny CNN with 10 output classes
171
+ N_EPOCHS = 50
172
+
173
+ # Create a train data loader
174
+ train_dataset = TensorDataset(torch.Tensor(X_train), torch.Tensor(Y_train))
175
+ train_dataloader = DataLoader(train_dataset, batch_size=64)
176
+
177
+ # Create a test data loader to supply batches for model_devwork evaluation (test)
178
+ test_dataset = TensorDataset(torch.Tensor(X_test), torch.Tensor(Y_test))
179
+ test_dataloader = DataLoader(test_dataset)
180
+
181
+ # Train the model_devwork with Adam, output the test set accuracy every epoch
182
+ model_dev = TinyCNN(10)
183
+ losses_bits = []
184
+ optimizer = torch.optim.Adam(model_dev.parameters())
185
+ for _ in tqdm(range(N_EPOCHS), desc="Training"):
186
+ losses_bits.append(train_one_epoch(model_dev, optimizer, train_dataloader))
187
+
188
+ # END: insert your ML task here
189
+
190
+ compile_and_make_it_deployable(model_dev, X_train)
191
+ print("Your model is ready to be deployable.")
handler.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any
2
+ import numpy as np
3
+ from concrete.ml.deployment import FHEModelServer
4
+
5
+
6
+ def from_json(python_object):
7
+ if "__class__" in python_object:
8
+ return bytes(python_object["__value__"])
9
+
10
+
11
+ def to_json(python_object):
12
+ if isinstance(python_object, bytes):
13
+ return {"__class__": "bytes", "__value__": list(python_object)}
14
+ raise TypeError(repr(python_object) + " is not JSON serializable")
15
+
16
+
17
+ class EndpointHandler:
18
+ def __init__(self, path=""):
19
+
20
+ # For server
21
+ self.fhemodel_server = FHEModelServer(path + "/compiled_model")
22
+
23
+ # Simulate a database of keys
24
+ self.key_database = {}
25
+
26
+ def __call__(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
27
+ """
28
+ data args:
29
+ inputs (:obj: `str`)
30
+ date (:obj: `str`)
31
+ Return:
32
+ A :obj:`list` | `dict`: will be serialized and returned
33
+ """
34
+
35
+ # Get method
36
+ method = data.pop("method", data)
37
+
38
+ if method == "save_key":
39
+
40
+ # Get keys
41
+ evaluation_keys = from_json(data.pop("evaluation_keys", data))
42
+
43
+ uid = np.random.randint(2**32)
44
+
45
+ while uid in self.key_database.keys():
46
+ uid = np.random.randint(2**32)
47
+
48
+ self.key_database[uid] = evaluation_keys
49
+
50
+ return {"uid": uid}
51
+
52
+ elif method == "append_key":
53
+
54
+ # Get key piece
55
+ evaluation_keys = from_json(data.pop("evaluation_keys", data))
56
+
57
+ uid = data.pop("uid", data)
58
+
59
+ self.key_database[uid] += evaluation_keys
60
+
61
+ return
62
+
63
+ elif method == "inference":
64
+
65
+ uid = data.pop("uid", data)
66
+
67
+ assert uid in self.key_database.keys(), f"{uid} not in DB, {self.key_database.keys()=}"
68
+
69
+ # Get inputs
70
+ encrypted_inputs = from_json(data.pop("encrypted_inputs", data))
71
+
72
+ # Find key in the database
73
+ evaluation_keys = self.key_database[uid]
74
+
75
+ # Run CML prediction
76
+ encrypted_prediction = self.fhemodel_server.run(encrypted_inputs, evaluation_keys)
77
+
78
+ return to_json(encrypted_prediction)
79
+
80
+ else:
81
+
82
+ return
play_with_endpoint.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import time
3
+ import os, sys
4
+
5
+ from pathlib import Path
6
+
7
+ from concrete.ml.deployment import FHEModelClient
8
+
9
+ import requests
10
+
11
+
12
+ def to_json(python_object):
13
+ if isinstance(python_object, bytes):
14
+ return {"__class__": "bytes", "__value__": list(python_object)}
15
+ raise TypeError(repr(python_object) + " is not JSON serializable")
16
+
17
+
18
+ def from_json(python_object):
19
+ if "__class__" in python_object:
20
+ return bytes(python_object["__value__"])
21
+
22
+
23
+ # TODO: put the right link `API_URL` for your entry point
24
+ API_URL = "https://XXXXXXX.us-east-1.aws.endpoints.huggingface.cloud"
25
+ headers = {
26
+ "Authorization": "Bearer " + os.environ.get("HF_TOKEN"),
27
+ "Content-Type": "application/json",
28
+ }
29
+
30
+
31
+ def query(payload):
32
+ response = requests.post(API_URL, headers=headers, json=payload)
33
+
34
+ if "error" in response:
35
+ assert False, f"Got an error: {response=}"
36
+
37
+ return response.json()
38
+
39
+
40
+ path_to_model = Path("compiled_model")
41
+
42
+ # BEGIN: replace this part with your privacy-preserving application
43
+ from sklearn.datasets import make_classification
44
+ from sklearn.model_selection import train_test_split
45
+
46
+ x, y = make_classification(n_samples=1000, class_sep=2, n_features=30, random_state=42)
47
+ _, X_test, _, Y_test = train_test_split(x, y, test_size=0.2, random_state=42)
48
+
49
+ # Recover parameters for client side
50
+ fhemodel_client = FHEModelClient(path_to_model)
51
+
52
+ # Generate the keys
53
+ fhemodel_client.generate_private_and_evaluation_keys()
54
+ evaluation_keys = fhemodel_client.get_serialized_evaluation_keys()
55
+
56
+ # Save the key in the database
57
+ evaluation_keys_remaining = evaluation_keys[:]
58
+ uid = None
59
+ is_first = True
60
+ is_finished = False
61
+ i = 0
62
+ packet_size = 1024 * 1024 * 100
63
+
64
+ while not is_finished:
65
+
66
+ # Send by packets of 100M
67
+ if sys.getsizeof(evaluation_keys_remaining) > packet_size:
68
+ evaluation_keys_piece = evaluation_keys_remaining[:packet_size]
69
+ evaluation_keys_remaining = evaluation_keys_remaining[packet_size:]
70
+ else:
71
+ evaluation_keys_piece = evaluation_keys_remaining
72
+ is_finished = True
73
+
74
+ print(
75
+ f"Sending {i}-th piece of the key (remaining size is {sys.getsizeof(evaluation_keys_remaining)})"
76
+ )
77
+ i += 1
78
+
79
+ if is_first:
80
+ is_first = False
81
+ payload = {
82
+ "inputs": "fake",
83
+ "evaluation_keys": to_json(evaluation_keys_piece),
84
+ "method": "save_key",
85
+ }
86
+
87
+ uid = query(payload)["uid"]
88
+ print(f"Storing the key in the database under {uid=}")
89
+
90
+ else:
91
+ payload = {
92
+ "inputs": "fake",
93
+ "evaluation_keys": to_json(evaluation_keys_piece),
94
+ "method": "append_key",
95
+ "uid": uid,
96
+ }
97
+
98
+ query(payload)
99
+
100
+ # Test the handler
101
+ nb_good = 0
102
+ nb_samples = len(X_test)
103
+ verbose = True
104
+ time_start = time.time()
105
+ duration = 0
106
+ is_first = True
107
+
108
+ for i in range(nb_samples):
109
+
110
+ # Quantize the input and encrypt it
111
+ encrypted_inputs = fhemodel_client.quantize_encrypt_serialize([X_test[i]])
112
+
113
+ # Prepare the payload
114
+ payload = {
115
+ "inputs": "fake",
116
+ "encrypted_inputs": to_json(encrypted_inputs),
117
+ "method": "inference",
118
+ "uid": uid,
119
+ }
120
+
121
+ if is_first:
122
+ print(f"Size of the payload: {sys.getsizeof(payload) / 1024} kilobytes")
123
+ is_first = False
124
+
125
+ # Run the inference on HF servers
126
+ duration -= time.time()
127
+ duration_inference = -time.time()
128
+ encrypted_prediction = query(payload)
129
+ duration += time.time()
130
+ duration_inference += time.time()
131
+
132
+ encrypted_prediction = from_json(encrypted_prediction)
133
+
134
+ # Decrypt the result and dequantize
135
+ prediction_proba = fhemodel_client.deserialize_decrypt_dequantize(encrypted_prediction)[0]
136
+ prediction = np.argmax(prediction_proba)
137
+
138
+ if verbose:
139
+ print(
140
+ f"for {i}-th input, {prediction=} with expected {Y_test[i]} in {duration_inference:.3f} seconds"
141
+ )
142
+
143
+ # Measure accuracy
144
+ nb_good += Y_test[i] == prediction
145
+
146
+ print(f"Accuracy on {nb_samples} samples is {nb_good * 1. / nb_samples}")
147
+ print(f"Total time: {time.time() - time_start:.3f} seconds")
148
+ print(f"Duration per inference: {duration / nb_samples:.3f} seconds")
149
+ # END: replace this part with your privacy-preserving application
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ concrete-ml==1.3.0